diff options
78 files changed, 2646 insertions, 677 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4681d4943256..d6a067d35f9b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -919,7 +919,7 @@ package android.content.pm { field public int id; field public String lastLoggedInFingerprint; field public long lastLoggedInTime; - field public String name; + field @Nullable public String name; field public boolean partial; field public boolean preCreated; field public int profileBadge; diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java index ea30ef704bce..08056847e12e 100644 --- a/core/java/android/app/admin/DevicePolicyResources.java +++ b/core/java/android/app/admin/DevicePolicyResources.java @@ -1173,7 +1173,62 @@ public final class DevicePolicyResources { /** * Header for items under the personal user */ - public static final String PERSONAL_CATEGORY_HEADER = PREFIX + "category_personal"; + public static final String PERSONAL_CATEGORY_HEADER = PREFIX + "CATEGORY_PERSONAL"; + + /** + * Text to indicate work notification content will be shown on the lockscreen. + */ + public static final String LOCK_SCREEN_SHOW_WORK_NOTIFICATION_CONTENT = + PREFIX + "LOCK_SCREEN_SHOW_WORK_NOTIFICATION_CONTENT"; + + /** + * Text to indicate work notification content will be shown on the lockscreen. + */ + public static final String LOCK_SCREEN_HIDE_WORK_NOTIFICATION_CONTENT = + PREFIX + "LOCK_SCREEN_HIDE_WORK_NOTIFICATION_CONTENT"; + + /** + * Text for toggle to enable auto-sycing personal data + */ + public static final String AUTO_SYNC_PERSONAL_DATA = PREFIX + + "AUTO_SYNC_PERSONAL_DATA"; + + /** + * Text for toggle to enable auto-sycing work data + */ + public static final String AUTO_SYNC_WORK_DATA = PREFIX + + "AUTO_SYNC_WORK_DATA"; + + /** + * Summary for "More security settings" section when a work profile is on the device. + */ + public static final String MORE_SECURITY_SETTINGS_WORK_PROFILE_SUMMARY = PREFIX + + "MORE_SECURITY_SETTINGS_WORK_PROFILE_SUMMARY"; + + /** + * Title for screen asking the user to choose a type of screen lock (such as a pattern, + * PIN, or password) that they need to enter to use their work apps + */ + public static final String LOCK_SETTINGS_NEW_PROFILE_LOCK_TITLE = PREFIX + + "LOCK_SETTINGS_NEW_PROFILE_LOCK_TITLE"; + + /** + * Title for section listing information that can be seen by organization + */ + public static final String INFORMATION_SEEN_BY_ORGANIZATION_TITLE = PREFIX + + "information_seen_by_organization_title"; + + /** + * Title for section listing changes made by the organization. + */ + public static final String CHANGES_BY_ORGANIZATION_TITLE = + PREFIX + "CHANGES_BY_ORGANIZATION_TITLE"; + + /** + * Footer for enterprise privacy screen. + */ + public static final String ENTERPRISE_PRIVACY_FOOTER = + PREFIX + "ENTERPRISE_PRIVACY_FOOTER"; } /** diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 76e9fcb07f22..d6e13ac90f82 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; @@ -170,7 +171,7 @@ public class UserInfo implements Parcelable { @UnsupportedAppUsage public int serialNumber; @UnsupportedAppUsage - public String name; + public @Nullable String name; @UnsupportedAppUsage public String iconPath; @UnsupportedAppUsage diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a64e63eacd56..196f2f94120e 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2179,7 +2179,10 @@ public class UserManager { } } else { UserInfo userInfo = getUserInfo(mUserId); - return userInfo == null ? "" : userInfo.name; + if (userInfo != null && userInfo.name != null) { + return userInfo.name; + } + return ""; } } diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl index 29b4570ac71e..67a930a17665 100644 --- a/core/java/android/os/logcat/ILogcatManagerService.aidl +++ b/core/java/android/os/logcat/ILogcatManagerService.aidl @@ -42,31 +42,4 @@ oneway interface ILogcatManagerService { * @param fd The FD (Socket) of client who makes the request. */ void finishThread(in int uid, in int gid, in int pid, in int fd); - - - /** - * The function is called by UX component to notify - * LogcatManagerService that the user approved - * the privileged log data access. - * - * @param uid The UID of client who makes the request. - * @param gid The GID of client who makes the request. - * @param pid The PID of client who makes the request. - * @param fd The FD (Socket) of client who makes the request. - */ - void approve(in int uid, in int gid, in int pid, in int fd); - - - /** - * The function is called by UX component to notify - * LogcatManagerService that the user declined - * the privileged log data access. - * - * @param uid The UID of client who makes the request. - * @param gid The GID of client who makes the request. - * @param pid The PID of client who makes the request. - * @param fd The FD (Socket) of client who makes the request. - */ - void decline(in int uid, in int gid, in int pid, in int fd); } - diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1ef1ac51544c..dac54cf6146e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7232,13 +7232,6 @@ public final class Settings { */ public static final String LOCATION_SHOW_SYSTEM_OPS = "locationShowSystemOps"; - - /** - * Whether or not an indicator experiment has started. - * @hide - */ - public static final String LOCATION_INDICATOR_EXPERIMENT_STARTED = - "locationIndicatorExperimentStarted"; /** * A flag containing settings used for biometric weak * @hide diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 425dbb9cb204..0ec95c687090 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -904,7 +904,6 @@ public abstract class WallpaperService extends Service { // based on its default wallpaper color hints. mShouldDim = dimAmount != 0f || mShouldDimByDefault; updateSurfaceDimming(); - updateSurface(false, false, true); } private void updateSurfaceDimming() { @@ -941,6 +940,7 @@ public abstract class WallpaperService extends Service { } else { Log.v(TAG, "Setting wallpaper dimming: " + 0); surfaceControlTransaction.setAlpha(mBbqSurfaceControl, 1.0f).apply(); + updateSurface(false, false, true); } mPreviousWallpaperDimAmount = mWallpaperDimAmount; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 6fc16c630305..8901d86e1f2a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -11752,6 +11752,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && (info.mHandwritingArea == null || !isAutoHandwritingEnabled())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); + info.mPositionUpdateListener = null; info.mPositionChangedUpdate = null; } } else { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 335cd2799c01..42b5691b239e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4196,26 +4196,6 @@ public final class ViewRootImpl implements ViewParent, }); } - @Nullable - private void registerFrameDrawingCallbackForBlur() { - if (!isHardwareEnabled()) { - return; - } - final boolean hasBlurUpdates = mBlurRegionAggregator.hasUpdates(); - final boolean needsCallbackForBlur = hasBlurUpdates || mBlurRegionAggregator.hasRegions(); - - if (!needsCallbackForBlur) { - return; - } - - final BackgroundBlurDrawable.BlurRegion[] blurRegionsForFrame = - mBlurRegionAggregator.getBlurRegionsCopyForRT(); - - // The callback will run on the render thread. - registerRtFrameCallback((frame) -> mBlurRegionAggregator - .dispatchBlurTransactionIfNeeded(frame, blurRegionsForFrame, hasBlurUpdates)); - } - private void registerCallbackForPendingTransactions() { registerRtFrameCallback(new FrameDrawingCallback() { @Override @@ -4253,7 +4233,6 @@ public final class ViewRootImpl implements ViewParent, mIsDrawing = true; Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); - registerFrameDrawingCallbackForBlur(); addFrameCommitCallbackIfNeeded(); boolean usingAsyncReport = isHardwareEnabled() && mSyncBufferCallback != null; diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index f0a685ec4d2e..3fee914f2def 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -245,7 +245,7 @@ public class ChooserActivity extends ResolverActivity implements SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP, DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP); - private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 250; + private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125; @VisibleForTesting int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, diff --git a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java index 402d7fed90c5..4e1ecc281df3 100644 --- a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java +++ b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java @@ -36,6 +36,7 @@ import android.util.ArraySet; import android.util.Log; import android.util.LongSparseArray; import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -232,9 +233,12 @@ public final class BackgroundBlurDrawable extends Drawable { private final ArraySet<BackgroundBlurDrawable> mDrawables = new ArraySet(); @GuardedBy("mRtLock") private final LongSparseArray<ArraySet<Runnable>> mFrameRtUpdates = new LongSparseArray(); + private long mLastFrameNumber = 0; + private BlurRegion[] mLastFrameBlurRegions = null; private final ViewRootImpl mViewRoot; private BlurRegion[] mTmpBlurRegionsForFrame = new BlurRegion[0]; private boolean mHasUiUpdates; + private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener; public Aggregator(ViewRootImpl viewRoot) { mViewRoot = viewRoot; @@ -277,6 +281,38 @@ public final class BackgroundBlurDrawable extends Drawable { Log.d(TAG, "Remove " + drawable); } } + + if (mOnPreDrawListener == null && mViewRoot.getView() != null + && hasRegions()) { + registerPreDrawListener(); + } + } + + private void registerPreDrawListener() { + mOnPreDrawListener = () -> { + final boolean hasUiUpdates = hasUpdates(); + + if (hasUiUpdates || hasRegions()) { + final BlurRegion[] blurRegionsForNextFrame = getBlurRegionsCopyForRT(); + + mViewRoot.registerRtFrameCallback(frame -> { + synchronized (mRtLock) { + mLastFrameNumber = frame; + mLastFrameBlurRegions = blurRegionsForNextFrame; + handleDispatchBlurTransactionLocked( + frame, blurRegionsForNextFrame, hasUiUpdates); + } + }); + } + if (!hasRegions() && mViewRoot.getView() != null) { + mViewRoot.getView().getViewTreeObserver() + .removeOnPreDrawListener(mOnPreDrawListener); + mOnPreDrawListener = null; + } + return true; + }; + + mViewRoot.getView().getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); } // Called from a thread pool @@ -290,7 +326,14 @@ public final class BackgroundBlurDrawable extends Drawable { mFrameRtUpdates.put(frameNumber, frameRtUpdates); } frameRtUpdates.add(update); + + if (mLastFrameNumber == frameNumber) { + // The transaction for this frame has already been sent, so we have to manually + // trigger sending a transaction here in order to apply this position update + handleDispatchBlurTransactionLocked(frameNumber, mLastFrameBlurRegions, true); + } } + } /** @@ -329,29 +372,27 @@ public final class BackgroundBlurDrawable extends Drawable { /** * Called on RenderThread. * - * @return all blur regions if there are any ui or position updates for this frame, - * null otherwise + * @return true if it is necessary to send an update to Sf this frame */ + @GuardedBy("mRtLock") @VisibleForTesting - public float[][] getBlurRegionsToDispatchToSf(long frameNumber, - BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) { - synchronized (mRtLock) { - if (!hasUiUpdatesForFrame && (mFrameRtUpdates.size() == 0 - || mFrameRtUpdates.keyAt(0) > frameNumber)) { - return null; - } + public float[][] getBlurRegionsForFrameLocked(long frameNumber, + BlurRegion[] blurRegionsForFrame, boolean forceUpdate) { + if (!forceUpdate && (mFrameRtUpdates.size() == 0 + || mFrameRtUpdates.keyAt(0) > frameNumber)) { + return null; + } - // mFrameRtUpdates holds position updates coming from a thread pool span from - // RenderThread. At this point, all position updates for frame frameNumber should - // have been added to mFrameRtUpdates. - // Here, we apply all updates for frames <= frameNumber in case some previous update - // has been missed. This also protects mFrameRtUpdates from memory leaks. - while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) { - final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0); - mFrameRtUpdates.removeAt(0); - for (int i = 0; i < frameUpdates.size(); i++) { - frameUpdates.valueAt(i).run(); - } + // mFrameRtUpdates holds position updates coming from a thread pool span from + // RenderThread. At this point, all position updates for frame frameNumber should + // have been added to mFrameRtUpdates. + // Here, we apply all updates for frames <= frameNumber in case some previous update + // has been missed. This also protects mFrameRtUpdates from memory leaks. + while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) { + final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0); + mFrameRtUpdates.removeAt(0); + for (int i = 0; i < frameUpdates.size(); i++) { + frameUpdates.valueAt(i).run(); } } @@ -370,13 +411,13 @@ public final class BackgroundBlurDrawable extends Drawable { } /** - * Called on RenderThread in FrameDrawingCallback. - * Dispatch all blur regions if there are any ui or position updates. + * Dispatch all blur regions if there are any ui or position updates for that frame. */ - public void dispatchBlurTransactionIfNeeded(long frameNumber, - BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) { - final float[][] blurRegionsArray = getBlurRegionsToDispatchToSf(frameNumber, - blurRegionsForFrame, hasUiUpdatesForFrame); + @GuardedBy("mRtLock") + private void handleDispatchBlurTransactionLocked(long frameNumber, BlurRegion[] blurRegions, + boolean forceUpdate) { + float[][] blurRegionsArray = + getBlurRegionsForFrameLocked(frameNumber, blurRegions, forceUpdate); if (blurRegionsArray != null) { mViewRoot.dispatchBlurRegions(blurRegionsArray, frameNumber); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 46b463074383..ef8f2db5ff57 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -88,7 +88,7 @@ interface IStatusBarService in int notificationLocation, boolean modifiedBeforeSending); void onNotificationSettingsViewed(String key); void onNotificationBubbleChanged(String key, boolean isBubble, int flags); - void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, boolean isBubbleSuppressed); + void onBubbleMetadataFlagChanged(String key, int flags); void hideCurrentInputMethodForBubbles(); void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName); oneway void clearInlineReplyUriPermissions(String key); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 217166c6810b..0f328b034f38 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6776,9 +6776,8 @@ </activity> <activity android:name="com.android.server.logcat.LogAccessDialogActivity" - android:theme="@style/Theme.DeviceDefault.Dialog.Alert.DayNight" + android:theme="@style/Theme.Translucent.NoTitleBar" android:excludeFromRecents="true" - android:label="@string/log_access_confirmation_title" android:exported="false"> </activity> @@ -7057,6 +7056,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.notification.ReviewNotificationPermissionsJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/res/OWNERS b/core/res/OWNERS index 95d2712a2b41..c54638a368a2 100644 --- a/core/res/OWNERS +++ b/core/res/OWNERS @@ -45,3 +45,4 @@ per-file res/xml/power_profile_test.xml = file:/BATTERY_STATS_OWNERS # Telephony per-file res/values/config_telephony.xml = file:/platform/frameworks/opt/telephony:/OWNERS +per-file res/xml/sms_short_codes.xml = file:/platform/frameworks/opt/telephony:/OWNERS diff --git a/core/tests/coretests/src/android/view/BlurAggregatorTest.java b/core/tests/coretests/src/android/view/BlurAggregatorTest.java index b01f2755efdd..ded925e50a6a 100644 --- a/core/tests/coretests/src/android/view/BlurAggregatorTest.java +++ b/core/tests/coretests/src/android/view/BlurAggregatorTest.java @@ -65,7 +65,7 @@ public class BlurAggregatorTest { drawable.setBlurRadius(TEST_BLUR_RADIUS); final boolean hasUpdates = mAggregator.hasUpdates(); final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); - mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions, hasUpdates); + mAggregator.getBlurRegionsForFrameLocked(TEST_FRAME_NUMBER, blurRegions, hasUpdates); return drawable; } @@ -154,7 +154,7 @@ public class BlurAggregatorTest { assertEquals(1, blurRegions.length); mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); - mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions, + mAggregator.getBlurRegionsForFrameLocked(TEST_FRAME_NUMBER, blurRegions, mAggregator.hasUpdates()); assertEquals(1, blurRegions[0].rect.left); assertEquals(2, blurRegions[0].rect.top); @@ -169,7 +169,7 @@ public class BlurAggregatorTest { final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); assertEquals(1, blurRegions.length); - float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, hasUpdates); assertNull(blurRegionsForSf); } @@ -182,7 +182,7 @@ public class BlurAggregatorTest { final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); assertEquals(1, blurRegions.length); - float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, hasUpdates); assertNotNull(blurRegionsForSf); assertEquals(1, blurRegionsForSf.length); @@ -197,7 +197,7 @@ public class BlurAggregatorTest { assertEquals(1, blurRegions.length); mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); - float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, hasUpdates); assertNotNull(blurRegionsForSf); assertEquals(1, blurRegionsForSf.length); @@ -216,7 +216,7 @@ public class BlurAggregatorTest { assertEquals(1, blurRegions.length); mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); - float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates); assertNotNull(blurRegionsForSf); assertEquals(1, blurRegionsForSf.length); @@ -237,19 +237,19 @@ public class BlurAggregatorTest { assertEquals(2, blurRegions.length); // Check that an update in one of the drawables triggers a dispatch of all blur regions - float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, hasUpdates); assertNotNull(blurRegionsForSf); assertEquals(2, blurRegionsForSf.length); // Check that the Aggregator deleted all position updates for frame TEST_FRAME_NUMBER - blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false); assertNull(blurRegionsForSf); // Check that a position update triggers a dispatch of all blur regions drawable2.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); - blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates); assertNotNull(blurRegionsForSf); assertEquals(2, blurRegionsForSf.length); @@ -292,7 +292,7 @@ public class BlurAggregatorTest { mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER + 1, 5, 6, 7, 8); - final float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + final float[][] blurRegionsForSf = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false); assertNotNull(blurRegionsForSf); assertEquals(1, blurRegionsForSf.length); @@ -303,7 +303,7 @@ public class BlurAggregatorTest { assertEquals(3f, blurRegionsForSf[0][4]); assertEquals(4f, blurRegionsForSf[0][5]); - final float[][] blurRegionsForSfForNextFrame = mAggregator.getBlurRegionsToDispatchToSf( + final float[][] blurRegionsForSfForNextFrame = mAggregator.getBlurRegionsForFrameLocked( TEST_FRAME_NUMBER + 1, blurRegions, /* hasUiUpdates= */ false); assertNotNull(blurRegionsForSfForNextFrame); assertEquals(1, blurRegionsForSfForNextFrame.length); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 82f8a131ae2a..faada1aa03ef 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -777,22 +777,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } - private void updateCallbackIfNecessary() { - updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */); - } - /** * Notifies listeners about changes to split states if necessary. - * - * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info - * callback should be deferred until all the - * organized activities have been created. */ - private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) { + private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; } - if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) { + if (!allActivitiesCreated()) { return; } List<SplitInfo> currentSplitStates = getActiveSplitStates(); @@ -848,9 +840,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; for (TaskFragmentContainer container : containers) { - if (container.getInfo() == null - || container.getInfo().getActivities().size() - != container.collectActivities().size()) { + if (!container.taskInfoActivityCountMatchesCreated()) { return false; } } @@ -1035,11 +1025,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) { // The onTaskFragmentInfoChanged callback containing this activity has not // reached the client yet, so add the activity to the pending appeared - // activities and send a split info callback to the client before - // {@link Activity#onCreate} is called. + // activities. container.addPendingAppearedActivity(activity); - updateCallbackIfNecessary( - false /* deferCallbackUntilAllActivitiesCreated */); return; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 35981d3af948..26bbcbb937f0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -145,6 +145,18 @@ class TaskFragmentContainer { return allActivities; } + /** + * Checks if the count of activities from the same process in task fragment info corresponds to + * the ones created and available on the client side. + */ + boolean taskInfoActivityCountMatchesCreated() { + if (mInfo == null) { + return false; + } + return mPendingAppearedActivities.isEmpty() + && mInfo.getActivities().size() == collectActivities().size(); + } + ActivityStack toActivityStack() { return new ActivityStack(collectActivities(), isEmpty()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 227494c04049..31fc6a5be589 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -71,7 +71,7 @@ public class Bubble implements BubbleViewProvider { private long mLastAccessed; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; /** Whether the bubble should show a dot for the notification indicating updated content. */ private boolean mShowBubbleUpdateDot = true; @@ -192,13 +192,13 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.SuppressionChangedListener listener, + @Nullable final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); - mSuppressionListener = listener; + mBubbleMetadataFlagListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { mIntent.unregisterCancelListener(mIntentCancelListener); @@ -606,8 +606,8 @@ public class Bubble implements BubbleViewProvider { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; } - if (showInShade() != prevShowInShade && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -626,8 +626,8 @@ public class Bubble implements BubbleViewProvider { } else { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; } - if (prevSuppressed != suppressBubble && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -771,12 +771,17 @@ public class Bubble implements BubbleViewProvider { return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } - void setShouldAutoExpand(boolean shouldAutoExpand) { + @VisibleForTesting + public void setShouldAutoExpand(boolean shouldAutoExpand) { + boolean prevAutoExpand = shouldAutoExpand(); if (shouldAutoExpand) { enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } else { disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } + if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); + } } public void setIsBubble(final boolean isBubble) { @@ -799,6 +804,10 @@ public class Bubble implements BubbleViewProvider { return (mFlags & option) != 0; } + public int getFlags() { + return mFlags; + } + @Override public String toString() { return "Bubble{" + mKey + '}'; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 806c395bf395..f407bdcb8852 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -323,7 +323,7 @@ public class BubbleController { public void initialize() { mBubbleData.setListener(mBubbleDataListener); - mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged); + mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { @@ -554,11 +554,10 @@ public class BubbleController { } @VisibleForTesting - public void onBubbleNotificationSuppressionChanged(Bubble bubble) { + public void onBubbleMetadataFlagChanged(Bubble bubble) { // Make sure NoMan knows suppression state so that anyone querying it can tell. try { - mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), - !bubble.showInShade(), bubble.isSuppressed()); + mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags()); } catch (RemoteException e) { // Bad things have happened } @@ -1038,7 +1037,15 @@ public class BubbleController { } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (notif.shouldSuppressNotificationList()) { + // If we're suppressing notifs for DND, we don't want the bubbles to randomly + // expand when DND turns off so flip the flag. + if (bubble.shouldAutoExpand()) { + bubble.setShouldAutoExpand(false); + } + } else { + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } } @@ -1070,7 +1077,8 @@ public class BubbleController { } } - private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + @VisibleForTesting + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1096,7 +1104,8 @@ public class BubbleController { } } - private void onRankingUpdated(RankingMap rankingMap, + @VisibleForTesting + public void onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); @@ -1107,19 +1116,22 @@ public class BubbleController { Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); BubbleEntry entry = entryData.first; boolean shouldBubbleUp = entryData.second; - if (entry != null && !isCurrentProfile( entry.getStatusBarNotification().getUser().getIdentifier())) { return; } - + if (entry != null && (entry.shouldSuppressNotificationList() + || entry.getRanking().isSuspended())) { + shouldBubbleUp = false; + } rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); - if (isActiveBubble && !mTmpRanking.canBubble()) { + boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key); + boolean isActive = mBubbleData.hasBubbleInStackWithKey(key); + if (isActiveOrInOverflow && !mTmpRanking.canBubble()) { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) { + } else if (isActiveOrInOverflow && !shouldBubbleUp) { // If this entry is allowed to bubble, but cannot currently bubble up or is // suspended, dismiss it. This happens when DND is enabled and configured to hide // bubbles, or focus mode is enabled and the app is designated as distracting. @@ -1127,9 +1139,9 @@ public class BubbleController { // notification, so that the bubble will be re-created if shouldBubbleUp returns // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { + } else if (entry != null && mTmpRanking.isBubble() && !isActive) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended()); + onEntryUpdated(entry, shouldBubbleUp); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index c98c0e69de15..e4a0fd03860c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -159,7 +159,7 @@ public class BubbleData { private Listener mListener; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; /** @@ -190,9 +190,8 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } - public void setSuppressionChangedListener( - Bubbles.SuppressionChangedListener listener) { - mSuppressionListener = listener; + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { + mBubbleMetadataFlagListener = listener; } public void setPendingIntentCancelledListener( @@ -311,7 +310,7 @@ public class BubbleData { bubbleToReturn = mPendingBubbles.get(key); } else if (entry != null) { // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, + bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, mMainExecutor); } else { // Persisted bubble being promoted @@ -1058,6 +1057,22 @@ public class BubbleData { return null; } + /** + * Get a pending bubble with given notification <code>key</code> + * + * @param key notification key + * @return bubble that matches or null + */ + @VisibleForTesting(visibility = PRIVATE) + public Bubble getPendingBubbleWithKey(String key) { + for (Bubble b : mPendingBubbles.values()) { + if (b.getKey().equals(key)) { + return b; + } + } + return null; + } + @VisibleForTesting(visibility = PRIVATE) void setTimeSource(TimeSource timeSource) { mTimeSource = timeSource; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 2b2a2f7e35df..c7db8d8d1646 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -263,10 +263,10 @@ public interface Bubbles { void onBubbleExpandChanged(boolean isExpanding, String key); } - /** Listener to be notified when the flags for notification or bubble suppression changes.*/ - interface SuppressionChangedListener { - /** Called when the notification suppression state of a bubble changes. */ - void onBubbleNotificationSuppressionChange(Bubble bubble); + /** Listener to be notified when the flags on BubbleMetadata have changed. */ + interface BubbleMetadataFlagListener { + /** Called when the flags on BubbleMetadata have changed for the provided bubble. */ + void onBubbleMetadataFlagChanged(Bubble bubble); } /** Listener to be notified when a pending intent has been canceled for a bubble. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 169f03e7bc3e..bde94d9d6c29 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -115,7 +115,7 @@ public class BubbleDataTest extends ShellTestCase { private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Mock private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener; @@ -136,30 +136,47 @@ public class BubbleDataTest extends ShellTestCase { mock(NotificationListenerService.Ranking.class); when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); - mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, + mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); - mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, + mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, new LocusId("locusId1")); - mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor); + mBubbleLocusId = new Bubble(mEntryLocusId, + mBubbleMetadataFlagListener, + null /* pendingIntentCanceledListener */, + mMainExecutor); - mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA1 = new Bubble(mEntryA1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA2 = new Bubble(mEntryA2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA3 = new Bubble(mEntryA3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB1 = new Bubble(mEntryB1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB2 = new Bubble(mEntryB2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB3 = new Bubble(mEntryB3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleC1 = new Bubble(mEntryC1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index 819a984b4a77..e8f3f69ca64e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -63,7 +63,7 @@ public class BubbleTest extends ShellTestCase { private Bubble mBubble; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Before public void setUp() { @@ -81,7 +81,7 @@ public class BubbleTest extends ShellTestCase { when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor); + mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); } @Test @@ -144,22 +144,22 @@ public class BubbleTest extends ShellTestCase { } @Test - public void testSuppressionListener_change_notified() { + public void testBubbleMetadataFlagListener_change_notified() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(true); assertThat(mBubble.showInShade()).isFalse(); - verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble); + verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble); } @Test - public void testSuppressionListener_noChange_doesntNotify() { + public void testBubbleMetadataFlagListener_noChange_doesntNotify() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(false); - verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any()); + verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } } diff --git a/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml b/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml index 212ae528a6b9..42700b3ace07 100644 --- a/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml +++ b/packages/SettingsLib/FooterPreference/res/layout-v31/preference_footer.xml @@ -60,6 +60,7 @@ android:text="@string/settingslib_learn_more_text" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingBottom="8dp" android:clickable="true" android:visibility="gone" style="@style/TextAppearance.Footer.Title.SettingsLib"/> diff --git a/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml b/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml index d403f9ec8e45..1adceadc88d2 100644 --- a/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml +++ b/packages/SettingsLib/FooterPreference/res/layout/preference_footer.xml @@ -59,6 +59,7 @@ android:text="@string/settingslib_learn_more_text" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingBottom="8dp" android:clickable="true" android:visibility="gone" style="@style/TextAppearance.Footer.Title.SettingsLib"/> diff --git a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java index 20fe495f1afa..988055e7d8db 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java @@ -1,9 +1,13 @@ package com.android.settingslib.core; +import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.os.Build; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.RequiresApi; +import androidx.core.os.BuildCompat; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; @@ -16,9 +20,12 @@ public abstract class AbstractPreferenceController { private static final String TAG = "AbstractPrefController"; protected final Context mContext; + private final DevicePolicyManager mDevicePolicyManager; public AbstractPreferenceController(Context context) { mContext = context; + mDevicePolicyManager = + (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); } /** @@ -102,4 +109,40 @@ public abstract class AbstractPreferenceController { public CharSequence getSummary() { return null; } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + protected void replaceEnterpriseStringTitle(PreferenceScreen screen, + String preferenceKey, String overrideKey, int resource) { + if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) { + return; + } + + Preference preference = screen.findPreference(preferenceKey); + if (preference == null) { + Log.d(TAG, "Could not find enterprise preference " + preferenceKey); + return; + } + + preference.setTitle( + mDevicePolicyManager.getResources().getString(overrideKey, + () -> mContext.getString(resource))); + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + protected void replaceEnterpriseStringSummary( + PreferenceScreen screen, String preferenceKey, String overrideKey, int resource) { + if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) { + return; + } + + Preference preference = screen.findPreference(preferenceKey); + if (preference == null) { + Log.d(TAG, "Could not find enterprise preference " + preferenceKey); + return; + } + + preference.setSummary( + mDevicePolicyManager.getResources().getString(overrideKey, + () -> mContext.getString(resource))); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java index 1b63e3e837c7..1b0738fab266 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java @@ -16,12 +16,16 @@ package com.android.settingslib; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyResourcesManager; import android.content.Context; import android.view.View; import android.widget.TextView; @@ -30,7 +34,6 @@ import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -45,6 +48,10 @@ public class RestrictedPreferenceHelperTest { @Mock private Preference mPreference; @Mock + private DevicePolicyManager mDevicePolicyManager; + @Mock + private DevicePolicyResourcesManager mDevicePolicyResourcesManager; + @Mock private RestrictedTopLevelPreference mRestrictedTopLevelPreference; private PreferenceViewHolder mViewHolder; @@ -53,18 +60,22 @@ public class RestrictedPreferenceHelperTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(mDevicePolicyResourcesManager).when(mDevicePolicyManager) + .getResources(); + doReturn(mDevicePolicyManager).when(mContext) + .getSystemService(DevicePolicyManager.class); mViewHolder = PreferenceViewHolder.createInstanceForTests(mock(View.class)); mHelper = new RestrictedPreferenceHelper(mContext, mPreference, null); } @Test - @Ignore public void bindPreference_disabled_shouldDisplayDisabledSummary() { final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS); when(mViewHolder.itemView.findViewById(android.R.id.summary)) .thenReturn(summaryView); when(summaryView.getContext().getText(R.string.disabled_by_admin_summary_text)) .thenReturn("test"); + when(mDevicePolicyResourcesManager.getString(any(), any())).thenReturn("test"); mHelper.useAdminDisabledSummary(true); mHelper.setDisabledByAdmin(new RestrictedLockUtils.EnforcedAdmin()); @@ -75,13 +86,13 @@ public class RestrictedPreferenceHelperTest { } @Test - @Ignore public void bindPreference_notDisabled_shouldNotHideSummary() { final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS); when(mViewHolder.itemView.findViewById(android.R.id.summary)) .thenReturn(summaryView); when(summaryView.getContext().getText(R.string.disabled_by_admin_summary_text)) .thenReturn("test"); + when(mDevicePolicyResourcesManager.getString(any(), any())).thenReturn("test"); when(summaryView.getText()).thenReturn("test"); mHelper.useAdminDisabledSummary(true); diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml index fc12d418d218..2abc9e3d6119 100644 --- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml @@ -27,7 +27,7 @@ <dimen name="qqs_layout_padding_bottom">40dp</dimen> - <dimen name="notification_panel_margin_horizontal">96dp</dimen> + <dimen name="notification_panel_margin_horizontal">80dp</dimen> <dimen name="notification_side_paddings">40dp</dimen> <dimen name="notification_section_divider_height">16dp</dimen> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index e7f97d25fabe..58b6ad3e51e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -55,8 +55,6 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.drawable.IconCompat; -import androidx.mediarouter.media.MediaRouter; -import androidx.mediarouter.media.MediaRouterParams; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.Utils; @@ -215,9 +213,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } boolean shouldShowLaunchSection() { - MediaRouterParams routerParams = MediaRouter.getInstance(mContext).getRouterParams(); - Log.d(TAG, "try to get routerParams: " + routerParams); - return routerParams != null && !routerParams.isMediaTransferReceiverEnabled(); + // TODO(b/231398073): Implements this when available. + return false; } void setRefreshing(boolean refreshing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index ebd610bb0af4..0c9e1ec1ff77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -180,13 +180,6 @@ public interface NotificationShadeWindowController extends RemoteInputController default void setRequestTopUi(boolean requestTopUi, String componentTag) {} /** - * Under low light conditions, we might want to increase the display brightness on devices that - * don't have an IR camera. - * @param brightness float from 0 to 1 or {@code LayoutParams.BRIGHTNESS_OVERRIDE_NONE} - */ - default void setFaceAuthDisplayBrightness(float brightness) {} - - /** * If {@link LightRevealScrim} obscures the UI. * @param opaque if the scrim is opaque */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt index a390e9f9b09d..15ad312b413e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt @@ -20,11 +20,13 @@ import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.PeopleHeader +import com.android.systemui.statusbar.notification.icon.ConversationIconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.PeopleNotificationType import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON @@ -39,12 +41,40 @@ import javax.inject.Inject @CoordinatorScope class ConversationCoordinator @Inject constructor( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, + private val conversationIconManager: ConversationIconManager, @PeopleHeader peopleHeaderController: NodeController ) : Coordinator { + private val promotedEntriesToSummaryOfSameChannel = + mutableMapOf<NotificationEntry, NotificationEntry>() + + private val onBeforeRenderListListener = OnBeforeRenderListListener { _ -> + val unimportantSummaries = promotedEntriesToSummaryOfSameChannel + .mapNotNull { (promoted, summary) -> + val originalGroup = summary.parent + when { + originalGroup == null -> null + originalGroup == promoted.parent -> null + originalGroup.parent == null -> null + originalGroup.summary != summary -> null + originalGroup.children.any { it.channel == summary.channel } -> null + else -> summary.key + } + } + conversationIconManager.setUnimportantConversations(unimportantSummaries) + promotedEntriesToSummaryOfSameChannel.clear() + } + private val notificationPromoter = object : NotifPromoter(TAG) { override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean { - return entry.channel?.isImportantConversation == true + val shouldPromote = entry.channel?.isImportantConversation == true + if (shouldPromote) { + val summary = entry.parent?.summary + if (summary != null && entry.channel == summary.channel) { + promotedEntriesToSummaryOfSameChannel[entry] = summary + } + } + return shouldPromote } } @@ -67,6 +97,7 @@ class ConversationCoordinator @Inject constructor( override fun attach(pipeline: NotifPipeline) { pipeline.addPromoter(notificationPromoter) + pipeline.addOnBeforeRenderListListener(onBeforeRenderListListener) } private fun isConversation(entry: ListEntry): Boolean = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 386e2d31380c..f460644ce71c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.collection.render import android.annotation.MainThread import android.view.View -import com.android.systemui.util.kotlin.transform import com.android.systemui.util.traceSection /** @@ -41,7 +40,6 @@ class ShadeViewDiffer( ) { private val rootNode = ShadeNode(rootController) private val nodes = mutableMapOf(rootController to rootNode) - private val views = mutableMapOf<View, ShadeNode>() /** * Adds and removes views from the root (and its children) until their structure matches the @@ -66,26 +64,25 @@ class ShadeViewDiffer( * * For debugging purposes. */ - fun getViewLabel(view: View): String = views[view]?.label ?: view.toString() - - private fun detachChildren( - parentNode: ShadeNode, - specMap: Map<NodeController, NodeSpec> - ) { - val parentSpec = specMap[parentNode.controller] - - for (i in parentNode.getChildCount() - 1 downTo 0) { - val childView = parentNode.getChildAt(i) - views[childView]?.let { childNode -> - val childSpec = specMap[childNode.controller] - - maybeDetachChild(parentNode, parentSpec, childNode, childSpec) - - if (childNode.controller.getChildCount() > 0) { - detachChildren(childNode, specMap) + fun getViewLabel(view: View): String = + nodes.values.firstOrNull { node -> node.view === view }?.label ?: view.toString() + + private fun detachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + val views = nodes.values.associateBy { it.view } + fun detachRecursively(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + val parentSpec = specMap[parentNode.controller] + for (i in parentNode.getChildCount() - 1 downTo 0) { + val childView = parentNode.getChildAt(i) + views[childView]?.let { childNode -> + val childSpec = specMap[childNode.controller] + maybeDetachChild(parentNode, parentSpec, childNode, childSpec) + if (childNode.controller.getChildCount() > 0) { + detachRecursively(childNode, specMap) + } } } } + detachRecursively(parentNode, specMap) } private fun maybeDetachChild( @@ -94,14 +91,13 @@ class ShadeViewDiffer( childNode: ShadeNode, childSpec: NodeSpec? ) { - val newParentNode = transform(childSpec?.parent) { getNode(it) } + val newParentNode = childSpec?.parent?.let { getNode(it) } if (newParentNode != parentNode) { val childCompletelyRemoved = newParentNode == null if (childCompletelyRemoved) { nodes.remove(childNode.controller) - views.remove(childNode.controller.view) } logger.logDetachingChild( @@ -115,10 +111,7 @@ class ShadeViewDiffer( } } - private fun attachChildren( - parentNode: ShadeNode, - specMap: Map<NodeController, NodeSpec> - ) { + private fun attachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { val parentSpec = checkNotNull(specMap[parentNode.controller]) for ((index, childSpec) in parentSpec.children.withIndex()) { @@ -160,7 +153,6 @@ class ShadeViewDiffer( if (node == null) { node = ShadeNode(spec.controller) nodes[node.controller] = node - views[node.view] = node } return node } @@ -194,10 +186,9 @@ class ShadeViewDiffer( private class DuplicateNodeException(message: String) : RuntimeException(message) -private class ShadeNode( - val controller: NodeController -) { - val view = controller.view +private class ShadeNode(val controller: NodeController) { + val view: View + get() = controller.view var parent: ShadeNode? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 34c8044ef0d3..d96590a82547 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -70,6 +70,8 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.icon.ConversationIconManager; +import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.init.NotificationsControllerImpl; import com.android.systemui.statusbar.notification.init.NotificationsControllerStub; @@ -370,6 +372,10 @@ public interface NotificationsModule { /** */ @Binds + ConversationIconManager bindConversationIconManager(IconManager iconManager); + + /** */ + @Binds BindEventManager bindBindEventManagerImpl(BindEventManagerImpl bindEventManagerImpl); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 5375ac345e50..d8965418b4c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -27,6 +27,7 @@ import android.view.View import android.widget.ImageView import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -44,11 +45,14 @@ import javax.inject.Inject * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. * Long-term, it should probably live somewhere in the content inflation pipeline. */ +@SysUISingleton class IconManager @Inject constructor( private val notifCollection: CommonNotifCollection, private val launcherApps: LauncherApps, private val iconBuilder: IconBuilder -) { +) : ConversationIconManager { + private var unimportantConversationKeys: Set<String> = emptySet() + fun attach() { notifCollection.addCollectionListener(entryListener) } @@ -63,16 +67,8 @@ class IconManager @Inject constructor( } override fun onRankingApplied() { - // When the sensitivity changes OR when the isImportantConversation status changes, - // we need to update the icons - for (entry in notifCollection.allNotifs) { - val isImportant = isImportantConversation(entry) - if (entry.icons.areIconsAvailable && - isImportant != entry.icons.isImportantConversation) { - updateIconsSafe(entry) - } - entry.icons.isImportantConversation = isImportant - } + // rankings affect whether a conversation is important, which can change the icons + recalculateForImportantConversationChange() } } @@ -80,6 +76,18 @@ class IconManager @Inject constructor( entry -> updateIconsSafe(entry) } + private fun recalculateForImportantConversationChange() { + for (entry in notifCollection.allNotifs) { + val isImportant = isImportantConversation(entry) + if (entry.icons.areIconsAvailable && + isImportant != entry.icons.isImportantConversation + ) { + updateIconsSafe(entry) + } + entry.icons.isImportantConversation = isImportant + } + } + /** * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the * result in [NotificationEntry.getIcons]. @@ -306,8 +314,28 @@ class IconManager @Inject constructor( } private fun isImportantConversation(entry: NotificationEntry): Boolean { - return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation + return entry.ranking.channel != null && + entry.ranking.channel.isImportantConversation && + entry.key !in unimportantConversationKeys + } + + override fun setUnimportantConversations(keys: Collection<String>) { + val newKeys = keys.toSet() + val changed = unimportantConversationKeys != newKeys + unimportantConversationKeys = newKeys + if (changed) { + recalculateForImportantConversationChange() + } } } -private const val TAG = "IconManager"
\ No newline at end of file +private const val TAG = "IconManager" + +interface ConversationIconManager { + /** + * Sets the complete current set of notification keys which should (for the purposes of icon + * presentation) be considered unimportant. This tells the icon manager to remove the avatar + * of a group from which the priority notification has been removed. + */ + fun setUnimportantConversations(keys: Collection<String>) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index 5646545dcd23..0ff152380fb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -164,12 +164,23 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( !lockscreenUserManager.shouldShowLockscreenNotifications() -> true // User settings do not allow this notification on the lockscreen, so hide it. userSettingsDisallowNotification(entry) -> true + // if entry is silent, apply custom logic to see if should hide + shouldHideIfEntrySilent(entry) -> true + else -> false + } + + private fun shouldHideIfEntrySilent(entry: ListEntry): Boolean = when { + // Show if high priority (not hidden) + highPriorityProvider.isHighPriority(entry) -> false + // Ambient notifications are hidden always from lock screen + entry.representativeEntry?.isAmbient == true -> true + // [Now notification is silent] + // Hide regardless of parent priority if user wants silent notifs hidden + hideSilentNotificationsOnLockscreen -> true // Parent priority is high enough to be shown on the lockscreen, do not hide. - entry.parent?.let(::priorityExceedsLockscreenShowingThreshold) == true -> false - // Entry priority is high enough to be shown on the lockscreen, do not hide. - priorityExceedsLockscreenShowingThreshold(entry) -> false - // Priority is too low, hide. - else -> true + entry.parent?.let(::shouldHideIfEntrySilent) == false -> false + // Show when silent notifications are allowed on lockscreen + else -> false } private fun userSettingsDisallowNotification(entry: NotificationEntry): Boolean { @@ -193,11 +204,6 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( } } - private fun priorityExceedsLockscreenShowingThreshold(entry: ListEntry): Boolean = when { - hideSilentNotificationsOnLockscreen -> highPriorityProvider.isHighPriority(entry) - else -> entry.representativeEntry?.ranking?.isAmbient == false - } - private fun readShowSilentNotificationSetting() { val showSilentNotifs = secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 36cd173d2d1c..3ea5e5b753a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -70,6 +70,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.SystemBarUtils; +import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.keyguard.KeyguardSliceView; import com.android.settingslib.Utils; import com.android.systemui.Dependency; @@ -1313,7 +1314,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable final float endTopPosition = mTopPadding + mExtraTopInsetForFullShadeTransition + mAmbientState.getOverExpansion() - getCurrentOverScrollAmount(false /* top */); - final float fraction = mAmbientState.getExpansionFraction(); + float fraction = mAmbientState.getExpansionFraction(); + if (mAmbientState.isBouncerInTransit()) { + fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction); + } final float stackY = MathUtils.lerp(0, endTopPosition, fraction); mAmbientState.setStackY(stackY); if (mOnStackYChanged != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java index 24660b261c51..01aa2ec9bfe6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java @@ -111,7 +111,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final SysuiColorExtractor mColorExtractor; private final ScreenOffAnimationController mScreenOffAnimationController; - private float mFaceAuthDisplayBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; /** * Layout params would be aggregated and dispatched all at once if this is > 0. * @@ -266,12 +265,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mScreenBrightnessDoze = value / 255f; } - @Override - public void setFaceAuthDisplayBrightness(float brightness) { - mFaceAuthDisplayBrightness = brightness; - apply(mCurrentState); - } - private void setKeyguardDark(boolean dark) { int vis = mNotificationShadeView.getSystemUiVisibility(); if (dark) { @@ -455,7 +448,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private void applyWindowLayoutParams() { if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) { + Trace.beginSection("updateViewLayout"); mWindowManager.updateViewLayout(mNotificationShadeView, mLp); + Trace.endSection(); } } @@ -523,7 +518,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW if (state.mForceDozeBrightness) { mLpChanged.screenBrightness = mScreenBrightnessDoze; } else { - mLpChanged.screenBrightness = mFaceAuthDisplayBrightness; + mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; } } @@ -572,6 +567,10 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setPanelVisible(boolean visible) { + if (mCurrentState.mPanelVisible == visible + && mCurrentState.mNotificationShadeFocusable == visible) { + return; + } mCurrentState.mPanelVisible = visible; mCurrentState.mNotificationShadeFocusable = visible; apply(mCurrentState); @@ -626,8 +625,14 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setScrimsVisibility(int scrimsVisibility) { + if (scrimsVisibility == mCurrentState.mScrimsVisibility) { + return; + } + boolean wasExpanded = isExpanded(mCurrentState); mCurrentState.mScrimsVisibility = scrimsVisibility; - apply(mCurrentState); + if (wasExpanded != isExpanded(mCurrentState)) { + apply(mCurrentState); + } mScrimsVisibilityListener.accept(scrimsVisibility); } @@ -687,6 +692,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setPanelExpanded(boolean isExpanded) { + if (mCurrentState.mPanelExpanded == isExpanded) { + return; + } mCurrentState.mPanelExpanded = isExpanded; apply(mCurrentState); } @@ -703,6 +711,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setForceDozeBrightness(boolean forceDozeBrightness) { + if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) { + return; + } mCurrentState.mForceDozeBrightness = forceDozeBrightness; apply(mCurrentState); } @@ -841,7 +852,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW boolean mLightRevealScrimOpaque; boolean mForceCollapsed; boolean mForceDozeBrightness; - int mFaceAuthDisplayBrightness; boolean mForceUserActivity; boolean mLaunchingActivity; boolean mBackdropShowing; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 935f87dc8221..c1d0769eaa44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -175,28 +175,36 @@ class UnlockedScreenOffAnimationController @Inject constructor( .setDuration(duration.toLong()) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - aodUiAnimationPlaying = false + .withEndAction { + aodUiAnimationPlaying = false + + // Lock the keyguard if it was waiting for the screen off animation to end. + keyguardViewMediatorLazy.get().maybeHandlePendingLock() - // Lock the keyguard if it was waiting for the screen off animation to end. - keyguardViewMediatorLazy.get().maybeHandlePendingLock() + // Tell the CentralSurfaces to become keyguard for real - we waited on that + // since it is slow and would have caused the animation to jank. + mCentralSurfaces.updateIsKeyguard() - // Tell the CentralSurfaces to become keyguard for real - we waited on that - // since it is slow and would have caused the animation to jank. - mCentralSurfaces.updateIsKeyguard() + // Run the callback given to us by the KeyguardVisibilityHelper. + after.run() - // Run the callback given to us by the KeyguardVisibilityHelper. - after.run() + // Done going to sleep, reset this flag. + decidedToAnimateGoingToSleep = null - // Done going to sleep, reset this flag. + // We need to unset the listener. These are persistent for future animators + keyguardView.animate().setListener(null) + interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) + } + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator?) { + // If we're cancelled, reset state flags/listeners. The end action above + // will not be called, which is what we want since that will finish the + // screen off animation and show the lockscreen, which we don't want if we + // were cancelled. + aodUiAnimationPlaying = false decidedToAnimateGoingToSleep = null - // We need to unset the listener. These are persistent for future animators keyguardView.animate().setListener(null) - interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) - } - override fun onAnimationCancel(animation: Animator?) { interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java index f26176c15451..f8c36dcc90a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java @@ -86,7 +86,6 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio private boolean mShouldDisplayAllAccesses; private boolean mShowSystemAccessesFlag; private boolean mShowSystemAccessesSetting; - private boolean mExperimentStarted; @Inject public LocationControllerImpl(Context context, AppOpsController appOpsController, @@ -108,9 +107,6 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio mShouldDisplayAllAccesses = getAllAccessesSetting(); mShowSystemAccessesFlag = getShowSystemFlag(); mShowSystemAccessesSetting = getShowSystemSetting(); - mExperimentStarted = getExperimentStarted(); - toggleSystemSettingIfExperimentJustStarted(); - mContentObserver = new ContentObserver(mBackgroundHandler) { @Override public void onChange(boolean selfChange) { @@ -127,15 +123,8 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio DeviceConfig.NAMESPACE_PRIVACY, backgroundHandler::post, properties -> { - // Update the Device Config flag which controls the experiment to display - // location accesses. mShouldDisplayAllAccesses = getAllAccessesSetting(); - // Update the Device Config flag which controls the experiment to display - // system location accesses. - mShowSystemAccessesFlag = getShowSystemFlag(); - // Update the local flag for the system accesses experiment and potentially - // update the behavior based on the flag value. - toggleSystemSettingIfExperimentJustStarted(); + mShowSystemAccessesFlag = getShowSystemSetting(); updateActiveLocationRequests(); }); @@ -234,33 +223,6 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio UserHandle.USER_CURRENT) == 1; } - private boolean getExperimentStarted() { - return mSecureSettings - .getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 0) == 1; - } - - private void toggleSystemSettingIfExperimentJustStarted() { - // mShowSystemAccessesFlag indicates whether the Device Config flag is flipped - // by an experiment. mExperimentStarted is the local device value which indicates the last - // value the device has seen for the Device Config flag. - // The local device value is needed to determine that the Device Config flag was just - // flipped, as the experiment behavior should only happen once after the experiment is - // enabled. - if (mShowSystemAccessesFlag && !mExperimentStarted) { - // If the Device Config flag is enabled, but the local device setting is not then the - // experiment just started. Update the local flag to match and enable the experiment - // behavior by flipping the show system setting value. - mSecureSettings.putInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 1); - mSecureSettings.putInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 1); - mExperimentStarted = true; - } else if (!mShowSystemAccessesFlag && mExperimentStarted) { - // If the Device Config flag is disabled, but the local device flag is enabled then - // update the local device flag to match. - mSecureSettings.putInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED, 0); - mExperimentStarted = false; - } - } - /** * Returns true if there currently exist active high power location requests. */ @@ -288,6 +250,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio } boolean hadActiveLocationRequests = mAreActiveLocationRequests; boolean shouldDisplay = false; + boolean showSystem = mShowSystemAccessesFlag || mShowSystemAccessesSetting; boolean systemAppOp = false; boolean nonSystemAppOp = false; boolean isSystemApp; @@ -305,7 +268,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio nonSystemAppOp = true; } - shouldDisplay = mShowSystemAccessesSetting || shouldDisplay || !isSystemApp; + shouldDisplay = showSystem || shouldDisplay || !isSystemApp; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 37517219f103..5a33603d81ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -269,8 +269,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene super.onEnd(animation); if (animation.getTypeMask() == WindowInsets.Type.ime()) { mEntry.mRemoteEditImeAnimatingAway = false; - mEntry.mRemoteEditImeVisible = - mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()); + WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets(); + if (editTextRootWindowInsets == null) { + Log.w(TAG, "onEnd called on detached view", new Exception()); + } + mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null + && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime()); if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { mController.removeRemoteInput(mEntry, mToken); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index b45d78d5502d..4b458f5a9123 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -88,4 +88,7 @@ public class GroupEntryBuilder { return this; } + public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) { + return groupEntry.getRawChildren(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt index 7692a05eb5fc..742fcf5e03c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt @@ -21,17 +21,22 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController +import com.android.systemui.statusbar.notification.icon.ConversationIconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertFalse @@ -52,8 +57,10 @@ class ConversationCoordinatorTest : SysuiTestCase() { private lateinit var promoter: NotifPromoter private lateinit var peopleSectioner: NotifSectioner private lateinit var peopleComparator: NotifComparator + private lateinit var beforeRenderListListener: OnBeforeRenderListListener @Mock private lateinit var pipeline: NotifPipeline + @Mock private lateinit var conversationIconManager: ConversationIconManager @Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier @Mock private lateinit var channel: NotificationChannel @Mock private lateinit var headerController: NodeController @@ -66,7 +73,11 @@ class ConversationCoordinatorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - coordinator = ConversationCoordinator(peopleNotificationIdentifier, headerController) + coordinator = ConversationCoordinator( + peopleNotificationIdentifier, + conversationIconManager, + headerController + ) whenever(channel.isImportantConversation).thenReturn(true) coordinator.attach(pipeline) @@ -75,6 +86,9 @@ class ConversationCoordinatorTest : SysuiTestCase() { promoter = withArgCaptor { verify(pipeline).addPromoter(capture()) } + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } peopleSectioner = coordinator.sectioner peopleComparator = peopleSectioner.comparator!! @@ -96,6 +110,25 @@ class ConversationCoordinatorTest : SysuiTestCase() { } @Test + fun testPromotedImportantConversationsMakesSummaryUnimportant() { + val altChildA = NotificationEntryBuilder().setTag("A").build() + val altChildB = NotificationEntryBuilder().setTag("B").build() + val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build() + val groupEntry = GroupEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .setSummary(summary) + .setChildren(listOf(entry, altChildA, altChildB)) + .build() + assertTrue(promoter.shouldPromoteToTopLevel(entry)) + assertFalse(promoter.shouldPromoteToTopLevel(altChildA)) + assertFalse(promoter.shouldPromoteToTopLevel(altChildB)) + NotificationEntryBuilder.setNewParent(entry, GroupEntry.ROOT_ENTRY) + GroupEntryBuilder.getRawChildren(groupEntry).remove(entry) + beforeRenderListListener.onBeforeRenderList(listOf(entry, groupEntry)) + verify(conversationIconManager).setUnimportantConversations(eq(listOf(summary.key))) + } + + @Test fun testInPeopleSection() { whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry)) .thenReturn(TYPE_PERSON) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java index cf996073f6a0..ed455a349bdc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java @@ -27,6 +27,7 @@ import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -228,6 +229,41 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test + public void hideSilentNotificationsPerUserSettingWithHighPriorityParent() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); + GroupEntry parent = new GroupEntryBuilder() + .setKey("parent") + .addChild(mEntry) + .setSummary(new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .build()) + .build(); + mEntry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .setParent(parent) + .build(); + when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void hideSilentNotificationsPerUserSetting() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); + mEntry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .build(); + when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test public void notifyListeners_onSettingChange_zenMode() { when(mKeyguardStateController.isShowing()).thenReturn(true); Consumer<String> listener = mock(Consumer.class); @@ -384,8 +420,8 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); when(mHighPriorityProvider.isHighPriority(parent)).thenReturn(true); - // THEN don't filter out the entry - assertFalse( + // THEN filter out the entry regardless of parent + assertTrue( mKeyguardNotificationVisibilityProvider.shouldHideNotification(entryWithParent)); // WHEN its parent doesn't exceed threshold to show on lockscreen diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java index ddccd834f76e..26199d53a2b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.IActivityManager; @@ -103,6 +104,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); + verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any()); } @Test @@ -174,6 +176,14 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void setScrimsVisibility_earlyReturn() { + clearInvocations(mWindowManager); + mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT); + // Abort early if value didn't change + verify(mWindowManager, never()).updateViewLayout(any(), mLayoutParameters.capture()); + } + + @Test public void attach_animatingKeyguardAndSurface_wallpaperVisible() { clearInvocations(mWindowManager); when(mKeyguardViewMediator.isShowingAndNotOccluded()).thenReturn(true); @@ -221,6 +231,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() { mNotificationShadeWindowController.setPanelExpanded(true); clearInvocations(mWindowManager); + mNotificationShadeWindowController.setPanelExpanded(true); + verifyNoMoreInteractions(mWindowManager); mNotificationShadeWindowController.setNotificationShadeFocusable(true); verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); @@ -287,6 +299,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void batchApplyWindowLayoutParams_doesNotDispatchEvents() { mNotificationShadeWindowController.setForceDozeBrightness(true); verify(mWindowManager).updateViewLayout(any(), any()); + mNotificationShadeWindowController.setForceDozeBrightness(true); + verifyNoMoreInteractions(mWindowManager); clearInvocations(mWindowManager); mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index 0936b773d4b3..011279721fd2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt @@ -117,11 +117,18 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { val keyguardSpy = spy(keyguardView) Mockito.`when`(keyguardSpy.animate()).thenReturn(animator) val listener = ArgumentCaptor.forClass(Animator.AnimatorListener::class.java) + val endAction = ArgumentCaptor.forClass(Runnable::class.java) controller.animateInKeyguard(keyguardSpy, Runnable {}) Mockito.verify(animator).setListener(listener.capture()) - // Verify that the listener is cleared when it ends - listener.value.onAnimationEnd(null) + Mockito.verify(animator).withEndAction(endAction.capture()) + + // Verify that the listener is cleared if we cancel it. + listener.value.onAnimationCancel(null) Mockito.verify(animator).setListener(null) + + // Verify that the listener is also cleared if the end action is triggered. + endAction.value.run() + verify(animator, times(2)).setListener(null) } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java index 14e087846417..9c7dc6b83059 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/LocationControllerImplTest.java @@ -365,46 +365,4 @@ public class LocationControllerImplTest extends SysuiTestCase { // No new callbacks verify(callback).onLocationSettingsChanged(anyBoolean()); } - - @Test - public void testExperimentFlipsSystemFlag() throws Exception { - mSecureSettings.putInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0); - mDeviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SMALL_ENABLED, - "true", - true); - // Show system experiment not running - mDeviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM, - "false", - false); - mTestableLooper.processAllMessages(); - - // Flip experiment on - mDeviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM, - "true", - true); - mTestableLooper.processAllMessages(); - - // Verify settings were flipped - assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_SHOW_SYSTEM_OPS)) - .isEqualTo(1); - assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED)) - .isEqualTo(1); - - // Flip experiment off - mDeviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_SHOW_SYSTEM, - "false", - false); - mTestableLooper.processAllMessages(); - - assertThat(mSecureSettings.getInt(Settings.Secure.LOCATION_INDICATOR_EXPERIMENT_STARTED)) - .isEqualTo(0); - } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 193879e5c55c..238a4d37a872 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -57,6 +57,7 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; @@ -70,6 +71,8 @@ import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import android.view.WindowManager; @@ -147,6 +150,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -1010,7 +1014,7 @@ public class BubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1027,7 +1031,7 @@ public class BubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1447,6 +1451,69 @@ public class BubblesTest extends SysuiTestCase { assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); } + @Test + public void testSetShouldAutoExpand_notifiesFlagChanged() { + mEntryListener.onPendingEntryAdded(mRow); + + assertTrue(mBubbleController.hasBubbles()); + Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + + // Set it to the same thing + b.setShouldAutoExpand(false); + + // Verify it doesn't notify + verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any()); + + // Set it to something different + b.setShouldAutoExpand(true); + verify(mBubbleController).onBubbleMetadataFlagChanged(b); + } + + @Test + public void testUpdateBubble_skipsDndSuppressListNotifs() { + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + mBubbleEntry.getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + + mBubbleController.updateBubble(mBubbleEntry); + + Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull(); + } + + @Test + public void testOnRankingUpdate_DndSuppressListNotif() { + // It's in the stack + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue(); + + // Set current user profile + SparseArray<UserInfo> userInfos = new SparseArray<>(); + userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(), + mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + + // Send ranking update that the notif is suppressed from the list. + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>(); + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true); + entryDataByKey.put(mBubbleEntry.getKey(), pair); + + NotificationListenerService.RankingMap rankingMap = + mock(NotificationListenerService.RankingMap.class); + when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() }); + mBubbleController.onRankingUpdated(rankingMap, entryDataByKey); + + // Should no longer be in the stack + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java index 02d869172030..dff89e0a5558 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java @@ -50,6 +50,7 @@ import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; +import android.content.pm.UserInfo; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Handler; import android.os.PowerManager; @@ -59,6 +60,8 @@ import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import android.view.WindowManager; @@ -128,6 +131,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -880,7 +884,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -897,7 +901,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1267,6 +1271,69 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); } + @Test + public void testSetShouldAutoExpand_notifiesFlagChanged() { + mBubbleController.updateBubble(mBubbleEntry); + + assertTrue(mBubbleController.hasBubbles()); + Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + + // Set it to the same thing + b.setShouldAutoExpand(false); + + // Verify it doesn't notify + verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any()); + + // Set it to something different + b.setShouldAutoExpand(true); + verify(mBubbleController).onBubbleMetadataFlagChanged(b); + } + + @Test + public void testUpdateBubble_skipsDndSuppressListNotifs() { + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + mBubbleEntry.getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + + mBubbleController.updateBubble(mBubbleEntry); + + Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull(); + } + + @Test + public void testOnRankingUpdate_DndSuppressListNotif() { + // It's in the stack + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue(); + + // Set current user profile + SparseArray<UserInfo> userInfos = new SparseArray<>(); + userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(), + mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + + // Send ranking update that the notif is suppressed from the list. + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>(); + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true); + entryDataByKey.put(mBubbleEntry.getKey(), pair); + + NotificationListenerService.RankingMap rankingMap = + mock(NotificationListenerService.RankingMap.class); + when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() }); + mBubbleController.onRankingUpdated(rankingMap, entryDataByKey); + + // Should no longer be in the stack + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); + } + /** * Sets the bubble metadata flags for this entry. These flags are normally set by * NotificationManagerService when the notification is sent, however, these tests do not diff --git a/services/core/java/com/android/server/am/ErrorDialogController.java b/services/core/java/com/android/server/am/ErrorDialogController.java index a4e8f92a7480..82f35adbb134 100644 --- a/services/core/java/com/android/server/am/ErrorDialogController.java +++ b/services/core/java/com/android/server/am/ErrorDialogController.java @@ -144,7 +144,8 @@ final class ErrorDialogController { if (mWaitDialog == null) { return; } - mWaitDialog.dismiss(); + final BaseErrorDialog dialog = mWaitDialog; + mService.mUiHandler.post(dialog::dismiss); mWaitDialog = null; } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 1f3fbff34536..aed63ce5b2c6 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -9135,6 +9135,8 @@ public class AudioService extends IAudioService.Stub if (timeOutMs <= 0 || usages.length == 0) { throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute"); } + Log.i(TAG, "muteAwaitConnection dev:" + device + " timeOutMs:" + timeOutMs + + " usages:" + usages); if (mDeviceBroker.isDeviceConnected(device)) { // not throwing an exception as there could be a race between a connection (server-side, @@ -9178,7 +9180,7 @@ public class AudioService extends IAudioService.Stub Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device"); return; } - if (!device.equals(mMutingExpectedDevice)) { + if (!device.equalTypeAddress(mMutingExpectedDevice)) { Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device + "] but expected device is" + mMutingExpectedDevice); throw new IllegalStateException("cancelMuteAwaitConnection for wrong device"); diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 74c899980d86..565783f91f1b 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -1158,6 +1158,8 @@ public final class PlaybackActivityMonitor //========================================================================================== void muteAwaitConnection(@NonNull int[] usagesToMute, @NonNull AudioDeviceAttributes dev, long timeOutMs) { + sEventLogger.loglogi( + "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs, TAG); synchronized (mPlayerLock) { mutePlayersExpectingDevice(usagesToMute); // schedule timeout (remove previously scheduled first) @@ -1169,6 +1171,7 @@ public final class PlaybackActivityMonitor } void cancelMuteAwaitConnection() { + sEventLogger.loglogi("cancelMuteAwaitConnection()", TAG); synchronized (mPlayerLock) { // cancel scheduled timeout, ignore device, only one expected device at a time mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION); diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java index 79088d0398d2..f9a84077817d 100644 --- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java +++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java @@ -16,26 +16,28 @@ package com.android.server.logcat; +import android.annotation.StyleRes; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; -import android.os.logcat.ILogcatManagerService; import android.util.Slog; +import android.view.ContextThemeWrapper; import android.view.InflateException; +import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.android.internal.R; +import com.android.server.LocalServices; /** * Dialog responsible for obtaining user consent per-use log access @@ -43,61 +45,61 @@ import com.android.internal.R; public class LogAccessDialogActivity extends Activity implements View.OnClickListener { private static final String TAG = LogAccessDialogActivity.class.getSimpleName(); - private Context mContext; - private final ILogcatManagerService mLogcatManagerService = - ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat")); + private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; + private static final int MSG_DISMISS_DIALOG = 0; - private String mPackageName; + private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal = + LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class); + private String mPackageName; private int mUid; - private int mGid; - private int mPid; - private int mFd; + private String mAlertTitle; private AlertDialog.Builder mAlertDialog; private AlertDialog mAlert; private View mAlertView; - private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000; - private static final int MSG_DISMISS_DIALOG = 0; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - try { - mContext = this; - - // retrieve Intent extra information - Intent intent = getIntent(); - getIntentInfo(intent); - - // retrieve the title string from passed intent extra - mAlertTitle = getTitleString(mContext, mPackageName, mUid); - - // creaet View - mAlertView = createView(); - - // create AlertDialog - mAlertDialog = new AlertDialog.Builder(this); - mAlertDialog.setView(mAlertView); - - // show Alert - mAlert = mAlertDialog.create(); - mAlert.show(); - - // set Alert Timeout - mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT); + // retrieve Intent extra information + if (!readIntentInfo(getIntent())) { + Slog.e(TAG, "Invalid Intent extras, finishing"); + finish(); + return; + } - } catch (Exception e) { - try { - Slog.e(TAG, "onCreate failed, declining the logd access", e); - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException ex) { - Slog.e(TAG, "Fails to call remote functions", ex); - } + // retrieve the title string from passed intent extra + try { + mAlertTitle = getTitleString(this, mPackageName, mUid); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e); + declineLogAccess(); + finish(); + return; } + + // create View + boolean isDarkTheme = (getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + int themeId = isDarkTheme ? android.R.style.Theme_DeviceDefault_Dialog_Alert : + android.R.style.Theme_DeviceDefault_Light_Dialog_Alert; + mAlertView = createView(themeId); + + // create AlertDialog + mAlertDialog = new AlertDialog.Builder(this, themeId); + mAlertDialog.setView(mAlertView); + mAlertDialog.setOnCancelListener(dialog -> declineLogAccess()); + mAlertDialog.setOnDismissListener(dialog -> finish()); + + // show Alert + mAlert = mAlertDialog.create(); + mAlert.show(); + + // set Alert Timeout + mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT); } @Override @@ -109,21 +111,26 @@ public class LogAccessDialogActivity extends Activity implements mAlert = null; } - private void getIntentInfo(Intent intent) throws Exception { - + private boolean readIntentInfo(Intent intent) { if (intent == null) { - throw new NullPointerException("Intent is null"); + Slog.e(TAG, "Intent is null"); + return false; } mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); if (mPackageName == null || mPackageName.length() == 0) { - throw new NullPointerException("Package Name is null"); + Slog.e(TAG, "Missing package name extra"); + return false; + } + + if (!intent.hasExtra(Intent.EXTRA_UID)) { + Slog.e(TAG, "Missing EXTRA_UID"); + return false; } - mUid = intent.getIntExtra("com.android.server.logcat.uid", 0); - mGid = intent.getIntExtra("com.android.server.logcat.gid", 0); - mPid = intent.getIntExtra("com.android.server.logcat.pid", 0); - mFd = intent.getIntExtra("com.android.server.logcat.fd", 0); + mUid = intent.getIntExtra(Intent.EXTRA_UID, 0); + + return true; } private Handler mHandler = new Handler() { @@ -133,11 +140,7 @@ public class LogAccessDialogActivity extends Activity implements if (mAlert != null) { mAlert.dismiss(); mAlert = null; - try { - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + declineLogAccess(); } break; @@ -148,25 +151,15 @@ public class LogAccessDialogActivity extends Activity implements }; private String getTitleString(Context context, String callingPackage, int uid) - throws Exception { - + throws NameNotFoundException { PackageManager pm = context.getPackageManager(); - if (pm == null) { - throw new NullPointerException("PackageManager is null"); - } CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage, PackageManager.MATCH_DIRECT_BOOT_AUTO, UserHandle.getUserId(uid)).loadLabel(pm); - if (appLabel == null || appLabel.length() == 0) { - throw new NameNotFoundException("Application Label is null"); - } String titleString = context.getString( com.android.internal.R.string.log_access_confirmation_title, appLabel); - if (titleString == null || titleString.length() == 0) { - throw new NullPointerException("Title is null"); - } return titleString; } @@ -176,9 +169,9 @@ public class LogAccessDialogActivity extends Activity implements * If we cannot retrieve the package name, it returns null and we decline the full device log * access */ - private View createView() throws Exception { - - final View view = getLayoutInflater().inflate( + private View createView(@StyleRes int themeId) { + Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId); + final View view = LayoutInflater.from(themedContext).inflate( R.layout.log_access_user_consent_dialog_permission, null /*root*/); if (view == null) { @@ -202,21 +195,17 @@ public class LogAccessDialogActivity extends Activity implements public void onClick(View view) { switch (view.getId()) { case R.id.log_access_dialog_allow_button: - try { - mLogcatManagerService.approve(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName); finish(); break; case R.id.log_access_dialog_deny_button: - try { - mLogcatManagerService.decline(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } + declineLogAccess(); finish(); break; } } + + private void declineLogAccess() { + mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName); + } } diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java index 5dccd071e250..21beb964529a 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -16,103 +16,332 @@ package com.android.server.logcat; +import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; import android.os.ILogd; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserHandle; import android.os.logcat.ILogcatManagerService; +import android.util.ArrayMap; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.SystemService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; /** * Service responsible for managing the access to Logcat. */ public final class LogcatManagerService extends SystemService { - private static final String TAG = "LogcatManagerService"; + private static final boolean DEBUG = false; + + /** How long to wait for the user to approve/decline before declining automatically */ + @VisibleForTesting + static final int PENDING_CONFIRMATION_TIMEOUT_MILLIS = Build.IS_DEBUGGABLE ? 70000 : 400000; + + /** + * How long an approved / declined status is valid for. + * + * After a client has been approved/declined log access, if they try to access logs again within + * this timeout, the new request will be automatically approved/declined. + * Only after this timeout expires will a new request generate another prompt to the user. + **/ + @VisibleForTesting + static final int STATUS_EXPIRATION_TIMEOUT_MILLIS = 60 * 1000; + + private static final int MSG_LOG_ACCESS_REQUESTED = 0; + private static final int MSG_APPROVE_LOG_ACCESS = 1; + private static final int MSG_DECLINE_LOG_ACCESS = 2; + private static final int MSG_LOG_ACCESS_FINISHED = 3; + private static final int MSG_PENDING_TIMEOUT = 4; + private static final int MSG_LOG_ACCESS_STATUS_EXPIRED = 5; + + private static final int STATUS_NEW_REQUEST = 0; + private static final int STATUS_PENDING = 1; + private static final int STATUS_APPROVED = 2; + private static final int STATUS_DECLINED = 3; + + @IntDef(prefix = {"STATUS_"}, value = { + STATUS_NEW_REQUEST, + STATUS_PENDING, + STATUS_APPROVED, + STATUS_DECLINED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface LogAccessRequestStatus { + } + private final Context mContext; + private final Injector mInjector; + private final Supplier<Long> mClock; private final BinderService mBinderService; - private final ExecutorService mThreadExecutor; - private ILogd mLogdService; - private @NonNull ActivityManager mActivityManager; + private final LogcatManagerServiceInternal mLocalService; + private final Handler mHandler; private ActivityManagerInternal mActivityManagerInternal; - private static final int MAX_UID_IMPORTANCE_COUNT_LISTENER = 2; - private static final String TARGET_PACKAGE_NAME = "android"; - private static final String TARGET_ACTIVITY_NAME = - "com.android.server.logcat.LogAccessDialogActivity"; - private static final String EXTRA_UID = "com.android.server.logcat.uid"; - private static final String EXTRA_GID = "com.android.server.logcat.gid"; - private static final String EXTRA_PID = "com.android.server.logcat.pid"; - private static final String EXTRA_FD = "com.android.server.logcat.fd"; + private ILogd mLogdService; + + private static final class LogAccessClient { + final int mUid; + @NonNull + final String mPackageName; + + LogAccessClient(int uid, @NonNull String packageName) { + mUid = uid; + mPackageName = packageName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LogAccessClient)) return false; + LogAccessClient that = (LogAccessClient) o; + return mUid == that.mUid && Objects.equals(mPackageName, that.mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mPackageName); + } + + @Override + public String toString() { + return "LogAccessClient{" + + "mUid=" + mUid + + ", mPackageName=" + mPackageName + + '}'; + } + } + + private static final class LogAccessRequest { + final int mUid; + final int mGid; + final int mPid; + final int mFd; + + private LogAccessRequest(int uid, int gid, int pid, int fd) { + mUid = uid; + mGid = gid; + mPid = pid; + mFd = fd; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LogAccessRequest)) return false; + LogAccessRequest that = (LogAccessRequest) o; + return mUid == that.mUid && mGid == that.mGid && mPid == that.mPid && mFd == that.mFd; + } + + @Override + public int hashCode() { + return Objects.hash(mUid, mGid, mPid, mFd); + } + + @Override + public String toString() { + return "LogAccessRequest{" + + "mUid=" + mUid + + ", mGid=" + mGid + + ", mPid=" + mPid + + ", mFd=" + mFd + + '}'; + } + } + + private static final class LogAccessStatus { + @LogAccessRequestStatus + int mStatus = STATUS_NEW_REQUEST; + final List<LogAccessRequest> mPendingRequests = new ArrayList<>(); + } + + private final Map<LogAccessClient, LogAccessStatus> mLogAccessStatus = new ArrayMap<>(); + private final Map<LogAccessClient, Integer> mActiveLogAccessCount = new ArrayMap<>(); private final class BinderService extends ILogcatManagerService.Stub { @Override public void startThread(int uid, int gid, int pid, int fd) { - mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, true)); + final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd); + if (DEBUG) { + Slog.d(TAG, "New log access request: " + logAccessRequest); + } + final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_REQUESTED, logAccessRequest); + mHandler.sendMessageAtTime(msg, mClock.get()); } @Override public void finishThread(int uid, int gid, int pid, int fd) { - // TODO This thread will be used to notify the AppOpsManager that - // the logd data access is finished. - mThreadExecutor.execute(new LogdMonitor(uid, gid, pid, fd, false)); + final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd); + if (DEBUG) { + Slog.d(TAG, "Log access finished: " + logAccessRequest); + } + final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_FINISHED, logAccessRequest); + mHandler.sendMessageAtTime(msg, mClock.get()); } + } - @Override - public void approve(int uid, int gid, int pid, int fd) { - try { - Slog.d(TAG, "Allow logd access for uid: " + uid); - getLogdService().approve(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); + final class LogcatManagerServiceInternal { + public void approveAccessForClient(int uid, @NonNull String packageName) { + final LogAccessClient client = new LogAccessClient(uid, packageName); + if (DEBUG) { + Slog.d(TAG, "Approving log access for client: " + client); } + final Message msg = mHandler.obtainMessage(MSG_APPROVE_LOG_ACCESS, client); + mHandler.sendMessageAtTime(msg, mClock.get()); } - @Override - public void decline(int uid, int gid, int pid, int fd) { - try { - Slog.d(TAG, "Decline logd access for uid: " + uid); - getLogdService().decline(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); + public void declineAccessForClient(int uid, @NonNull String packageName) { + final LogAccessClient client = new LogAccessClient(uid, packageName); + if (DEBUG) { + Slog.d(TAG, "Declining log access for client: " + client); } + final Message msg = mHandler.obtainMessage(MSG_DECLINE_LOG_ACCESS, client); + mHandler.sendMessageAtTime(msg, mClock.get()); } } private ILogd getLogdService() { - synchronized (LogcatManagerService.this) { - if (mLogdService == null) { - LogcatManagerService.this.addLogdService(); + if (mLogdService == null) { + mLogdService = mInjector.getLogdService(); + } + return mLogdService; + } + + private static class LogAccessRequestHandler extends Handler { + private final LogcatManagerService mService; + + LogAccessRequestHandler(Looper looper, LogcatManagerService service) { + super(looper); + mService = service; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOG_ACCESS_REQUESTED: { + LogAccessRequest request = (LogAccessRequest) msg.obj; + mService.onLogAccessRequested(request); + break; + } + case MSG_APPROVE_LOG_ACCESS: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessApprovedForClient(client); + break; + } + case MSG_DECLINE_LOG_ACCESS: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessDeclinedForClient(client); + break; + } + case MSG_LOG_ACCESS_FINISHED: { + LogAccessRequest request = (LogAccessRequest) msg.obj; + mService.onLogAccessFinished(request); + break; + } + case MSG_PENDING_TIMEOUT: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onPendingTimeoutExpired(client); + break; + } + case MSG_LOG_ACCESS_STATUS_EXPIRED: { + LogAccessClient client = (LogAccessClient) msg.obj; + mService.onAccessStatusExpired(client); + break; + } } - return mLogdService; } } + static class Injector { + protected Supplier<Long> createClock() { + return SystemClock::uptimeMillis; + } + + protected Looper getLooper() { + return Looper.getMainLooper(); + } + + protected ILogd getLogdService() { + return ILogd.Stub.asInterface(ServiceManager.getService("logd")); + } + } + + public LogcatManagerService(Context context) { + this(context, new Injector()); + } + + public LogcatManagerService(Context context, Injector injector) { + super(context); + mContext = context; + mInjector = injector; + mClock = injector.createClock(); + mBinderService = new BinderService(); + mLocalService = new LogcatManagerServiceInternal(); + mHandler = new LogAccessRequestHandler(injector.getLooper(), this); + } + + @Override + public void onStart() { + try { + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + publishBinderService("logcat", mBinderService); + publishLocalService(LogcatManagerServiceInternal.class, mLocalService); + } catch (Throwable t) { + Slog.e(TAG, "Could not start the LogcatManagerService.", t); + } + } + + @VisibleForTesting + LogcatManagerServiceInternal getLocalService() { + return mLocalService; + } + + @VisibleForTesting + ILogcatManagerService getBinderService() { + return mBinderService; + } + + @Nullable + private LogAccessClient getClientForRequest(LogAccessRequest request) { + final String packageName = getPackageName(request); + if (packageName == null) { + return null; + } + + return new LogAccessClient(request.mUid, packageName); + } + /** * Returns the package name. * If we cannot retrieve the package name, it returns null and we decline the full device log * access */ - private String getPackageName(int uid, int gid, int pid, int fd) { - - final ActivityManagerInternal activityManagerInternal = - LocalServices.getService(ActivityManagerInternal.class); - if (activityManagerInternal != null) { - String packageName = activityManagerInternal.getPackageNameByPid(pid); + private String getPackageName(LogAccessRequest request) { + if (mActivityManagerInternal != null) { + String packageName = mActivityManagerInternal.getPackageNameByPid(request.mPid); if (packageName != null) { return packageName; } @@ -125,7 +354,7 @@ public final class LogcatManagerService extends SystemService { return null; } - String[] packageNames = pm.getPackagesForUid(uid); + String[] packageNames = pm.getPackagesForUid(request.mUid); if (ArrayUtils.isEmpty(packageNames)) { // Decline the logd access if the app name is unknown @@ -142,127 +371,164 @@ public final class LogcatManagerService extends SystemService { } return firstPackageName; - } - private void declineLogdAccess(int uid, int gid, int pid, int fd) { - try { - getLogdService().decline(uid, gid, pid, fd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); + void onLogAccessRequested(LogAccessRequest request) { + final LogAccessClient client = getClientForRequest(request); + if (client == null) { + declineRequest(request); + return; + } + + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus == null) { + logAccessStatus = new LogAccessStatus(); + mLogAccessStatus.put(client, logAccessStatus); + } + + switch (logAccessStatus.mStatus) { + case STATUS_NEW_REQUEST: + logAccessStatus.mPendingRequests.add(request); + processNewLogAccessRequest(client); + break; + case STATUS_PENDING: + logAccessStatus.mPendingRequests.add(request); + return; + case STATUS_APPROVED: + approveRequest(client, request); + break; + case STATUS_DECLINED: + declineRequest(request); + break; } } - private static String getClientInfo(int uid, int gid, int pid, int fd) { - return "UID=" + Integer.toString(uid) + " GID=" + Integer.toString(gid) + " PID=" - + Integer.toString(pid) + " FD=" + Integer.toString(fd); + private boolean shouldShowConfirmationDialog(LogAccessClient client) { + // If the process is foreground, show a dialog for user consent + final int procState = mActivityManagerInternal.getUidProcessState(client.mUid); + return procState == ActivityManager.PROCESS_STATE_TOP; } - private class LogdMonitor implements Runnable { + private void processNewLogAccessRequest(LogAccessClient client) { + boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid); - private final int mUid; - private final int mGid; - private final int mPid; - private final int mFd; - private final boolean mStart; + // The instrumented apks only run for testing, so we don't check user permission. + if (isInstrumented) { + onAccessApprovedForClient(client); + return; + } - /** - * For starting a thread, the start value is true. - * For finishing a thread, the start value is false. - */ - LogdMonitor(int uid, int gid, int pid, int fd, boolean start) { - mUid = uid; - mGid = gid; - mPid = pid; - mFd = fd; - mStart = start; + if (!shouldShowConfirmationDialog(client)) { + onAccessDeclinedForClient(client); + return; } - /** - * LogdMonitor generates a prompt for users. - * The users decide whether the logd access is allowed. - */ - @Override - public void run() { - if (mLogdService == null) { - LogcatManagerService.this.addLogdService(); - } + final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + logAccessStatus.mStatus = STATUS_PENDING; - if (mStart) { + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client), + mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS); + final Intent mIntent = createIntent(client); + mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM); + } - ActivityManagerInternal ami = LocalServices.getService( - ActivityManagerInternal.class); - boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(mUid); + void onAccessApprovedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); - // The instrumented apks only run for testing, so we don't check user permission. - if (isCallerInstrumented) { - try { - getLogdService().approve(mUid, mGid, mPid, mFd); - } catch (RemoteException e) { - Slog.e(TAG, "Fails to call remote functions", e); - } - return; - } + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + approveRequest(client, request); + } + logAccessStatus.mStatus = STATUS_APPROVED; + logAccessStatus.mPendingRequests.clear(); + } + } - final int procState = LocalServices.getService(ActivityManagerInternal.class) - .getUidProcessState(mUid); - // If the process is foreground and we can retrieve the package name, show a dialog - // for user consent - if (procState == ActivityManager.PROCESS_STATE_TOP) { - String packageName = getPackageName(mUid, mGid, mPid, mFd); - if (packageName != null) { - final Intent mIntent = createIntent(packageName, mUid, mGid, mPid, mFd); - mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM); - return; - } - } + void onAccessDeclinedForClient(LogAccessClient client) { + scheduleStatusExpiry(client); - /** - * If the process is background or cannot retrieve the package name, - * decline the logd access. - **/ - declineLogdAccess(mUid, mGid, mPid, mFd); - return; + LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null) { + for (LogAccessRequest request : logAccessStatus.mPendingRequests) { + declineRequest(request); } + logAccessStatus.mStatus = STATUS_DECLINED; + logAccessStatus.mPendingRequests.clear(); } } - public LogcatManagerService(Context context) { - super(context); - mContext = context; - mBinderService = new BinderService(); - mThreadExecutor = Executors.newCachedThreadPool(); - mActivityManager = context.getSystemService(ActivityManager.class); + private void scheduleStatusExpiry(LogAccessClient client) { + mHandler.removeMessages(MSG_PENDING_TIMEOUT, client); + mHandler.removeMessages(MSG_LOG_ACCESS_STATUS_EXPIRED, client); + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_LOG_ACCESS_STATUS_EXPIRED, client), + mClock.get() + STATUS_EXPIRATION_TIMEOUT_MILLIS); } - @Override - public void onStart() { + void onPendingTimeoutExpired(LogAccessClient client) { + final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client); + if (logAccessStatus != null && logAccessStatus.mStatus == STATUS_PENDING) { + onAccessDeclinedForClient(client); + } + } + + void onAccessStatusExpired(LogAccessClient client) { + if (DEBUG) { + Slog.d(TAG, "Log access status expired for " + client); + } + mLogAccessStatus.remove(client); + } + + void onLogAccessFinished(LogAccessRequest request) { + final LogAccessClient client = getClientForRequest(request); + final int activeCount = mActiveLogAccessCount.getOrDefault(client, 1) - 1; + + if (activeCount == 0) { + mActiveLogAccessCount.remove(client); + if (DEBUG) { + Slog.d(TAG, "Client is no longer accessing logs: " + client); + } + // TODO This will be used to notify the AppOpsManager that the logd data access + // is finished. + } else { + mActiveLogAccessCount.put(client, activeCount); + } + } + + private void approveRequest(LogAccessClient client, LogAccessRequest request) { + if (DEBUG) { + Slog.d(TAG, "Approving log access: " + request); + } try { - publishBinderService("logcat", mBinderService); - } catch (Throwable t) { - Slog.e(TAG, "Could not start the LogcatManagerService.", t); + getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd); + Integer activeCount = mActiveLogAccessCount.getOrDefault(client, 0); + mActiveLogAccessCount.put(client, activeCount + 1); + } catch (RemoteException e) { + Slog.e(TAG, "Fails to call remote functions", e); } } - private void addLogdService() { - mLogdService = ILogd.Stub.asInterface(ServiceManager.getService("logd")); + private void declineRequest(LogAccessRequest request) { + if (DEBUG) { + Slog.d(TAG, "Declining log access: " + request); + } + try { + getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd); + } catch (RemoteException e) { + Slog.e(TAG, "Fails to call remote functions", e); + } } /** * Create the Intent for LogAccessDialogActivity. */ - public Intent createIntent(String targetPackageName, int uid, int gid, int pid, int fd) { - final Intent intent = new Intent(); + public Intent createIntent(LogAccessClient client) { + final Intent intent = new Intent(mContext, LogAccessDialogActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName); - intent.putExtra(EXTRA_UID, uid); - intent.putExtra(EXTRA_GID, gid); - intent.putExtra(EXTRA_PID, pid); - intent.putExtra(EXTRA_FD, fd); - - intent.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME)); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName); + intent.putExtra(Intent.EXTRA_UID, client.mUid); return intent; } diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 02f9ceb2d11d..89902f7f8321 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -51,14 +51,16 @@ public interface NotificationDelegate { void onNotificationSettingsViewed(String key); /** * Called when the state of {@link Notification#FLAG_BUBBLE} is changed. + * + * @param key the notification key + * @param isBubble whether the notification should have {@link Notification#FLAG_BUBBLE} applied + * @param flags the flags to apply to the notification's {@link Notification.BubbleMetadata} */ void onNotificationBubbleChanged(String key, boolean isBubble, int flags); /** - * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION} - * or {@link Notification.BubbleMetadata#FLAG_SUPPRESS_BUBBLE} changes. + * Called when the flags on {@link Notification.BubbleMetadata} are changed. */ - void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed); + void onBubbleMetadataFlagChanged(String key, int flags); /** * Grant permission to read the specified URI to the package associated with the diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java index c548e7edc3cf..8a627367c1dc 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java +++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java @@ -42,4 +42,7 @@ public interface NotificationManagerInternal { /** Does the specified package/uid have permission to post notifications? */ boolean areNotificationsEnabledForPackage(String pkg, int uid); + + /** Send a notification to the user prompting them to review their notification permissions. */ + void sendReviewPermissionsNotification(); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6078bfc95488..83c576e9259d 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -18,6 +18,7 @@ package com.android.server.notification; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; @@ -274,6 +275,7 @@ import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BackgroundThread; import com.android.internal.os.SomeArgs; @@ -442,6 +444,18 @@ public class NotificationManagerService extends SystemService { private static final int NOTIFICATION_INSTANCE_ID_MAX = (1 << 13); + // States for the review permissions notification + static final int REVIEW_NOTIF_STATE_UNKNOWN = -1; + static final int REVIEW_NOTIF_STATE_SHOULD_SHOW = 0; + static final int REVIEW_NOTIF_STATE_USER_INTERACTED = 1; + static final int REVIEW_NOTIF_STATE_DISMISSED = 2; + static final int REVIEW_NOTIF_STATE_RESHOWN = 3; + + // Action strings for review permissions notification + static final String REVIEW_NOTIF_ACTION_REMIND = "REVIEW_NOTIF_ACTION_REMIND"; + static final String REVIEW_NOTIF_ACTION_DISMISS = "REVIEW_NOTIF_ACTION_DISMISS"; + static final String REVIEW_NOTIF_ACTION_CANCELED = "REVIEW_NOTIF_ACTION_CANCELED"; + /** * Apps that post custom toasts in the background will have those blocked. Apps can * still post toasts created with @@ -652,6 +666,9 @@ public class NotificationManagerService extends SystemService { private InstanceIdSequence mNotificationInstanceIdSequence; private Set<String> mMsgPkgsAllowedAsConvos = new HashSet(); + // Broadcast intent receiver for notification permissions review-related intents + private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver; + static class Archive { final SparseArray<Boolean> mEnabled; final int mBufferSize; @@ -1413,8 +1430,7 @@ public class NotificationManagerService extends SystemService { } @Override - public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed) { + public void onBubbleMetadataFlagChanged(String key, int flags) { synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { @@ -1424,17 +1440,12 @@ public class NotificationManagerService extends SystemService { return; } - boolean flagChanged = false; - if (data.isNotificationSuppressed() != isNotifSuppressed) { - flagChanged = true; - data.setSuppressNotification(isNotifSuppressed); - } - if (data.isBubbleSuppressed() != isBubbleSuppressed) { - flagChanged = true; - data.setSuppressBubble(isBubbleSuppressed); - } - if (flagChanged) { + if (flags != data.getFlags()) { + data.setFlags(flags); + // Shouldn't alert again just because of a flag change. r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; + // Force isAppForeground true here, because for sysui's purposes we + // want to be able to adjust the flag behaviour. mHandler.post( new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r, true /* isAppForeground */, SystemClock.elapsedRealtime())); @@ -2416,6 +2427,11 @@ public class NotificationManagerService extends SystemService { IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); getContext().registerReceiver(mLocaleChangeReceiver, localeChangedFilter); + + mReviewNotificationPermissionsReceiver = new ReviewNotificationPermissionsReceiver(); + getContext().registerReceiver(mReviewNotificationPermissionsReceiver, + ReviewNotificationPermissionsReceiver.getFilter(), + Context.RECEIVER_NOT_EXPORTED); } /** @@ -2709,6 +2725,7 @@ public class NotificationManagerService extends SystemService { mHistoryManager.onBootPhaseAppsCanStart(); registerDeviceConfigChange(); migrateDefaultNAS(); + maybeShowInitialReviewPermissionsNotification(); } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis()); } @@ -6336,6 +6353,21 @@ public class NotificationManagerService extends SystemService { public boolean areNotificationsEnabledForPackage(String pkg, int uid) { return areNotificationsEnabledForPackageInt(pkg, uid); } + + @Override + public void sendReviewPermissionsNotification() { + // This method is meant to be called from the JobService upon running the job for this + // notification having been rescheduled; so without checking any other state, it will + // send the notification. + checkCallerIsSystem(); + NotificationManager nm = getContext().getSystemService(NotificationManager.class); + nm.notify(TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS, + createReviewPermissionsNotification()); + Settings.Global.putInt(getContext().getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + } }; int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) { @@ -7145,10 +7177,12 @@ public class NotificationManagerService extends SystemService { && r.getNotification().isBubbleNotification()) || (mReason == REASON_CLICK && r.canBubble() && r.isFlagBubbleRemoved())) { - boolean isBubbleSuppressed = r.getNotification().getBubbleMetadata() != null - && r.getNotification().getBubbleMetadata().isBubbleSuppressed(); - mNotificationDelegate.onBubbleNotificationSuppressionChanged( - r.getKey(), true /* notifSuppressed */, isBubbleSuppressed); + int flags = 0; + if (r.getNotification().getBubbleMetadata() != null) { + flags = r.getNotification().getBubbleMetadata().getFlags(); + } + flags |= FLAG_SUPPRESS_NOTIFICATION; + mNotificationDelegate.onBubbleMetadataFlagChanged(r.getKey(), flags); return; } if ((r.getNotification().flags & mMustHaveFlags) != mMustHaveFlags) { @@ -11608,6 +11642,76 @@ public class NotificationManagerService extends SystemService { out.endTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG); } + // Creates a notification that informs the user about changes due to the migration to + // use permissions for notifications. + protected Notification createReviewPermissionsNotification() { + int title = R.string.review_notification_settings_title; + int content = R.string.review_notification_settings_text; + + // Tapping on the notification leads to the settings screen for managing app notifications, + // using the intent reserved for system services to indicate it comes from this notification + Intent tapIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW); + Intent remindIntent = new Intent(REVIEW_NOTIF_ACTION_REMIND); + Intent dismissIntent = new Intent(REVIEW_NOTIF_ACTION_DISMISS); + Intent swipeIntent = new Intent(REVIEW_NOTIF_ACTION_CANCELED); + + // Both "remind me" and "dismiss" actions will be actions received by the BroadcastReceiver + final Notification.Action remindMe = new Notification.Action.Builder(null, + getContext().getResources().getString( + R.string.review_notification_settings_remind_me_action), + PendingIntent.getBroadcast( + getContext(), 0, remindIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + final Notification.Action dismiss = new Notification.Action.Builder(null, + getContext().getResources().getString( + R.string.review_notification_settings_dismiss), + PendingIntent.getBroadcast( + getContext(), 0, dismissIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + + return new Notification.Builder(getContext(), SystemNotificationChannels.SYSTEM_CHANGES) + .setSmallIcon(R.drawable.stat_sys_adb) + .setContentTitle(getContext().getResources().getString(title)) + .setContentText(getContext().getResources().getString(content)) + .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .setStyle(new Notification.BigTextStyle()) + .setFlag(Notification.FLAG_NO_CLEAR, true) + .setAutoCancel(true) + .addAction(remindMe) + .addAction(dismiss) + .setDeleteIntent(PendingIntent.getBroadcast(getContext(), 0, swipeIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + } + + protected void maybeShowInitialReviewPermissionsNotification() { + int currentState = Settings.Global.getInt(getContext().getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + REVIEW_NOTIF_STATE_UNKNOWN); + + // now check the last known state of the notification -- this determination of whether the + // user is in the correct target audience occurs elsewhere, and will have written the + // REVIEW_NOTIF_STATE_SHOULD_SHOW to indicate it should be shown in the future. + // + // alternatively, if the user has rescheduled the notification (so it has been shown + // again) but not yet interacted with the new notification, then show it again on boot, + // as this state indicates that the user had the notification open before rebooting. + // + // sending the notification here does not record a new state for the notification; + // that will be written by parts of the system further down the line if at any point + // the user interacts with the notification. + if (currentState == REVIEW_NOTIF_STATE_SHOULD_SHOW + || currentState == REVIEW_NOTIF_STATE_RESHOWN) { + NotificationManager nm = getContext().getSystemService(NotificationManager.class); + nm.notify(TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS, + createReviewPermissionsNotification()); + } + } + /** * Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too * aggressive and annoying the user. diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 0525b1e33267..ef3c770f125b 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -96,6 +96,10 @@ public class PreferencesHelper implements RankingConfig { private final int XML_VERSION; /** What version to check to do the upgrade for bubbles. */ private static final int XML_VERSION_BUBBLES_UPGRADE = 1; + /** The first xml version with notification permissions enabled. */ + private static final int XML_VERSION_NOTIF_PERMISSION = 3; + /** The first xml version that notifies users to review their notification permissions */ + private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4; @VisibleForTesting static final int UNKNOWN_UID = UserHandle.USER_NULL; private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":"; @@ -206,7 +210,7 @@ public class PreferencesHelper implements RankingConfig { mStatsEventBuilderFactory = statsEventBuilderFactory; if (mPermissionHelper.isMigrationEnabled()) { - XML_VERSION = 3; + XML_VERSION = 4; } else { XML_VERSION = 2; } @@ -226,8 +230,16 @@ public class PreferencesHelper implements RankingConfig { final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1); boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE; - boolean migrateToPermission = - (xmlVersion < XML_VERSION) && mPermissionHelper.isMigrationEnabled(); + boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION) + && mPermissionHelper.isMigrationEnabled(); + if (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION) { + // make a note that we should show the notification at some point. + // it shouldn't be possible for the user to already have seen it, as the XML version + // would be newer then. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + } ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>(); synchronized (mPackagePreferences) { while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java new file mode 100644 index 000000000000..fde45f71a844 --- /dev/null +++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 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 android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; + +/** + * JobService implementation for scheduling the notification informing users about + * notification permissions updates and taking them to review their existing permissions. + * @hide + */ +public class ReviewNotificationPermissionsJobService extends JobService { + public static final String TAG = "ReviewNotificationPermissionsJobService"; + + @VisibleForTesting + protected static final int JOB_ID = 225373531; + + /** + * Schedule a new job that will show a notification the specified amount of time in the future. + */ + public static void scheduleJob(Context context, long rescheduleTimeMillis) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + // if the job already exists for some reason, cancel & reschedule + if (jobScheduler.getPendingJob(JOB_ID) != null) { + jobScheduler.cancel(JOB_ID); + } + ComponentName component = new ComponentName( + context, ReviewNotificationPermissionsJobService.class); + JobInfo newJob = new JobInfo.Builder(JOB_ID, component) + .setPersisted(true) // make sure it'll still get rescheduled after reboot + .setMinimumLatency(rescheduleTimeMillis) // run after specified amount of time + .build(); + jobScheduler.schedule(newJob); + } + + @Override + public boolean onStartJob(JobParameters params) { + // While jobs typically should be run on different threads, this + // job only posts a notification, which is not a long-running operation + // as notification posting is asynchronous. + NotificationManagerInternal nmi = + LocalServices.getService(NotificationManagerInternal.class); + nmi.sendReviewPermissionsNotification(); + + // once the notification is posted, the job is done, so no need to + // keep it alive afterwards + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + // If we're interrupted for some reason, try again (though this may not + // ever happen due to onStartJob not leaving a job running after being + // called) + return true; + } +} diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java new file mode 100644 index 000000000000..b99aeac44025 --- /dev/null +++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 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 android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.provider.Settings; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.messages.nano.SystemMessageProto; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +/** + * Broadcast receiver for intents that come from the "review notification permissions" notification, + * shown to users who upgrade to T from an earlier OS to inform them of notification setup changes + * and invite them to review their notification permissions. + */ +public class ReviewNotificationPermissionsReceiver extends BroadcastReceiver { + public static final String TAG = "ReviewNotifPermissions"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // 7 days in millis, as the amount of time to wait before re-sending the notification + private static final long JOB_RESCHEDULE_TIME = 1000 /* millis */ * 60 /* seconds */ + * 60 /* minutes */ * 24 /* hours */ * 7 /* days */; + + static IntentFilter getFilter() { + IntentFilter filter = new IntentFilter(); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + return filter; + } + + // Cancels the "review notification permissions" notification. + @VisibleForTesting + protected void cancelNotification(Context context) { + NotificationManager nm = context.getSystemService(NotificationManager.class); + if (nm != null) { + nm.cancel(NotificationManagerService.TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS); + } else { + Slog.w(TAG, "could not cancel notification: NotificationManager not found"); + } + } + + @VisibleForTesting + protected void rescheduleNotification(Context context) { + ReviewNotificationPermissionsJobService.scheduleJob(context, JOB_RESCHEDULE_TIME); + // log if needed + if (DEBUG) { + Slog.d(TAG, "Scheduled review permissions notification for on or after: " + + LocalDateTime.now(ZoneId.systemDefault()) + .plus(JOB_RESCHEDULE_TIME, ChronoUnit.MILLIS)); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND)) { + // Reschedule the notification for 7 days in the future + rescheduleNotification(context); + + // note that the user has interacted; no longer needed to show the initial + // notification + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + cancelNotification(context); + } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS)) { + // user dismissed; write to settings so we don't show ever again + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + cancelNotification(context); + } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED)) { + // we may get here from the user swiping away the notification, + // or from the notification being canceled in any other way. + // only in the case that the user hasn't interacted with it in + // any other way yet, reschedule + int notifState = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + /* default */ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); + if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW) { + // user hasn't interacted in the past, so reschedule once and then note that the + // user *has* interacted now so we don't re-reschedule if they swipe again + rescheduleNotification(context); + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + } else if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN) { + // swiping away on a rescheduled notification; mark as interacted and + // don't reschedule again. + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + } + } + } +} diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 8c33dd935822..f1296e0cd7b0 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -29,7 +29,9 @@ import static com.android.server.pm.PackageManagerService.TAG; import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter; import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PKG; +import android.Manifest; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.AppGlobals; import android.content.Intent; @@ -42,6 +44,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.Log; import android.util.Slog; @@ -250,10 +253,60 @@ final class DexOptHelper { numberOfPackagesFailed}; } + /** + * Checks if system UI package (typically "com.android.systemui") needs to be re-compiled, and + * compiles it if needed. + */ + private void checkAndDexOptSystemUi() { + Computer snapshot = mPm.snapshotComputer(); + String sysUiPackageName = + mPm.mContext.getString(com.android.internal.R.string.config_systemUi); + AndroidPackage pkg = snapshot.getPackage(sysUiPackageName); + if (pkg == null) { + Log.w(TAG, "System UI package " + sysUiPackageName + " is not found for dexopting"); + return; + } + + boolean useProfileForDexopt = false; + File profileFile = new File(getPrebuildProfilePath(pkg)); + // Copy the profile to the reference profile path if it exists. Installd can only use a + // profile at the reference profile path for dexopt. + if (profileFile.exists()) { + try { + synchronized (mPm.mInstallLock) { + if (mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(), + pkg.getUid(), pkg.getPackageName(), + ArtManager.getProfileName(null))) { + useProfileForDexopt = true; + } else { + Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath()); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath(), e); + } + } + + // It could also be after mainline update, but we're not introducing a new reason just for + // this special case. + performDexOptTraced(new DexoptOptions(pkg.getPackageName(), REASON_BOOT_AFTER_OTA, + useProfileForDexopt ? "speed-profile" : "speed", null /* splitName */, + 0 /* dexoptFlags */)); + } + + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public void performPackageDexOptUpgradeIfNeeded() { PackageManagerServiceUtils.enforceSystemOrRoot( "Only the system can request package update"); + // The default is "true". + if (!"false".equals(DeviceConfig.getProperty("runtime", "dexopt_system_ui_on_boot"))) { + // System UI is important to user experience, so we check it on every boot. It may need + // to be re-compiled after a mainline update or an OTA. + // TODO(b/227310505): Only do this after a mainline update or an OTA. + checkAndDexOptSystemUi(); + } + // We need to re-extract after an OTA. boolean causeUpgrade = mPm.isDeviceUpgrading(); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0dabff8370ba..bcdf4291ed41 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1430,6 +1430,8 @@ public class UserManagerService extends IUserManager.Stub { /** * Returns a UserInfo object with the name filled in, for Owner and Guest, or the original * if the name is already set. + * + * Note: Currently, the resulting name can be null if a user was truly created with a null name. */ private UserInfo userWithName(UserInfo orig) { if (orig != null && orig.name == null) { @@ -1638,7 +1640,7 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public String getUserName() { + public @NonNull String getUserName() { final int callingUid = Binder.getCallingUid(); if (!hasQueryOrCreateUsersPermission() && !hasPermissionGranted( @@ -1649,7 +1651,10 @@ public class UserManagerService extends IUserManager.Stub { final int userId = UserHandle.getUserId(callingUid); synchronized (mUsersLock) { UserInfo userInfo = userWithName(getUserInfoLU(userId)); - return userInfo == null ? "" : userInfo.name; + if (userInfo != null && userInfo.name != null) { + return userInfo.name; + } + return ""; } } @@ -4165,7 +4170,7 @@ public class UserManagerService extends IUserManager.Stub { * @return the converted user, or {@code null} if no pre-created user could be converted. */ private @Nullable UserInfo convertPreCreatedUserIfPossible(String userType, - @UserInfoFlag int flags, String name, @Nullable Object token) { + @UserInfoFlag int flags, @Nullable String name, @Nullable Object token) { final UserData preCreatedUserData; synchronized (mUsersLock) { preCreatedUserData = getPreCreatedUserLU(userType); diff --git a/services/core/java/com/android/server/power/LowPowerStandbyController.java b/services/core/java/com/android/server/power/LowPowerStandbyController.java index 2d2bad27ecd3..5964fa49f035 100644 --- a/services/core/java/com/android/server/power/LowPowerStandbyController.java +++ b/services/core/java/com/android/server/power/LowPowerStandbyController.java @@ -68,7 +68,7 @@ import java.util.Arrays; * * @hide */ -public final class LowPowerStandbyController { +public class LowPowerStandbyController { private static final String TAG = "LowPowerStandbyController"; private static final boolean DEBUG = false; private static final boolean DEFAULT_ACTIVE_DURING_MAINTENANCE = false; @@ -173,7 +173,9 @@ public final class LowPowerStandbyController { mSettingsObserver = new SettingsObserver(mHandler); } - void systemReady() { + /** Call when system services are ready */ + @VisibleForTesting + public void systemReady() { final Resources resources = mContext.getResources(); synchronized (mLock) { mSupportedConfig = resources.getBoolean( @@ -435,7 +437,9 @@ public final class LowPowerStandbyController { } } - void setActiveDuringMaintenance(boolean activeDuringMaintenance) { + /** Set whether Low Power Standby should be active during doze maintenance mode. */ + @VisibleForTesting + public void setActiveDuringMaintenance(boolean activeDuringMaintenance) { synchronized (mLock) { if (!mSupportedConfig) { Slog.w(TAG, "Low Power Standby settings cannot be changed " diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index b6855726c122..d48f26332017 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -1650,13 +1650,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed) { + public void onBubbleMetadataFlagChanged(String key, int flags) { enforceStatusBarService(); final long identity = Binder.clearCallingIdentity(); try { - mNotificationDelegate.onBubbleNotificationSuppressionChanged(key, isNotifSuppressed, - isBubbleSuppressed); + mNotificationDelegate.onBubbleMetadataFlagChanged(key, flags); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 732cb71227ed..791d193f36ab 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5096,7 +5096,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS); if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn()) && (appTransition.isTransitionSet() - || (recentsAnimating && !isActivityTypeHome()))) { + || (recentsAnimating && !isActivityTypeHome())) + // If the visibility change during enter PIP, we don't want to include it in app + // transition to affect the animation theme, because the Pip organizer will animate + // the entering PIP instead. + && !mWaitForEnteringPinnedMode) { if (visible) { displayContent.mOpeningApps.add(this); mEnteringAnimation = true; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 34c083aa8ac1..b97ee7ef76b3 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1807,6 +1807,10 @@ class ActivityStarter { // Check if starting activity on given task or on a new task is allowed. int startResult = isAllowedToStart(r, newTask, targetTask); if (startResult != START_SUCCESS) { + if (r.resultTo != null) { + r.resultTo.sendResult(INVALID_UID, r.resultWho, r.requestCode, RESULT_CANCELED, + null /* data */, null /* dataGrants */); + } return startResult; } @@ -1976,13 +1980,9 @@ class ActivityStarter { mPreferredWindowingMode = mLaunchParams.mWindowingMode; } - private int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) { - if (mStartActivity.packageName == null) { - if (mStartActivity.resultTo != null) { - mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho, - mStartActivity.requestCode, RESULT_CANCELED, - null /* data */, null /* dataGrants */); - } + @VisibleForTesting + int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) { + if (r.packageName == null) { ActivityOptions.abort(mOptions); return START_CLASS_NOT_FOUND; } @@ -2005,8 +2005,7 @@ class ActivityStarter { || !targetTask.isUidPresent(mCallingUid) || (LAUNCH_SINGLE_INSTANCE == mLaunchMode && targetTask.inPinnedWindowingMode())); - if (mRestrictedBgActivity && blockBalInTask - && handleBackgroundActivityAbort(mStartActivity)) { + if (mRestrictedBgActivity && blockBalInTask && handleBackgroundActivityAbort(r)) { Slog.e(TAG, "Abort background activity starts from " + mCallingUid); return START_ABORTED; } @@ -2020,12 +2019,12 @@ class ActivityStarter { if (!newTask) { if (mService.getLockTaskController().isLockTaskModeViolation(targetTask, isNewClearTask)) { - Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity); + Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r); return START_RETURN_LOCK_TASK_MODE_VIOLATION; } } else { - if (mService.getLockTaskController().isNewTaskLockTaskModeViolation(mStartActivity)) { - Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity); + if (mService.getLockTaskController().isNewTaskLockTaskModeViolation(r)) { + Slog.e(TAG, "Attempted Lock Task Mode violation r=" + r); return START_RETURN_LOCK_TASK_MODE_VIOLATION; } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ed1bbf8e4b74..2975a95426bb 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4272,7 +4272,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp setImeInputTarget(target); mInsetsStateController.updateAboveInsetsState(mInsetsStateController .getRawInsetsState().getSourceOrDefaultVisibility(ITYPE_IME)); - updateImeControlTarget(); + // Force updating the IME parent when the IME control target has been updated to the + // remote target but updateImeParent not happen because ImeLayeringTarget and + // ImeInputTarget are different. Then later updateImeParent would be ignored when there + // is no new IME control target to change the IME parent. + final boolean forceUpdateImeParent = mImeControlTarget == mRemoteInsetsControlTarget + && (mInputMethodSurfaceParent != null + && !mInputMethodSurfaceParent.isSameSurface( + mImeWindowsContainer.getParent().mSurfaceControl)); + updateImeControlTarget(forceUpdateImeParent); } // Unfreeze IME insets after the new target updated, in case updateAboveInsetsState may // deliver unrelated IME insets change to the non-IME requester. diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java index 83be73a47eb4..4068a97a881a 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.util.TimeUtils.NANOS_PER_MS; import static android.view.Choreographer.CALLBACK_TRAVERSAL; import static android.view.Choreographer.getSfInstance; @@ -26,14 +27,13 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.Nullable; -import android.graphics.Canvas; import android.graphics.Insets; -import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.hardware.power.Boost; import android.os.Handler; import android.os.PowerManagerInternal; +import android.os.Trace; import android.util.ArrayMap; import android.util.Log; import android.view.Choreographer; @@ -50,6 +50,8 @@ import com.android.server.AnimationThread; import com.android.server.wm.LocalAnimationAdapter.AnimationSpec; import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Supplier; /** @@ -83,6 +85,12 @@ class SurfaceAnimationRunner { private final PowerManagerInternal mPowerManagerInternal; private boolean mApplyScheduled; + // Executor to perform the edge extension. + // With two threads because in practice we will want to extend two surfaces in one animation, + // in which case we want to be able to parallelize those two extensions to cut down latency in + // starting the animation. + private final ExecutorService mEdgeExtensionExecutor = Executors.newFixedThreadPool(2); + @GuardedBy("mLock") @VisibleForTesting final ArrayMap<SurfaceControl, RunningAnimation> mPendingAnimations = new ArrayMap<>(); @@ -173,7 +181,7 @@ class SurfaceAnimationRunner { // We must wait for t to be committed since otherwise the leash doesn't have the // windows we want to screenshot and extend as children. - t.addTransactionCommittedListener(Runnable::run, () -> { + t.addTransactionCommittedListener(mEdgeExtensionExecutor, () -> { final WindowAnimationSpec animationSpec = a.asWindowAnimationSpec(); final Transaction edgeExtensionCreationTransaction = new Transaction(); @@ -403,30 +411,17 @@ class SurfaceAnimationRunner { private void createExtensionSurface(SurfaceControl leash, Rect edgeBounds, Rect extensionRect, int xPos, int yPos, String layerName, Transaction startTransaction) { - synchronized (mEdgeExtensionLock) { - if (!mEdgeExtensions.containsKey(leash)) { - // Animation leash has already been removed so we shouldn't perform any extension - return; - } - createExtensionSurfaceLocked(leash, edgeBounds, extensionRect, xPos, yPos, layerName, - startTransaction); - } + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "createExtensionSurface"); + doCreateExtensionSurface(leash, edgeBounds, extensionRect, xPos, yPos, layerName, + startTransaction); + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - private void createExtensionSurfaceLocked(SurfaceControl surfaceToExtend, Rect edgeBounds, + private void doCreateExtensionSurface(SurfaceControl leash, Rect edgeBounds, Rect extensionRect, int xPos, int yPos, String layerName, Transaction startTransaction) { - final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() - .setName(layerName) - .setParent(surfaceToExtend) - .setHidden(true) - .setCallsite("DefaultTransitionHandler#startAnimation") - .setOpaque(true) - .setBufferSize(extensionRect.width(), extensionRect.height()) - .build(); - SurfaceControl.LayerCaptureArgs captureArgs = - new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) + new SurfaceControl.LayerCaptureArgs.Builder(leash /* surfaceToExtend */) .setSourceCrop(edgeBounds) .setFrameScale(1) .setPixelFormat(PixelFormat.RGBA_8888) @@ -437,31 +432,76 @@ class SurfaceAnimationRunner { SurfaceControl.captureLayers(captureArgs); if (edgeBuffer == null) { + // The leash we are trying to screenshot may have been removed by this point, which is + // likely the reason for ending up with a null edgeBuffer, in which case we just want to + // return and do nothing. Log.e("SurfaceAnimationRunner", "Failed to create edge extension - " + "edge buffer is null"); return; } - android.graphics.BitmapShader shader = - new android.graphics.BitmapShader(edgeBuffer.asBitmap(), - android.graphics.Shader.TileMode.CLAMP, - android.graphics.Shader.TileMode.CLAMP); - final Paint paint = new Paint(); - paint.setShader(shader); + final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() + .setName(layerName) + .setHidden(true) + .setCallsite("DefaultTransitionHandler#startAnimation") + .setOpaque(true) + .setBufferSize(edgeBounds.width(), edgeBounds.height()) + .build(); final Surface surface = new Surface(edgeExtensionLayer); - Canvas c = surface.lockHardwareCanvas(); - c.drawRect(extensionRect, paint); - surface.unlockCanvasAndPost(c); + surface.attachAndQueueBufferWithColorSpace(edgeBuffer.getHardwareBuffer(), + edgeBuffer.getColorSpace()); surface.release(); - startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); - startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); - startTransaction.setVisibility(edgeExtensionLayer, true); + final float scaleX = getScaleXForExtensionSurface(edgeBounds, extensionRect); + final float scaleY = getScaleYForExtensionSurface(edgeBounds, extensionRect); - mEdgeExtensions.get(surfaceToExtend).add(edgeExtensionLayer); + synchronized (mEdgeExtensionLock) { + if (!mEdgeExtensions.containsKey(leash)) { + // The animation leash has already been removed, so we don't want to attach the + // edgeExtension layer and should immediately remove it instead. + startTransaction.remove(edgeExtensionLayer); + return; + } + + startTransaction.setScale(edgeExtensionLayer, scaleX, scaleY); + startTransaction.reparent(edgeExtensionLayer, leash); + startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); + startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); + startTransaction.setVisibility(edgeExtensionLayer, true); + + mEdgeExtensions.get(leash).add(edgeExtensionLayer); + } } + private float getScaleXForExtensionSurface(Rect edgeBounds, Rect extensionRect) { + if (edgeBounds.width() == extensionRect.width()) { + // Top or bottom edge extension, no need to scale the X axis of the extension surface. + return 1; + } + if (edgeBounds.width() == 1) { + // Left or right edge extension, scale the surface to be the extensionRect's width. + return extensionRect.width(); + } + + throw new RuntimeException("Unexpected edgeBounds and extensionRect widths"); + } + + private float getScaleYForExtensionSurface(Rect edgeBounds, Rect extensionRect) { + if (edgeBounds.height() == extensionRect.height()) { + // Left or right edge extension, no need to scale the Y axis of the extension surface. + return 1; + } + if (edgeBounds.height() == 1) { + // Top or bottom edge extension, scale the surface to be the extensionRect's height. + return extensionRect.height(); + } + + throw new RuntimeException("Unexpected edgeBounds and extensionRect heights"); + } + + + private static final class RunningAnimation { final AnimationSpec mAnimSpec; final SurfaceControl mLeash; diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java new file mode 100644 index 000000000000..f33001774263 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2022 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.logcat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.content.ContextWrapper; +import android.os.ILogd; +import android.os.Looper; +import android.os.UserHandle; +import android.os.test.TestLooper; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.LocalServices; +import com.android.server.logcat.LogcatManagerService.Injector; +import com.android.server.testutils.OffsettableClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Supplier; + +/** + * Tests for {@link com.android.server.logcat.LogcatManagerService}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:LogcatManagerServiceTest + */ +@SuppressWarnings("GuardedBy") +public class LogcatManagerServiceTest { + private static final String APP1_PACKAGE_NAME = "app1"; + private static final int APP1_UID = 10001; + private static final int APP1_GID = 10001; + private static final int APP1_PID = 10001; + private static final String APP2_PACKAGE_NAME = "app2"; + private static final int APP2_UID = 10002; + private static final int APP2_GID = 10002; + private static final int APP2_PID = 10002; + private static final int FD1 = 10; + private static final int FD2 = 11; + + @Mock + private ActivityManagerInternal mActivityManagerInternalMock; + @Mock + private ILogd mLogdMock; + + private LogcatManagerService mService; + private LogcatManagerService.LogcatManagerServiceInternal mLocalService; + private ContextWrapper mContextSpy; + private OffsettableClock mClock; + private TestLooper mTestLooper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock); + mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + mClock = new OffsettableClock.Stopped(); + mTestLooper = new TestLooper(mClock::now); + + when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn( + APP1_PACKAGE_NAME); + when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn( + APP2_PACKAGE_NAME); + + mService = new LogcatManagerService(mContextSpy, new Injector() { + @Override + protected Supplier<Long> createClock() { + return mClock::now; + } + + @Override + protected Looper getLooper() { + return mTestLooper.getLooper(); + } + + @Override + protected ILogd getLogdService() { + return mLogdMock; + } + }); + mLocalService = mService.getLocalService(); + mService.onStart(); + } + + @After + public void tearDown() throws Exception { + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + } + + /** + * Creates a mock and registers it to {@link LocalServices}. + */ + private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { + LocalServices.removeServiceForTest(clazz); + LocalServices.addService(clazz, mock); + } + + @Test + public void test_RequestFromBackground_DeclinedWithoutPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_RECEIVER); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, never()).startActivityAsUser(any(), any()); + } + + @Test + public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, never()).startActivityAsUser(any(), any()); + } + + @Test + public void test_RequestFromTop_ShowsPrompt() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + } + + @Test + public void test_RequestFromTop_NoInteractionWithPrompt_DeclinesAfterTimeout() + throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + advanceTime(LogcatManagerService.PENDING_CONFIRMATION_TIMEOUT_MILLIS); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_Approved() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_Declined() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + } + + @Test + public void test_RequestFromTop_MultipleRequestsApprovedTogether() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_MultipleRequestsDeclinedTogether() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt()); + + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_DoesNotShowPromptAgain() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Declined_DoesNotShowPromptAgain() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1); + verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2); + verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_ShowsPromptForDifferentClient() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + when(mActivityManagerInternalMock.getUidProcessState(APP2_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + verify(mLogdMock, never()).decline(APP2_UID, APP2_GID, APP2_PID, FD2); + verify(mLogdMock, never()).approve(APP2_UID, APP2_GID, APP2_PID, FD2); + } + + @Test + public void test_RequestFromTop_Approved_ShowPromptAgainAfterTimeout() throws Exception { + when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn( + ActivityManager.PROCESS_STATE_TOP); + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME); + mTestLooper.dispatchAll(); + + advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS); + + mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1); + mTestLooper.dispatchAll(); + + verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM)); + } + + private void advanceTime(long timeMs) { + mClock.fastForward(timeMs); + mTestLooper.dispatchAll(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index f2495e1545b5..9ff7d69e09a6 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -71,7 +71,6 @@ import android.os.IWakeLockCallback; import android.os.Looper; import android.os.PowerManager; import android.os.PowerSaveState; -import android.os.SystemClock; import android.os.UserHandle; import android.os.test.TestLooper; import android.provider.Settings; @@ -144,6 +143,7 @@ public class PowerManagerServiceTest { @Mock private AmbientDisplayConfiguration mAmbientDisplayConfigurationMock; @Mock private SystemPropertiesWrapper mSystemPropertiesMock; @Mock private AppOpsManager mAppOpsManagerMock; + @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock; @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; @@ -298,8 +298,7 @@ public class PowerManagerServiceTest { @Override LowPowerStandbyController createLowPowerStandbyController(Context context, Looper looper) { - return new LowPowerStandbyController(context, mTestLooper.getLooper(), - SystemClock::elapsedRealtime); + return mLowPowerStandbyControllerMock; } @Override @@ -316,7 +315,6 @@ public class PowerManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.removeServiceForTest(BatteryManagerInternal.class); LocalServices.removeServiceForTest(ActivityManagerInternal.class); - LocalServices.removeServiceForTest(LowPowerStandbyControllerInternal.class); FakeSettingsProvider.clearSettingsProvider(); } @@ -1888,6 +1886,18 @@ public class PowerManagerServiceTest { assertThat(wakeLock.mDisabled).isFalse(); } + @Test + public void testSetLowPowerStandbyActiveDuringMaintenance_redirectsCallToNativeWrapper() { + createService(); + startSystem(); + + mService.getBinderServiceInstance().setLowPowerStandbyActiveDuringMaintenance(true); + verify(mLowPowerStandbyControllerMock).setActiveDuringMaintenance(true); + + mService.getBinderServiceInstance().setLowPowerStandbyActiveDuringMaintenance(false); + verify(mLowPowerStandbyControllerMock).setActiveDuringMaintenance(false); + } + private WakeLock acquireWakeLock(String tag, int flags) { IBinder token = new Binder(); String packageName = "pkg.name"; 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 348e015500fe..c0cd7a755e25 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -198,6 +198,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.InstanceIdSequenceFake; +import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.statusbar.NotificationVisibility; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; @@ -303,6 +304,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ActivityManagerInternal mAmi; @Mock private Looper mMainLooper; + @Mock + private NotificationManager mMockNm; @Mock IIntentSender pi1; @@ -405,6 +408,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { LocalServices.removeServiceForTest(PermissionPolicyInternal.class); LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal); mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager); + mContext.addMockSystemService(NotificationManager.class, mMockNm); doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any()); @@ -7516,46 +7520,53 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testOnBubbleNotificationSuppressionChanged() throws Exception { + public void testOnBubbleMetadataFlagChanged() throws Exception { setUpPrefsForBubbles(PKG, mUid, true /* global */, BUBBLE_PREFERENCE_ALL /* app */, true /* channel */); - // Bubble notification + // Post a bubble notification NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag"); - + // Set this so that the bubble can be suppressed + nr.getNotification().getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE); mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); waitForIdle(); - // NOT suppressed + // Check the flags Notification n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); assertFalse(n.getBubbleMetadata().isNotificationSuppressed()); + assertFalse(n.getBubbleMetadata().getAutoExpandBubble()); + assertFalse(n.getBubbleMetadata().isBubbleSuppressed()); + assertTrue(n.getBubbleMetadata().isBubbleSuppressable()); // Reset as this is called when the notif is first sent reset(mListeners); - // Test: update suppression to true - mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), true, - false); + // Test: change the flags + int flags = Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE; + flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; + mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), flags); waitForIdle(); // Check n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); - assertTrue(n.getBubbleMetadata().isNotificationSuppressed()); + assertEquals(flags, n.getBubbleMetadata().getFlags()); // Reset to check again reset(mListeners); - // Test: update suppression to false - mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), false, - false); + // Test: clear flags + mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), 0); waitForIdle(); // Check n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); - assertFalse(n.getBubbleMetadata().isNotificationSuppressed()); + assertEquals(0, n.getBubbleMetadata().getFlags()); } @Test @@ -9294,4 +9305,77 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // the notifyPostedLocked function is called twice. verify(mListeners, times(2)).notifyPostedLocked(any(), any()); } + + @Test + public void testMaybeShowReviewPermissionsNotification_unknown() { + // Set up various possible states of the settings int and confirm whether or not the + // notification is shown as expected + + // Initial state: default/unknown setting, make sure nothing happens + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_shouldShow() { + // If state is SHOULD_SHOW, it ... should show + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_alreadyShown() { + // If state is either USER_INTERACTED or DISMISSED, we should not show this on boot + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + mService.maybeShowInitialReviewPermissionsNotification(); + + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + mService.maybeShowInitialReviewPermissionsNotification(); + + verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_reshown() { + // If we have re-shown the notification and the user did not subsequently interacted with + // it, then make sure we show when trying on boot + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + } + + @Test + public void testRescheduledReviewPermissionsNotification() { + // when rescheduled, the notification goes through the NotificationManagerInternal service + // this call doesn't need to know anything about previously scheduled state -- if called, + // it should send the notification & write the appropriate int to Settings + mInternalService.sendReviewPermissionsNotification(); + + // Notification should be sent + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + + // write STATE_RESHOWN to settings + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 63d7453450d2..6d0895935877 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -289,6 +289,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) .build(); + + // make sure that the settings for review notification permissions are unset to begin with + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); } private ByteArrayOutputStream writeXmlAndPurge( @@ -656,6 +661,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { verify(mPermissionHelper).setNotificationPermission(nMr1Expected); verify(mPermissionHelper).setNotificationPermission(oExpected); verify(mPermissionHelper).setNotificationPermission(pExpected); + + // verify that we also write a state for review_permissions_notification to eventually + // show a notification + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); } @Test @@ -738,7 +750,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testReadXml_newXml_noMigration() throws Exception { + public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { when(mPermissionHelper.isMigrationEnabled()).thenReturn(true); mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory); @@ -786,6 +798,70 @@ public class PreferencesHelperTest extends UiServiceTestCase { compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false)); verify(mPermissionHelper, never()).setNotificationPermission(any()); + + // verify that we do, however, write a state for review_permissions_notification to + // eventually show a notification, since this XML version is older than the notification + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { + when(mPermissionHelper.isMigrationEnabled()).thenReturn(true); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory); + + String xml = "<ranking version=\"4\">\n" + + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" + + "<channel id=\"idn\" name=\"name\" importance=\"2\"/>\n" + + "<channel id=\"miscellaneous\" name=\"Uncategorized\" />\n" + + "</package>\n" + + "<package name=\"" + PKG_O + "\" >\n" + + "<channel id=\"ido\" name=\"name2\" importance=\"2\" show_badge=\"true\"/>\n" + + "</package>\n" + + "<package name=\"" + PKG_P + "\" >\n" + + "<channel id=\"idp\" name=\"name3\" importance=\"4\" locked=\"2\" />\n" + + "</package>\n" + + "</ranking>\n"; + NotificationChannel idn = new NotificationChannel("idn", "name", IMPORTANCE_LOW); + idn.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + idn.setShowBadge(false); + NotificationChannel ido = new NotificationChannel("ido", "name2", IMPORTANCE_LOW); + ido.setShowBadge(true); + ido.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + NotificationChannel idp = new NotificationChannel("idp", "name3", IMPORTANCE_HIGH); + idp.lockFields(2); + idp.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + + loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM); + + assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); + + assertEquals(idn, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, idn.getId(), false)); + compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false)); + compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false)); + + verify(mPermissionHelper, never()).setNotificationPermission(any()); + + // this XML is new enough, we should not be attempting to show a notification or anything + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); } @Test @@ -903,7 +979,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, false, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" + "<package name=\"com.example.o\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " + "sent_valid_msg=\"false\" user_demote_msg_app=\"false\" uid=\"1111\">\n" @@ -984,7 +1060,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Importance 0 because off in permissionhelper + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " @@ -1067,7 +1143,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Importance 0 because off in permissionhelper + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " @@ -1121,7 +1197,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Packages that exist solely in permissionhelper + "<package name=\"" + PKG_P + "\" importance=\"3\" />\n" + "<package name=\"" + PKG_O + "\" importance=\"0\" />\n" diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java new file mode 100644 index 000000000000..5a4ce5da676e --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 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 junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.testing.AndroidTestingRunner; + +import androidx.test.rule.ServiceTestRule; + +import com.android.server.LocalServices; +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +@RunWith(AndroidTestingRunner.class) +public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase { + private ReviewNotificationPermissionsJobService mJobService; + private JobParameters mJobParams = new JobParameters(null, + ReviewNotificationPermissionsJobService.JOB_ID, null, null, null, + 0, false, false, null, null, null); + + @Captor + ArgumentCaptor<JobInfo> mJobInfoCaptor; + + @Mock + private JobScheduler mMockJobScheduler; + + @Mock + private NotificationManagerInternal mMockNotificationManagerInternal; + + @Rule + public final ServiceTestRule mServiceRule = new ServiceTestRule(); + + @Before + public void setUp() throws Exception { + mJobService = new ReviewNotificationPermissionsJobService(); + mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler); + + // add NotificationManagerInternal to LocalServices + LocalServices.removeServiceForTest(NotificationManagerInternal.class); + LocalServices.addService(NotificationManagerInternal.class, + mMockNotificationManagerInternal); + } + + @Test + public void testScheduleJob() { + // if asked, the job doesn't currently exist yet + when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null); + + final int rescheduleTimeMillis = 350; // arbitrary number + + // attempt to schedule the job + ReviewNotificationPermissionsJobService.scheduleJob(mContext, rescheduleTimeMillis); + verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture()); + + // verify various properties of the job that is passed in to the job scheduler + JobInfo jobInfo = mJobInfoCaptor.getValue(); + assertEquals(ReviewNotificationPermissionsJobService.JOB_ID, jobInfo.getId()); + assertEquals(rescheduleTimeMillis, jobInfo.getMinLatencyMillis()); + assertTrue(jobInfo.isPersisted()); // should continue after reboot + assertFalse(jobInfo.isPeriodic()); // one time + } + + @Test + public void testOnStartJob() { + // the job need not be persisted after it does its work, so it'll return + // false + assertFalse(mJobService.onStartJob(mJobParams)); + + // verify that starting the job causes the notification to be sent + verify(mMockNotificationManagerInternal).sendReviewPermissionsNotification(); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java new file mode 100644 index 000000000000..12281a742a50 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 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 junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReviewNotificationPermissionsReceiverTest extends UiServiceTestCase { + + // Simple mock class that just overrides the reschedule and cancel behavior so that it's easy + // to tell whether the receiver has sent requests to either reschedule or cancel the + // notification (or both). + private class MockReviewNotificationPermissionsReceiver + extends ReviewNotificationPermissionsReceiver { + boolean mCanceled = false; + boolean mRescheduled = false; + + @Override + protected void cancelNotification(Context context) { + mCanceled = true; + } + + @Override + protected void rescheduleNotification(Context context) { + mRescheduled = true; + } + } + + private MockReviewNotificationPermissionsReceiver mReceiver; + private Intent mIntent; + + @Before + public void setUp() { + mReceiver = new MockReviewNotificationPermissionsReceiver(); + mIntent = new Intent(); // actions will be set in test cases + } + + @Test + public void testReceive_remindMeLater_firstTime() { + // Test what happens when we receive a "remind me later" intent coming from + // a previously-not-interacted notification + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + + // Upon receipt of the intent, the following things should happen: + // - notification rescheduled + // - notification explicitly canceled + // - settings state updated to indicate user has interacted + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_remindMeLater_laterTimes() { + // Test what happens when we receive a "remind me later" intent coming from + // a previously-interacted notification that has been rescheduled + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + + // Upon receipt of the intent, the following things should still happen + // regardless of the fact that the user has interacted before: + // - notification rescheduled + // - notification explicitly canceled + // - settings state still indicate user has interacted + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_dismiss() { + // Test that dismissing the notification does *not* reschedule the notification, + // does cancel it, and writes that it has been dismissed to settings + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS); + + // send intent, watch what happens + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_firstSwipe() { + // Test the basic swipe away case: the first time the user swipes the notification + // away, it will not have been interacted with yet, so make sure it's rescheduled + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // send intent, make sure it gets: + // - rescheduled + // - not explicitly canceled, the notification was already canceled + // - noted that it's been interacted with + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_secondSwipe() { + // Test the swipe away case for a rescheduled notification: in this case + // it should not be rescheduled anymore + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // send intent, make sure it gets: + // - not rescheduled on the second+ swipe + // - not explicitly canceled, the notification was already canceled + // - mark as user interacted + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_fromDismiss() { + // Test that if the notification delete intent is called due to us canceling + // the notification from the receiver, we don't do anything extra + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // nothing should happen, nothing at all + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 908de34352c9..f59ec42a4a71 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.Activity.RESULT_CANCELED; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.START_ABORTED; import static android.app.ActivityManager.START_CANCELED; @@ -1297,6 +1298,22 @@ public class ActivityStarterTests extends WindowTestsBase { assertEquals(targetRecord.getLaunchIntoPipHostActivity(), sourceRecord); } + @Test + public void testResultCanceledWhenNotAllowedStartingActivity() { + final ActivityStarter starter = prepareStarter(0, false); + final ActivityRecord targetRecord = new ActivityBuilder(mAtm).build(); + final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).build(); + targetRecord.resultTo = sourceRecord; + + // Abort the activity start and ensure the sourceRecord gets the result (RESULT_CANCELED). + spyOn(starter); + doReturn(START_ABORTED).when(starter).isAllowedToStart(any(), anyBoolean(), any()); + startActivityInner(starter, targetRecord, sourceRecord, null /* options */, + null /* inTask */, null /* inTaskFragment */); + verify(sourceRecord).sendResult(anyInt(), any(), anyInt(), eq(RESULT_CANCELED), any(), + any()); + } + private static void startActivityInner(ActivityStarter starter, ActivityRecord target, ActivityRecord source, ActivityOptions options, Task inTask, TaskFragment inTaskFragment) { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 263c9364c965..c5f785ea7680 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1184,6 +1184,31 @@ public class DisplayContentTests extends WindowTestsBase { } @Test + public void testComputeImeParent_remoteControlTarget() throws Exception { + final DisplayContent dc = mDisplayContent; + WindowState app1 = createWindow(null, TYPE_BASE_APPLICATION, "app1"); + WindowState app2 = createWindow(null, TYPE_BASE_APPLICATION, "app2"); + + dc.setImeLayeringTarget(app1); + dc.setImeInputTarget(app2); + dc.setRemoteInsetsController(createDisplayWindowInsetsController()); + dc.getImeTarget(IME_TARGET_LAYERING).getWindow().setWindowingMode( + WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW); + dc.getImeInputTarget().getWindowState().setWindowingMode( + WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW); + + // Expect ImeParent is null since ImeLayeringTarget and ImeInputTarget are different. + assertNull(dc.computeImeParent()); + + // ImeLayeringTarget and ImeInputTarget are updated to the same. + dc.setImeInputTarget(app1); + assertEquals(dc.getImeTarget(IME_TARGET_LAYERING), dc.getImeInputTarget()); + + // The ImeParent should be the display. + assertEquals(dc.getImeContainer().getParent().getSurfaceControl(), dc.computeImeParent()); + } + + @Test public void testInputMethodInputTarget_isClearedWhenWindowStateIsRemoved() throws Exception { final DisplayContent dc = createNewDisplay(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index f31cdcba5830..43fcc8feaec2 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -323,6 +323,16 @@ public class VoiceInteractionManagerService extends SystemService { new RoleObserver(mContext.getMainExecutor()); } + void handleUserStop(String packageName, int userHandle) { + synchronized (VoiceInteractionManagerServiceStub.this) { + ComponentName curInteractor = getCurInteractor(userHandle); + if (curInteractor != null && packageName.equals(curInteractor.getPackageName())) { + Slog.d(TAG, "switchImplementation for user stop."); + switchImplementationIfNeededLocked(true); + } + } + } + @Override public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator( @NonNull Identity originatorIdentity, IBinder client) { @@ -2071,6 +2081,7 @@ public class VoiceInteractionManagerService extends SystemService { } PackageMonitor mPackageMonitor = new PackageMonitor() { + @Override public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { if (DEBUG) Slog.d(TAG, "onHandleForceStop uid=" + uid + " doit=" + doit); @@ -2103,11 +2114,17 @@ public class VoiceInteractionManagerService extends SystemService { } setCurInteractor(null, userHandle); + // TODO: should not reset null here. But even remove this line, the + // initForUser() still reset it because the interactor will be null. Keep + // it now but we should still need to fix it. setCurRecognizer(null, userHandle); resetCurAssistant(userHandle); initForUser(userHandle); switchImplementationIfNeededLocked(true); + // When resetting the interactor, the recognizer and the assistant settings + // value, we also need to reset the assistant role to keep the values + // consistent. Clear the assistant role will reset to the default value. Context context = getContext(); context.getSystemService(RoleManager.class).clearRoleHoldersAsUser( RoleManager.ROLE_ASSISTANT, 0, UserHandle.of(userHandle), diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index edf1002221ba..055864834b3b 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -29,6 +29,7 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.AppGlobals; +import android.app.ApplicationExitInfo; import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.content.BroadcastReceiver; @@ -38,6 +39,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.content.pm.ServiceInfo; import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.SoundTrigger; @@ -149,6 +151,32 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne resetHotwordDetectionConnectionLocked(); } } + + @Override + public void onBindingDied(ComponentName name) { + Slog.d(TAG, "onBindingDied to " + name); + String packageName = name.getPackageName(); + ParceledListSlice<ApplicationExitInfo> plistSlice = null; + try { + plistSlice = mAm.getHistoricalProcessExitReasons(packageName, 0, 1, mUser); + } catch (RemoteException e) { + // do nothing. The local binder so it can not throw it. + } + if (plistSlice == null) { + return; + } + List<ApplicationExitInfo> list = plistSlice.getList(); + if (list.isEmpty()) { + return; + } + // TODO(b/229956310): Refactor the logic of PackageMonitor and onBindingDied + ApplicationExitInfo info = list.get(0); + if (info.getReason() == ApplicationExitInfo.REASON_USER_REQUESTED + && info.getSubReason() == ApplicationExitInfo.SUBREASON_STOP_APP) { + // only handle user stopped the application from the task manager + mServiceStub.handleUserStop(packageName, mUser); + } + } }; VoiceInteractionManagerServiceImpl(Context context, Handler handler, diff --git a/telephony/java/android/telephony/ims/ProvisioningManager.java b/telephony/java/android/telephony/ims/ProvisioningManager.java index 2833489d654c..f65b7c2a4ca7 100644 --- a/telephony/java/android/telephony/ims/ProvisioningManager.java +++ b/telephony/java/android/telephony/ims/ProvisioningManager.java @@ -1500,8 +1500,8 @@ public class ProvisioningManager { * Get the provisioning status for the IMS RCS capability specified. * * If provisioning is not required for the queried - * {@link ImsRcsManager.RcsImsCapabilityFlag} this method will always return - * {@code true}. + * {@link ImsRcsManager.RcsImsCapabilityFlag} or if the device does not support IMS + * this method will always return {@code true}. * * @see CarrierConfigManager.Ims#KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL * @return true if the device is provisioned for the capability or does not require @@ -1530,8 +1530,8 @@ public class ProvisioningManager { * Get the provisioning status for the IMS RCS capability specified. * * If provisioning is not required for the queried - * {@link ImsRcsManager.RcsImsCapabilityFlag} this method - * will always return {@code true}. + * {@link ImsRcsManager.RcsImsCapabilityFlag} or if the device does not support IMS + * this method will always return {@code true}. * * <p> Requires Permission: * <ul> @@ -1640,7 +1640,8 @@ public class ProvisioningManager { * </ul> * * @return true if provisioning is required for the MMTEL capability and IMS - * registration technology specified, false if it is not required. + * registration technology specified, false if it is not required or if the device does not + * support IMS. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public boolean isProvisioningRequiredForCapability( @@ -1667,7 +1668,8 @@ public class ProvisioningManager { * </ul> * * @return true if provisioning is required for the RCS capability and IMS - * registration technology specified, false if it is not required. + * registration technology specified, false if it is not required or if the device does not + * support IMS. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public boolean isRcsProvisioningRequiredForCapability( |