diff options
72 files changed, 1605 insertions, 5115 deletions
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index f7f0235cd508..93748f81ffa1 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -55,6 +55,14 @@ public final class AssociationInfo implements Parcelable { private final boolean mSelfManaged; private final boolean mNotifyOnDeviceNearby; + + /** + * Indicates that the association has been revoked (removed), but we keep the association + * record for final clean up (e.g. removing the app from the list of the role holders). + * + * @see CompanionDeviceManager#disassociate(int) + */ + private final boolean mRevoked; private final long mTimeApprovedMs; /** * A long value indicates the last time connected reported by selfManaged devices @@ -71,7 +79,7 @@ public final class AssociationInfo implements Parcelable { public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, boolean selfManaged, boolean notifyOnDeviceNearby, - long timeApprovedMs, long lastTimeConnectedMs) { + boolean revoked, long timeApprovedMs, long lastTimeConnectedMs) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } @@ -91,6 +99,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; + mRevoked = revoked; mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; } @@ -176,6 +185,14 @@ public final class AssociationInfo implements Parcelable { } /** + * @return if the association has been revoked (removed). + * @hide + */ + public boolean isRevoked() { + return mRevoked; + } + + /** * @return the last time self reported disconnected for selfManaged only. * @hide */ @@ -244,6 +261,7 @@ public final class AssociationInfo implements Parcelable { + ", mDeviceProfile='" + mDeviceProfile + '\'' + ", mSelfManaged=" + mSelfManaged + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + + ", mRevoked=" + mRevoked + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + ", mLastTimeConnectedMs=" + ( mLastTimeConnectedMs == Long.MAX_VALUE @@ -260,6 +278,7 @@ public final class AssociationInfo implements Parcelable { && mUserId == that.mUserId && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby + && mRevoked == that.mRevoked && mTimeApprovedMs == that.mTimeApprovedMs && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) @@ -271,7 +290,7 @@ public final class AssociationInfo implements Parcelable { @Override public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mDeviceMacAddress, mDisplayName, - mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs, + mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mTimeApprovedMs, mLastTimeConnectedMs); } @@ -293,6 +312,7 @@ public final class AssociationInfo implements Parcelable { dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); + dest.writeBoolean(mRevoked); dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); } @@ -309,6 +329,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); + mRevoked = in.readBoolean(); mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); } @@ -352,11 +373,13 @@ public final class AssociationInfo implements Parcelable { @NonNull private final AssociationInfo mOriginalInfo; private boolean mNotifyOnDeviceNearby; + private boolean mRevoked; private long mLastTimeConnectedMs; private Builder(@NonNull AssociationInfo info) { mOriginalInfo = info; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; + mRevoked = info.mRevoked; mLastTimeConnectedMs = info.mLastTimeConnectedMs; } @@ -388,6 +411,17 @@ public final class AssociationInfo implements Parcelable { } /** + * Should only be used by the CompanionDeviceManagerService. + * @hide + */ + @Override + @NonNull + public Builder setRevoked(boolean revoked) { + mRevoked = revoked; + return this; + } + + /** * @hide */ @NonNull @@ -401,6 +435,7 @@ public final class AssociationInfo implements Parcelable { mOriginalInfo.mDeviceProfile, mOriginalInfo.mSelfManaged, mNotifyOnDeviceNearby, + mRevoked, mOriginalInfo.mTimeApprovedMs, mLastTimeConnectedMs ); @@ -433,5 +468,12 @@ public final class AssociationInfo implements Parcelable { */ @NonNull Builder setLastTimeConnected(long lastTimeConnectedMs); + + /** + * Should only be used by the CompanionDeviceManagerService. + * @hide + */ + @NonNull + Builder setRevoked(boolean revoked); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 28427a808d90..c80c14f353d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -42,6 +42,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SP import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.isOpeningType; +import android.animation.Animator; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; @@ -248,6 +249,13 @@ public class PipTransition extends PipTransitionController { return false; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + /** Helper to identify whether this handler is currently the one playing an animation */ private boolean isAnimatingLocally() { return mFinishTransaction != null; @@ -283,6 +291,13 @@ public class PipTransition extends PipTransitionController { } @Override + public void end() { + Animator animator = mPipAnimationController.getCurrentAnimator(); + if (animator == null) return; + animator.end(); + } + + @Override public boolean handleRotateDisplay(int startRotation, int endRotation, WindowContainerTransaction wct) { if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index d3f69f6762f9..a43b6043908b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -237,6 +237,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH @NonNull final Transitions.TransitionFinishCallback finishCallback) { } + /** End the currently-playing PiP animation. */ + public void end() { + } + /** * Callback interface for PiP transitions (both from and to PiP mode) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index f7057d454df9..e55729a883e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -225,9 +225,25 @@ class SplitScreenTransitions { void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) { + if (mergeTarget != mAnimatingTransition) return; + if (mActiveRemoteHandler != null) { mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } else { + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); + } + } + } + + boolean end() { + // If its remote, there's nothing we can do right now. + if (mActiveRemoteHandler != null) return false; + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animator anim = mAnimations.get(i); + mTransitions.getAnimExecutor().execute(anim::end); } + return true; } void onTransitionMerged(@NonNull IBinder transition) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 6cfb700fc16a..4e7b20e3ae84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1521,6 +1521,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } + /** Jump the current transition animation to the end. */ + public boolean end() { + return mSplitTransitions.end(); + } + @Override public void onTransitionMerged(@NonNull IBinder transition) { mSplitTransitions.onTransitionMerged(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 1ffe26df729f..7234d559e153 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -53,10 +53,18 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { private static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; + /** The default animation for this mixed transition. */ + static final int ANIM_TYPE_DEFAULT = 0; + + /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */ + static final int ANIM_TYPE_GOING_HOME = 1; + final int mType; + int mAnimType = 0; final IBinder mTransition; Transitions.TransitionFinishCallback mFinishCallback = null; + Transitions.TransitionHandler mLeftoversHandler = null; /** * Mixed transitions are made up of multiple "parts". This keeps track of how many @@ -128,7 +136,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { MixedTransition mixed = null; for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { if (mActiveTransitions.get(i).mTransition != transition) continue; - mixed = mActiveTransitions.remove(i); + mixed = mActiveTransitions.get(i); break; } if (mixed == null) return false; @@ -137,6 +145,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction, finishCallback); } else { + mActiveTransitions.remove(mixed); throw new IllegalStateException("Starting mixed animation without a known mixed type? " + mixed.mType); } @@ -178,6 +187,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { --mixed.mInFlightSubAnimations; if (mixed.mInFlightSubAnimations > 0) return; + mActiveTransitions.remove(mixed); if (isGoingHome) { mSplitHandler.onTransitionAnimationComplete(); } @@ -216,8 +226,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { finishCB); // Dispatch the rest of the transition normally. This will most-likely be taken by // recents or default handler. - mPlayer.dispatchTransition(mixed.mTransition, everythingElse, otherStartT, - finishTransaction, finishCB, this); + mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse, + otherStartT, finishTransaction, finishCB, this); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + "forward animation to Pip-Handler."); @@ -235,6 +245,32 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (int i = 0; i < mActiveTransitions.size(); ++i) { + if (mActiveTransitions.get(i) != mergeTarget) continue; + MixedTransition mixed = mActiveTransitions.get(i); + if (mixed.mInFlightSubAnimations <= 0) { + // Already done, so no need to end it. + return; + } + if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) { + boolean ended = mSplitHandler.end(); + // If split couldn't end (because it is remote), then don't end everything else + // since we have to play out the animation anyways. + if (!ended) return; + mPipHandler.end(); + if (mixed.mLeftoversHandler != null) { + mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + } + } else { + mPipHandler.end(); + } + } else { + throw new IllegalStateException("Playing a mixed transition with unknown type? " + + mixed.mType); + } + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index c3eaa8ee1da0..05e5b8e66a00 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -523,6 +523,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ArrayList<Animator> anims = mAnimations.get(mergeTarget); + if (anims == null) return; + for (int i = anims.size() - 1; i >= 0; --i) { + final Animator anim = anims.get(i); + mAnimExecutor.execute(anim::end); + } + } + private void edgeExtendWindow(TransitionInfo.Change change, Animation a, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index 20fac93df0af..db14fdf5930b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -474,7 +474,13 @@ class TextInterpolator(layout: Layout) { val out = mutableListOf<List<PositionedGlyphs>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) - val count = layout.getLineEnd(lineNo) - lineStart + var count = layout.getLineEnd(lineNo) - lineStart + // Do not render the last character in the line if it's a newline and unprintable + val last = lineStart + count - 1 + if (last > lineStart && last < layout.text.length && layout.text[last] == '\n') { + count-- + } + val runs = mutableListOf<PositionedGlyphs>() TextShaper.shapeText( layout.text, diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 0c1916074e0c..cafdc8676173 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -13,9 +13,9 @@ */ package com.android.systemui.plugins +import android.content.res.Resources import android.graphics.drawable.Drawable import android.view.View -import com.android.internal.colorextraction.ColorExtractor import com.android.systemui.plugins.annotations.ProvidesInterface import java.io.PrintWriter import java.util.Locale @@ -57,7 +57,15 @@ interface Clock { val events: ClockEvents /** Triggers for various animations */ - val animation: ClockAnimation + val animations: ClockAnimations + + /** Initializes various rendering parameters. If never called, provides reasonable defaults. */ + fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { + events.onColorPaletteChanged(resources) + animations.doze(dozeFraction) + animations.fold(foldFraction) + events.onTimeTick() + } /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) { } @@ -80,15 +88,12 @@ interface ClockEvents { /** Call whenever font settings change */ fun onFontSettingChanged() { } - /** Call whenever the color pallete should update */ - fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) { } + /** Call whenever the color palette should update */ + fun onColorPaletteChanged(resources: Resources) { } } /** Methods which trigger various clock animations */ -interface ClockAnimation { - /** Initializes the doze & fold animation positions. Defaults to neither folded nor dozing. */ - fun initialize(dozeFraction: Float, foldFraction: Float) { } - +interface ClockAnimations { /** Runs an enter animation (if any) */ fun enter() { } diff --git a/packages/SystemUI/res-keyguard/font/clock.xml b/packages/SystemUI/res-keyguard/font/clock.xml deleted file mode 100644 index 0137dc39921f..000000000000 --- a/packages/SystemUI/res-keyguard/font/clock.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -** -** Copyright 2020, 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. -*/ ---> - -<!-- -** AOD/LockScreen Clock font. -** Should include all numeric glyphs in all supported locales. -** Recommended: font with variable width to support AOD => LS animations ---> -<!-- TODO: Remove when clock migration complete --> -<font-family xmlns:android="http://schemas.android.com/apk/res/android"> - <font android:typeface="monospace"/> -</font-family>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index 6a38507b2ad7..8b8ebf00e190 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -31,42 +31,14 @@ android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:paddingStart="@dimen/clock_padding_start"> - <com.android.systemui.shared.clocks.AnimatableClockView - android:id="@+id/animatable_clock_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="start" - android:gravity="start" - android:textSize="@dimen/clock_text_size" - android:fontFamily="@font/clock" - android:elegantTextHeight="false" - android:singleLine="true" - android:fontFeatureSettings="pnum" - chargeAnimationDelay="350" - dozeWeight="200" - lockScreenWeight="400" - /> </FrameLayout> <FrameLayout android:id="@+id/lockscreen_clock_view_large" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:layout_below="@id/keyguard_slice_view" + android:paddingTop="@dimen/keyguard_large_clock_top_padding" android:visibility="gone"> - <com.android.systemui.shared.clocks.AnimatableClockView - android:id="@+id/animatable_clock_view_large" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:gravity="center_horizontal" - android:textSize="@dimen/large_clock_text_size" - android:fontFamily="@font/clock" - android:typeface="monospace" - android:elegantTextHeight="false" - chargeAnimationDelay="200" - dozeWeight="200" - lockScreenWeight="400" - /> </FrameLayout> <!-- Not quite optimal but needed to translate these items as a group. The diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index e066d38e446f..27ee428d3528 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -19,7 +19,7 @@ android:id="@+id/time_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:fontFamily="@font/clock" + android:fontFamily="@*android:string/config_clockFontFamily" android:includeFontPadding="false" android:textColor="@android:color/white" android:format12Hour="h:mm" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3b8505015f3e..1210b79d3ff5 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -661,13 +661,7 @@ <!-- When large clock is showing, offset the smartspace by this amount --> <dimen name="keyguard_smartspace_top_offset">12dp</dimen> <!-- With the large clock, move up slightly from the center --> - <dimen name="keyguard_large_clock_top_margin">-60dp</dimen> - - <!-- TODO: Remove during migration --> - <!-- Default line spacing multiplier between hours and minutes of the keyguard clock --> - <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item> - <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock --> - <item name="keyguard_clock_line_spacing_scale_burmese" type="dimen" format="float">1</item> + <dimen name="keyguard_large_clock_top_padding">100dp</dimen> <dimen name="notification_scrim_corner_radius">32dp</dimen> @@ -890,11 +884,6 @@ burn-in on AOD. --> <dimen name="burn_in_prevention_offset_y_clock">42dp</dimen> - <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) --> - <!-- TODO: Remove when clock migration complete --> - <dimen name="large_clock_text_size">150dp</dimen> - <dimen name="clock_text_size">86dp</dimen> - <!-- The maximum offset in either direction that icons move to prevent burn-in on AOD. --> <dimen name="default_burn_in_prevention_offset">15dp</dimen> diff --git a/packages/SystemUI/shared/res/layout/clock_default_large.xml b/packages/SystemUI/shared/res/layout/clock_default_large.xml index 8510a0a8b550..0139d50dcfba 100644 --- a/packages/SystemUI/shared/res/layout/clock_default_large.xml +++ b/packages/SystemUI/shared/res/layout/clock_default_large.xml @@ -18,7 +18,6 @@ --> <com.android.systemui.shared.clocks.AnimatableClockView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/animatable_clock_view_large" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" diff --git a/packages/SystemUI/shared/res/layout/clock_default_small.xml b/packages/SystemUI/shared/res/layout/clock_default_small.xml index ec0e427e6a4d..390ff5e3ff78 100644 --- a/packages/SystemUI/shared/res/layout/clock_default_small.xml +++ b/packages/SystemUI/shared/res/layout/clock_default_small.xml @@ -18,7 +18,6 @@ --> <com.android.systemui.shared.clocks.AnimatableClockView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/animatable_clock_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" @@ -26,6 +25,7 @@ android:textSize="@dimen/small_clock_text_size" android:fontFamily="@*android:string/config_clockFontFamily" android:elegantTextHeight="false" + android:ellipsize="none" android:singleLine="true" android:fontFeatureSettings="pnum" chargeAnimationDelay="350" diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 2739d59dbf00..8f1959e884cf 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -20,12 +20,15 @@ import android.annotation.ColorInt import android.annotation.FloatRange import android.annotation.IntRange import android.annotation.SuppressLint +import android.app.compat.ChangeIdStateCache.invalidate import android.content.Context import android.graphics.Canvas import android.text.TextUtils import android.text.format.DateFormat import android.util.AttributeSet import android.widget.TextView +import com.android.internal.R.attr.contentDescription +import com.android.internal.R.attr.format import com.android.systemui.animation.GlyphCallback import com.android.systemui.animation.Interpolators import com.android.systemui.animation.TextAnimator @@ -75,6 +78,12 @@ class AnimatableClockView @JvmOverloads constructor( val lockScreenWeight: Int get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal + /** + * The number of pixels below the baseline. For fonts that support languages such as + * Burmese, this space can be significant and should be accounted for when computing layout. + */ + val bottom get() = paint?.fontMetrics?.bottom ?: 0f + init { val animatableClockViewAttributes = context.obtainStyledAttributes( attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes @@ -133,6 +142,15 @@ class AnimatableClockView @JvmOverloads constructor( // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText + + // Because the TextLayout may mutate under the hood as a result of the new text, we + // notify the TextAnimator that it may have changed and request a measure/layout. A + // crash will occur on the next invocation of setTextStyle if the layout is mutated + // without being notified TextInterpolator being notified. + if (layout != null) { + textAnimator?.updateLayout(layout) + } + requestLayout() } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index a4c03b0b57c8..4b8b46d54848 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -35,8 +35,6 @@ import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName private val DEBUG = true -typealias ClockChangeListener = () -> Unit - /** ClockRegistry aggregates providers and plugins */ open class ClockRegistry( val context: Context, @@ -51,12 +49,17 @@ open class ClockRegistry( defaultClockProvider: DefaultClockProvider ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } + // Usually this would be a typealias, but a SAM provides better java interop + fun interface ClockChangeListener { + fun onClockChanged() + } + private val gson = Gson() private val availableClocks = mutableMapOf<ClockId, ClockInfo>() private val clockChangeListeners = mutableListOf<ClockChangeListener>() private val settingObserver = object : ContentObserver(handler) { override fun onChange(selfChange: Boolean, uris: Collection<Uri>, flags: Int, userId: Int) = - clockChangeListeners.forEach { it() } + clockChangeListeners.forEach { it.onClockChanged() } } private val pluginListener = object : PluginListener<ClockProviderPlugin> { @@ -73,7 +76,7 @@ open class ClockRegistry( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE ) - return gson.fromJson(json, ClockSetting::class.java).clockId + return gson.fromJson(json, ClockSetting::class.java)?.clockId ?: DEFAULT_CLOCK_ID } set(value) { val json = gson.toJson(ClockSetting(value, System.currentTimeMillis())) @@ -100,8 +103,11 @@ open class ClockRegistry( val id = clock.clockId val current = availableClocks[id] if (current != null) { - Log.e(TAG, "Clock Id conflict: $id is registered by both " + - "${provider::class.simpleName} and ${current.provider::class.simpleName}") + Log.e( + TAG, + "Clock Id conflict: $id is registered by both " + + "${provider::class.simpleName} and ${current.provider::class.simpleName}" + ) return } @@ -110,7 +116,7 @@ open class ClockRegistry( if (DEBUG) { Log.i(TAG, "Current clock ($currentId) was connected") } - clockChangeListeners.forEach { it() } + clockChangeListeners.forEach { it.onClockChanged() } } } } @@ -122,7 +128,7 @@ open class ClockRegistry( if (currentId == clock.clockId) { Log.w(TAG, "Current clock ($currentId) was disconnected") - clockChangeListeners.forEach { it() } + clockChangeListeners.forEach { it.onClockChanged() } } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 5d8da5985768..1d8abe3fdf42 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -19,10 +19,9 @@ import android.graphics.drawable.Drawable import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater -import com.android.internal.colorextraction.ColorExtractor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.Clock -import com.android.systemui.plugins.ClockAnimation +import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata @@ -102,10 +101,13 @@ class DefaultClock( TypedValue.COMPLEX_UNIT_PX, resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() ) + recomputePadding() } - override fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) = - clocks.forEach { it.setColors(DOZE_COLOR, palette.mainColor) } + override fun onColorPaletteChanged(resources: Resources) { + val color = resources.getColor(android.R.color.system_accent1_100) + clocks.forEach { it.setColors(DOZE_COLOR, color) } + } override fun onLocaleChanged(locale: Locale) { val nf = NumberFormat.getInstance(locale) @@ -119,8 +121,17 @@ class DefaultClock( } } - override val animation = object : ClockAnimation { - override fun initialize(dozeFraction: Float, foldFraction: Float) { + override var animations = DefaultClockAnimations(0f, 0f) + private set + + inner class DefaultClockAnimations( + dozeFraction: Float, + foldFraction: Float + ) : ClockAnimations { + private var foldState = AnimationState(0f) + private var dozeState = AnimationState(0f) + + init { dozeState = AnimationState(dozeFraction) foldState = AnimationState(foldFraction) @@ -132,14 +143,13 @@ class DefaultClock( } override fun enter() { - if (dozeState.isActive) { + if (!dozeState.isActive) { clocks.forEach { it.animateAppearOnLockscreen() } } } override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } } - private var foldState = AnimationState(0f) override fun fold(fraction: Float) { val (hasChanged, hasJumped) = foldState.update(fraction) if (hasChanged) { @@ -147,7 +157,6 @@ class DefaultClock( } } - private var dozeState = AnimationState(0f) override fun doze(fraction: Float) { val (hasChanged, hasJumped) = dozeState.update(fraction) if (hasChanged) { @@ -172,6 +181,19 @@ class DefaultClock( init { events.onLocaleChanged(Locale.getDefault()) + clocks.forEach { it.setColors(DOZE_COLOR, DOZE_COLOR) } + } + + override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { + recomputePadding() + animations = DefaultClockAnimations(dozeFraction, foldFraction) + events.onColorPaletteChanged(resources) + events.onTimeTick() + } + + private fun recomputePadding() { + val topPadding = -1 * (largeClock.bottom.toInt() - 180) + largeClock.setPadding(0, topPadding, 0, 0) } override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 76a09b38036c..302ba8444f05 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -114,6 +114,8 @@ public class RemoteAnimationAdapterCompat { private static IRemoteTransition.Stub wrapRemoteTransition( final RemoteAnimationRunnerCompat remoteAnimationAdapter) { return new IRemoteTransition.Stub() { + final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); + @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -229,19 +231,32 @@ public class RemoteAnimationAdapterCompat { } } }; + synchronized (mFinishRunnables) { + mFinishRunnables.put(token, animationFinishedCallback); + } // TODO(bc-unlcok): Pass correct transit type. - remoteAnimationAdapter.onAnimationStart( - TRANSIT_OLD_NONE, - appsCompat, wallpapersCompat, nonAppsCompat, - animationFinishedCallback); + remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE, + appsCompat, wallpapersCompat, nonAppsCompat, () -> { + synchronized (mFinishRunnables) { + if (mFinishRunnables.remove(token) == null) return; + } + animationFinishedCallback.run(); + }); } @Override public void mergeAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { - // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, ignore - // any incoming merges. + // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt + // to legacy cancel. + final Runnable finishRunnable; + synchronized (mFinishRunnables) { + finishRunnable = mFinishRunnables.remove(mergeTarget); + } + if (finishRunnable == null) return; + remoteAnimationAdapter.onAnimationCancelled(); + finishRunnable.run(); } }; } diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java index c69ff7ee1cd8..e0b11d83bf75 100644 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java @@ -182,18 +182,6 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie mStatusBarStateController.removeCallback(mStatusBarStateListener); } - /** - * @return the number of pixels below the baseline. For fonts that support languages such as - * Burmese, this space can be significant. - */ - public float getBottom() { - if (mView.getPaint() != null && mView.getPaint().getFontMetrics() != null) { - return mView.getPaint().getFontMetrics().bottom; - } - - return 0f; - } - /** Animate the clock appearance */ public void animateAppear() { if (!mIsDozing) mView.animateAppearOnLockscreen(); diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt new file mode 100644 index 000000000000..efd7bcf10cd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -0,0 +1,153 @@ +/* + * 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.keyguard + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Resources +import android.text.format.DateFormat +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.Clock +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback +import com.android.systemui.statusbar.policy.ConfigurationController +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone +import javax.inject.Inject + +/** + * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by + * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. + */ +class ClockEventController @Inject constructor( + private val statusBarStateController: StatusBarStateController, + private val broadcastDispatcher: BroadcastDispatcher, + private val batteryController: BatteryController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val configurationController: ConfigurationController, + @Main private val resources: Resources, + private val context: Context +) { + var clock: Clock? = null + set(value) { + field = value + if (value != null) { + value.initialize(resources, dozeAmount, 0f) + } + } + + private var isDozing = false + private set + + private var isCharging = false + private var dozeAmount = 0f + private var isKeyguardShowing = false + + private val configListener = object : ConfigurationController.ConfigurationListener { + override fun onThemeChanged() { + clock?.events?.onColorPaletteChanged(resources) + } + } + + private val batteryCallback = object : BatteryStateChangeCallback { + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + if (isKeyguardShowing && !isCharging && charging) { + clock?.animations?.charge() + } + isCharging = charging + } + } + + private val localeBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + clock?.events?.onLocaleChanged(Locale.getDefault()) + } + } + + private val statusBarStateListener = object : StatusBarStateController.StateListener { + override fun onDozeAmountChanged(linear: Float, eased: Float) { + clock?.animations?.doze(linear) + + isDozing = linear > dozeAmount + dozeAmount = linear + } + } + + private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardVisibilityChanged(showing: Boolean) { + isKeyguardShowing = showing + if (!isKeyguardShowing) { + clock?.animations?.doze(if (isDozing) 1f else 0f) + } + } + + override fun onTimeFormatChanged(timeFormat: String) { + clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + clock?.events?.onTimeZoneChanged(timeZone) + } + + override fun onUserSwitchComplete(userId: Int) { + clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context)) + } + } + + init { + isDozing = statusBarStateController.isDozing + } + + fun registerListeners() { + dozeAmount = statusBarStateController.dozeAmount + isDozing = statusBarStateController.isDozing || dozeAmount != 0f + + broadcastDispatcher.registerReceiver( + localeBroadcastReceiver, + IntentFilter(Intent.ACTION_LOCALE_CHANGED) + ) + configurationController.addCallback(configListener) + batteryController.addCallback(batteryCallback) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + statusBarStateController.addCallback(statusBarStateListener) + } + + fun unregisterListeners() { + broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) + configurationController.removeCallback(configListener) + batteryController.removeCallback(batteryCallback) + keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) + statusBarStateController.removeCallback(statusBarStateListener) + } + + /** + * Dump information for debugging + */ + fun dump(pw: PrintWriter) { + pw.println(this) + clock?.dump(pw) + } + + companion object { + private val TAG = ClockEventController::class.simpleName + private const val FORMAT_NUMBER = 1234567890 + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 206b8bee323c..e1fabdef3651 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -5,10 +5,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.Paint; -import android.graphics.Paint.Style; import android.util.AttributeSet; -import android.util.TypedValue; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -17,19 +15,14 @@ import android.widget.RelativeLayout; import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; -import com.android.internal.colorextraction.ColorExtractor; import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; -import com.android.systemui.plugins.ClockPlugin; -import com.android.systemui.shared.clocks.AnimatableClockView; +import com.android.systemui.plugins.Clock; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; -import java.util.TimeZone; - /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -50,17 +43,10 @@ public class KeyguardClockSwitch extends RelativeLayout { public static final int SMALL = 1; /** - * Optional/alternative clock injected via plugin. - */ - private ClockPlugin mClockPlugin; - - /** * Frame for small/large clocks */ - private FrameLayout mClockFrame; + private FrameLayout mSmallClockFrame; private FrameLayout mLargeClockFrame; - private AnimatableClockView mClockView; - private AnimatableClockView mLargeClockView; private View mStatusArea; private int mSmartspaceTopOffset; @@ -80,12 +66,6 @@ public class KeyguardClockSwitch extends RelativeLayout { @VisibleForTesting AnimatorSet mClockOutAnim = null; private ObjectAnimator mStatusAreaAnim = null; - /** - * If the Keyguard Slice has a header (big center-aligned text.) - */ - private boolean mSupportsDarkText; - private int[] mColorPalette; - private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; @@ -97,97 +77,38 @@ public class KeyguardClockSwitch extends RelativeLayout { * Apply dp changes on font/scale change */ public void onDensityOrFontScaleChanged() { - mLargeClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources() - .getDimensionPixelSize(R.dimen.large_clock_text_size)); - mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources() - .getDimensionPixelSize(R.dimen.clock_text_size)); - mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_clock_switch_y_shift); - mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_smartspace_top_offset); } - /** - * Returns if this view is presenting a custom clock, or the default implementation. - */ - public boolean hasCustomClock() { - return mClockPlugin != null; - } - @Override protected void onFinishInflate() { super.onFinishInflate(); - mClockFrame = findViewById(R.id.lockscreen_clock_view); - mClockView = findViewById(R.id.animatable_clock_view); + mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); - mLargeClockView = findViewById(R.id.animatable_clock_view_large); mStatusArea = findViewById(R.id.keyguard_status_area); onDensityOrFontScaleChanged(); } - void setClockPlugin(ClockPlugin plugin, int statusBarState) { + void setClock(Clock clock, int statusBarState) { // Disconnect from existing plugin. - if (mClockPlugin != null) { - View smallClockView = mClockPlugin.getView(); - if (smallClockView != null && smallClockView.getParent() == mClockFrame) { - mClockFrame.removeView(smallClockView); - } - View bigClockView = mClockPlugin.getBigClockView(); - if (bigClockView != null && bigClockView.getParent() == mLargeClockFrame) { - mLargeClockFrame.removeView(bigClockView); - } - mClockPlugin.onDestroyView(); - mClockPlugin = null; - } - if (plugin == null) { - mClockView.setVisibility(View.VISIBLE); - mLargeClockView.setVisibility(View.VISIBLE); - return; - } - // Attach small and big clock views to hierarchy. - View smallClockView = plugin.getView(); - if (smallClockView != null) { - mClockFrame.addView(smallClockView, -1, - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - mClockView.setVisibility(View.GONE); - } - View bigClockView = plugin.getBigClockView(); - if (bigClockView != null) { - mLargeClockFrame.addView(bigClockView); - mLargeClockView.setVisibility(View.GONE); - } - - // Initialize plugin parameters. - mClockPlugin = plugin; - mClockPlugin.setStyle(getPaint().getStyle()); - mClockPlugin.setTextColor(getCurrentTextColor()); - mClockPlugin.setDarkAmount(mDarkAmount); - if (mColorPalette != null) { - mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); - } - } + mSmallClockFrame.removeAllViews(); + mLargeClockFrame.removeAllViews(); - /** - * It will also update plugin setStyle if plugin is connected. - */ - public void setStyle(Style style) { - if (mClockPlugin != null) { - mClockPlugin.setStyle(style); + if (clock == null) { + Log.e(TAG, "No clock being shown"); + return; } - } - /** - * It will also update plugin setTextColor if plugin is connected. - */ - public void setTextColor(int color) { - if (mClockPlugin != null) { - mClockPlugin.setTextColor(color); - } + // Attach small and big clock views to hierarchy. + mSmallClockFrame.addView(clock.getSmallClock(), -1, + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mLargeClockFrame.addView(clock.getLargeClock()); } private void updateClockViews(boolean useLargeClock, boolean animate) { @@ -203,14 +124,14 @@ public class KeyguardClockSwitch extends RelativeLayout { int direction = 1; float statusAreaYTranslation; if (useLargeClock) { - out = mClockFrame; + out = mSmallClockFrame; in = mLargeClockFrame; if (indexOfChild(in) == -1) addView(in); direction = -1; - statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop() + statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() + mSmartspaceTopOffset; } else { - in = mClockFrame; + in = mSmallClockFrame; out = mLargeClockFrame; statusAreaYTranslation = 0f; @@ -269,18 +190,6 @@ public class KeyguardClockSwitch extends RelativeLayout { } /** - * Set the amount (ratio) that the device has transitioned to doze. - * - * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. - */ - public void setDarkAmount(float darkAmount) { - mDarkAmount = darkAmount; - if (mClockPlugin != null) { - mClockPlugin.setDarkAmount(darkAmount); - } - } - - /** * Display the desired clock and hide the other one * * @return true if desired clock appeared and false if it was already visible @@ -311,64 +220,11 @@ public class KeyguardClockSwitch extends RelativeLayout { mChildrenAreLaidOut = true; } - public Paint getPaint() { - return mClockView.getPaint(); - } - - public int getCurrentTextColor() { - return mClockView.getCurrentTextColor(); - } - - public float getTextSize() { - return mClockView.getTextSize(); - } - - /** - * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm. - */ - public void refresh() { - if (mClockPlugin != null) { - mClockPlugin.onTimeTick(); - } - } - - /** - * Notifies that the time zone has changed. - */ - public void onTimeZoneChanged(TimeZone timeZone) { - if (mClockPlugin != null) { - mClockPlugin.onTimeZoneChanged(timeZone); - } - } - - /** - * Notifies that the time format has changed. - * - * @param timeFormat "12" for 12-hour format, "24" for 24-hour format - */ - public void onTimeFormatChanged(String timeFormat) { - if (mClockPlugin != null) { - mClockPlugin.onTimeFormatChanged(timeFormat); - } - } - - void updateColors(ColorExtractor.GradientColors colors) { - mSupportsDarkText = colors.supportsDarkText(); - mColorPalette = colors.getColorPalette(); - if (mClockPlugin != null) { - mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); - } - } - public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); - pw.println(" mClockPlugin: " + mClockPlugin); - pw.println(" mClockFrame: " + mClockFrame); + pw.println(" mClockFrame: " + mSmallClockFrame); pw.println(" mLargeClockFrame: " + mLargeClockFrame); pw.println(" mStatusArea: " + mStatusArea); - pw.println(" mDarkAmount: " + mDarkAmount); - pw.println(" mSupportsDarkText: " + mSupportsDarkText); - pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 6c32a4910c56..d566f49c04ff 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -22,8 +22,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; -import android.app.WallpaperManager; -import android.content.res.Resources; import android.database.ContentObserver; import android.os.UserHandle; import android.provider.Settings; @@ -32,34 +30,28 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.RelativeLayout; import androidx.annotation.NonNull; -import com.android.internal.colorextraction.ColorExtractor; -import com.android.keyguard.clock.ClockManager; import com.android.systemui.Dumpable; import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.plugins.ClockPlugin; +import com.android.systemui.plugins.Clock; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; -import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.ViewController; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.util.Locale; -import java.util.TimeZone; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -69,48 +61,24 @@ import javax.inject.Inject; */ public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch> implements Dumpable { - private static final boolean CUSTOM_CLOCKS_ENABLED = true; - private final StatusBarStateController mStatusBarStateController; - private final SysuiColorExtractor mColorExtractor; - private final ClockManager mClockManager; + private final ClockRegistry mClockRegistry; private final KeyguardSliceViewController mKeyguardSliceViewController; private final NotificationIconAreaController mNotificationIconAreaController; - private final BroadcastDispatcher mBroadcastDispatcher; - private final BatteryController mBatteryController; private final LockscreenSmartspaceController mSmartspaceController; - private final Resources mResources; private final SecureSettings mSecureSettings; private final DumpManager mDumpManager; + private final ClockEventController mClockEventController; - /** - * Clock for both small and large sizes - */ - private AnimatableClockController mClockViewController; - private FrameLayout mClockFrame; // top aligned clock - private AnimatableClockController mLargeClockViewController; + /** Clock frames for both small and large sizes */ + private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @KeyguardClockSwitch.ClockSize private int mCurrentClockSize = SMALL; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private int mKeyguardClockTopMargin = 0; - - /** - * Listener for changes to the color palette. - * - * The color palette changes when the wallpaper is changed. - */ - private final ColorExtractor.OnColorsChangedListener mColorsListener = - (extractor, which) -> { - if ((which & WallpaperManager.FLAG_LOCK) != 0) { - mView.updateColors(getGradientColors()); - } - }; - - private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; + private final ClockRegistry.ClockChangeListener mClockChangedListener; private ViewGroup mStatusArea; // If set will replace keyguard_slice_view @@ -119,9 +87,9 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private boolean mOnlyClock = false; - private Executor mUiExecutor; + private final Executor mUiExecutor; private boolean mCanShowDoubleLineClock = true; - private ContentObserver mDoubleLineClockObserver = new ContentObserver(null) { + private final ContentObserver mDoubleLineClockObserver = new ContentObserver(null) { @Override public void onChange(boolean change) { updateDoubleLineClock(); @@ -142,34 +110,30 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS public KeyguardClockSwitchController( KeyguardClockSwitch keyguardClockSwitch, StatusBarStateController statusBarStateController, - SysuiColorExtractor colorExtractor, - ClockManager clockManager, + ClockRegistry clockRegistry, KeyguardSliceViewController keyguardSliceViewController, NotificationIconAreaController notificationIconAreaController, - BroadcastDispatcher broadcastDispatcher, - BatteryController batteryController, - KeyguardUpdateMonitor keyguardUpdateMonitor, LockscreenSmartspaceController smartspaceController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, SecureSettings secureSettings, @Main Executor uiExecutor, - @Main Resources resources, - DumpManager dumpManager) { + DumpManager dumpManager, + ClockEventController clockEventController) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; - mColorExtractor = colorExtractor; - mClockManager = clockManager; + mClockRegistry = clockRegistry; mKeyguardSliceViewController = keyguardSliceViewController; mNotificationIconAreaController = notificationIconAreaController; - mBroadcastDispatcher = broadcastDispatcher; - mBatteryController = batteryController; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; mSmartspaceController = smartspaceController; - mResources = resources; mSecureSettings = secureSettings; mUiExecutor = uiExecutor; mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mDumpManager = dumpManager; + mClockEventController = clockEventController; + + mClockChangedListener = () -> { + setClock(mClockRegistry.createCurrentClock()); + }; } /** @@ -186,40 +150,18 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS public void onInit() { mKeyguardSliceViewController.init(); - mClockFrame = mView.findViewById(R.id.lockscreen_clock_view); + mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large); - mClockViewController = - new AnimatableClockController( - mView.findViewById(R.id.animatable_clock_view), - mStatusBarStateController, - mBroadcastDispatcher, - mBatteryController, - mKeyguardUpdateMonitor, - mResources); - mClockViewController.init(); - - mLargeClockViewController = - new AnimatableClockController( - mView.findViewById(R.id.animatable_clock_view_large), - mStatusBarStateController, - mBroadcastDispatcher, - mBatteryController, - mKeyguardUpdateMonitor, - mResources); - mLargeClockViewController.init(); - mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks mDumpManager.registerDumpable(getClass().toString(), this); } @Override protected void onViewAttached() { - if (CUSTOM_CLOCKS_ENABLED) { - mClockManager.addOnClockChangedListener(mClockChangedListener); - } - mColorExtractor.addOnColorsChangedListener(mColorsListener); - mView.updateColors(getGradientColors()); + mClockRegistry.registerClockChangeListener(mClockChangedListener); + setClock(mClockRegistry.createCurrentClock()); + mClockEventController.registerListeners(); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); @@ -242,7 +184,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS ksv.setVisibility(View.GONE); addSmartspaceView(ksvIndex); - updateClockLayout(); } mSecureSettings.registerContentObserverForUser( @@ -264,11 +205,9 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Override protected void onViewDetached() { - if (CUSTOM_CLOCKS_ENABLED) { - mClockManager.removeOnClockChangedListener(mClockChangedListener); - } - mColorExtractor.removeOnColorsChangedListener(mColorsListener); - mView.setClockPlugin(null, mStatusBarStateController.getState()); + mClockRegistry.unregisterClockChangeListener(mClockChangedListener); + mClockEventController.unregisterListeners(); + setClock(null); mSecureSettings.unregisterContentObserver(mDoubleLineClockObserver); @@ -307,18 +246,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mView.onDensityOrFontScaleChanged(); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); - - updateClockLayout(); - } - - private void updateClockLayout() { - int largeClockTopMargin = getContext().getResources().getDimensionPixelSize( - R.dimen.keyguard_large_clock_top_margin) - - (int) mLargeClockViewController.getBottom(); - RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, - MATCH_PARENT); - lp.topMargin = largeClockTopMargin; - mLargeClockFrame.setLayoutParams(lp); } /** @@ -334,44 +261,31 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS boolean appeared = mView.switchToClock(clockSize, animate); if (animate && appeared && clockSize == LARGE) { - mLargeClockViewController.animateAppear(); + getClock().getAnimations().enter(); } } - public void animateFoldToAod() { - if (mClockViewController != null) { - mClockViewController.animateFoldAppear(); - mLargeClockViewController.animateFoldAppear(); - } - } - - /** - * If we're presenting a custom clock of just the default one. - */ - public boolean hasCustomClock() { - return mView.hasCustomClock(); - } - /** - * Get the clock text size. + * Animates the clock view between folded and unfolded states */ - public float getClockTextSize() { - return mView.getTextSize(); + public void animateFoldToAod(float foldFraction) { + Clock clock = getClock(); + if (clock != null) { + clock.getAnimations().fold(foldFraction); + } } /** * Refresh clock. Called in response to TIME_TICK broadcasts. */ void refresh() { - if (mClockViewController != null) { - mClockViewController.refreshTime(); - mLargeClockViewController.refreshTime(); - } if (mSmartspaceController != null) { mSmartspaceController.requestSmartspaceUpdate(); } - - mView.refresh(); + Clock clock = getClock(); + if (clock != null) { + clock.getEvents().onTimeTick(); + } } /** @@ -383,7 +297,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS void updatePosition(int x, float scale, AnimationProperties props, boolean animate) { x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x; - PropertyAnimator.setProperty(mClockFrame, AnimatableProperty.TRANSLATION_X, + PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X, x, props, animate); PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X, scale, props, animate); @@ -396,25 +310,39 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } } - void updateTimeZone(TimeZone timeZone) { - mView.onTimeZoneChanged(timeZone); - } - /** * Get y-bottom position of the currently visible clock on the keyguard. * We can't directly getBottom() because clock changes positions in AOD for burn-in */ int getClockBottom(int statusBarHeaderHeight) { + Clock clock = getClock(); + if (clock == null) { + return 0; + } + if (mLargeClockFrame.getVisibility() == View.VISIBLE) { - View clock = mLargeClockFrame.findViewById( - com.android.systemui.R.id.animatable_clock_view_large); int frameHeight = mLargeClockFrame.getHeight(); - int clockHeight = clock.getHeight(); + int clockHeight = clock.getLargeClock().getHeight(); return frameHeight / 2 + clockHeight / 2; } else { - return mClockFrame.findViewById( - com.android.systemui.R.id.animatable_clock_view).getHeight() - + statusBarHeaderHeight + mKeyguardClockTopMargin; + int clockHeight = clock.getSmallClock().getHeight(); + return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin; + } + } + + /** + * Get the height of the currently visible clock on the keyguard. + */ + int getClockHeight() { + Clock clock = getClock(); + if (clock == null) { + return 0; + } + + if (mLargeClockFrame.getVisibility() == View.VISIBLE) { + return clock.getLargeClock().getHeight(); + } else { + return clock.getSmallClock().getHeight(); } } @@ -429,12 +357,13 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mNotificationIconAreaController.setupAodIcons(nic); } - private void setClockPlugin(ClockPlugin plugin) { - mView.setClockPlugin(plugin, mStatusBarStateController.getState()); + private void setClock(Clock clock) { + mClockEventController.setClock(clock); + mView.setClock(clock, mStatusBarStateController.getState()); } - private ColorExtractor.GradientColors getGradientColors() { - return mColorExtractor.getColors(WallpaperManager.FLAG_LOCK); + private Clock getClock() { + return mClockEventController.getClock(); } private int getCurrentLayoutDirection() { @@ -467,8 +396,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("currentClockSizeLarge=" + (mCurrentClockSize == LARGE)); pw.println("mCanShowDoubleLineClock=" + mCanShowDoubleLineClock); - mClockViewController.dump(pw); - mLargeClockViewController.dump(pw); + Clock clock = getClock(); + if (clock != null) { + clock.dump(pw); + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index cb3172dabdb1..83e23bd52f19 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -17,14 +17,11 @@ package com.android.keyguard; import android.content.Context; -import android.graphics.Color; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.GridLayout; -import androidx.core.graphics.ColorUtils; - import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; @@ -46,7 +43,6 @@ public class KeyguardStatusView extends GridLayout { private View mMediaHostContainer; private float mDarkAmount = 0; - private int mTextColor; public KeyguardStatusView(Context context) { this(context, null, 0); @@ -71,7 +67,6 @@ public class KeyguardStatusView extends GridLayout { } mKeyguardSlice = findViewById(R.id.keyguard_slice_view); - mTextColor = mClockView.getCurrentTextColor(); mMediaHostContainer = findViewById(R.id.status_view_media_container); @@ -83,15 +78,12 @@ public class KeyguardStatusView extends GridLayout { return; } mDarkAmount = darkAmount; - mClockView.setDarkAmount(darkAmount); CrossFadeHelper.fadeOut(mMediaHostContainer, darkAmount); updateDark(); } void updateDark() { - final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); mKeyguardSlice.setDarkAmount(mDarkAmount); - mClockView.setTextColor(blendedTextColor); } /** Sets a translationY value on every child view except for the media view. */ @@ -113,7 +105,6 @@ public class KeyguardStatusView extends GridLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardStatusView:"); pw.println(" mDarkAmount: " + mDarkAmount); - pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); if (mClockView != null) { mClockView.dump(pw, args); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 014d08288158..c715a4eaef2b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -30,8 +30,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.ViewController; -import java.util.TimeZone; - import javax.inject.Inject; /** @@ -96,13 +94,6 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } /** - * The amount we're in doze. - */ - public void setDarkAmount(float darkAmount) { - mView.setDarkAmount(darkAmount); - } - - /** * Set which clock should be displayed on the keyguard. The other one will be automatically * hidden. */ @@ -114,16 +105,10 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV * Performs fold to aod animation of the clocks (changes font weight from bold to thin). * This animation is played when AOD is enabled and foldable device is fully folded, it is * displayed on the outer screen + * @param foldFraction current fraction of fold animation complete */ - public void animateFoldToAod() { - mKeyguardClockSwitchController.animateFoldToAod(); - } - - /** - * If we're presenting a custom clock of just the default one. - */ - public boolean hasCustomClock() { - return mKeyguardClockSwitchController.hasCustomClock(); + public void animateFoldToAod(float foldFraction) { + mKeyguardClockSwitchController.animateFoldToAod(foldFraction); } /** @@ -143,24 +128,11 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } /** - * Set pivot x. - */ - public void setPivotX(float pivot) { - mView.setPivotX(pivot); - } - - /** - * Set pivot y. - */ - public void setPivotY(float pivot) { - mView.setPivotY(pivot); - } - - /** - * Get the clock text size. + * Update the pivot position based on the parent view */ - public float getClockTextSize() { - return mKeyguardClockSwitchController.getClockTextSize(); + public void updatePivot(float parentWidth, float parentHeight) { + mView.setPivotX(parentWidth / 2f); + mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f); } /** @@ -240,11 +212,6 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } @Override - public void onTimeZoneChanged(TimeZone timeZone) { - mKeyguardClockSwitchController.updateTimeZone(timeZone); - } - - @Override public void onKeyguardVisibilityChanged(boolean showing) { if (showing) { if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index fc71e2fb2329..69e41ba9b284 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -101,6 +101,9 @@ public class DreamOverlayStateController implements public void addComplication(Complication complication) { mExecutor.execute(() -> { if (mComplications.add(complication)) { + if (DEBUG) { + Log.d(TAG, "addComplication: added " + complication); + } mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); @@ -112,6 +115,9 @@ public class DreamOverlayStateController implements public void removeComplication(Complication complication) { mExecutor.execute(() -> { if (mComplications.remove(complication)) { + if (DEBUG) { + Log.d(TAG, "removeComplication: removed " + complication); + } mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java index 486fc893732f..be94e5031917 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java @@ -82,6 +82,7 @@ public class SmartSpaceComplication implements Complication { mSmartSpaceController.addListener(mSmartspaceListener); } else { mSmartSpaceController.removeListener(mSmartspaceListener); + mDreamOverlayStateController.removeComplication(mComplication); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index c1173aea8b15..fd6cfc0700ad 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -21,6 +21,7 @@ import static com.android.systemui.dreams.complication.dagger.ComplicationModule import android.graphics.Rect; import android.graphics.Region; +import android.os.Debug; import android.util.Log; import android.view.View; @@ -44,7 +45,8 @@ import javax.inject.Named; * a {@link ComplicationLayoutEngine}. */ public class ComplicationHostViewController extends ViewController<ConstraintLayout> { - public static final String TAG = "ComplicationHostVwCtrl"; + private static final String TAG = "ComplicationHostVwCtrl"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final ComplicationLayoutEngine mLayoutEngine; private final LifecycleOwner mLifecycleOwner; @@ -90,6 +92,11 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay } private void updateComplications(Collection<ComplicationViewModel> complications) { + if (DEBUG) { + Log.d(TAG, "updateComplications called. Callers = " + Debug.getCallers(25)); + Log.d(TAG, " mComplications = " + mComplications.toString()); + Log.d(TAG, " complications = " + complications.toString()); + } final Collection<ComplicationId> ids = complications.stream() .map(complicationViewModel -> complicationViewModel.getId()) .collect(Collectors.toSet()); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java index ded61a8b80d9..9cd149b9bfee 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java @@ -54,7 +54,7 @@ import javax.inject.Named; */ @DreamOverlayComponent.DreamOverlayScope public class ComplicationLayoutEngine implements Complication.VisibilityController { - public static final String TAG = "ComplicationLayoutEngine"; + public static final String TAG = "ComplicationLayoutEng"; /** * {@link ViewEntry} is an internal container, capturing information necessary for working with @@ -529,7 +529,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll */ public void addComplication(ComplicationId id, View view, ComplicationLayoutParams lp, @Complication.Category int category) { - Log.d(TAG, "engine: " + this + " addComplication"); + Log.d(TAG, "@" + Integer.toHexString(this.hashCode()) + " addComplication: " + id); // If the complication is present, remove. if (mEntries.containsKey(id)) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java index f0239371ee63..00cf58c0c665 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java @@ -64,4 +64,9 @@ public class ComplicationViewModel extends ViewModel { public void exitDream() { mHost.requestExitDream(); } + + @Override + public String toString() { + return mId + "=" + mComplication.toString(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt index 9789cef949d3..63f63a5093d2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt @@ -145,9 +145,6 @@ class DreamSmartspaceController @Inject constructor( if (view !is View) { return null } - - view.setIsDreaming(true) - return view } else { null diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 66b6a5078462..ab6fe89a0588 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -157,7 +157,7 @@ public class Flags { /***************************************/ // 900 - media - public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true); + public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, false); public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false); public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true); public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index b96eee717260..d71956d054be 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -55,6 +55,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; import android.os.Trace; +import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.view.IRemoteAnimationFinishedCallback; @@ -157,7 +158,7 @@ public class KeyguardService extends Service { Rect localBounds = new Rect(change.getEndAbsBounds()); localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); - out.add(new RemoteAnimationTarget( + final RemoteAnimationTarget target = new RemoteAnimationTarget( taskId, newModeToLegacyMode(change.getMode()), change.getLeash(), @@ -168,7 +169,15 @@ public class KeyguardService extends Service { info.getChanges().size() - i, new Point(), localBounds, new Rect(change.getEndAbsBounds()), windowConfiguration, isNotInRecents, null /* startLeash */, - change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */)); + change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */); + // Use hasAnimatingParent to mark the anything below root task + if (taskId != -1 && change.getParent() != null) { + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + if (parentChange != null && parentChange.getTaskInfo() != null) { + target.hasAnimatingParent = true; + } + } + out.add(target); } return out.toArray(new RemoteAnimationTarget[out.size()]); } @@ -189,8 +198,12 @@ public class KeyguardService extends Service { } } + // Wrap Keyguard going away animation private static IRemoteTransition wrap(IRemoteAnimationRunner runner) { return new IRemoteTransition.Stub() { + final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks = + new ArrayMap<>(); + @Override public void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) @@ -200,16 +213,37 @@ public class KeyguardService extends Service { final RemoteAnimationTarget[] wallpapers = wrap(info, true /* wallpapers */); final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0]; - // TODO: Remove this, and update alpha value in the IAnimationRunner. - for (TransitionInfo.Change change : info.getChanges()) { - t.setAlpha(change.getLeash(), 1.0f); + // Sets the alpha to 0 for the opening root task for fade in animation. And since + // the fade in animation can only apply on the first opening app, so set alpha to 1 + // for anything else. + boolean foundOpening = false; + for (RemoteAnimationTarget target : apps) { + if (target.taskId != -1 + && target.mode == RemoteAnimationTarget.MODE_OPENING + && !target.hasAnimatingParent) { + if (foundOpening) { + Log.w(TAG, "More than one opening target"); + t.setAlpha(target.leash, 1.0f); + continue; + } + t.setAlpha(target.leash, 0.0f); + foundOpening = true; + } else { + t.setAlpha(target.leash, 1.0f); + } } t.apply(); + synchronized (mFinishCallbacks) { + mFinishCallbacks.put(transition, finishCallback); + } runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps), apps, wallpapers, nonApps, new IRemoteAnimationFinishedCallback.Stub() { @Override public void onAnimationFinished() throws RemoteException { + synchronized (mFinishCallbacks) { + if (mFinishCallbacks.remove(transition) == null) return; + } Slog.d(TAG, "Finish IRemoteAnimationRunner."); finishCallback.onTransitionFinished(null /* wct */, null /* t */); } @@ -220,7 +254,20 @@ public class KeyguardService extends Service { public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback) { - + try { + final IRemoteTransitionFinishedCallback origFinishCB; + synchronized (mFinishCallbacks) { + origFinishCB = mFinishCallbacks.remove(transition); + } + if (origFinishCB == null) { + // already finished (or not started yet), so do nothing. + return; + } + runner.onAnimationCancelled(); + origFinishCB.onTransitionFinished(null /* wct */, null /* t */); + } catch (RemoteException e) { + // nothing, we'll just let it finish on its own I guess. + } } }; } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 99b57200a397..382323f3aed9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -26,6 +26,7 @@ import android.os.Handler import android.os.RemoteException import android.util.Log import android.view.RemoteAnimationTarget +import android.view.SurfaceControl import android.view.SyncRtSurfaceTransactionApplier import android.view.View import androidx.annotation.VisibleForTesting @@ -293,6 +294,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private val handler = Handler() + private val tmpFloat = FloatArray(9) + init { with(surfaceBehindAlphaAnimator) { duration = SURFACE_BEHIND_SWIPE_FADE_DURATION_MS @@ -723,13 +726,27 @@ class KeyguardUnlockAnimationController @Inject constructor( if (keyguardStateController.isSnappingKeyguardBackAfterSwipe) amount else surfaceBehindAlpha - applyParamsToSurface( - SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( - surfaceBehindRemoteAnimationTarget!!.leash) - .withMatrix(surfaceBehindMatrix) - .withCornerRadius(roundedCornerRadius) - .withAlpha(animationAlpha) - .build()) + // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is unable + // to draw + val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget?.leash + if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE && + sc?.isValid == true) { + with(SurfaceControl.Transaction()) { + setMatrix(sc, surfaceBehindMatrix, tmpFloat) + setCornerRadius(sc, roundedCornerRadius) + setAlpha(sc, animationAlpha) + apply() + } + } else { + applyParamsToSurface( + SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( + surfaceBehindRemoteAnimationTarget!!.leash) + .withMatrix(surfaceBehindMatrix) + .withCornerRadius(roundedCornerRadius) + .withAlpha(animationAlpha) + .build() + ) + } } /** @@ -744,8 +761,11 @@ class KeyguardUnlockAnimationController @Inject constructor( handler.removeCallbacksAndMessages(null) // Make sure we made the surface behind fully visible, just in case. It should already be - // fully visible. If the launcher is doing its own animation, let it continue without - // forcing it to 1f. + // fully visible. The exit animation is finished, and we should not hold the leash anymore, + // so forcing it to 1f. + surfaceBehindAlphaAnimator.cancel() + surfaceBehindEntryAnimator.cancel() + surfaceBehindAlpha = 1f setSurfaceBehindAppearAmount(1f) launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */) @@ -910,4 +930,4 @@ class KeyguardUnlockAnimationController @Inject constructor( return context.resources.getIntArray(R.array.config_foldedDeviceStates).isNotEmpty() } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 72dd36fec349..de8b0cfa495a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2497,10 +2497,18 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mInteractionJankMonitor.begin( createInteractionJankMonitorConf("DismissPanel")); + // Apply the opening animation on root task if exists + RemoteAnimationTarget aniTarget = apps[0]; + for (RemoteAnimationTarget tmpTarget : apps) { + if (tmpTarget.taskId != -1 && !tmpTarget.hasAnimatingParent) { + aniTarget = tmpTarget; + break; + } + } // Pass the surface and metadata to the unlock animation controller. mKeyguardUnlockAnimationControllerLazy.get() .notifyStartSurfaceBehindRemoteAnimation( - apps[0], startTime, mSurfaceBehindRemoteAnimationRequested); + aniTarget, startTime, mSurfaceBehindRemoteAnimationRequested); } else { mInteractionJankMonitor.begin( createInteractionJankMonitorConf("RemoteAnimationDisabled")); diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 892c28306f46..0288c9fce64a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -342,7 +342,8 @@ class FgsManagerController @Inject constructor( } val addedPackages = runningServiceTokens.keys.filter { - it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true + currentProfileIds.contains(it.userId) && + it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true } val removedPackages = runningApps.keys.filter { !runningServiceTokens.containsKey(it) } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt index d3ae198e8e35..236ba1f92d1f 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt @@ -56,6 +56,8 @@ interface SmartspaceViewComponent { ): BcSmartspaceDataPlugin.SmartspaceView { val ssView = plugin.getView(parent) + // Currently, this is only used to provide SmartspaceView on Dream surface. + ssView.setIsDreaming(true) ssView.registerDataProvider(plugin) ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { @@ -81,4 +83,4 @@ interface SmartspaceViewComponent { return ssView } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 86f9fa155179..9e029095ea6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_MANAGEMENT_DISCLOSURE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -407,7 +408,7 @@ public class KeyguardIndicationController { organizationName); } else { return mDevicePolicyManager.getResources().getString( - KEYGUARD_MANAGEMENT_DISCLOSURE, + KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE, () -> packageResources.getString( R.string.do_disclosure_with_name, organizationName), organizationName); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt index 9e8b35af1bce..1494574b26f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt @@ -68,4 +68,4 @@ class RowAppearanceCoordinator @Inject internal constructor( // Show the "alerted" bell icon controller.setLastAudiblyAlertedMs(entry.lastAudiblyAlertedMs) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt index 2148d3bb336a..82c7aae08f6a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.notification.collection.provider import android.content.Context -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt index 50e7d1ce4ba0..7b9483022fd8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt @@ -43,4 +43,4 @@ class SectionStyleProvider @Inject constructor() { fun isMinimizedSection(section: NotifSection): Boolean { return lowPrioritySections.contains(section.sectioner) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt index 34d25cf9c3be..d234e54e6725 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt @@ -107,4 +107,4 @@ class NodeSpecBuilder( .apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } } else -> throw RuntimeException("Unexpected entry: $entry") } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 954f74ccc0d2..4e7624cf4994 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -276,11 +276,6 @@ public class NotificationPanelViewController extends PanelViewController { private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; private final NotificationIconAreaController mNotificationIconAreaController; - // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is - // changed. - private static final int CAP_HEIGHT = 1456; - private static final int FONT_HEIGHT = 2163; - /** * Maximum time before which we will expand the panel even for slow motions when getting a * touch passed over from launcher. @@ -1123,13 +1118,6 @@ public class NotificationPanelViewController extends PanelViewController { } } - /** - * Returns if there's a custom clock being presented. - */ - public boolean hasCustomClock() { - return mKeyguardStatusViewController.hasCustomClock(); - } - private void setCentralSurfaces(CentralSurfaces centralSurfaces) { // TODO: this can be injected. mCentralSurfaces = centralSurfaces; @@ -4008,9 +3996,10 @@ public class NotificationPanelViewController extends PanelViewController { endAction.run(); } }) + .setUpdateListener(anim -> { + mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); + }) .start(); - - mKeyguardStatusViewController.animateFoldToAod(); } /** @@ -4872,7 +4861,6 @@ public class NotificationPanelViewController extends PanelViewController { public void onDozeAmountChanged(float linearAmount, float amount) { mInterpolatedDarkAmount = amount; mLinearDarkAmount = linearAmount; - mKeyguardStatusViewController.setDarkAmount(mInterpolatedDarkAmount); mKeyguardBottomArea.setDarkAmount(mInterpolatedDarkAmount); positionClockAndNotifications(); } @@ -4981,11 +4969,8 @@ public class NotificationPanelViewController extends PanelViewController { updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount()); setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); - // Update Clock Pivot - mKeyguardStatusViewController.setPivotX(mView.getWidth() / 2); - mKeyguardStatusViewController.setPivotY( - (FONT_HEIGHT - CAP_HEIGHT) / 2048f - * mKeyguardStatusViewController.getClockTextSize()); + // Update Clock Pivot (used by anti-burnin transformations) + mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight()); // Calculate quick setting heights. int oldMaxHeight = mQsMaxExpansionHeight; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 670ba96ca1ff..25892d9db080 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -24,7 +24,6 @@ import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedul import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT; import android.animation.Animator; -import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.Fragment; @@ -395,14 +394,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue state |= DISABLE_CLOCK; } - - // The shelf will be hidden when dozing with a custom clock, we must show notification - // icons in this occasion. - if (mStatusBarStateController.isDozing() - && mNotificationPanelViewController.hasCustomClock()) { - state |= DISABLE_CLOCK | DISABLE_SYSTEM_INFO; - } - if (mOngoingCallController.hasOngoingCall()) { state &= ~DISABLE_ONGOING_CALL_CHIP; } else { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt new file mode 100644 index 000000000000..4f3995252b54 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -0,0 +1,259 @@ +/* + * 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.keyguard + +import android.content.BroadcastReceiver +import android.testing.AndroidTestingRunner +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.plugins.Clock +import com.android.systemui.plugins.ClockAnimations +import com.android.systemui.plugins.ClockEvents +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import java.util.TimeZone +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ClockEventControllerTest : SysuiTestCase() { + + @JvmField @Rule val mockito = MockitoJUnit.rule() + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var batteryController: BatteryController + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var animations: ClockAnimations + @Mock private lateinit var events: ClockEvents + @Mock private lateinit var clock: Clock + + private lateinit var clockEventController: ClockEventController + + @Before + fun setUp() { + whenever(clock.smallClock).thenReturn(TextView(context)) + whenever(clock.largeClock).thenReturn(TextView(context)) + whenever(clock.events).thenReturn(events) + whenever(clock.animations).thenReturn(animations) + + clockEventController = ClockEventController( + statusBarStateController, + broadcastDispatcher, + batteryController, + keyguardUpdateMonitor, + configurationController, + context.resources, + context + ) + } + + @Test + fun clockSet_validateInitialization() { + clockEventController.clock = clock + + verify(clock).initialize(any(), anyFloat(), anyFloat()) + } + + @Test + fun clockUnset_validateState() { + clockEventController.clock = clock + clockEventController.clock = null + + assertEquals(clockEventController.clock, null) + } + + @Test + fun themeChanged_verifyClockPaletteUpdated() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() + verify(configurationController).addCallback(capture(captor)) + captor.value.onThemeChanged() + + verify(events).onColorPaletteChanged(any()) + } + + @Test + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations).charge() + } + + @Test + fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, times(1)).charge() + } + + @Test + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(false) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, never()).charge() + } + + @Test + fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, false) + + verify(animations, never()).charge() + } + + @Test + fun localeCallback_verifyClockNotified() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<BroadcastReceiver>() + verify(broadcastDispatcher).registerReceiver( + capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) + ) + captor.value.onReceive(context, mock()) + + verify(events).onLocaleChanged(any()) + } + + @Test + fun keyguardCallback_visibilityChanged_clockDozeCalled() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(captor)) + + captor.value.onKeyguardVisibilityChanged(true) + verify(animations, never()).doze(0f) + + captor.value.onKeyguardVisibilityChanged(false) + verify(animations, times(1)).doze(0f) + } + + @Test + fun keyguardCallback_timeFormat_clockNotified() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(captor)) + captor.value.onTimeFormatChanged("12h") + + verify(events).onTimeFormatChanged(false) + } + + @Test + fun keyguardCallback_timezoneChanged_clockNotified() { + val mockTimeZone = mock<TimeZone>() + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(captor)) + captor.value.onTimeZoneChanged(mockTimeZone) + + verify(events).onTimeZoneChanged(mockTimeZone) + } + + @Test + fun keyguardCallback_userSwitched_clockNotified() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(captor)) + captor.value.onUserSwitchComplete(10) + + verify(events).onTimeFormatChanged(false) + } + + @Test + fun keyguardCallback_verifyKeyguardChanged() { + clockEventController.clock = clock + clockEventController.registerListeners() + + val captor = argumentCaptor<StatusBarStateController.StateListener>() + verify(statusBarStateController).addCallback(capture(captor)) + captor.value.onDozeAmountChanged(0.4f, 0.6f) + + verify(animations).doze(0.4f) + } + + @Test + fun unregisterListeners_validate() { + clockEventController.unregisterListeners() + verify(broadcastDispatcher).unregisterReceiver(any()) + verify(configurationController).removeCallback(any()) + verify(batteryController).removeCallback(any()) + verify(keyguardUpdateMonitor).removeCallback(any()) + verify(statusBarStateController).removeCallback(any()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 8b9a1e022d26..cd2a43fc6a44 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -19,7 +19,6 @@ package com.android.keyguard; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -41,23 +40,18 @@ import android.widget.RelativeLayout; import androidx.test.filters.SmallTest; -import com.android.internal.colorextraction.ColorExtractor; -import com.android.keyguard.clock.ClockManager; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.plugins.ClockPlugin; +import com.android.systemui.plugins.Clock; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; +import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; -import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; -import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -79,22 +73,12 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock - private SysuiColorExtractor mColorExtractor; - @Mock - private ClockManager mClockManager; + private ClockRegistry mClockRegistry; @Mock KeyguardSliceViewController mKeyguardSliceViewController; @Mock NotificationIconAreaController mNotificationIconAreaController; @Mock - BroadcastDispatcher mBroadcastDispatcher; - @Mock - BatteryController mBatteryController; - @Mock - KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - KeyguardBypassController mBypassController; - @Mock LockscreenSmartspaceController mSmartspaceController; @Mock @@ -102,11 +86,11 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; @Mock - private ClockPlugin mClockPlugin; - @Mock - ColorExtractor.GradientColors mGradientColors; + private Clock mClock; @Mock DumpManager mDumpManager; + @Mock + ClockEventController mClockEventController; @Mock private NotificationIconContainer mNotificationIcons; @@ -136,8 +120,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { when(mView.getContext()).thenReturn(getContext()); when(mView.getResources()).thenReturn(mResources); - when(mView.findViewById(R.id.animatable_clock_view)).thenReturn(mClockView); - when(mView.findViewById(R.id.animatable_clock_view_large)).thenReturn(mLargeClockView); when(mView.findViewById(R.id.lockscreen_clock_view_large)).thenReturn(mLargeClockFrame); when(mClockView.getContext()).thenReturn(getContext()); when(mLargeClockView.getContext()).thenReturn(getContext()); @@ -148,23 +130,19 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mController = new KeyguardClockSwitchController( mView, mStatusBarStateController, - mColorExtractor, - mClockManager, + mClockRegistry, mKeyguardSliceViewController, mNotificationIconAreaController, - mBroadcastDispatcher, - mBatteryController, - mKeyguardUpdateMonitor, mSmartspaceController, mKeyguardUnlockAnimationController, mSecureSettings, mExecutor, - mResources, - mDumpManager + mDumpManager, + mClockEventController ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors); + when(mClockRegistry.createCurrentClock()).thenReturn(mClock); mSliceView = new View(getContext()); when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); @@ -211,20 +189,20 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verifyAttachment(times(1)); listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); - verify(mColorExtractor).removeOnColorsChangedListener( - any(ColorExtractor.OnColorsChangedListener.class)); + verify(mClockEventController).unregisterListeners(); } @Test public void testPluginPassesStatusBarState() { - ArgumentCaptor<ClockManager.ClockChangedListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(ClockManager.ClockChangedListener.class); + ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); mController.init(); - verify(mClockManager).addOnClockChangedListener(listenerArgumentCaptor.capture()); + verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture()); - listenerArgumentCaptor.getValue().onClockChanged(mClockPlugin); - verify(mView).setClockPlugin(mClockPlugin, StatusBarState.SHADE); + listenerArgumentCaptor.getValue().onClockChanged(); + verify(mView, times(2)).setClock(mClock, StatusBarState.SHADE); + verify(mClockEventController, times(2)).setClock(mClock); } @Test @@ -281,10 +259,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { } private void verifyAttachment(VerificationMode times) { - verify(mClockManager, times).addOnClockChangedListener( - any(ClockManager.ClockChangedListener.class)); - verify(mColorExtractor, times).addOnColorsChangedListener( - any(ColorExtractor.OnColorsChangedListener.class)); - verify(mView, times).updateColors(mGradientColors); + verify(mClockRegistry, times).registerClockChangeListener( + any(ClockRegistry.ClockChangeListener.class)); + verify(mClockEventController, times).registerListeners(); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 6c6f0acd7085..a0295d09826f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import static android.view.View.GONE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -24,56 +23,61 @@ import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.assertEquals; + import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; -import android.graphics.Color; -import android.graphics.Paint.Style; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.TextClock; +import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ClockPlugin; -import com.android.systemui.shared.clocks.AnimatableClockView; +import com.android.systemui.plugins.Clock; import com.android.systemui.statusbar.StatusBarState; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) // Need to run on the main thread because KeyguardSliceView$Row init checks for // the main thread before acquiring a wake lock. This class is constructed when -// the keyguard_clcok_switch layout is inflated. +// the keyguard_clock_switch layout is inflated. @RunWithLooper(setAsMainLooper = true) public class KeyguardClockSwitchTest extends SysuiTestCase { - private FrameLayout mClockFrame; + @Mock + ViewGroup mMockKeyguardSliceView; + + @Mock + Clock mClock; + + private FrameLayout mSmallClockFrame; private FrameLayout mLargeClockFrame; - private TextClock mBigClock; - private AnimatableClockView mClockView; - private AnimatableClockView mLargeClockView; - View mMockKeyguardSliceView; KeyguardClockSwitch mKeyguardClockSwitch; @Before public void setUp() { - mMockKeyguardSliceView = mock(KeyguardSliceView.class); + MockitoAnnotations.initMocks(this); when(mMockKeyguardSliceView.getContext()).thenReturn(mContext); when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area)) .thenReturn(mMockKeyguardSliceView); + when(mClock.getSmallClock()).thenReturn(new TextView(getContext())); + when(mClock.getLargeClock()).thenReturn(new TextView(getContext())); + LayoutInflater layoutInflater = LayoutInflater.from(getContext()); layoutInflater.setPrivateFactory(new LayoutInflater.Factory2() { @@ -93,164 +97,68 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { }); mKeyguardClockSwitch = (KeyguardClockSwitch) layoutInflater.inflate(R.layout.keyguard_clock_switch, null); - mClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view); - mClockView = mKeyguardClockSwitch.findViewById(R.id.animatable_clock_view); + mSmallClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view_large); - mLargeClockView = mKeyguardClockSwitch.findViewById(R.id.animatable_clock_view_large); - mBigClock = new TextClock(getContext()); mKeyguardClockSwitch.mChildrenAreLaidOut = true; - MockitoAnnotations.initMocks(this); - } - - @Test - public void onPluginConnected_showPluginClock() { - ClockPlugin plugin = mock(ClockPlugin.class); - TextClock pluginView = new TextClock(getContext()); - when(plugin.getView()).thenReturn(pluginView); - - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - - assertThat(mClockView.getVisibility()).isEqualTo(GONE); - assertThat(plugin.getView().getParent()).isEqualTo(mClockFrame); } @Test - public void onPluginConnected_showPluginBigClock() { - // GIVEN the plugin returns a view for the big clock - ClockPlugin plugin = mock(ClockPlugin.class); - when(plugin.getBigClockView()).thenReturn(mBigClock); - // WHEN the plugin is connected - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - // THEN the big clock container is visible and it is the parent of the - // big clock view. - assertThat(mLargeClockView.getVisibility()).isEqualTo(View.GONE); - assertThat(mBigClock.getParent()).isEqualTo(mLargeClockFrame); + public void noPluginConnected_showNothing() { + mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); + assertEquals(mLargeClockFrame.getChildCount(), 0); + assertEquals(mSmallClockFrame.getChildCount(), 0); } @Test - public void onPluginConnected_nullView() { - ClockPlugin plugin = mock(ClockPlugin.class); - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE); - } - - @Test - public void onPluginConnected_showSecondPluginClock() { - // GIVEN a plugin has already connected - ClockPlugin plugin1 = mock(ClockPlugin.class); - when(plugin1.getView()).thenReturn(new TextClock(getContext())); - mKeyguardClockSwitch.setClockPlugin(plugin1, StatusBarState.KEYGUARD); - // WHEN a second plugin is connected - ClockPlugin plugin2 = mock(ClockPlugin.class); - when(plugin2.getView()).thenReturn(new TextClock(getContext())); - mKeyguardClockSwitch.setClockPlugin(plugin2, StatusBarState.KEYGUARD); - // THEN only the view from the second plugin should be a child of KeyguardClockSwitch. - assertThat(plugin2.getView().getParent()).isEqualTo(mClockFrame); - assertThat(plugin1.getView().getParent()).isNull(); - } + public void pluginConnectedThenDisconnected_showNothing() { + mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); + assertEquals(mLargeClockFrame.getChildCount(), 1); + assertEquals(mSmallClockFrame.getChildCount(), 1); - @Test - public void onPluginConnected_darkAmountInitialized() { - // GIVEN that the dark amount has already been set - mKeyguardClockSwitch.setDarkAmount(0.5f); - // WHEN a plugin is connected - ClockPlugin plugin = mock(ClockPlugin.class); - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - // THEN dark amount should be initalized on the plugin. - verify(plugin).setDarkAmount(0.5f); + mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); + assertEquals(mLargeClockFrame.getChildCount(), 0); + assertEquals(mSmallClockFrame.getChildCount(), 0); } @Test - public void onPluginDisconnected_showDefaultClock() { - ClockPlugin plugin = mock(ClockPlugin.class); - TextClock pluginView = new TextClock(getContext()); - when(plugin.getView()).thenReturn(pluginView); - - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - assertThat(mClockView.getVisibility()).isEqualTo(GONE); + public void onPluginConnected_showClock() { + mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD); - assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE); - - assertThat(plugin.getView().getParent()).isNull(); + assertEquals(mClock.getSmallClock().getParent(), mSmallClockFrame); + assertEquals(mClock.getLargeClock().getParent(), mLargeClockFrame); } @Test - public void onPluginDisconnected_hidePluginBigClock() { - // GIVEN the plugin returns a view for the big clock - ClockPlugin plugin = mock(ClockPlugin.class); - TextClock pluginView = new TextClock(getContext()); - when(plugin.getBigClockView()).thenReturn(pluginView); - // WHEN the plugin is connected and then disconnected - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD); - // THEN the big lock container is GONE and the big clock view doesn't have - // a parent. - assertThat(mLargeClockView.getVisibility()).isEqualTo(VISIBLE); - assertThat(pluginView.getParent()).isNull(); - } + public void onPluginConnected_showSecondPluginClock() { + // GIVEN a plugin has already connected + Clock otherClock = mock(Clock.class); + when(otherClock.getSmallClock()).thenReturn(new TextView(getContext())); + when(otherClock.getLargeClock()).thenReturn(new TextView(getContext())); + mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); + mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); - @Test - public void onPluginDisconnected_nullView() { - ClockPlugin plugin = mock(ClockPlugin.class); - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD); - assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE); + // THEN only the view from the second plugin should be a child of KeyguardClockSwitch. + assertThat(otherClock.getSmallClock().getParent()).isEqualTo(mSmallClockFrame); + assertThat(otherClock.getLargeClock().getParent()).isEqualTo(mLargeClockFrame); + assertThat(mClock.getSmallClock().getParent()).isNull(); + assertThat(mClock.getLargeClock().getParent()).isNull(); } @Test public void onPluginDisconnected_secondOfTwoDisconnected() { // GIVEN two plugins are connected - ClockPlugin plugin1 = mock(ClockPlugin.class); - when(plugin1.getView()).thenReturn(new TextClock(getContext())); - mKeyguardClockSwitch.setClockPlugin(plugin1, StatusBarState.KEYGUARD); - ClockPlugin plugin2 = mock(ClockPlugin.class); - when(plugin2.getView()).thenReturn(new TextClock(getContext())); - mKeyguardClockSwitch.setClockPlugin(plugin2, StatusBarState.KEYGUARD); + Clock otherClock = mock(Clock.class); + when(otherClock.getSmallClock()).thenReturn(new TextView(getContext())); + when(otherClock.getLargeClock()).thenReturn(new TextView(getContext())); + mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); + mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); // WHEN the second plugin is disconnected - mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD); - // THEN the default clock should be shown. - assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE); - assertThat(plugin1.getView().getParent()).isNull(); - assertThat(plugin2.getView().getParent()).isNull(); - } - - @Test - public void onPluginDisconnected_onDestroyView() { - // GIVEN a plugin is connected - ClockPlugin clockPlugin = mock(ClockPlugin.class); - when(clockPlugin.getView()).thenReturn(new TextClock(getContext())); - mKeyguardClockSwitch.setClockPlugin(clockPlugin, StatusBarState.KEYGUARD); - // WHEN the plugin is disconnected - mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD); - // THEN onDestroyView is called on the plugin - verify(clockPlugin).onDestroyView(); - } - - @Test - public void setTextColor_pluginClockSetTextColor() { - ClockPlugin plugin = mock(ClockPlugin.class); - TextClock pluginView = new TextClock(getContext()); - when(plugin.getView()).thenReturn(pluginView); - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - - mKeyguardClockSwitch.setTextColor(Color.WHITE); - - verify(plugin).setTextColor(Color.WHITE); - } - - - @Test - public void setStyle_pluginClockSetStyle() { - ClockPlugin plugin = mock(ClockPlugin.class); - TextClock pluginView = new TextClock(getContext()); - when(plugin.getView()).thenReturn(pluginView); - Style style = mock(Style.class); - mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD); - - mKeyguardClockSwitch.setStyle(style); - - verify(plugin).setStyle(style); + mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); + // THEN nothing should be shown + assertThat(otherClock.getSmallClock().getParent()).isNull(); + assertThat(otherClock.getLargeClock().getParent()).isNull(); + assertThat(mClock.getSmallClock().getParent()).isNull(); + assertThat(mClock.getLargeClock().getParent()).isNull(); } @Test @@ -262,7 +170,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); - assertThat(mClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); } @Test @@ -271,7 +179,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); - assertThat(mClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); } @Test @@ -281,8 +189,8 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { mKeyguardClockSwitch.mClockInAnim.end(); mKeyguardClockSwitch.mClockOutAnim.end(); - assertThat(mClockFrame.getAlpha()).isEqualTo(1); - assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE); + assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE); // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); @@ -292,8 +200,8 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { public void switchingToSmallClockNoAnimation_makesBigClockDisappear() { mKeyguardClockSwitch.switchToClock(SMALL, false); - assertThat(mClockFrame.getAlpha()).isEqualTo(1); - assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE); + assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE); // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java index dc1ae0e93757..964e6d79f0bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java @@ -72,19 +72,67 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { } /** - * Ensures {@link SmartSpaceComplication} is only registered when it is available. + * Ensures {@link SmartSpaceComplication} isn't registered right away on start. */ @Test - public void testAvailability() { + public void testRegistrantStart_doesNotAddComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication)); + } - final SmartSpaceComplication.Registrant registrant = new SmartSpaceComplication.Registrant( + private SmartSpaceComplication.Registrant getRegistrant() { + return new SmartSpaceComplication.Registrant( mContext, mDreamOverlayStateController, mComplication, mSmartspaceController); + } + + @Test + public void testOverlayActive_addsTargetListener() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); registrant.start(); - verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication)); + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + // Test + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + } + + @Test + public void testOverlayActive_targetsNonEmpty_addsComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + + // Test + final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); + verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + } + + @Test + public void testOverlayActive_targetsEmpty_removesComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); @@ -100,10 +148,41 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + + // Test + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList()); + verify(mDreamOverlayStateController).removeComplication(eq(mComplication)); + } + + @Test + public void testOverlayInActive_removesTargetListener_removesComplication() { + final SmartSpaceComplication.Registrant registrant = getRegistrant(); + registrant.start(); + + final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + + final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); + verify(mDreamOverlayStateController).addComplication(eq(mComplication)); + + // Test + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(false); + dreamCallbackCaptor.getValue().onStateChanged(); + verify(mSmartspaceController).removeListener(listenerCaptor.getValue()); + verify(mDreamOverlayStateController).removeComplication(eq(mComplication)); } @Test - public void testGetViewReusesSameView() { + public void testGetView_reusesSameView() { final SmartSpaceComplication complication = new SmartSpaceComplication(getContext(), mSmartspaceController); final Complication.ViewHolder viewHolder = complication.createView(mComplicationViewModel); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index d61989fc3128..136a395196e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -129,13 +129,16 @@ class ClockRegistryTest : SysuiTestCase() { pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin2, mockContext) val list = registry.getClocks() - assertEquals(list, listOf( - ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), - ClockMetadata("clock_1", "clock 1"), - ClockMetadata("clock_2", "clock 2"), - ClockMetadata("clock_3", "clock 3"), - ClockMetadata("clock_4", "clock 4") - )) + assertEquals( + list, + listOf( + ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), + ClockMetadata("clock_1", "clock 1"), + ClockMetadata("clock_2", "clock 2"), + ClockMetadata("clock_3", "clock 3"), + ClockMetadata("clock_4", "clock 4") + ) + ) } @Test @@ -157,11 +160,14 @@ class ClockRegistryTest : SysuiTestCase() { pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin2, mockContext) val list = registry.getClocks() - assertEquals(list, listOf( - ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), - ClockMetadata("clock_1", "clock 1"), - ClockMetadata("clock_2", "clock 2") - )) + assertEquals( + list, + listOf( + ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), + ClockMetadata("clock_1", "clock 1"), + ClockMetadata("clock_2", "clock 2") + ) + ) assertEquals(registry.createExampleClock("clock_1"), mockClock) assertEquals(registry.createExampleClock("clock_2"), mockClock) @@ -221,7 +227,7 @@ class ClockRegistryTest : SysuiTestCase() { pluginListener.onPluginConnected(plugin2, mockContext) var changeCallCount = 0 - registry.registerClockChangeListener({ changeCallCount++ }) + registry.registerClockChangeListener { changeCallCount++ } pluginListener.onPluginDisconnected(plugin1) assertEquals(0, changeCallCount) diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index 2f0f0a0a1b8f..37f96c8d7023 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -36,17 +36,17 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Spy import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.Optional -import java.util.concurrent.Executor +import org.mockito.Spy @SmallTest @RunWith(AndroidTestingRunner::class) @@ -87,6 +87,34 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { private lateinit var controller: DreamSmartspaceController + /** + * A class which implements SmartspaceView and extends View. This is mocked to provide the right + * object inheritance and interface implementation used in DreamSmartspaceController + */ + private class TestView(context: Context?) : View(context), SmartspaceView { + override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} + + override fun setPrimaryTextColor(color: Int) {} + + override fun setIsDreaming(isDreaming: Boolean) {} + + override fun setDozeAmount(amount: Float) {} + + override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {} + + override fun setFalsingManager(falsingManager: FalsingManager?) {} + + override fun setDnd(image: Drawable?, description: String?) {} + + override fun setNextAlarm(image: Drawable?, description: String?) {} + + override fun setMediaTarget(target: SmartspaceTarget?) {} + + override fun getSelectedPage(): Int { return 0; } + + override fun getCurrentCardTopPadding(): Int { return 0; } + } + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -130,34 +158,6 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { } /** - * A class which implements SmartspaceView and extends View. This is mocked to provide the right - * object inheritance and interface implementation used in DreamSmartspaceController - */ - private class TestView(context: Context?) : View(context), SmartspaceView { - override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {} - - override fun setPrimaryTextColor(color: Int) {} - - override fun setIsDreaming(isDreaming: Boolean) {} - - override fun setDozeAmount(amount: Float) {} - - override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {} - - override fun setFalsingManager(falsingManager: FalsingManager?) {} - - override fun setDnd(image: Drawable?, description: String?) {} - - override fun setNextAlarm(image: Drawable?, description: String?) {} - - override fun setMediaTarget(target: SmartspaceTarget?) {} - - override fun getSelectedPage(): Int { return 0; } - - override fun getCurrentCardTopPadding(): Int { return 0; } - } - - /** * Ensures session begins when a view is attached. */ @Test @@ -180,16 +180,4 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { verify(session).close() } - - /** - * Ensures setIsDreaming(true) is called when the view is built. - */ - @Test - fun testSetIsDreamingTrueOnViewCreate() { - `when`(precondition.conditionsMet()).thenReturn(true) - - controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java)) - - verify(smartspaceView).setIsDreaming(true) - } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java deleted file mode 100644 index 6171e2f760d3..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.coordinator; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.os.Handler; -import android.os.UserHandle; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -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.pluggable.NotifFilter; -import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; -import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider; -import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; -import com.android.systemui.statusbar.policy.KeyguardStateController; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * TODO(b/224771204) Create test cases - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@Ignore -public class KeyguardCoordinatorTest extends SysuiTestCase { - private static final int NOTIF_USER_ID = 0; - private static final int CURR_USER_ID = 1; - - @Mock private Handler mMainHandler; - @Mock private KeyguardStateController mKeyguardStateController; - @Mock private BroadcastDispatcher mBroadcastDispatcher; - @Mock private StatusBarStateController mStatusBarStateController; - @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock private HighPriorityProvider mHighPriorityProvider; - @Mock private SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider; - @Mock private NotifPipeline mNotifPipeline; - @Mock private KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; - - private NotificationEntry mEntry; - private NotifFilter mKeyguardFilter; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - KeyguardCoordinator keyguardCoordinator = new KeyguardCoordinator( - mKeyguardNotificationVisibilityProvider, - mSectionHeaderVisibilityProvider, - mock(SharedCoordinatorLogger.class), - mStatusBarStateController); - - mEntry = new NotificationEntryBuilder() - .setUser(new UserHandle(NOTIF_USER_ID)) - .build(); - - ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class); - keyguardCoordinator.attach(mNotifPipeline); - verify(mNotifPipeline, times(1)).addFinalizeFilter(filterCaptor.capture()); - mKeyguardFilter = filterCaptor.getValue(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt new file mode 100644 index 000000000000..8c506a6d16ae --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -0,0 +1,82 @@ +/* + * 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.systemui.statusbar.notification.collection.coordinator + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider +import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.withArgCaptor +import java.util.function.Consumer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class KeyguardCoordinatorTest : SysuiTestCase() { + private val notifPipeline: NotifPipeline = mock() + private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() + private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() + private val sharedCoordinatorLogger: SharedCoordinatorLogger = mock() + private val statusBarStateController: StatusBarStateController = mock() + + private lateinit var onStateChangeListener: Consumer<String> + private lateinit var keyguardFilter: NotifFilter + + @Before + fun setup() { + val keyguardCoordinator = KeyguardCoordinator( + keyguardNotifVisibilityProvider, + sectionHeaderVisibilityProvider, + sharedCoordinatorLogger, + statusBarStateController + ) + keyguardCoordinator.attach(notifPipeline) + onStateChangeListener = withArgCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } + keyguardFilter = withArgCaptor { + verify(notifPipeline).addFinalizeFilter(capture()) + } + } + + @Test + fun testSetSectionHeadersVisibleInShade() { + clearInvocations(sectionHeaderVisibilityProvider) + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + onStateChangeListener.accept("state change") + verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(true) + } + + @Test + fun testSetSectionHeadersNotVisibleOnKeyguard() { + clearInvocations(sectionHeaderVisibilityProvider) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + onStateChangeListener.accept("state change") + verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt index 40859d0e6304..3f3de009fb04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt @@ -37,8 +37,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations.initMocks import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations.initMocks @SmallTest @RunWith(AndroidTestingRunner::class) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt index ff601938d544..ac254abe60b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt @@ -413,4 +413,4 @@ private fun buildSection( return nodeController } }, index) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index b94aac21acad..3d03c4750869 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -223,10 +223,9 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - public void disable_isDozingButNoCustomClock_clockAndSystemInfoVisible() { + public void disable_isDozing_clockAndSystemInfoVisible() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mStatusBarStateController.isDozing()).thenReturn(true); - when(mNotificationPanelViewController.hasCustomClock()).thenReturn(false); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); @@ -235,10 +234,9 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - public void disable_customClockButNotDozing_clockAndSystemInfoVisible() { + public void disable_NotDozing_clockAndSystemInfoVisible() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); @@ -247,40 +245,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - public void disable_dozingAndCustomClock_clockAndSystemInfoHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - when(mStatusBarStateController.isDozing()).thenReturn(true); - when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true); - - // Make sure they start out as visible - assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility()); - assertEquals(View.VISIBLE, getClockView().getVisibility()); - - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.INVISIBLE, getSystemIconAreaView().getVisibility()); - assertEquals(View.GONE, getClockView().getVisibility()); - } - - @Test - public void onDozingChanged_clockAndSystemInfoVisibilitiesUpdated() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - when(mStatusBarStateController.isDozing()).thenReturn(true); - when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true); - - // Make sure they start out as visible - assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility()); - assertEquals(View.VISIBLE, getClockView().getVisibility()); - - fragment.onDozingChanged(true); - - // When this callback is triggered, we want to make sure the clock and system info - // visibilities are recalculated. Since dozing=true, they shouldn't be visible. - assertEquals(View.INVISIBLE, getSystemIconAreaView().getVisibility()); - assertEquals(View.GONE, getClockView().getVisibility()); - } - - @Test public void disable_headsUpShouldBeVisibleTrue_clockDisabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true); diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java index 229799a8457d..d5991d3930a8 100644 --- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java +++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java @@ -73,6 +73,9 @@ class AssociationStoreImpl implements AssociationStore { private final Set<OnChangeListener> mListeners = new LinkedHashSet<>(); void addAssociation(@NonNull AssociationInfo association) { + // Validity check first. + checkNotRevoked(association); + final int id = association.getId(); if (DEBUG) { @@ -99,6 +102,9 @@ class AssociationStoreImpl implements AssociationStore { } void updateAssociation(@NonNull AssociationInfo updated) { + // Validity check first. + checkNotRevoked(updated); + final int id = updated.getId(); if (DEBUG) { @@ -292,6 +298,9 @@ class AssociationStoreImpl implements AssociationStore { } void setAssociations(Collection<AssociationInfo> allAssociations) { + // Validity check first. + allAssociations.forEach(AssociationStoreImpl::checkNotRevoked); + if (DEBUG) { Log.i(TAG, "setAssociations() n=" + allAssociations.size()); final StringJoiner stringJoiner = new StringJoiner(", "); @@ -324,4 +333,11 @@ class AssociationStoreImpl implements AssociationStore { mAddressMap.clear(); mCachedPerUser.clear(); } + + private static void checkNotRevoked(@NonNull AssociationInfo association) { + if (association.isRevoked()) { + throw new IllegalArgumentException( + "Revoked (removed) associations MUST NOT appear in the AssociationStore"); + } + } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 04468ed47640..ab9966f218c1 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -19,6 +19,7 @@ package com.android.server.companion; import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -49,6 +50,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.app.NotificationManager; @@ -93,6 +96,7 @@ import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; import com.android.internal.content.PackageMonitor; +import com.android.internal.infra.PerUser; import com.android.internal.notification.NotificationAccessConfirmationActivityContract; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; @@ -110,6 +114,7 @@ import com.android.server.pm.UserManagerInternal; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -133,6 +138,9 @@ public class CompanionDeviceManagerService extends SystemService { private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); + private final ActivityManager mActivityManager; + private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; + private PersistentDataStore mPersistentStore; private final PersistUserStateHandler mUserPersistenceHandler; @@ -160,12 +168,40 @@ public class CompanionDeviceManagerService extends SystemService { @GuardedBy("mPreviouslyUsedIds") private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>(); + /** + * A structure that consists of a set of revoked associations that pending for role holder + * removal per each user. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval = + new PerUserAssociationSet(); + /** + * Contains uid-s of packages pending to be removed from the role holder list (after + * revocation of an association), which will happen one the package is no longer visible to the + * user. + * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but + * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived + * from uid-s using {@link UserHandle#getUserId(int)}). + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>(); + private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners = new RemoteCallbackList<>(); public CompanionDeviceManagerService(Context context) { super(context); + mActivityManager = context.getSystemService(ActivityManager.class); mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class); mAppOpsManager = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); @@ -176,6 +212,9 @@ public class CompanionDeviceManagerService extends SystemService { mUserPersistenceHandler = new PersistUserStateHandler(); mAssociationStore = new AssociationStoreImpl(); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); + + mOnPackageVisibilityChangeListener = + new OnPackageVisibilityChangeListener(mActivityManager); } @Override @@ -217,7 +256,33 @@ public class CompanionDeviceManagerService extends SystemService { mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds); } - mAssociationStore.setAssociations(allAssociations); + final Set<AssociationInfo> activeAssociations = + new ArraySet<>(/* capacity */ allAssociations.size()); + // A set contains the userIds that need to persist state after remove the app + // from the list of role holders. + final Set<Integer> usersToPersistStateFor = new ArraySet<>(); + + for (AssociationInfo association : allAssociations) { + if (!association.isRevoked()) { + activeAssociations.add(association); + } else if (maybeRemoveRoleHolderForAssociation(association)) { + // Nothing more to do here, but we'll need to persist all the associations to the + // disk afterwards. + usersToPersistStateFor.add(association.getUserId()); + } else { + addToPendingRoleHolderRemoval(association); + } + } + + mAssociationStore.setAssociations(activeAssociations); + + // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because + // persistStateForUser() queries AssociationStore. + // (If persistStateForUser() is invoked before mAssociationStore.setAssociations() it + // would effectively just clear-out all the persisted associations). + for (int userId : usersToPersistStateFor) { + persistStateForUser(userId); + } } @Override @@ -367,10 +432,18 @@ public class CompanionDeviceManagerService extends SystemService { } private void persistStateForUser(@UserIdInt int userId) { - final List<AssociationInfo> updatedAssociations = - mAssociationStore.getAssociationsForUser(userId); + // We want to store both active associations and the revoked (removed) association that we + // are keeping around for the final clean-up (delayed role holder removal). + final List<AssociationInfo> allAssociations; + // Start with the active associations - these we can get from the AssociationStore. + allAssociations = new ArrayList<>( + mAssociationStore.getAssociationsForUser(userId)); + // ... and add the revoked (removed) association, that are yet to be permanently removed. + allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId)); + final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId); - mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser); + + mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser); } private void notifyListeners( @@ -438,13 +511,17 @@ public class CompanionDeviceManagerService extends SystemService { removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; } - for (AssociationInfo ai : mAssociationStore.getAssociations()) { - if (!ai.isSelfManaged()) continue; - final boolean isInactive = currentTime - ai.getLastTimeConnectedMs() >= removalWindow; - if (isInactive) { - Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId()); - disassociateInternal(ai.getId()); - } + for (AssociationInfo association : mAssociationStore.getAssociations()) { + if (!association.isSelfManaged()) continue; + + final boolean isInactive = + currentTime - association.getLastTimeConnectedMs() >= removalWindow; + if (!isInactive) continue; + + final int id = association.getId(); + + Slog.i(TAG, "Removing inactive self-managed association id=" + id); + disassociateInternal(id); } } @@ -712,7 +789,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerIsSystemOr(userId, packageName); AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress( - userId, packageName, deviceAddress); + userId, packageName, deviceAddress); if (association == null) { throw new RemoteException(new DeviceNotAssociatedException("App " + packageName @@ -772,7 +849,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage); checkState(!ArrayUtils.isEmpty( - mAssociationStore.getAssociationsForPackage(userId, callingPackage)), + mAssociationStore.getAssociationsForPackage(userId, callingPackage)), "App must have an association before calling this API"); } @@ -832,8 +909,8 @@ public class CompanionDeviceManagerService extends SystemService { final long timestamp = System.currentTimeMillis(); final AssociationInfo association = new AssociationInfo(id, userId, packageName, - macAddress, displayName, deviceProfile, selfManaged, false, timestamp, - Long.MAX_VALUE); + macAddress, displayName, deviceProfile, selfManaged, + /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE); Slog.i(TAG, "New CDM association created=" + association); mAssociationStore.addAssociation(association); @@ -845,6 +922,11 @@ public class CompanionDeviceManagerService extends SystemService { updateSpecialAccessPermissionForAssociatedPackage(association); logCreateAssociation(deviceProfile); + + // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since + // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case + // that there are other devices with the same profile, so the role holder won't be removed. + return association; } @@ -925,39 +1007,187 @@ public class CompanionDeviceManagerService extends SystemService { final String packageName = association.getPackageName(); final String deviceProfile = association.getDeviceProfile(); + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Need to remove the app from list of the role holders, but will have to do it later + // (the app is in foreground at the moment). + addToPendingRoleHolderRemoval(association); + } + + // Need to check if device still present now because CompanionDevicePresenceMonitor will + // remove current connected device after mAssociationStore.removeAssociation final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId); // Removing the association. mAssociationStore.removeAssociation(associationId); + // Do not need to persistUserState since CompanionDeviceManagerService will get callback + // from #onAssociationChanged, and it will handle the persistUserState which including + // active and revoked association. logRemoveAssociation(deviceProfile); // Remove all the system data transfer requests for the association. mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, associationId); - final List<AssociationInfo> otherAssociations = - mAssociationStore.getAssociationsForPackage(userId, packageName); - - // Check if the package is associated with other devices with the same profile. - // If not: take away the role. - if (deviceProfile != null) { - final boolean shouldKeepTheRole = any(otherAssociations, - it -> deviceProfile.equals(it.getDeviceProfile())); - if (!shouldKeepTheRole) { - Binder.withCleanCallingIdentity(() -> - removeRoleHolderForAssociation(getContext(), association)); - } - } - if (!wasPresent || !association.isNotifyOnDeviceNearby()) return; // The device was connected and the app was notified: check if we need to unbind the app // now. - final boolean shouldStayBound = any(otherAssociations, + final boolean shouldStayBound = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), it -> it.isNotifyOnDeviceNearby() && mDevicePresenceMonitor.isDevicePresent(it.getId())); if (shouldStayBound) return; mCompanionAppController.unbindCompanionApplication(userId, packageName); } + /** + * First, checks if the companion application should be removed from the list role holders when + * upon association's removal, i.e.: association's profile (matches the role) is not null, + * the application does not have other associations with the same profile, etc. + * + * <p> + * Then, if establishes that the application indeed has to be removed from the list of the role + * holders, checks if it could be done right now - + * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()} + * will kill the application's process, which leads poor user experience if the application was + * in foreground when this happened, to avoid this CDMS delays invoking + * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground. + * + * @return {@code true} if the application does NOT need be removed from the list of the role + * holders OR if the application was successfully removed from the list of role holders. + * I.e.: from the role-management perspective the association is done with. + * {@code false} if the application needs to be removed from the list of role the role + * holders, BUT it CDMS would prefer to do it later. + * I.e.: application is in the foreground at the moment, but invoking + * {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process, + * which would lead to the poor UX, hence need to try later. + */ + + private boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) { + if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association); + + final String deviceProfile = association.getDeviceProfile(); + if (deviceProfile == null) { + // No role was granted to for this association, there is nothing else we need to here. + return true; + } + + // Check if the applications is associated with another devices with the profile. If so, + // it should remain the role holder. + final int id = association.getId(); + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final boolean roleStillInUse = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), + it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId()); + if (roleStillInUse) { + // Application should remain a role holder, there is nothing else we need to here. + return true; + } + + final int packageProcessImportance = getPackageProcessImportance(userId, packageName); + if (packageProcessImportance <= IMPORTANCE_VISIBLE) { + // Need to remove the app from the list of role holders, but the process is visible to + // the user at the moment, so we'll need to it later: log and return false. + Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id + + " now - process is visible."); + return false; + } + + removeRoleHolderForAssociation(getContext(), association); + return true; + } + + private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { + return Binder.withCleanCallingIdentity(() -> { + final int uid = + mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + return mActivityManager.getUidImportance(uid); + }); + } + + /** + * Set revoked flag for active association and add the revoked association and the uid into + * the caches. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + private void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + // First: set revoked flag. + association = AssociationInfo.builder(association) + .setRevoked(true) + .build(); + + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + + // Second: add to the set. + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId()) + .add(association); + if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) { + mUidsPendingRoleHolderRemoval.put(uid, packageName); + + if (mUidsPendingRoleHolderRemoval.size() == 1) { + // Just added first uid: start the listener + mOnPackageVisibilityChangeListener.startListening(); + } + } + } + } + + /** + * Remove the revoked association form the cache and also remove the uid form the map if + * there are other associations with the same package still pending for role holder removal. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId) + .remove(association); + + final boolean shouldKeepUidForRemoval = any( + getPendingRoleHolderRemovalAssociationsForUser(userId), + ai -> packageName.equals(ai.getPackageName())); + // Do not remove the uid form the map since other associations with + // the same packageName still pending for role holder removal. + if (!shouldKeepUidForRemoval) { + mUidsPendingRoleHolderRemoval.remove(uid); + } + + if (mUidsPendingRoleHolderRemoval.isEmpty()) { + // The set is empty now - can "turn off" the listener. + mOnPackageVisibilityChangeListener.stopListening(); + } + } + } + + /** + * @return a copy of the revoked associations set (safeguarding against + * {@code ConcurrentModificationException}-s). + */ + private @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser( + @UserIdInt int userId) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + // Return a copy. + return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)); + } + } + + private String getPackageNameByUid(int uid) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + return mUidsPendingRoleHolderRemoval.get(uid); + } + } + private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) { final PackageInfo packageInfo = getPackageInfo(getContext(), association.getUserId(), association.getPackageName()); @@ -1175,4 +1405,80 @@ public class CompanionDeviceManagerService extends SystemService { persistStateForUser(userId); } } + + /** + * An OnUidImportanceListener class which watches the importance of the packages. + * In this class, we ONLY interested in the importance of the running process is greater than + * {@link RunningAppProcessInfo.IMPORTANCE_VISIBLE} for the uids have been added into the + * {@link mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the revoked + * associations for the same packages. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + private class OnPackageVisibilityChangeListener implements + ActivityManager.OnUidImportanceListener { + final @NonNull ActivityManager mAm; + + OnPackageVisibilityChangeListener(@NonNull ActivityManager am) { + this.mAm = am; + } + + void startListening() { + Binder.withCleanCallingIdentity( + () -> mAm.addOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this, + RunningAppProcessInfo.IMPORTANCE_VISIBLE)); + } + + void stopListening() { + Binder.withCleanCallingIdentity( + () -> mAm.removeOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this)); + } + + @Override + public void onUidImportance(int uid, int importance) { + if (importance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + // The lower the importance value the more "important" the process is. + // We are only interested when the process ceases to be visible. + return; + } + + final String packageName = getPackageNameByUid(uid); + if (packageName == null) { + // Not interested in this uid. + return; + } + + final int userId = UserHandle.getUserId(uid); + + boolean needToPersistStateForUser = false; + + for (AssociationInfo association : + getPendingRoleHolderRemovalAssociationsForUser(userId)) { + if (!packageName.equals(association.getPackageName())) continue; + + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Did not remove the role holder, will have to try again later. + continue; + } + + removeFromPendingRoleHolderRemoval(association); + needToPersistStateForUser = true; + } + + if (needToPersistStateForUser) { + mUserPersistenceHandler.postPersistUserState(userId); + } + } + } + + private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { + @Override + protected @NonNull Set<AssociationInfo> create(int userId) { + return new ArraySet<>(); + } + } } diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index 4d42838fff50..4b56c1b28036 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -103,7 +103,7 @@ import java.util.concurrent.ConcurrentMap; * Since Android T the data is stored to "companion_device_manager.xml" file in * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. * - * See {@link #getStorageFileForUser(int)} + * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)} * * <p> * Since Android T the data is stored using the v1 schema. @@ -120,7 +120,7 @@ import java.util.concurrent.ConcurrentMap; * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()} * </ul> * - * The following snippet is a sample of a file that is using v0 schema. + * The following snippet is a sample of a file that is using v1 schema. * <pre>{@code * <state persistence-version="1"> * <associations> @@ -130,6 +130,8 @@ import java.util.concurrent.ConcurrentMap; * mac_address="AA:BB:CC:DD:EE:00" * self_managed="false" * notify_device_nearby="false" + * revoked="false" + * last_time_connected="1634641160229" * time_approved="1634389553216"/> * * <association @@ -139,6 +141,8 @@ import java.util.concurrent.ConcurrentMap; * display_name="Jhon's Chromebook" * self_managed="true" * notify_device_nearby="false" + * revoked="false" + * last_time_connected="1634641160229" * time_approved="1634641160229"/> * </associations> * @@ -178,6 +182,7 @@ final class PersistentDataStore { private static final String XML_ATTR_PROFILE = "profile"; private static final String XML_ATTR_SELF_MANAGED = "self_managed"; private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; + private static final String XML_ATTR_REVOKED = "revoked"; private static final String XML_ATTR_TIME_APPROVED = "time_approved"; private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; @@ -415,7 +420,8 @@ final class PersistentDataStore { out.add(new AssociationInfo(associationId, userId, appPackage, MacAddress.fromString(deviceAddress), null, profile, - /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE)); + /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved, + Long.MAX_VALUE)); } private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, @@ -444,13 +450,14 @@ final class PersistentDataStore { final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME); final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); + final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); final long lastTimeConnected = readLongAttribute( parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, - appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved, - lastTimeConnected); + appPackage, macAddress, displayName, profile, selfManaged, notify, revoked, + timeApproved, lastTimeConnected); if (associationInfo != null) { out.add(associationInfo); } @@ -503,6 +510,8 @@ final class PersistentDataStore { writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); writeBooleanAttribute( serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); + writeBooleanAttribute( + serializer, XML_ATTR_REVOKED, a.isRevoked()); writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); writeLongAttribute( serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); @@ -544,11 +553,12 @@ final class PersistentDataStore { private static AssociationInfo createAssociationInfoNoThrow(int associationId, @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, - boolean notify, long timeApproved, long lastTimeConnected) { + boolean notify, boolean revoked, long timeApproved, long lastTimeConnected) { AssociationInfo associationInfo = null; try { associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress, - displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected); + displayName, profile, selfManaged, notify, revoked, timeApproved, + lastTimeConnected); } catch (Exception e) { if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); } diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java index 35488a80b78b..0fff3f488562 100644 --- a/services/companion/java/com/android/server/companion/RolesUtils.java +++ b/services/companion/java/com/android/server/companion/RolesUtils.java @@ -85,6 +85,8 @@ final class RolesUtils { final int userId = associationInfo.getUserId(); final UserHandle userHandle = UserHandle.of(userId); + Slog.i(TAG, "Removing CDM role holder, role=" + deviceProfile + + ", package=u" + userId + "\\" + packageName); roleManager.removeRoleHolderAsUser(deviceProfile, packageName, MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(), success -> { diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 395cf1805b63..70745ba0b368 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -24,8 +24,6 @@ import static android.content.ComponentName.createRelative; import static com.android.server.companion.Utils.prepareForIpc; -import static java.nio.charset.StandardCharsets.UTF_8; - import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.PendingIntent; @@ -41,23 +39,17 @@ import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; import android.os.UserHandle; +import android.permission.PermissionControllerManager; import android.util.Slog; -import android.util.Xml; import com.android.server.companion.AssociationStore; import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.PermissionsUtils; -import com.android.server.companion.datatransfer.permbackup.BackupHelper; import com.android.server.companion.proto.CompanionMessage; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * This processor builds user consent intent for a given SystemDataTransferRequest and processes the @@ -83,6 +75,8 @@ public class SystemDataTransferProcessor { private final AssociationStore mAssociationStore; private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; private final CompanionMessageProcessor mCompanionMessageProcessor; + private final PermissionControllerManager mPermissionControllerManager; + private final ExecutorService mExecutor; public SystemDataTransferProcessor(CompanionDeviceManagerService service, AssociationStore associationStore, @@ -93,6 +87,8 @@ public class SystemDataTransferProcessor { mSystemDataTransferRequestStore = systemDataTransferRequestStore; mCompanionMessageProcessor = companionMessageProcessor; mCompanionMessageProcessor.setListener(this::onCompleteMessageReceived); + mPermissionControllerManager = mContext.getSystemService(PermissionControllerManager.class); + mExecutor = Executors.newSingleThreadExecutor(); } /** @@ -180,23 +176,13 @@ public class SystemDataTransferProcessor { // TODO: Establish a secure channel - final long callingIdentityToken = Binder.clearCallingIdentity(); - // Start permission sync + final long callingIdentityToken = Binder.clearCallingIdentity(); try { - BackupHelper backupHelper = new BackupHelper(mContext, UserHandle.of(userId)); - XmlSerializer serializer = Xml.newSerializer(); - ByteArrayOutputStream backup = new ByteArrayOutputStream(); - serializer.setOutput(backup, UTF_8.name()); - - backupHelper.writeState(serializer); - - serializer.flush(); - - mCompanionMessageProcessor.paginateAndDispatchMessagesToApp(backup.toByteArray(), - CompanionMessage.PERMISSION_SYNC, packageName, userId, associationId); - } catch (IOException ioe) { - Slog.e(LOG_TAG, "Error while writing permission state."); + mPermissionControllerManager.getRuntimePermissionBackup(UserHandle.of(userId), + mExecutor, + backup -> mCompanionMessageProcessor.paginateAndDispatchMessagesToApp(backup, + CompanionMessage.PERMISSION_SYNC, packageName, userId, associationId)); } finally { Binder.restoreCallingIdentity(callingIdentityToken); } @@ -219,21 +205,11 @@ public class SystemDataTransferProcessor { private void processPermissionSyncMessage(CompanionMessageInfo messageInfo) { Slog.i(LOG_TAG, "Applying permissions."); // Start applying permissions + UserHandle user = mContext.getUser(); final long callingIdentityToken = Binder.clearCallingIdentity(); try { - BackupHelper backupHelper = new BackupHelper(mContext, mContext.getUser()); - XmlPullParser parser = Xml.newPullParser(); - ByteArrayInputStream stream = new ByteArrayInputStream( - messageInfo.getData()); - parser.setInput(stream, UTF_8.name()); - - backupHelper.restoreState(parser); - } catch (IOException e) { - Slog.e(LOG_TAG, "IOException reading message: " - + new String(messageInfo.getData())); - } catch (XmlPullParserException e) { - Slog.e(LOG_TAG, "Error parsing message: " - + new String(messageInfo.getData())); + mPermissionControllerManager.stageAndApplyRuntimePermissionsBackup( + messageInfo.getData(), user); } finally { Slog.i(LOG_TAG, "Permissions applied."); Binder.restoreCallingIdentity(callingIdentityToken); diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/BackupHelper.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/BackupHelper.java deleted file mode 100644 index 5e3c4c7834fe..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/BackupHelper.java +++ /dev/null @@ -1,782 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup; - -import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED; -import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED; -import static android.content.pm.PackageManager.GET_PERMISSIONS; -import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES; - -import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; -import static org.xmlpull.v1.XmlPullParser.END_TAG; -import static org.xmlpull.v1.XmlPullParser.START_TAG; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.content.pm.SigningInfo; -import android.os.Build; -import android.os.UserHandle; -import android.permission.PermissionManager; -import android.permission.PermissionManager.SplitPermissionInfo; -import android.util.ArraySet; -import android.util.Log; -import android.util.Slog; - -import com.android.server.companion.datatransfer.permbackup.model.AppPermissionGroup; -import com.android.server.companion.datatransfer.permbackup.model.AppPermissions; -import com.android.server.companion.datatransfer.permbackup.model.Permission; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.List; - -/** - * Helper for creating and restoring permission backups. - */ -public class BackupHelper { - private static final String LOG_TAG = BackupHelper.class.getSimpleName(); - - private static final String TAG_PERMISSION_BACKUP = "perm-grant-backup"; - private static final String ATTR_PLATFORM_VERSION = "version"; - - private static final String TAG_ALL_GRANTS = "rt-grants"; - - private static final String TAG_GRANT = "grant"; - private static final String ATTR_PACKAGE_NAME = "pkg"; - private static final String ATTR_HAS_MULTIPLE_SIGNERS = "multi-signers"; - - private static final String TAG_SIGNATURE = "sig"; - private static final String ATTR_SIGNATURE_VALUE = "v"; - - private static final String TAG_PERMISSION = "perm"; - private static final String ATTR_PERMISSION_NAME = "name"; - private static final String ATTR_IS_GRANTED = "g"; - private static final String ATTR_USER_SET = "set"; - private static final String ATTR_USER_FIXED = "fixed"; - private static final String ATTR_WAS_REVIEWED = "was-reviewed"; - - /** Flags of permissions to <u>not</u> back up */ - private static final int SYSTEM_RUNTIME_GRANT_MASK = FLAG_PERMISSION_POLICY_FIXED - | FLAG_PERMISSION_SYSTEM_FIXED; - - /** Make sure only one user can change the delayed permissions at a time */ - private static final Object sLock = new Object(); - - private final Context mContext; - - /** - * Create a new backup utils for a user. - * - * @param context A context to use - * @param user The user that is backed up / restored - */ - public BackupHelper(@NonNull Context context, @NonNull UserHandle user) { - try { - mContext = context.createPackageContextAsUser(context.getPackageName(), 0, user); - } catch (PackageManager.NameNotFoundException doesNotHappen) { - throw new IllegalStateException(); - } - } - - /** - * Forward parser and skip everything up to the end of the current tag. - * - * @param parser The parser to forward - */ - private static void skipToEndOfTag(@NonNull XmlPullParser parser) - throws IOException, XmlPullParserException { - int numOpenTags = 1; - while (numOpenTags > 0) { - switch (parser.next()) { - case START_TAG: - numOpenTags++; - break; - case END_TAG: - numOpenTags--; - break; - default: - // ignore - } - } - } - - /** - * Forward parser to a given direct sub-tag. - * - * @param parser The parser to forward - * @param tag The tag to search for - */ - private void skipToTag(@NonNull XmlPullParser parser, @NonNull String tag) - throws IOException, XmlPullParserException { - int type; - do { - type = parser.next(); - - switch (type) { - case START_TAG: - if (!parser.getName().equals(tag)) { - skipToEndOfTag(parser); - } - - return; - } - } while (type != END_DOCUMENT); - } - - /** - * Read a XML file and return the packages stored in it. - * - * @param parser The file to read - * - * @return The packages in this file - */ - private @NonNull ArrayList<BackupPackageState> parseFromXml(@NonNull XmlPullParser parser) - throws IOException, XmlPullParserException { - ArrayList<BackupPackageState> pkgStates = new ArrayList<>(); - - skipToTag(parser, TAG_PERMISSION_BACKUP); - - int backupPlatformVersion; - try { - backupPlatformVersion = Integer.parseInt( - parser.getAttributeValue(null, ATTR_PLATFORM_VERSION)); - } catch (NumberFormatException ignored) { - // Platforms P and before did not store the platform version - backupPlatformVersion = Build.VERSION_CODES.P; - } - - skipToTag(parser, TAG_ALL_GRANTS); - - if (parser.getEventType() != START_TAG && !parser.getName().equals(TAG_ALL_GRANTS)) { - throw new XmlPullParserException("Could not find " + TAG_PERMISSION_BACKUP + " > " - + TAG_ALL_GRANTS); - } - - // Read packages to restore from xml - int type; - do { - type = parser.next(); - - switch (type) { - case START_TAG: - switch (parser.getName()) { - case TAG_GRANT: - try { - pkgStates.add(BackupPackageState.parseFromXml(parser, mContext, - backupPlatformVersion)); - } catch (XmlPullParserException e) { - Log.e(LOG_TAG, "Could not parse permissions ", e); - skipToEndOfTag(parser); - } - break; - default: - // ignore tag - Log.w(LOG_TAG, "Found unexpected tag " + parser.getName() - + " during restore"); - skipToEndOfTag(parser); - } - } - } while (type != END_DOCUMENT); - - return pkgStates; - } - - /** - * Try to restore the permission state from XML. - * - * @param parser The xml to read - */ - public void restoreState(@NonNull XmlPullParser parser) throws IOException, - XmlPullParserException { - ArrayList<BackupPackageState> pkgStates = parseFromXml(parser); - - ArrayList<BackupPackageState> packagesToRestoreLater = new ArrayList<>(); - int numPkgStates = pkgStates.size(); - if (numPkgStates > 0) { - // Try to restore packages - for (int i = 0; i < numPkgStates; i++) { - BackupPackageState pkgState = pkgStates.get(i); - - PackageInfo pkgInfo; - try { - pkgInfo = mContext.getPackageManager().getPackageInfo(pkgState.mPackageName, - GET_PERMISSIONS | GET_SIGNING_CERTIFICATES); - } catch (PackageManager.NameNotFoundException ignored) { - packagesToRestoreLater.add(pkgState); - continue; - } - - pkgState.restore(mContext, pkgInfo); - } - } - -// synchronized (sLock) { -// writeDelayedStorePkgsLocked(packagesToRestoreLater); -// } - } - - /** - * Write a xml file for the given packages. - * - * @param serializer The file to write to - * @param pkgs The packages to write - */ - private static void writePkgsAsXml(@NonNull XmlSerializer serializer, - @NonNull ArrayList<BackupPackageState> pkgs) throws IOException { - serializer.startDocument(null, true); - - serializer.startTag(null, TAG_PERMISSION_BACKUP); - -// if (SDK_INT >= Build.VERSION_CODES.Q) { - // STOPSHIP: Remove compatibility code once Q SDK level is declared - serializer.attribute(null, ATTR_PLATFORM_VERSION, - Integer.valueOf(Build.VERSION_CODES.Q).toString()); -// } else { -// serializer.attribute(null, ATTR_PLATFORM_VERSION, -// Integer.valueOf(SDK_INT).toString()); -// } - - serializer.startTag(null, TAG_ALL_GRANTS); - - int numPkgs = pkgs.size(); - for (int i = 0; i < numPkgs; i++) { - BackupPackageState packageState = pkgs.get(i); - - if (packageState != null) { - packageState.writeAsXml(serializer); - } - } - - serializer.endTag(null, TAG_ALL_GRANTS); - serializer.endTag(null, TAG_PERMISSION_BACKUP); - - serializer.endDocument(); - } - - /** - * Write the state of all packages as XML. - * - * @param serializer The xml to write to - */ - public void writeState(@NonNull XmlSerializer serializer) throws IOException { - List<PackageInfo> pkgs = mContext.getPackageManager().getInstalledPackages( - GET_PERMISSIONS | GET_SIGNING_CERTIFICATES); - ArrayList<BackupPackageState> backupPkgs = new ArrayList<>(); - - int numPkgs = pkgs.size(); - for (int i = 0; i < numPkgs; i++) { - BackupPackageState packageState = BackupPackageState.fromAppPermissions(mContext, - pkgs.get(i)); - - if (packageState != null) { - backupPkgs.add(packageState); - } - } - - writePkgsAsXml(serializer, backupPkgs); - } - - /** - * State that needs to be backed up for a permission. - */ - private static class BackupPermissionState { - private final @NonNull String mPermissionName; - private final boolean mIsGranted; - private final boolean mIsUserSet; - private final boolean mIsUserFixed; - private final boolean mWasReviewed; - - private BackupPermissionState(@NonNull String permissionName, boolean isGranted, - boolean isUserSet, boolean isUserFixed, boolean wasReviewed) { - mPermissionName = permissionName; - mIsGranted = isGranted; - mIsUserSet = isUserSet; - mIsUserFixed = isUserFixed; - mWasReviewed = wasReviewed; - } - - /** - * Parse a package state from XML. - * - * @param parser The data to read - * @param context a context to use - * @param backupPlatformVersion The platform version the backup was created on - * - * @return The state - */ - static @NonNull List<BackupPermissionState> parseFromXml(@NonNull XmlPullParser parser, - @NonNull Context context, int backupPlatformVersion) - throws XmlPullParserException { - String permName = parser.getAttributeValue(null, ATTR_PERMISSION_NAME); - if (permName == null) { - throw new XmlPullParserException("Found " + TAG_PERMISSION + " without " - + ATTR_PERMISSION_NAME); - } - - ArrayList<String> expandedPermissions = new ArrayList<>(); - expandedPermissions.add(permName); - - List<SplitPermissionInfo> splitPerms = context.getSystemService( - PermissionManager.class).getSplitPermissions(); - - // Expand the properties to permissions that were split between the platform version the - // backup was taken and the current version. - int numSplitPerms = splitPerms.size(); - for (int i = 0; i < numSplitPerms; i++) { - SplitPermissionInfo splitPerm = splitPerms.get(i); - if (backupPlatformVersion < splitPerm.getTargetSdk() - && permName.equals(splitPerm.getSplitPermission())) { - expandedPermissions.addAll(splitPerm.getNewPermissions()); - } - } - - ArrayList<BackupPermissionState> parsedPermissions = new ArrayList<>( - expandedPermissions.size()); - int numExpandedPerms = expandedPermissions.size(); - for (int i = 0; i < numExpandedPerms; i++) { - parsedPermissions.add(new BackupPermissionState(expandedPermissions.get(i), - "true".equals(parser.getAttributeValue(null, ATTR_IS_GRANTED)), - "true".equals(parser.getAttributeValue(null, ATTR_USER_SET)), - "true".equals(parser.getAttributeValue(null, ATTR_USER_FIXED)), - "true".equals(parser.getAttributeValue(null, ATTR_WAS_REVIEWED)))); - } - - return parsedPermissions; - } - - /** - * Is the permission granted, also considering the app-op. - * - * <p>This does not consider the review-required state of the permission. - * - * @param perm The permission that might be granted - * - * @return {@code true} iff the permission and app-op is granted - */ - private static boolean isPermGrantedIncludingAppOp(@NonNull Permission perm) { - return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed()); - } - - /** - * Get the state of a permission to back up. - * - * @param perm The permission to back up - * @param appSupportsRuntimePermissions If the app supports runtimePermissions - * - * @return The state to back up or {@code null} if the permission does not need to be - * backed up. - */ - private static @Nullable BackupPermissionState fromPermission(@NonNull Permission perm, - boolean appSupportsRuntimePermissions) { - int grantFlags = perm.getFlags(); - - if ((grantFlags & SYSTEM_RUNTIME_GRANT_MASK) != 0) { - return null; - } - - if (!perm.isUserSet() && perm.isGrantedByDefault()) { - return null; - } - - boolean permissionWasReviewed; - boolean isNotInDefaultGrantState; - if (appSupportsRuntimePermissions) { - isNotInDefaultGrantState = isPermGrantedIncludingAppOp(perm); - permissionWasReviewed = false; - } else { - isNotInDefaultGrantState = !isPermGrantedIncludingAppOp(perm); - permissionWasReviewed = !perm.isReviewRequired(); - } - -// if (isNotInDefaultGrantState || perm.isUserSet() || perm.isUserFixed() -// || permissionWasReviewed) { -// return new BackupPermissionState(perm.getName(), -// isPermGrantedIncludingAppOp(perm), -// perm.isUserSet(), perm.isUserFixed(), permissionWasReviewed); -// } else { -// return null; -// } - if (perm.isUserSet() && isPermGrantedIncludingAppOp(perm)) { - return new BackupPermissionState(perm.getName(), /* isGranted */ true, - /* isUserSet */ true, perm.isUserFixed(), permissionWasReviewed); - } else { - return null; - } - } - - /** - * Get the states of all permissions of a group to back up. - * - * @param group The group of the permissions to back up - * - * @return The state to back up. Empty list if no permissions in the group need to be backed - * up - */ - static @NonNull ArrayList<BackupPermissionState> fromPermissionGroup( - @NonNull AppPermissionGroup group) { - ArrayList<BackupPermissionState> permissionsToRestore = new ArrayList<>(); - List<Permission> perms = group.getPermissions(); - - boolean appSupportsRuntimePermissions = - group.getApp().applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M; - - int numPerms = perms.size(); - for (int i = 0; i < numPerms; i++) { - BackupPermissionState permState = fromPermission(perms.get(i), - appSupportsRuntimePermissions); - if (permState != null) { - permissionsToRestore.add(permState); - } - } - - return permissionsToRestore; - } - - /** - * Write this state as XML. - * - * @param serializer The file to write to - */ - void writeAsXml(@NonNull XmlSerializer serializer) throws IOException { - serializer.startTag(null, TAG_PERMISSION); - - serializer.attribute(null, ATTR_PERMISSION_NAME, mPermissionName); - - if (mIsGranted) { - serializer.attribute(null, ATTR_IS_GRANTED, "true"); - } - - if (mIsUserSet) { - serializer.attribute(null, ATTR_USER_SET, "true"); - } - - if (mIsUserFixed) { - serializer.attribute(null, ATTR_USER_FIXED, "true"); - } - - if (mWasReviewed) { - serializer.attribute(null, ATTR_WAS_REVIEWED, "true"); - } - - serializer.endTag(null, TAG_PERMISSION); - } - - /** - * Restore this permission state. - * - * @param appPerms The {@link AppPermissions} to restore the state to - * @param restoreBackgroundPerms if {@code true} only restore background permissions, - * if {@code false} do not restore background permissions - */ - void restore(@NonNull AppPermissions appPerms, boolean restoreBackgroundPerms) { - AppPermissionGroup group = appPerms.getGroupForPermission(mPermissionName); - if (group == null) { - Log.w(LOG_TAG, "Could not find group for " + mPermissionName + " in " - + appPerms.getPackageInfo().packageName); - return; - } - - if (restoreBackgroundPerms != group.isBackgroundGroup()) { - return; - } - - Permission perm = group.getPermission(mPermissionName); - if (mWasReviewed) { - perm.unsetReviewRequired(); - } - - // Don't grant or revoke fixed permission groups - if (group.isSystemFixed() || group.isPolicyFixed()) { - return; - } - - if (!perm.isUserSet()) { - if (mIsGranted) { - group.grantRuntimePermissions(false, mIsUserFixed, - new String[]{mPermissionName}); - } else { - group.revokeRuntimePermissions(mIsUserFixed, - new String[]{mPermissionName}); - } - - perm.setUserSet(mIsUserSet); - } - } - } - - /** - * State that needs to be backed up for a package. - */ - private static class BackupPackageState { - final @NonNull String mPackageName; - final boolean mHasMultipleSigners; - @NonNull Signature[] mSignatures; - private final @NonNull ArrayList<BackupPermissionState> mPermissionsToRestore; - - private BackupPackageState(@NonNull String packageName, boolean hasMultipleSigners, - @NonNull Signature[] signatures, - @NonNull ArrayList<BackupPermissionState> permissionsToRestore) { - mPackageName = packageName; - mHasMultipleSigners = hasMultipleSigners; - mSignatures = signatures; - mPermissionsToRestore = permissionsToRestore; - } - - /** - * Parse a package state from XML. - * - * @param parser The data to read - * @param context a context to use - * @param backupPlatformVersion The platform version the backup was created on - * - * @return The state - */ - static @NonNull BackupPackageState parseFromXml(@NonNull XmlPullParser parser, - @NonNull Context context, int backupPlatformVersion) - throws IOException, XmlPullParserException { - String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); - if (packageName == null) { - throw new XmlPullParserException("Found " + TAG_GRANT + " without " - + ATTR_PACKAGE_NAME); - } - - boolean hasMultipleSigners = Boolean.parseBoolean( - parser.getAttributeValue(null, ATTR_HAS_MULTIPLE_SIGNERS)); - ArrayList<Signature> signatureList = new ArrayList<>(); - - ArrayList<BackupPermissionState> permissionsToRestore = new ArrayList<>(); - - while (true) { - switch (parser.next()) { - case START_TAG: - switch (parser.getName()) { - case TAG_PERMISSION: - try { - permissionsToRestore.addAll( - BackupPermissionState.parseFromXml(parser, context, - backupPlatformVersion)); - } catch (XmlPullParserException e) { - Log.e(LOG_TAG, "Could not parse permission for " - + packageName, e); - } - - skipToEndOfTag(parser); - break; - case TAG_SIGNATURE: - signatureList.add(new Signature( - parser.getAttributeValue(null, ATTR_SIGNATURE_VALUE))); - skipToEndOfTag(parser); - break; - default: - // ignore tag - Log.w(LOG_TAG, "Found unexpected tag " + parser.getName() - + " while restoring " + packageName); - skipToEndOfTag(parser); - } - - break; - case END_TAG: - Signature[] signatures = new Signature[signatureList.size()]; - for (int i = 0; i < signatureList.size(); i++) { - signatures[i] = signatureList.get(i); - } - return new BackupPackageState(packageName, hasMultipleSigners, signatures, - permissionsToRestore); - case END_DOCUMENT: - throw new XmlPullParserException("Could not parse state for " - + packageName); - } - } - } - - /** - * Get the state of a package to back up. - * - * @param context A context to use - * @param pkgInfo The package to back up. - * - * @return The state to back up or {@code null} if no permission of the package need to be - * backed up. - */ - static @Nullable BackupPackageState fromAppPermissions(@NonNull Context context, - @NonNull PackageInfo pkgInfo) { - AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, null); - - ArrayList<BackupPermissionState> permissionsToRestore = new ArrayList<>(); - List<AppPermissionGroup> groups = appPerms.getPermissionGroups(); - - // Check if the package has signatures - SigningInfo signingInfo = pkgInfo.signingInfo; - Signature[] signatures; - boolean hasMultipleSigners; - if (signingInfo.hasMultipleSigners()) { - hasMultipleSigners = true; - signatures = signingInfo.getApkContentsSigners(); - } else { - hasMultipleSigners = false; - signatures = signingInfo.getSigningCertificateHistory(); - } - if (signatures == null) { - Slog.d(LOG_TAG, "Skipping " + pkgInfo.packageName + ", it's unsigned."); - return null; - } - - int numGroups = groups.size(); - for (int groupNum = 0; groupNum < numGroups; groupNum++) { - AppPermissionGroup group = groups.get(groupNum); - - permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup(group)); - - // Background permissions are in a subgroup that is not part of - // {@link AppPermission#getPermissionGroups}. Hence add it explicitly here. - if (group.getBackgroundPermissions() != null) { - permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup( - group.getBackgroundPermissions())); - } - } - - if (permissionsToRestore.size() == 0) { - return null; - } - - return new BackupPackageState(pkgInfo.packageName, hasMultipleSigners, signatures, - permissionsToRestore); - } - - /** - * Write this state as XML. - * - * @param serializer The file to write to - */ - void writeAsXml(@NonNull XmlSerializer serializer) throws IOException { - if (mPermissionsToRestore.size() == 0) { - return; - } - - serializer.startTag(null, TAG_GRANT); - serializer.attribute(null, ATTR_PACKAGE_NAME, mPackageName); - - // Add signing info - serializer.attribute(null, ATTR_HAS_MULTIPLE_SIGNERS, - String.valueOf(mHasMultipleSigners)); - for (Signature signature : mSignatures) { - serializer.startTag(null, TAG_SIGNATURE); - serializer.attribute(null, ATTR_SIGNATURE_VALUE, signature.toCharsString()); - serializer.endTag(null, TAG_SIGNATURE); - } - - int numPerms = mPermissionsToRestore.size(); - for (int i = 0; i < numPerms; i++) { - mPermissionsToRestore.get(i).writeAsXml(serializer); - } - - serializer.endTag(null, TAG_GRANT); - } - - /** - * Restore this package state. - * - * @param context A context to use - * @param pkgInfo The package to restore. - */ - void restore(@NonNull Context context, @NonNull PackageInfo pkgInfo) { - Slog.e(LOG_TAG, "Restoring permissions for package [" + mPackageName + "]"); - - // Verify signature info - try { - if (mHasMultipleSigners && pkgInfo.signingInfo.hasMultipleSigners()) { - // If both packages are signed by multi signers, check if two signature sets are - // effectively matched. - if (!Signature.areEffectiveMatch(mSignatures, - pkgInfo.signingInfo.getApkContentsSigners())) { - Slog.e(LOG_TAG, "Multi-signers signatures don't match for package [" - + mPackageName + "], skipped."); - return; - } - } else if (!mHasMultipleSigners && !pkgInfo.signingInfo.hasMultipleSigners()) { - // If both packages are not signed by multi signers, check if two signature sets - // have overlaps. - Signature[] signatures = pkgInfo.signingInfo.getSigningCertificateHistory(); - if (signatures == null) { - Slog.e(LOG_TAG, "The dest package is unsigned."); - return; - } - boolean isMatched = false; - for (int i = 0; i < mSignatures.length; i++) { - for (int j = 0; j < signatures.length; j++) { - isMatched = Signature.areEffectiveMatch(mSignatures[i], signatures[j]); - } - } - if (!isMatched) { - Slog.e(LOG_TAG, "Single signer signatures don't match for package [" - + mPackageName + "], skipped."); - return; - } - } else { - Slog.e(LOG_TAG, "Number of signers don't match."); - return; - } - } catch (CertificateException ce) { - Slog.e(LOG_TAG, "Either the source or the dest package's bounced cert length " - + "looks fishy, skipped package [" + pkgInfo.packageName + "]"); - } - - AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, true, null); - - ArraySet<String> affectedPermissions = new ArraySet<>(); - // Restore background permissions after foreground permissions as for pre-M apps bg - // granted and fg revoked cannot be expressed. - int numPerms = mPermissionsToRestore.size(); - for (int i = 0; i < numPerms; i++) { - mPermissionsToRestore.get(i).restore(appPerms, false); - affectedPermissions.add(mPermissionsToRestore.get(i).mPermissionName); - } - for (int i = 0; i < numPerms; i++) { - mPermissionsToRestore.get(i).restore(appPerms, true); - } - - int numGroups = appPerms.getPermissionGroups().size(); - for (int i = 0; i < numGroups; i++) { - AppPermissionGroup group = appPerms.getPermissionGroups().get(i); - - // Only denied groups can be user fixed - if (group.areRuntimePermissionsGranted()) { - group.setUserFixed(false); - } - - AppPermissionGroup bgGroup = group.getBackgroundPermissions(); - if (bgGroup != null) { - // Only denied groups can be user fixed - if (bgGroup.areRuntimePermissionsGranted()) { - bgGroup.setUserFixed(false); - } - } - } - - appPerms.persistChanges(true, affectedPermissions); - } - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissionGroup.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissionGroup.java deleted file mode 100644 index cf146ac7a48e..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissionGroup.java +++ /dev/null @@ -1,1574 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.model; - -import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_FOREGROUND; -import static android.app.AppOpsManager.MODE_IGNORED; -import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.StringRes; -import android.app.ActivityManager; -import android.app.AppOpsManager; -import android.app.Application; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageItemInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.PermissionGroupInfo; -import android.content.pm.PermissionInfo; -import android.os.Build; -import android.os.UserHandle; -import android.permission.PermissionManager; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.Log; - -import com.android.server.companion.datatransfer.permbackup.utils.ArrayUtils; -import com.android.server.companion.datatransfer.permbackup.utils.LocationUtils; -import com.android.server.companion.datatransfer.permbackup.utils.SoftRestrictedPermissionPolicy; -import com.android.server.companion.datatransfer.permbackup.utils.Utils; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -/** - * All permissions of a permission group that are requested by an app. - * - * <p>Some permissions only grant access to the protected resource while the app is running in the - * foreground. These permissions are considered "split" into this foreground and a matching - * "background" permission. - * - * <p>All background permissions of the group are not in the main group and will not be affected - * by operations on the group. The background permissions can be found in the {@link - * #getBackgroundPermissions() background permissions group}. - */ -public final class AppPermissionGroup implements Comparable<AppPermissionGroup> { - private static final String LOG_TAG = AppPermissionGroup.class.getSimpleName(); - private static final String PLATFORM_PACKAGE_NAME = "android"; - - private static final String KILL_REASON_APP_OP_CHANGE = "Permission related app op changed"; - - /** - * Importance level to define the threshold for whether a package is in a state which resets the - * timer on its one-time permission session - */ - private static final int ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER = - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; - - /** - * Importance level to define the threshold for whether a package is in a state which keeps its - * one-time permission session alive after the timer ends - */ - private static final int ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE = - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; - - private final Context mContext; - private final UserHandle mUserHandle; - private final PackageManager mPackageManager; - private final AppOpsManager mAppOps; - private final ActivityManager mActivityManager; - private final Collator mCollator; - - private final PackageInfo mPackageInfo; - private final String mName; - private final String mDeclaringPackage; - private final CharSequence mLabel; - private final CharSequence mFullLabel; - private final @StringRes int mRequest; - private final @StringRes int mRequestDetail; - private final @StringRes int mBackgroundRequest; - private final @StringRes int mBackgroundRequestDetail; - private final @StringRes int mUpgradeRequest; - private final @StringRes int mUpgradeRequestDetail; - private final CharSequence mDescription; - private final ArrayMap<String, Permission> mPermissions = new ArrayMap<>(); - private final String mIconPkg; - private final int mIconResId; - - /** Delay changes until {@link #persistChanges} is called */ - private final boolean mDelayChanges; - - /** - * Some permissions are split into foreground and background permission. All non-split and - * foreground permissions are in {@link #mPermissions}, all background permissions are in - * this field. - */ - private AppPermissionGroup mBackgroundPermissions; - - private final boolean mAppSupportsRuntimePermissions; - private final boolean mIsEphemeralApp; - private final boolean mIsNonIsolatedStorage; - private boolean mContainsEphemeralPermission; - private boolean mContainsPreRuntimePermission; - - /** - * Does this group contain at least one permission that is split into a foreground and - * background permission? This does not necessarily mean that the app also requested the - * background permission. - */ - private boolean mHasPermissionWithBackgroundMode; - - private boolean mTriggerLocationAccessCheckOnPersist; - - private boolean mIsSelfRevoked; - - /** - * Create the app permission group. - * - * @param context the {@code Context} to retrieve system services. - * @param packageInfo package information about the app. - * @param permissionName the name of the permission this object represents. - * @param delayChanges whether to delay changes until {@link #persistChanges} is called. - * - * @return the AppPermissionGroup. - */ - public static AppPermissionGroup create(Context context, PackageInfo packageInfo, - String permissionName, boolean delayChanges) { - PermissionInfo permissionInfo; - try { - permissionInfo = context.getPackageManager().getPermissionInfo(permissionName, 0); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - - if ((permissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) - != PermissionInfo.PROTECTION_DANGEROUS - || (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) == 0 - || (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) != 0) { - return null; - } - - String group = Utils.getGroupOfPermission(permissionInfo); - PackageItemInfo groupInfo = permissionInfo; - if (group != null) { - try { - groupInfo = context.getPackageManager().getPermissionGroupInfo(group, 0); - } catch (PackageManager.NameNotFoundException e) { - /* ignore */ - } - } - - List<PermissionInfo> permissionInfos = null; - if (groupInfo instanceof PermissionGroupInfo) { - try { - permissionInfos = Utils.getPermissionInfosForGroup(context.getPackageManager(), - groupInfo.name); - } catch (PackageManager.NameNotFoundException e) { - /* ignore */ - } - } - - return create(context, packageInfo, groupInfo, permissionInfos, delayChanges); - } - - /** - * Create the app permission group. - * - * @param app the current application - * @param packageName the name of the package - * @param permissionGroupName the name of the permission group - * @param user the user of the package - * @param delayChanges whether to delay changes until {@link #persistChanges} is called. - * - * @return the AppPermissionGroup. - */ - public static AppPermissionGroup create(Application app, String packageName, - String permissionGroupName, UserHandle user, boolean delayChanges) { - try { - PackageInfo packageInfo = Utils.getUserContext(app, user).getPackageManager() - .getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); - PackageItemInfo groupInfo = Utils.getGroupInfo(permissionGroupName, app); - if (groupInfo == null) { - return null; - } - - List<PermissionInfo> permissionInfos = null; - if (groupInfo instanceof PermissionGroupInfo) { - permissionInfos = Utils.getPermissionInfosForGroup(app.getPackageManager(), - groupInfo.name); - } - return create(app, packageInfo, groupInfo, permissionInfos, delayChanges); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - - /** - * Create the app permission group. - * - * @param context the {@code Context} to retrieve system services. - * @param packageInfo package information about the app. - * @param groupInfo the information about the group created. - * @param permissionInfos the information about the permissions belonging to the group. - * @param delayChanges whether to delay changes until {@link #persistChanges} is called. - * - * @return the AppPermissionGroup. - */ - public static AppPermissionGroup create(Context context, PackageInfo packageInfo, - PackageItemInfo groupInfo, List<PermissionInfo> permissionInfos, boolean delayChanges) { - PackageManager packageManager = context.getPackageManager(); - CharSequence groupLabel = groupInfo.loadLabel(packageManager); - CharSequence fullGroupLabel = groupInfo.loadSafeLabel(packageManager, 0, - TextUtils.SAFE_STRING_FLAG_TRIM | TextUtils.SAFE_STRING_FLAG_FIRST_LINE); - return create(context, packageInfo, groupInfo, permissionInfos, groupLabel, - fullGroupLabel, delayChanges); - } - - /** - * Create the app permission group. - * - * @param context the {@code Context} to retrieve system services. - * @param packageInfo package information about the app. - * @param groupInfo the information about the group created. - * @param permissionInfos the information about the permissions belonging to the group. - * @param groupLabel the label of the group. - * @param fullGroupLabel the untruncated label of the group. - * @param delayChanges whether to delay changes until {@link #persistChanges} is called. - * - * @return the AppPermissionGroup. - */ - public static AppPermissionGroup create(Context context, PackageInfo packageInfo, - PackageItemInfo groupInfo, List<PermissionInfo> permissionInfos, - CharSequence groupLabel, CharSequence fullGroupLabel, boolean delayChanges) { - PackageManager packageManager = context.getPackageManager(); - UserHandle userHandle = UserHandle.getUserHandleForUid(packageInfo.applicationInfo.uid); - - if (groupInfo instanceof PermissionInfo) { - permissionInfos = new ArrayList<>(); - permissionInfos.add((PermissionInfo) groupInfo); - } - - if (permissionInfos == null || permissionInfos.isEmpty()) { - return null; - } - - AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); - - AppPermissionGroup group = new AppPermissionGroup(context, packageInfo, groupInfo.name, - groupInfo.packageName, groupLabel, fullGroupLabel, - /* description */ null, /* request */ 0, - /* requestDetail */ 0, /* backgroundRequest */ 0, - /* backgroundRequestDetail */ 0, /* upgradeRequest */0, - /* upgradeRequestDetail */ 0, groupInfo.packageName, groupInfo.icon, - userHandle, delayChanges, appOpsManager); - - final Set<String> exemptedRestrictedPermissions = context.getPackageManager() - .getWhitelistedRestrictedPermissions(packageInfo.packageName, - Utils.FLAGS_PERMISSION_WHITELIST_ALL); - - // Parse and create permissions requested by the app - ArrayMap<String, Permission> allPermissions = new ArrayMap<>(); - final int permissionCount = packageInfo.requestedPermissions == null ? 0 - : packageInfo.requestedPermissions.length; - String packageName = packageInfo.packageName; - for (int i = 0; i < permissionCount; i++) { - String requestedPermission = packageInfo.requestedPermissions[i]; - - PermissionInfo requestedPermissionInfo = null; - - for (PermissionInfo permissionInfo : permissionInfos) { - if (requestedPermission.equals(permissionInfo.name)) { - requestedPermissionInfo = permissionInfo; - break; - } - } - - if (requestedPermissionInfo == null) { - continue; - } - - // Collect only runtime permissions. - if ((requestedPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) - != PermissionInfo.PROTECTION_DANGEROUS) { - continue; - } - - // Don't allow toggling non-platform permission groups for legacy apps via app ops. - if (packageInfo.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1 - && !PLATFORM_PACKAGE_NAME.equals(groupInfo.packageName)) { - continue; - } - - final boolean granted = (packageInfo.requestedPermissionsFlags[i] - & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0; - - final String appOp = PLATFORM_PACKAGE_NAME.equals(requestedPermissionInfo.packageName) - ? AppOpsManager.permissionToOp(requestedPermissionInfo.name) : null; - - final boolean appOpAllowed; - if (appOp == null) { - appOpAllowed = false; - } else { - int appOpsMode = appOpsManager.unsafeCheckOpRaw(appOp, - packageInfo.applicationInfo.uid, packageName); - appOpAllowed = appOpsMode == MODE_ALLOWED || appOpsMode == MODE_FOREGROUND; - } - - final int flags = packageManager.getPermissionFlags( - requestedPermission, packageName, userHandle); - - Permission permission = new Permission(requestedPermission, requestedPermissionInfo, - granted, appOp, appOpAllowed, flags); - - if (requestedPermissionInfo.backgroundPermission != null) { - group.mHasPermissionWithBackgroundMode = true; - } - - allPermissions.put(requestedPermission, permission); - } - - int numPermissions = allPermissions.size(); - if (numPermissions == 0) { - return null; - } - - // Link up foreground and background permissions - for (int i = 0; i < allPermissions.size(); i++) { - Permission permission = allPermissions.valueAt(i); - - if (permission.getBackgroundPermissionName() != null) { - Permission backgroundPermission = allPermissions.get( - permission.getBackgroundPermissionName()); - - if (backgroundPermission != null) { - backgroundPermission.addForegroundPermissions(permission); - permission.setBackgroundPermission(backgroundPermission); - - // The background permissions isAppOpAllowed refers to the background state of - // the foregound permission's appOp. Hence we can only set it once we know the - // matching foreground permission. - // @see #allowAppOp - if (context.getSystemService(AppOpsManager.class).unsafeCheckOpRaw( - permission.getAppOp(), packageInfo.applicationInfo.uid, - packageInfo.packageName) == MODE_ALLOWED) { - backgroundPermission.setAppOpAllowed(true); - } - } - } - } - - // Add permissions found to this group - for (int i = 0; i < numPermissions; i++) { - Permission permission = allPermissions.valueAt(i); - - if ((!permission.isHardRestricted() - || exemptedRestrictedPermissions.contains(permission.getName())) - && (!permission.isSoftRestricted() - || SoftRestrictedPermissionPolicy.shouldShow(packageInfo, permission))) { - if (permission.isBackgroundPermission()) { - if (group.getBackgroundPermissions() == null) { - group.mBackgroundPermissions = new AppPermissionGroup(group.mContext, - group.getApp(), group.getName(), group.getDeclaringPackage(), - group.getLabel(), group.getFullLabel(), group.getDescription(), - group.getRequest(), group.getRequestDetail(), - group.getBackgroundRequest(), group.getBackgroundRequestDetail(), - group.getUpgradeRequest(), group.getUpgradeRequestDetail(), - group.getIconPkg(), group.getIconResId(), group.getUser(), - delayChanges, appOpsManager); - } - - group.getBackgroundPermissions().addPermission(permission); - } else { - group.addPermission(permission); - } - } - } - - if (group.getPermissions().isEmpty()) { - return null; - } - - return group; - } - - private AppPermissionGroup(Context context, PackageInfo packageInfo, String name, - String declaringPackage, CharSequence label, CharSequence fullLabel, - CharSequence description, @StringRes int request, @StringRes int requestDetail, - @StringRes int backgroundRequest, @StringRes int backgroundRequestDetail, - @StringRes int upgradeRequest, @StringRes int upgradeRequestDetail, - String iconPkg, int iconResId, UserHandle userHandle, boolean delayChanges, - @NonNull AppOpsManager appOpsManager) { - int targetSDK = packageInfo.applicationInfo.targetSdkVersion; - - mContext = context; - mUserHandle = userHandle; - mPackageManager = mContext.getPackageManager(); - mPackageInfo = packageInfo; - mAppSupportsRuntimePermissions = targetSDK > Build.VERSION_CODES.LOLLIPOP_MR1; - mIsEphemeralApp = packageInfo.applicationInfo.isInstantApp(); - mAppOps = appOpsManager; - mActivityManager = context.getSystemService(ActivityManager.class); - mDeclaringPackage = declaringPackage; - mName = name; - mLabel = label; - mFullLabel = fullLabel; - mDescription = description; - mCollator = Collator.getInstance( - context.getResources().getConfiguration().getLocales().get(0)); - mRequest = request; - mRequestDetail = requestDetail; - mBackgroundRequest = backgroundRequest; - mBackgroundRequestDetail = backgroundRequestDetail; - mUpgradeRequest = upgradeRequest; - mUpgradeRequestDetail = upgradeRequestDetail; - mDelayChanges = delayChanges; - if (iconResId != 0) { - mIconPkg = iconPkg; - mIconResId = iconResId; - } else { - mIconPkg = context.getPackageName(); - mIconResId = 0; // doesn't matter to CDM - } - - mIsNonIsolatedStorage = targetSDK < Build.VERSION_CODES.P - || (targetSDK < Build.VERSION_CODES.R - && mAppOps.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, - packageInfo.applicationInfo.uid, packageInfo.packageName) == MODE_ALLOWED); - } - - boolean doesSupportRuntimePermissions() { - return mAppSupportsRuntimePermissions; - } - - boolean isGrantingAllowed() { - return (!mIsEphemeralApp || mContainsEphemeralPermission) - && (mAppSupportsRuntimePermissions || mContainsPreRuntimePermission); - } - - boolean isReviewRequired() { - if (mAppSupportsRuntimePermissions) { - return false; - } - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isReviewRequired()) { - return true; - } - } - return false; - } - - /** - * Are any of the permissions in this group user sensitive. - * - * @return {@code true} if any of the permissions in the group is user sensitive. - */ - public boolean isUserSensitive() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isUserSensitive()) { - return true; - } - } - return false; - } - - void unsetReviewRequired() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isReviewRequired()) { - permission.unsetReviewRequired(); - } - } - - if (!mDelayChanges) { - persistChanges(false); - } - } - - boolean hasGrantedByDefaultPermission() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isGrantedByDefault()) { - return true; - } - } - return false; - } - - public PackageInfo getApp() { - return mPackageInfo; - } - - String getName() { - return mName; - } - - String getDeclaringPackage() { - return mDeclaringPackage; - } - - String getIconPkg() { - return mIconPkg; - } - - int getIconResId() { - return mIconResId; - } - - CharSequence getLabel() { - return mLabel; - } - - /** - * Get the full un-ellipsized label of the permission group. - * - * @return the full label of the group. - */ - public CharSequence getFullLabel() { - return mFullLabel; - } - - /** - * @hide - * @return The resource Id of the request string. - */ - public @StringRes int getRequest() { - return mRequest; - } - - /** - * Get the (subtitle) message explaining to the user that the permission is only granted to - * the apps running in the foreground. - * - * @return the message or 0 if unset - */ - public @StringRes int getRequestDetail() { - return mRequestDetail; - } - - /** - * Get the title of the dialog explaining to the user that the permission is granted while - * the app is in background and in foreground. - * - * @return the message or 0 if unset - */ - public @StringRes int getBackgroundRequest() { - return mBackgroundRequest; - } - - /** - * Get the (subtitle) message explaining to the user that the she/he is about to allow the - * app to have background access. - * - * @return the message or 0 if unset - */ - public @StringRes int getBackgroundRequestDetail() { - return mBackgroundRequestDetail; - } - - /** - * Get the title of the dialog explaining to the user that the permission, which was - * previously only granted for foreground, is granted while the app is in background and in - * foreground. - * - * @return the message or 0 if unset - */ - public @StringRes int getUpgradeRequest() { - return mUpgradeRequest; - } - - /** - * Get the (subtitle) message explaining to the user that the she/he is about to allow the - * app to have background access while currently having foreground only. - * - * @return the message or 0 if unset - */ - public @StringRes int getUpgradeRequestDetail() { - return mUpgradeRequestDetail; - } - - public CharSequence getDescription() { - return mDescription; - } - - public UserHandle getUser() { - return mUserHandle; - } - - /** - * Check if the group contains the permission. - */ - public boolean hasPermission(String permission) { - return mPermissions.get(permission) != null; - } - - /** - * Return a permission if in this group. - * - * @param permissionName The name of the permission - * - * @return The permission - */ - public @Nullable Permission getPermission(@NonNull String permissionName) { - return mPermissions.get(permissionName); - } - - /** - * Check if at least one of the permissions in the entire permission group should be considered - * granted. - */ - public boolean areRuntimePermissionsGranted() { - return areRuntimePermissionsGranted(null); - } - - /** - * Check if at least one of the permissions in the filterPermissions should be considered - * granted. - */ - public boolean areRuntimePermissionsGranted(String[] filterPermissions) { - return areRuntimePermissionsGranted(filterPermissions, false); - } - - /** - * @param filterPermissions the permissions to check for, null for all in this group - * @param asOneTime add the requirement that at least one of the granted permissions must have - * the ONE_TIME flag to return true - */ - public boolean areRuntimePermissionsGranted(String[] filterPermissions, boolean asOneTime) { - return areRuntimePermissionsGranted(filterPermissions, asOneTime, true); - } - - /** - * Returns true if at least one of the permissions in filterPermissions (or the entire - * permission group if null) should be considered granted and satisfy the requirements - * described by asOneTime and includingAppOp. - * - * @param filterPermissions the permissions to check for, null for all in this group - * @param asOneTime add the requirement that the granted permission must have the ONE_TIME flag - * @param includingAppOp add the requirement that if the granted permissions has a - * corresponding AppOp, it must be allowed. - */ - public boolean areRuntimePermissionsGranted(String[] filterPermissions, boolean asOneTime, - boolean includingAppOp) { - if (LocationUtils.isLocationGroupAndProvider(mContext, mName, mPackageInfo.packageName)) { - return LocationUtils.isLocationEnabled(mContext) && !asOneTime; - } - // The permission of the extra location controller package is determined by the status of - // the controller package itself. - if (LocationUtils.isLocationGroupAndControllerExtraPackage( - mContext, mName, mPackageInfo.packageName)) { - return LocationUtils.isExtraLocationControllerPackageEnabled(mContext) && !asOneTime; - } - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (filterPermissions != null - && !ArrayUtils.contains(filterPermissions, permission.getName())) { - continue; - } - boolean isGranted = includingAppOp ? permission.isGrantedIncludingAppOp() - : permission.isGranted(); - if (isGranted && (!asOneTime || permission.isOneTime())) { - return true; - } - } - if (mBackgroundPermissions != null) { - // If asOneTime is true and none of the foreground permissions are one-time, but some - // background permissions are, then we still want to return true. - return mBackgroundPermissions.areRuntimePermissionsGranted(filterPermissions, - asOneTime, includingAppOp); - } - return false; - } - - boolean grantRuntimePermissions(boolean setByTheUser, boolean fixedByTheUser) { - return grantRuntimePermissions(setByTheUser, fixedByTheUser, null); - } - - /** - * Set mode of an app-op if needed. - * - * @param op The op to set - * @param uid The uid the app-op belongs to - * @param mode The new mode - * - * @return {@code true} iff app-op was changed - */ - private boolean setAppOpMode(@NonNull String op, int uid, int mode) { - int currentMode = mAppOps.unsafeCheckOpRaw(op, uid, mPackageInfo.packageName); - if (currentMode == mode) { - return false; - } - - mAppOps.setUidMode(op, uid, mode); - return true; - } - - /** - * Allow the app op for a permission/uid. - * - * <p>There are three cases: - * <dl> - * <dt>The permission is not split into foreground/background</dt> - * <dd>The app op matching the permission will be set to {@link AppOpsManager#MODE_ALLOWED}</dd> - * <dt>The permission is a foreground permission:</dt> - * <dd><dl><dt>The background permission permission is granted</dt> - * <dd>The app op matching the permission will be set to {@link AppOpsManager#MODE_ALLOWED}</dd> - * <dt>The background permission permission is <u>not</u> granted</dt> - * <dd>The app op matching the permission will be set to - * {@link AppOpsManager#MODE_FOREGROUND}</dd> - * </dl></dd> - * <dt>The permission is a background permission:</dt> - * <dd>All granted foreground permissions for this background permission will be set to - * {@link AppOpsManager#MODE_ALLOWED}</dd> - * </dl> - * - * @param permission The permission which has an appOps that should be allowed - * @param uid The uid of the process the app op is for - * - * @return {@code true} iff app-op was changed - */ - private boolean allowAppOp(Permission permission, int uid) { - boolean wasChanged = false; - - if (permission.isBackgroundPermission()) { - ArrayList<Permission> foregroundPermissions = permission.getForegroundPermissions(); - - int numForegroundPermissions = foregroundPermissions.size(); - for (int i = 0; i < numForegroundPermissions; i++) { - Permission foregroundPermission = foregroundPermissions.get(i); - if (foregroundPermission.isAppOpAllowed()) { - wasChanged |= setAppOpMode(foregroundPermission.getAppOp(), uid, MODE_ALLOWED); - } - } - } else { - if (permission.hasBackgroundPermission()) { - Permission backgroundPermission = permission.getBackgroundPermission(); - - if (backgroundPermission == null) { - // The app requested a permission that has a background permission but it did - // not request the background permission, hence it can never get background - // access - wasChanged = setAppOpMode(permission.getAppOp(), uid, MODE_FOREGROUND); - } else { - if (backgroundPermission.isAppOpAllowed()) { - wasChanged = setAppOpMode(permission.getAppOp(), uid, MODE_ALLOWED); - } else { - wasChanged = setAppOpMode(permission.getAppOp(), uid, MODE_FOREGROUND); - } - } - } else { - wasChanged = setAppOpMode(permission.getAppOp(), uid, MODE_ALLOWED); - } - } - - return wasChanged; - } - - /** - * Kills the app the permissions belong to (and all apps sharing the same uid) - * - * @param reason The reason why the apps are killed - */ - private void killApp(String reason) { - mActivityManager.killUid(mPackageInfo.applicationInfo.uid, reason); - } - - /** - * Grant permissions of the group. - * - * <p>This also automatically grants all app ops for permissions that have app ops. - * <p>This does <u>only</u> grant permissions in {@link #mPermissions}, i.e. usually not - * the background permissions. - * - * @param setByTheUser If the user has made the decision. This does not unset the flag - * @param fixedByTheUser If the user requested that she/he does not want to be asked again - * @param filterPermissions If {@code null} all permissions of the group will be granted. - * Otherwise only permissions in {@code filterPermissions} will be - * granted. - * - * @return {@code true} iff all permissions of this group could be granted. - */ - public boolean grantRuntimePermissions(boolean setByTheUser, boolean fixedByTheUser, - String[] filterPermissions) { - boolean killApp = false; - boolean wasAllGranted = true; - - // We toggle permissions only to apps that support runtime - // permissions, otherwise we toggle the app op corresponding - // to the permission if the permission is granted to the app. - for (Permission permission : mPermissions.values()) { - if (filterPermissions != null - && !ArrayUtils.contains(filterPermissions, permission.getName())) { - continue; - } - - if (!permission.isGrantingAllowed(mIsEphemeralApp, mAppSupportsRuntimePermissions)) { - // Skip unallowed permissions. - continue; - } - - boolean wasGranted = permission.isGrantedIncludingAppOp(); - - if (mAppSupportsRuntimePermissions) { - // Do not touch permissions fixed by the system. - if (permission.isSystemFixed()) { - wasAllGranted = false; - break; - } - - // Ensure the permission app op is enabled before the permission grant. - if (permission.affectsAppOp() && !permission.isAppOpAllowed()) { - permission.setAppOpAllowed(true); - } - - // Grant the permission if needed. - if (!permission.isGranted()) { - permission.setGranted(true); - } - - // Update the permission flags. - if (!fixedByTheUser) { - if (permission.isUserFixed()) { - permission.setUserFixed(false); - } - if (setByTheUser) { - if (!permission.isUserSet()) { - permission.setUserSet(true); - } - } - } else { - if (!permission.isUserFixed()) { - permission.setUserFixed(true); - } - if (permission.isUserSet()) { - permission.setUserSet(false); - } - } - if (permission.isReviewRequired()) { - permission.unsetReviewRequired(); - } - } else { - // Legacy apps cannot have a not granted permission but just in case. - if (!permission.isGranted()) { - continue; - } - - // If the permissions has no corresponding app op, then it is a - // third-party one and we do not offer toggling of such permissions. - if (permission.affectsAppOp()) { - if (!permission.isAppOpAllowed()) { - permission.setAppOpAllowed(true); - - // Legacy apps do not know that they have to retry access to a - // resource due to changes in runtime permissions (app ops in this - // case). Therefore, we restart them on app op change, so they - // can pick up the change. - killApp = true; - } - - // Mark that the permission is not kept granted only for compatibility. - if (permission.isRevokedCompat()) { - permission.setRevokedCompat(false); - } - } - - // Granting a permission explicitly means the user already - // reviewed it so clear the review flag on every grant. - if (permission.isReviewRequired()) { - permission.unsetReviewRequired(); - } - } - - // If we newly grant background access to the fine location, double-guess the user some - // time later if this was really the right choice. - if (!wasGranted && permission.isGrantedIncludingAppOp()) { - if (permission.getName().equals(ACCESS_FINE_LOCATION)) { - Permission bgPerm = permission.getBackgroundPermission(); - if (bgPerm != null) { - if (bgPerm.isGrantedIncludingAppOp()) { - mTriggerLocationAccessCheckOnPersist = true; - } - } - } else if (permission.getName().equals(ACCESS_BACKGROUND_LOCATION)) { - ArrayList<Permission> fgPerms = permission.getForegroundPermissions(); - if (fgPerms != null) { - int numFgPerms = fgPerms.size(); - for (int fgPermNum = 0; fgPermNum < numFgPerms; fgPermNum++) { - Permission fgPerm = fgPerms.get(fgPermNum); - - if (fgPerm.getName().equals(ACCESS_FINE_LOCATION)) { - if (fgPerm.isGrantedIncludingAppOp()) { - mTriggerLocationAccessCheckOnPersist = true; - } - - break; - } - } - } - } - } - } - - if (!mDelayChanges) { - persistChanges(false); - - if (killApp) { - killApp(KILL_REASON_APP_OP_CHANGE); - } - } - - return wasAllGranted; - } - - boolean revokeRuntimePermissions(boolean fixedByTheUser) { - return revokeRuntimePermissions(fixedByTheUser, null); - } - - /** - * Disallow the app op for a permission/uid. - * - * <p>There are three cases: - * <dl> - * <dt>The permission is not split into foreground/background</dt> - * <dd>The app op matching the permission will be set to {@link AppOpsManager#MODE_IGNORED}</dd> - * <dt>The permission is a foreground permission:</dt> - * <dd>The app op matching the permission will be set to {@link AppOpsManager#MODE_IGNORED}</dd> - * <dt>The permission is a background permission:</dt> - * <dd>All granted foreground permissions for this background permission will be set to - * {@link AppOpsManager#MODE_FOREGROUND}</dd> - * </dl> - * - * @param permission The permission which has an appOps that should be disallowed - * @param uid The uid of the process the app op if for - * - * @return {@code true} iff app-op was changed - */ - private boolean disallowAppOp(Permission permission, int uid) { - boolean wasChanged = false; - - if (permission.isBackgroundPermission()) { - ArrayList<Permission> foregroundPermissions = permission.getForegroundPermissions(); - - int numForegroundPermissions = foregroundPermissions.size(); - for (int i = 0; i < numForegroundPermissions; i++) { - Permission foregroundPermission = foregroundPermissions.get(i); - if (foregroundPermission.isAppOpAllowed()) { - wasChanged |= setAppOpMode(foregroundPermission.getAppOp(), uid, - MODE_FOREGROUND); - } - } - } else { - wasChanged = setAppOpMode(permission.getAppOp(), uid, MODE_IGNORED); - } - - return wasChanged; - } - - /** - * Revoke permissions of the group. - * - * <p>This also disallows all app ops for permissions that have app ops. - * <p>This does <u>only</u> revoke permissions in {@link #mPermissions}, i.e. usually not - * the background permissions. - * - * @param fixedByTheUser If the user requested that she/he does not want to be asked again - * @param filterPermissions If {@code null} all permissions of the group will be revoked. - * Otherwise only permissions in {@code filterPermissions} will be - * revoked. - * - * @return {@code true} iff all permissions of this group could be revoked. - */ - public boolean revokeRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) { - boolean killApp = false; - boolean wasAllRevoked = true; - - // We toggle permissions only to apps that support runtime - // permissions, otherwise we toggle the app op corresponding - // to the permission if the permission is granted to the app. - for (Permission permission : mPermissions.values()) { - if (filterPermissions != null - && !ArrayUtils.contains(filterPermissions, permission.getName())) { - continue; - } - - // Do not touch permissions fixed by the system. - if (permission.isSystemFixed()) { - wasAllRevoked = false; - break; - } - - if (mAppSupportsRuntimePermissions) { - // Revoke the permission if needed. - if (permission.isGranted()) { - permission.setGranted(false); - } - - // Update the permission flags. - if (fixedByTheUser) { - // Take a note that the user fixed the permission. - if (permission.isUserSet() || !permission.isUserFixed()) { - permission.setUserSet(false); - permission.setUserFixed(true); - } - } else { - if (!permission.isUserSet() || permission.isUserFixed()) { - permission.setUserSet(true); - permission.setUserFixed(false); - } - } - - if (permission.affectsAppOp()) { - permission.setAppOpAllowed(false); - } - } else { - // Legacy apps cannot have a non-granted permission but just in case. - if (!permission.isGranted()) { - continue; - } - - // If the permission has no corresponding app op, then it is a - // third-party one and we do not offer toggling of such permissions. - if (permission.affectsAppOp()) { - if (permission.isAppOpAllowed()) { - permission.setAppOpAllowed(false); - - // Disabling an app op may put the app in a situation in which it - // has a handle to state it shouldn't have, so we have to kill the - // app. This matches the revoke runtime permission behavior. - killApp = true; - } - - // Mark that the permission is kept granted only for compatibility. - if (!permission.isRevokedCompat()) { - permission.setRevokedCompat(true); - } - } - } - } - - if (!mDelayChanges) { - persistChanges(false); - - if (killApp) { - killApp(KILL_REASON_APP_OP_CHANGE); - } - } - - return wasAllRevoked; - } - - /** - * Mark permissions in this group as policy fixed. - * - * @param filterPermissions The permissions to mark - */ - public void setPolicyFixed(@NonNull String[] filterPermissions) { - for (String permissionName : filterPermissions) { - Permission permission = mPermissions.get(permissionName); - - if (permission != null) { - permission.setPolicyFixed(true); - } - } - - if (!mDelayChanges) { - persistChanges(false); - } - } - - /** - * Set the user-fixed flag for all permissions in this group. - * - * @param isUsedFixed if the flag should be set or not - */ - public void setUserFixed(boolean isUsedFixed) { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - permission.setUserFixed(isUsedFixed); - } - - if (!mDelayChanges) { - persistChanges(false); - } - } - - /** - * Mark this group as having been self-revoked. - */ - public void setSelfRevoked() { - mIsSelfRevoked = true; - } - - /** - * Set the one-time flag for all permissions in this group. - * - * @param isOneTime if the flag should be set or not - */ - public void setOneTime(boolean isOneTime) { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - permission.setOneTime(isOneTime); - } - - if (!mDelayChanges) { - persistChanges(false); - } - } - - /** - * Set the user-set flag for all permissions in this group. - * - * @param isUserSet if the flag should be set or not - */ - public void setUserSet(boolean isUserSet) { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - permission.setUserSet(isUserSet); - } - - if (!mDelayChanges) { - persistChanges(false); - } - } - - /** - * Get all permissions in the group. - */ - public ArrayList<Permission> getPermissions() { - return new ArrayList<>(mPermissions.values()); - } - - /** - * @return An {@link AppPermissionGroup}-object that contains all background permissions for - * this group. - */ - public AppPermissionGroup getBackgroundPermissions() { - return mBackgroundPermissions; - } - - /** - * @return {@code true} iff the app request at least one permission in this group that has a - * background permission. It is possible that the app does not request the matching background - * permission and hence will only ever get foreground access, never background access. - */ - public boolean hasPermissionWithBackgroundMode() { - return mHasPermissionWithBackgroundMode; - } - - /** - * Is the group a storage permission group that is referring to an app that does not have - * isolated storage - * - * @return {@code true} iff this is a storage group on an app that does not have isolated - * storage - */ - public boolean isNonIsolatedStorage() { - return mIsNonIsolatedStorage; - } - - /** - * Whether this is group that contains all the background permission for regular permission - * group. - * - * @return {@code true} iff this is a background permission group. - * - * @see #getBackgroundPermissions() - */ - public boolean isBackgroundGroup() { - return mPermissions.valueAt(0).isBackgroundPermission(); - } - - /** - * Whether this group supports one-time permissions - * @return {@code true} iff this group supports one-time permissions - */ - public boolean supportsOneTimeGrant() { - return Utils.supportsOneTimeGrant(getName()); - } - - int getFlags() { - int flags = 0; - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - flags |= permission.getFlags(); - } - return flags; - } - - boolean isUserFixed() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isUserFixed()) { - return true; - } - } - return false; - } - - /** - * Check if there's a permission in the group is policy fixed. - */ - public boolean isPolicyFixed() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isPolicyFixed()) { - return true; - } - } - return false; - } - - boolean isUserSet() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isUserSet()) { - return true; - } - } - return false; - } - - /** - * Check if there's a permission in the group is system fixed. - */ - public boolean isSystemFixed() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isSystemFixed()) { - return true; - } - } - return false; - } - - /** - * @return Whether any of the permissions in this group is one-time - */ - public boolean isOneTime() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isOneTime()) { - return true; - } - } - return false; - } - - /** - * @return Whether at least one permission is granted and every granted permission is one-time - */ - public boolean isStrictlyOneTime() { - boolean oneTimePermissionFound = false; - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.isGranted()) { - if (!permission.isOneTime()) { - return false; - } - oneTimePermissionFound = true; - } - } - return oneTimePermissionFound; - } - - @Override - public int compareTo(AppPermissionGroup another) { - final int result = mCollator.compare(mLabel.toString(), another.mLabel.toString()); - if (result == 0) { - // Unbadged before badged. - return mPackageInfo.applicationInfo.uid - - another.mPackageInfo.applicationInfo.uid; - } - return result; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof AppPermissionGroup)) { - return false; - } - - AppPermissionGroup other = (AppPermissionGroup) o; - - boolean equal = mName.equals(other.mName) - && mPackageInfo.packageName.equals(other.mPackageInfo.packageName) - && mUserHandle.equals(other.mUserHandle) - && mPermissions.equals(other.mPermissions); - if (!equal) { - return false; - } - - if (mBackgroundPermissions != null && other.getBackgroundPermissions() != null) { - return mBackgroundPermissions.getPermissions().equals( - other.getBackgroundPermissions().getPermissions()); - } - return mBackgroundPermissions == other.getBackgroundPermissions(); - } - - @Override - public int hashCode() { - ArrayList<Permission> backgroundPermissions = new ArrayList<>(); - if (mBackgroundPermissions != null) { - backgroundPermissions = mBackgroundPermissions.getPermissions(); - } - return Objects.hash(mName, mPackageInfo.packageName, mUserHandle, mPermissions, - backgroundPermissions); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(getClass().getSimpleName()); - builder.append("{name=").append(mName); - if (mBackgroundPermissions != null) { - builder.append(", <has background permissions>}"); - } - if (!mPermissions.isEmpty()) { - builder.append(", <has permissions>}"); - } else { - builder.append('}'); - } - return builder.toString(); - } - - private void addPermission(Permission permission) { - mPermissions.put(permission.getName(), permission); - if (permission.isEphemeral()) { - mContainsEphemeralPermission = true; - } - if (!permission.isRuntimeOnly()) { - mContainsPreRuntimePermission = true; - } - } - - /** - * If the changes to this group were delayed, persist them to the platform. - * - * @param mayKillBecauseOfAppOpsChange If the app these permissions belong to may be killed if - * app ops change. If this is set to {@code false} the - * caller has to make sure to kill the app if needed. - */ - public void persistChanges(boolean mayKillBecauseOfAppOpsChange) { - persistChanges(mayKillBecauseOfAppOpsChange, null, null); - } - - /** - * If the changes to this group were delayed, persist them to the platform. - * - * @param mayKillBecauseOfAppOpsChange If the app these permissions belong to may be killed if - * app ops change. If this is set to {@code false} the - * caller has to make sure to kill the app if needed. - * @param revokeReason If any permissions are getting revoked, the reason for revoking them. - */ - public void persistChanges(boolean mayKillBecauseOfAppOpsChange, String revokeReason) { - persistChanges(mayKillBecauseOfAppOpsChange, revokeReason, null); - } - - /** - * If the changes to this group were delayed, persist them to the platform. - * - * @param mayKillBecauseOfAppOpsChange If the app these permissions belong to may be killed if - * app ops change. If this is set to {@code false} the - * caller has to make sure to kill the app if needed. - * @param revokeReason If any permissions are getting revoked, the reason for revoking them. - * @param filterPermissions If provided, only persist state for the given permissions - */ - public void persistChanges(boolean mayKillBecauseOfAppOpsChange, String revokeReason, - Set<String> filterPermissions) { - int uid = mPackageInfo.applicationInfo.uid; - - int numPermissions = mPermissions.size(); - boolean shouldKillApp = false; - - for (int i = 0; i < numPermissions; i++) { - Permission permission = mPermissions.valueAt(i); - - if (filterPermissions != null && !filterPermissions.contains(permission.getName())) { - continue; - } - - if (!permission.isSystemFixed()) { - if (permission.isGranted()) { - mPackageManager.grantRuntimePermission(mPackageInfo.packageName, - permission.getName(), mUserHandle); - } else { - boolean isCurrentlyGranted = mContext.checkPermission(permission.getName(), -1, - uid) == PERMISSION_GRANTED; - - if (isCurrentlyGranted) { - if (revokeReason == null) { - mPackageManager.revokeRuntimePermission(mPackageInfo.packageName, - permission.getName(), mUserHandle); - } else { - mPackageManager.revokeRuntimePermission(mPackageInfo.packageName, - permission.getName(), mUserHandle, revokeReason); - } - } - } - } - -// int flags = (permission.isUserSet() ? PackageManager.FLAG_PERMISSION_USER_SET : 0) -// | (permission.isUserFixed() ? PackageManager.FLAG_PERMISSION_USER_FIXED : 0) -// | (permission.isRevokedCompat() -// ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0) -// | (permission.isPolicyFixed() ? -// PackageManager.FLAG_PERMISSION_POLICY_FIXED : 0) -// | (permission.isReviewRequired() -// ? PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED : 0) -// | (permission.isOneTime() ? PackageManager.FLAG_PERMISSION_ONE_TIME : 0) -// | (permission.isSelectedLocationAccuracy() -// ? PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY : 0); - -// mPackageManager.updatePermissionFlags(permission.getName(), -// mPackageInfo.packageName, -// PackageManager.FLAG_PERMISSION_USER_SET -// | PackageManager.FLAG_PERMISSION_USER_FIXED -// | PackageManager.FLAG_PERMISSION_REVOKED_COMPAT -// | PackageManager.FLAG_PERMISSION_POLICY_FIXED -// | (permission.isReviewRequired() -// ? 0 : PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) -// | PackageManager.FLAG_PERMISSION_ONE_TIME -// | PackageManager.FLAG_PERMISSION_AUTO_REVOKED // clear auto revoke -// | PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY, -// flags, mUserHandle); - - if (permission.affectsAppOp()) { - if (!permission.isSystemFixed()) { - // Enabling/Disabling an app op may put the app in a situation in which it has - // a handle to state it shouldn't have, so we have to kill the app. This matches - // the revoke runtime permission behavior. - if (permission.isAppOpAllowed()) { - boolean wasChanged = allowAppOp(permission, uid); - shouldKillApp |= wasChanged && !mAppSupportsRuntimePermissions; - } else { - shouldKillApp |= disallowAppOp(permission, uid); - } - } - } - } - - if (mayKillBecauseOfAppOpsChange && shouldKillApp) { - killApp(KILL_REASON_APP_OP_CHANGE); - } - -// if (mTriggerLocationAccessCheckOnPersist) { -// new LocationAccessCheck(mContext, null).checkLocationAccessSoon(); -// mTriggerLocationAccessCheckOnPersist = false; -// } - -// String packageName = mPackageInfo.packageName; -// if (areRuntimePermissionsGranted(null, true, false)) { -// // Required to read device config in Utils.getOneTimePermissions*(). -// final long token = Binder.clearCallingIdentity(); -// try { -// if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { -// mContext.getSystemService(PermissionManager.class) -// .startOneTimePermissionSession(packageName, -// Utils.getOneTimePermissionsTimeout(), -// Utils.getOneTimePermissionsKilledDelay(mIsSelfRevoked), -// ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER, -// ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE); -// } else { -// mContext.getSystemService(PermissionManager.class) -// .startOneTimePermissionSession(packageName, -// Utils.getOneTimePermissionsTimeout(), -// ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER, -// ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE); -// } -// } finally { -// Binder.restoreCallingIdentity(token); -// } -// } else { -// mContext.getSystemService(PermissionManager.class) -// .stopOneTimePermissionSession(packageName); -// } - } - - /** - * Check if permission group contains a runtime permission that split from an installed - * permission and the split happened in an Android version higher than app's targetSdk. - * - * @return {@code true} if there is such permission, {@code false} otherwise - */ - public boolean hasInstallToRuntimeSplit() { - PermissionManager permissionManager = - (PermissionManager) mContext.getSystemService(PermissionManager.class); - - int numSplitPerms = permissionManager.getSplitPermissions().size(); - for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) { - PermissionManager.SplitPermissionInfo spi = - permissionManager.getSplitPermissions().get(splitPermNum); - String splitPerm = spi.getSplitPermission(); - - PermissionInfo pi; - try { - pi = mPackageManager.getPermissionInfo(splitPerm, 0); - } catch (NameNotFoundException e) { - Log.w(LOG_TAG, "No such permission: " + splitPerm, e); - continue; - } - - // Skip if split permission is not "install" permission. - if (pi.getProtection() != pi.PROTECTION_NORMAL) { - continue; - } - - List<String> newPerms = spi.getNewPermissions(); - int numNewPerms = newPerms.size(); - for (int newPermNum = 0; newPermNum < numNewPerms; newPermNum++) { - String newPerm = newPerms.get(newPermNum); - - if (!hasPermission(newPerm)) { - continue; - } - - try { - pi = mPackageManager.getPermissionInfo(newPerm, 0); - } catch (NameNotFoundException e) { - Log.w(LOG_TAG, "No such permission: " + newPerm, e); - continue; - } - - // Skip if new permission is not "runtime" permission. - if (pi.getProtection() != pi.PROTECTION_DANGEROUS) { - continue; - } - - if (mPackageInfo.applicationInfo.targetSdkVersion < spi.getTargetSdk()) { - return true; - } - } - } - return false; - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissions.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissions.java deleted file mode 100644 index 9ef8d532dbfc..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/AppPermissions.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.model; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.UserHandle; -import android.util.ArrayMap; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -/** - * An app that requests permissions. - * - * <p>Allows to query all permission groups of the app and which permission belongs to which group. - */ -public final class AppPermissions { - /** - * All permission groups the app requests. Background permission groups are attached to their - * foreground groups. - */ - private final ArrayList<AppPermissionGroup> mGroups = new ArrayList<>(); - - /** Cache: group name -> group */ - private final ArrayMap<String, AppPermissionGroup> mGroupNameToGroup = new ArrayMap<>(); - - /** Cache: permission name -> group. Might point to background group */ - private final ArrayMap<String, AppPermissionGroup> mPermissionNameToGroup = new ArrayMap<>(); - - private final Context mContext; - - private final CharSequence mAppLabel; - - private final Runnable mOnErrorCallback; - - private final boolean mSortGroups; - - /** Do not actually commit changes to the platform until {@link #persistChanges} is called */ - private final boolean mDelayChanges; - - private PackageInfo mPackageInfo; - - public AppPermissions(Context context, PackageInfo packageInfo, boolean sortGroups, - Runnable onErrorCallback) { - this(context, packageInfo, sortGroups, false, onErrorCallback); - } - - public AppPermissions(Context context, PackageInfo packageInfo, boolean sortGroups, - boolean delayChanges, Runnable onErrorCallback) { - mContext = context; - mPackageInfo = packageInfo; - mAppLabel = null; // doesn't matter for CDM - mSortGroups = sortGroups; - mDelayChanges = delayChanges; - mOnErrorCallback = onErrorCallback; - loadPermissionGroups(); - } - - public PackageInfo getPackageInfo() { - return mPackageInfo; - } - - /** - * Refresh package info and permission groups. - */ - public void refresh() { - loadPackageInfo(); - loadPermissionGroups(); - } - - public CharSequence getAppLabel() { - return mAppLabel; - } - - /** - * Get permission group by name. - */ - public AppPermissionGroup getPermissionGroup(String name) { - return mGroupNameToGroup.get(name); - } - - public List<AppPermissionGroup> getPermissionGroups() { - return mGroups; - } - - /** - * Check if the group is review required. - */ - public boolean isReviewRequired() { - final int groupCount = mGroups.size(); - for (int i = 0; i < groupCount; i++) { - AppPermissionGroup group = mGroups.get(i); - if (group.isReviewRequired()) { - return true; - } - } - return false; - } - - private void loadPackageInfo() { - try { - mPackageInfo = mContext.createPackageContextAsUser(mPackageInfo.packageName, 0, - UserHandle.getUserHandleForUid(mPackageInfo.applicationInfo.uid)) - .getPackageManager().getPackageInfo(mPackageInfo.packageName, - PackageManager.GET_PERMISSIONS); - } catch (PackageManager.NameNotFoundException e) { - if (mOnErrorCallback != null) { - mOnErrorCallback.run(); - } - } - } - - /** - * Add all individual permissions of the {@code group} to the {@link #mPermissionNameToGroup} - * lookup table. - * - * @param group The group of permissions to add - */ - private void addAllPermissions(AppPermissionGroup group) { - ArrayList<Permission> perms = group.getPermissions(); - - int numPerms = perms.size(); - for (int permNum = 0; permNum < numPerms; permNum++) { - mPermissionNameToGroup.put(perms.get(permNum).getName(), group); - } - } - - private void loadPermissionGroups() { - mGroups.clear(); - mGroupNameToGroup.clear(); - mPermissionNameToGroup.clear(); - - if (mPackageInfo.requestedPermissions != null) { - for (String requestedPerm : mPackageInfo.requestedPermissions) { - if (getGroupForPermission(requestedPerm) == null) { - AppPermissionGroup group = AppPermissionGroup.create(mContext, mPackageInfo, - requestedPerm, mDelayChanges); - if (group == null) { - continue; - } - - mGroups.add(group); - mGroupNameToGroup.put(group.getName(), group); - - addAllPermissions(group); - - AppPermissionGroup backgroundGroup = group.getBackgroundPermissions(); - if (backgroundGroup != null) { - addAllPermissions(backgroundGroup); - } - } - } - - if (mSortGroups) { - Collections.sort(mGroups); - } - } - } - - /** - * Find the group a permission belongs to. - * - * <p>The group found might be a background group. - * - * @param permission The name of the permission - * - * @return The group the permission belongs to - */ - public AppPermissionGroup getGroupForPermission(String permission) { - return mPermissionNameToGroup.get(permission); - } - - /** - * If the changes to the permission groups were delayed, persist them now. - * - * @param mayKillBecauseOfAppOpsChange If the app may be killed if app ops change. If this is - * set to {@code false} the caller has to make sure to kill - * the app if needed. - */ - public void persistChanges(boolean mayKillBecauseOfAppOpsChange) { - persistChanges(mayKillBecauseOfAppOpsChange, null); - } - - /** - * If the changes to the permission groups were delayed, persist them now. - * - * @param mayKillBecauseOfAppOpsChange If the app may be killed if app ops change. If this is - * set to {@code false} the caller has to make sure to kill - * the app if needed. - * @param filterPermissions If provided, only persist state for the given permissions - */ - public void persistChanges(boolean mayKillBecauseOfAppOpsChange, - Set<String> filterPermissions) { - if (mDelayChanges) { - int numGroups = mGroups.size(); - - for (int i = 0; i < numGroups; i++) { - AppPermissionGroup group = mGroups.get(i); - group.persistChanges(mayKillBecauseOfAppOpsChange, null, filterPermissions); - - AppPermissionGroup backgroundGroup = group.getBackgroundPermissions(); - if (backgroundGroup != null) { - backgroundGroup.persistChanges(mayKillBecauseOfAppOpsChange, null, - filterPermissions); - } - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/Permission.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/Permission.java deleted file mode 100644 index 2bec970a1dea..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/model/Permission.java +++ /dev/null @@ -1,414 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.model; - -import android.annotation.NonNull; -import android.content.pm.PackageManager; -import android.content.pm.PermissionInfo; - -import java.util.ArrayList; -import java.util.Objects; - -/** - * A permission and its properties. - * - * @see AppPermissionGroup - */ -public final class Permission { - private final @NonNull PermissionInfo mPermissionInfo; - private final String mName; - private final String mBackgroundPermissionName; - private final String mAppOp; - - private boolean mGranted; - private boolean mAppOpAllowed; - private int mFlags; - private boolean mIsEphemeral; - private boolean mIsRuntimeOnly; - private Permission mBackgroundPermission; - private ArrayList<Permission> mForegroundPermissions; - private boolean mWhitelisted; - - public Permission(String name, @NonNull PermissionInfo permissionInfo, boolean granted, - String appOp, boolean appOpAllowed, int flags) { - mPermissionInfo = permissionInfo; - mName = name; - mBackgroundPermissionName = permissionInfo.backgroundPermission; - mGranted = granted; - mAppOp = appOp; - mAppOpAllowed = appOpAllowed; - mFlags = flags; - mIsEphemeral = - (permissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0; - mIsRuntimeOnly = - (permissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0; - } - - /** - * Mark this permission as background permission for {@code foregroundPermissions}. - * - * @param foregroundPermission The foreground permission - */ - public void addForegroundPermissions(Permission foregroundPermission) { - if (mForegroundPermissions == null) { - mForegroundPermissions = new ArrayList<>(1); - } - mForegroundPermissions.add(foregroundPermission); - } - - /** - * Mark this permission as foreground permission for {@code backgroundPermission}. - * - * @param backgroundPermission The background permission - */ - public void setBackgroundPermission(Permission backgroundPermission) { - mBackgroundPermission = backgroundPermission; - } - - public PermissionInfo getPermissionInfo() { - return mPermissionInfo; - } - - public String getName() { - return mName; - } - - public String getAppOp() { - return mAppOp; - } - - public int getFlags() { - return mFlags; - } - - boolean isHardRestricted() { - return (mPermissionInfo.flags & PermissionInfo.FLAG_HARD_RESTRICTED) != 0; - } - - boolean isSoftRestricted() { - return (mPermissionInfo.flags & PermissionInfo.FLAG_SOFT_RESTRICTED) != 0; - } - - /** - * Does this permission affect app ops. - * - * <p>I.e. does this permission have a matching app op or is this a background permission. All - * background permissions affect the app op of its assigned foreground permission. - * - * @return {@code true} if this permission affects app ops - */ - public boolean affectsAppOp() { - return mAppOp != null || isBackgroundPermission(); - } - - /** - * Check if the permission is granted. - * - * <p>This ignores the state of the app-op. I.e. for apps not handling runtime permissions, this - * always returns {@code true}. - * - * @return If the permission is granted - */ - public boolean isGranted() { - return mGranted; - } - - /** - * Check if the permission is granted, also considering the state of the app-op. - * - * <p>For the UI, check the grant state of the whole group via - * {@link AppPermissionGroup#areRuntimePermissionsGranted}. - * - * @return {@code true} if the permission (and the app-op) is granted. - */ - public boolean isGrantedIncludingAppOp() { - return mGranted && (!affectsAppOp() || isAppOpAllowed()) && !isReviewRequired(); - } - - public boolean isReviewRequired() { - return (mFlags & PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) != 0; - } - - /** - * Unset review required flag. - */ - public void unsetReviewRequired() { - mFlags &= ~PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; - } - - public void setGranted(boolean mGranted) { - this.mGranted = mGranted; - } - - public boolean isAppOpAllowed() { - return mAppOpAllowed; - } - - /** - * Check if it's user fixed. - */ - public boolean isUserFixed() { - return (mFlags & PackageManager.FLAG_PERMISSION_USER_FIXED) != 0; - } - - /** - * Set user fixed flag. - */ - public void setUserFixed(boolean userFixed) { - if (userFixed) { - mFlags |= PackageManager.FLAG_PERMISSION_USER_FIXED; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_USER_FIXED; - } - } - - /** - * Sets the one-time permission flag - * @param oneTime true to set the flag, false to unset it - */ - public void setOneTime(boolean oneTime) { - if (oneTime) { - mFlags |= PackageManager.FLAG_PERMISSION_ONE_TIME; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_ONE_TIME; - } - } - - public boolean isSelectedLocationAccuracy() { - return (mFlags & PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY) != 0; - } - - /** - * Sets the selected-location-accuracy permission flag - * @param selectedLocationAccuracy true to set the flag, false to unset it - */ - public void setSelectedLocationAccuracy(boolean selectedLocationAccuracy) { - if (selectedLocationAccuracy) { - mFlags |= PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY; - } - } - - public boolean isSystemFixed() { - return (mFlags & PackageManager.FLAG_PERMISSION_SYSTEM_FIXED) != 0; - } - - public boolean isPolicyFixed() { - return (mFlags & PackageManager.FLAG_PERMISSION_POLICY_FIXED) != 0; - } - - public boolean isUserSet() { - return (mFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0; - } - - public boolean isGrantedByDefault() { - return (mFlags & PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT) != 0; - } - - /** - * Is the permission user sensitive, i.e. should it always be shown to the user. - * - * <p>Non-sensitive permission are usually hidden behind a setting in an overflow menu or - * some other kind of flag. - * - * @return {@code true} if the permission is user sensitive. - */ - public boolean isUserSensitive() { - if (isGrantedIncludingAppOp()) { - return (mFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0; - } else { - return (mFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) != 0; - } - } - - /** - * If this permission is split into a foreground and background permission, this is the name - * of the background permission. - * - * @return The name of the background permission or {@code null} if the permission is not split - */ - public String getBackgroundPermissionName() { - return mBackgroundPermissionName; - } - - /** - * @return If this permission is split into a foreground and background permission, - * returns the background permission - */ - public Permission getBackgroundPermission() { - return mBackgroundPermission; - } - - /** - * @return If this permission is split into a foreground and background permission, - * returns the foreground permission - */ - public ArrayList<Permission> getForegroundPermissions() { - return mForegroundPermissions; - } - - /** - * @return {@code true} iff this is the foreground permission of a background-foreground-split - * permission - */ - public boolean hasBackgroundPermission() { - return mBackgroundPermissionName != null; - } - - /** - * @return {@code true} iff this is the background permission of a background-foreground-split - * permission - */ - public boolean isBackgroundPermission() { - return mForegroundPermissions != null; - } - - /** - * @see PackageManager#FLAG_PERMISSION_ONE_TIME - */ - public boolean isOneTime() { - return (mFlags & PackageManager.FLAG_PERMISSION_ONE_TIME) != 0; - } - - /** - * Set userSet flag. - */ - public void setUserSet(boolean userSet) { - if (userSet) { - mFlags |= PackageManager.FLAG_PERMISSION_USER_SET; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_USER_SET; - } - } - - /** - * Set policy fixed flag. - */ - public void setPolicyFixed(boolean policyFixed) { - if (policyFixed) { - mFlags |= PackageManager.FLAG_PERMISSION_POLICY_FIXED; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_POLICY_FIXED; - } - } - - /** - * Check if the permission is revoke compat. - */ - public boolean isRevokedCompat() { - return (mFlags & PackageManager.FLAG_PERMISSION_REVOKED_COMPAT) != 0; - } - - /** - * Set revoke compat flag. - */ - public void setRevokedCompat(boolean revokedCompat) { - if (revokedCompat) { - mFlags |= PackageManager.FLAG_PERMISSION_REVOKED_COMPAT; - } else { - mFlags &= ~PackageManager.FLAG_PERMISSION_REVOKED_COMPAT; - } - } - - /** - * Set app op allowed flag. - */ - public void setAppOpAllowed(boolean mAppOpAllowed) { - this.mAppOpAllowed = mAppOpAllowed; - } - - /** - * Check if it's ephemeral. - */ - public boolean isEphemeral() { - return mIsEphemeral; - } - - /** - * Check if it's runtime only. - */ - public boolean isRuntimeOnly() { - return mIsRuntimeOnly; - } - - /** - * Check if it's granting allowed. - */ - public boolean isGrantingAllowed(boolean isEphemeralApp, boolean supportsRuntimePermissions) { - return (!isEphemeralApp || isEphemeral()) - && (supportsRuntimePermissions || !isRuntimeOnly()); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Permission)) { - return false; - } - - Permission other = (Permission) o; - - if (!Objects.equals(getName(), other.getName()) || getFlags() != other.getFlags() - || isGranted() != other.isGranted()) { - return false; - } - - - // Only compare permission names, in order to avoid recursion - if (getBackgroundPermission() != null && other.getBackgroundPermission() != null) { - if (!Objects.equals(getBackgroundPermissionName(), - other.getBackgroundPermissionName())) { - return false; - } - } else if (getBackgroundPermission() != other.getBackgroundPermission()) { - return false; - } - - if (getForegroundPermissions() != null && other.getForegroundPermissions() != null) { - ArrayList<Permission> others = other.getForegroundPermissions(); - if (getForegroundPermissions().size() != others.size()) { - return false; - } - for (int i = 0; i < others.size(); i++) { - if (!getForegroundPermissions().get(i).getName().equals(others.get(i).getName())) { - return false; - } - } - } else if (getForegroundPermissions() != null || other.getForegroundPermissions() != null) { - return false; - } - - return Objects.equals(getAppOp(), other.getAppOp()) - && isAppOpAllowed() == other.isAppOpAllowed(); - } - - @Override - public int hashCode() { - ArrayList<String> linkedPermissionNames = new ArrayList<>(); - if (mBackgroundPermission != null) { - linkedPermissionNames.add(mBackgroundPermission.getName()); - } - if (mForegroundPermissions != null) { - for (Permission linkedPermission: mForegroundPermissions) { - if (linkedPermission != null) { - linkedPermissionNames.add(linkedPermission.getName()); - } - } - } - return Objects.hash(mName, mFlags, mGranted, mAppOp, mAppOpAllowed, linkedPermissionNames); - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/ArrayUtils.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/ArrayUtils.java deleted file mode 100644 index 7027528fc203..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/ArrayUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.utils; - -import android.annotation.Nullable; - -import java.util.Objects; - -/** - * Utils for array manipulation. - */ -public final class ArrayUtils { - private ArrayUtils() { /* cannot be instantiated */ } - - /** - * Checks if an array is null or has no elements. - * - * @param array the array to check for - * - * @return whether the array is null or has no elements. - */ - public static <T> boolean isEmpty(@Nullable T[] array) { - return array == null || array.length == 0; - } - - /** - * Checks that value is present as at least one of the elements of the array. - * @param array the array to check in - * @param value the value to check for - * @return true if the value is present in the array - */ - public static <T> boolean contains(T[] array, T value) { - return indexOf(array, value) != -1; - } - - /** - * Return first index of {@code value} in {@code array}, or {@code -1} if - * not found. - */ - public static <T> int indexOf(T[] array, T value) { - if (array == null) return -1; - for (int i = 0; i < array.length; i++) { - if (Objects.equals(array[i], value)) return i; - } - return -1; - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/LocationUtils.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/LocationUtils.java deleted file mode 100644 index 9402e46d16d9..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/LocationUtils.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.utils; - -import static android.location.LocationManager.EXTRA_LOCATION_ENABLED; - -import android.Manifest; -import android.annotation.NonNull; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.location.LocationManager; -import android.os.Handler; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.Log; - -import java.util.ArrayList; - -/** - * Utils for location service. - */ -public class LocationUtils { - - public static final String LOCATION_PERMISSION = Manifest.permission_group.LOCATION; - public static final String ACTIVITY_RECOGNITION_PERMISSION = - Manifest.permission_group.ACTIVITY_RECOGNITION; - - private static final String TAG = LocationUtils.class.getSimpleName(); - private static final long LOCATION_UPDATE_DELAY_MS = 1000; - private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); - - - /** Start the settings page for the location controller extra package. */ - public static void startLocationControllerExtraPackageSettings(@NonNull Context context, - @NonNull UserHandle user) { - try { - context.startActivityAsUser(new Intent( - Settings.ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS), user); - } catch (ActivityNotFoundException e) { - // In rare cases where location controller extra package is set, but - // no activity exists to handle the location controller extra package settings - // intent, log an error instead of crashing permission controller. - Log.e(TAG, "No activity to handle " - + "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS"); - } - } - - /** - * Check if location is enabled. - */ - public static boolean isLocationEnabled(Context context) { - return context.getSystemService(LocationManager.class).isLocationEnabled(); - } - - /** Checks if the provided package is a location provider. */ - public static boolean isLocationProvider(Context context, String packageName) { - return context.getSystemService(LocationManager.class).isProviderPackage(packageName); - } - - /** - * Check if group is location and the package is a location provider. - */ - public static boolean isLocationGroupAndProvider(Context context, String groupName, - String packageName) { - return LOCATION_PERMISSION.equals(groupName) && isLocationProvider(context, packageName); - } - - /** - * Check if group is location and package is extra location controller. - */ - public static boolean isLocationGroupAndControllerExtraPackage(@NonNull Context context, - @NonNull String groupName, @NonNull String packageName) { - return (LOCATION_PERMISSION.equals(groupName) - || ACTIVITY_RECOGNITION_PERMISSION.equals(groupName)) - && packageName.equals(context.getSystemService(LocationManager.class) - .getExtraLocationControllerPackage()); - } - - /** Returns whether the location controller extra package is enabled. */ - public static boolean isExtraLocationControllerPackageEnabled(Context context) { - try { - return context.getSystemService(LocationManager.class) - .isExtraLocationControllerPackageEnabled(); - } catch (Exception e) { - return false; - } - - } - - /** - * A Listener which responds to enabling or disabling of location on the device - */ - public interface LocationListener { - - /** - * A callback run any time we receive a broadcast stating the location enable state has - * changed. - * @param enabled Whether or not location is enabled - */ - void onLocationStateChange(boolean enabled); - } - - private static final ArrayList<LocationListener> sLocationListeners = new ArrayList<>(); - - private static BroadcastReceiver sLocationBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - boolean isEnabled = intent.getBooleanExtra(EXTRA_LOCATION_ENABLED, true); - sMainHandler.postDelayed(() -> { - synchronized (sLocationListeners) { - for (LocationListener l : sLocationListeners) { - l.onLocationStateChange(isEnabled); - } - } - }, LOCATION_UPDATE_DELAY_MS); - } - }; -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/SoftRestrictedPermissionPolicy.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/SoftRestrictedPermissionPolicy.java deleted file mode 100644 index d4940502f1e2..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/SoftRestrictedPermissionPolicy.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.utils; - -import static android.Manifest.permission.READ_EXTERNAL_STORAGE; -import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; - -import android.annotation.NonNull; -import android.content.pm.PackageInfo; -import android.os.Build; - -import com.android.server.companion.datatransfer.permbackup.model.Permission; - -/** - * The behavior of soft restricted permissions is different for each permission. This class collects - * the policies in one place. - * - * This is the twin of {@link com.android.server.policy.SoftRestrictedPermissionPolicy} - */ -public abstract class SoftRestrictedPermissionPolicy { - - /** - * Check if the permission should be shown in the UI. - * - * @param pkg the package the permission belongs to - * @param permission the permission - * - * @return {@code true} iff the permission should be shown in the UI. - */ - public static boolean shouldShow(@NonNull PackageInfo pkg, @NonNull Permission permission) { - switch (permission.getName()) { - case READ_EXTERNAL_STORAGE: - case WRITE_EXTERNAL_STORAGE: { - boolean isWhiteListed = - (permission.getFlags() & Utils.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) - != 0; - int targetSDK = pkg.applicationInfo.targetSdkVersion; - - return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q; - } - default: - return true; - } - } - - /** - * Check if the permission should be shown in the UI. - * - * @param pkg the LightPackageInfo the permission belongs to - * @param permissionName the name of the permission - * @param permissionFlags the PermissionController flags (not the PermissionInfo flags) for - * the permission - * - * @return {@code true} iff the permission should be shown in the UI. - */ - public static boolean shouldShow(@NonNull PackageInfo pkg, @NonNull String permissionName, - int permissionFlags) { - switch (permissionName) { - case READ_EXTERNAL_STORAGE: - case WRITE_EXTERNAL_STORAGE: { - boolean isWhiteListed = - (permissionFlags & Utils.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0; - return isWhiteListed || pkg.applicationInfo.targetSdkVersion - >= Build.VERSION_CODES.Q; - } - default: - return true; - } - } -} diff --git a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/Utils.java b/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/Utils.java deleted file mode 100644 index 9350549d233e..000000000000 --- a/services/companion/java/com/android/server/companion/datatransfer/permbackup/utils/Utils.java +++ /dev/null @@ -1,819 +0,0 @@ -/* - * 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.companion.datatransfer.permbackup.utils; - -import static android.Manifest.permission_group.ACTIVITY_RECOGNITION; -import static android.Manifest.permission_group.CALENDAR; -import static android.Manifest.permission_group.CALL_LOG; -import static android.Manifest.permission_group.CAMERA; -import static android.Manifest.permission_group.CONTACTS; -import static android.Manifest.permission_group.LOCATION; -import static android.Manifest.permission_group.MICROPHONE; -import static android.Manifest.permission_group.NEARBY_DEVICES; -import static android.Manifest.permission_group.NOTIFICATIONS; -import static android.Manifest.permission_group.PHONE; -import static android.Manifest.permission_group.READ_MEDIA_AURAL; -import static android.Manifest.permission_group.READ_MEDIA_VISUAL; -import static android.Manifest.permission_group.SENSORS; -import static android.Manifest.permission_group.SMS; -import static android.Manifest.permission_group.STORAGE; -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE; -import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; -import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; -import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; -import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED; -import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED; -import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.Manifest; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.AppOpsManager; -import android.app.Application; -import android.app.role.RoleManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageItemInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.PermissionInfo; -import android.content.pm.ResolveInfo; -import android.hardware.SensorPrivacyManager; -import android.os.Build; -import android.os.Process; -import android.os.UserHandle; -import android.provider.DeviceConfig; -import android.provider.Settings; -import android.text.format.DateFormat; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Log; - -import com.android.server.companion.datatransfer.permbackup.model.AppPermissionGroup; - -import java.lang.annotation.Retention; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -/** - * Util class for BackupHelper - */ -public final class Utils { - - @Retention(SOURCE) - @IntDef(value = {LAST_24H_SENSOR_TODAY, LAST_24H_SENSOR_YESTERDAY, - LAST_24H_CONTENT_PROVIDER, NOT_IN_LAST_7D}) - public @interface AppPermsLastAccessType {} - public static final int LAST_24H_SENSOR_TODAY = 1; - public static final int LAST_24H_SENSOR_YESTERDAY = 2; - public static final int LAST_24H_CONTENT_PROVIDER = 3; - public static final int LAST_7D_SENSOR = 4; - public static final int LAST_7D_CONTENT_PROVIDER = 5; - public static final int NOT_IN_LAST_7D = 6; - - private static final List<String> SENSOR_DATA_PERMISSIONS = List.of( - Manifest.permission_group.LOCATION, - Manifest.permission_group.CAMERA, - Manifest.permission_group.MICROPHONE - ); - - public static final List<String> STORAGE_SUPERGROUP_PERMISSIONS = -// (SDK_INT < Build.VERSION_CODES.TIRAMISU) ? List.of() : - List.of( - Manifest.permission_group.STORAGE, - Manifest.permission_group.READ_MEDIA_AURAL, - Manifest.permission_group.READ_MEDIA_VISUAL - ); - - private static final String LOG_TAG = "Utils"; - - public static final String OS_PKG = "android"; - - public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f; - - /** The time an app needs to be unused in order to be hibernated */ - public static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = - "auto_revoke_unused_threshold_millis2"; - - /** The frequency of running the job for hibernating apps */ - public static final String PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS = - "auto_revoke_check_frequency_millis"; - - /** Whether hibernation targets apps that target a pre-S SDK */ - public static final String PROPERTY_HIBERNATION_TARGETS_PRE_S_APPS = - "app_hibernation_targets_pre_s_apps"; - - /** Whether or not app hibernation is enabled on the device **/ - public static final String PROPERTY_APP_HIBERNATION_ENABLED = "app_hibernation_enabled"; - - /** Whether to show the Permissions Hub. */ - private static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled"; - - /** The timeout for one-time permissions */ - private static final String PROPERTY_ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS = - "one_time_permissions_timeout_millis"; - - /** The delay before ending a one-time permission session when all processes are dead */ - private static final String PROPERTY_ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS = - "one_time_permissions_killed_delay_millis"; - - /** Whether to show location access check notifications. */ - private static final String PROPERTY_LOCATION_ACCESS_CHECK_ENABLED = - "location_access_check_enabled"; - - /** The time an app needs to be unused in order to be hibernated */ - public static final String PROPERTY_PERMISSION_DECISIONS_CHECK_OLD_FREQUENCY_MILLIS = - "permission_decisions_check_old_frequency_millis"; - - /** The time an app needs to be unused in order to be hibernated */ - public static final String PROPERTY_PERMISSION_DECISIONS_MAX_DATA_AGE_MILLIS = - "permission_decisions_max_data_age_millis"; - - /** Whether or not warning banner is displayed when device sensors are off **/ - public static final String PROPERTY_WARNING_BANNER_DISPLAY_ENABLED = "warning_banner_enabled"; - - /** All permission whitelists. */ - public static final int FLAGS_PERMISSION_WHITELIST_ALL = - PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM - | PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE - | PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER; - - /** All permission restriction exemptions. */ - public static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT = - FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT - | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT - | FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; - - /** - * The default length of the timeout for one-time permissions - */ - public static final long ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS = 1 * 60 * 1000; // 1 minute - - /** - * The default length to wait before ending a one-time permission session after all processes - * are dead. - */ - public static final long ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS = 5 * 1000; - - /** Mapping permission -> group for all dangerous platform permissions */ - private static final ArrayMap<String, String> PLATFORM_PERMISSIONS; - - /** Mapping group -> permissions for all dangerous platform permissions */ - private static final ArrayMap<String, ArrayList<String>> PLATFORM_PERMISSION_GROUPS; - - /** Set of groups that will be able to receive one-time grant */ - private static final ArraySet<String> ONE_TIME_PERMISSION_GROUPS; - - /** Permission -> Sensor codes */ - private static final ArrayMap<String, Integer> PERM_SENSOR_CODES; - - public static final int FLAGS_ALWAYS_USER_SENSITIVE = - FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED - | FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED; - - private static final String SYSTEM_PKG = "android"; - - private static final String SYSTEM_AMBIENT_AUDIO_INTELLIGENCE = - "android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE"; - private static final String SYSTEM_UI_INTELLIGENCE = - "android.app.role.SYSTEM_UI_INTELLIGENCE"; - private static final String SYSTEM_AUDIO_INTELLIGENCE = - "android.app.role.SYSTEM_AUDIO_INTELLIGENCE"; - private static final String SYSTEM_NOTIFICATION_INTELLIGENCE = - "android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE"; - private static final String SYSTEM_TEXT_INTELLIGENCE = - "android.app.role.SYSTEM_TEXT_INTELLIGENCE"; - private static final String SYSTEM_VISUAL_INTELLIGENCE = - "android.app.role.SYSTEM_VISUAL_INTELLIGENCE"; - - // TODO: theianchen Using hardcoded values here as a WIP solution for now. - private static final String[] EXEMPTED_ROLES = { - SYSTEM_AMBIENT_AUDIO_INTELLIGENCE, - SYSTEM_UI_INTELLIGENCE, - SYSTEM_AUDIO_INTELLIGENCE, - SYSTEM_NOTIFICATION_INTELLIGENCE, - SYSTEM_TEXT_INTELLIGENCE, - SYSTEM_VISUAL_INTELLIGENCE, - }; - - static { - PLATFORM_PERMISSIONS = new ArrayMap<>(); - - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CONTACTS, CONTACTS); - PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CONTACTS, CONTACTS); - PLATFORM_PERMISSIONS.put(Manifest.permission.GET_ACCOUNTS, CONTACTS); - - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CALENDAR, CALENDAR); - PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CALENDAR, CALENDAR); - - PLATFORM_PERMISSIONS.put(Manifest.permission.SEND_SMS, SMS); - PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_SMS, SMS); - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_SMS, SMS); - PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_MMS, SMS); - PLATFORM_PERMISSIONS.put(Manifest.permission.RECEIVE_WAP_PUSH, SMS); - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CELL_BROADCASTS, SMS); - - // If permissions are added to the Storage group, they must be added to the - // STORAGE_PERMISSIONS list in PermissionManagerService in frameworks/base - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_EXTERNAL_STORAGE, STORAGE); - PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, STORAGE); -// if (SDK_INT < Build.VERSION_CODES.TIRAMISU) { -// PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_MEDIA_LOCATION, STORAGE); -// } - -// if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_AUDIO, READ_MEDIA_AURAL); - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_IMAGES, READ_MEDIA_VISUAL); - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_MEDIA_VIDEO, READ_MEDIA_VISUAL); - PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_MEDIA_LOCATION, READ_MEDIA_VISUAL); -// } - - PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION); - PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION); - PLATFORM_PERMISSIONS.put(Manifest.permission.ACCESS_BACKGROUND_LOCATION, LOCATION); - -// if (SDK_INT >= Build.VERSION_CODES.S) { - PLATFORM_PERMISSIONS.put(Manifest.permission.BLUETOOTH_ADVERTISE, NEARBY_DEVICES); - PLATFORM_PERMISSIONS.put(Manifest.permission.BLUETOOTH_CONNECT, NEARBY_DEVICES); - PLATFORM_PERMISSIONS.put(Manifest.permission.BLUETOOTH_SCAN, NEARBY_DEVICES); - PLATFORM_PERMISSIONS.put(Manifest.permission.UWB_RANGING, NEARBY_DEVICES); -// } -// if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - PLATFORM_PERMISSIONS.put(Manifest.permission.NEARBY_WIFI_DEVICES, NEARBY_DEVICES); -// } - - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_CALL_LOG, CALL_LOG); - PLATFORM_PERMISSIONS.put(Manifest.permission.WRITE_CALL_LOG, CALL_LOG); - PLATFORM_PERMISSIONS.put(Manifest.permission.PROCESS_OUTGOING_CALLS, CALL_LOG); - - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_PHONE_STATE, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.READ_PHONE_NUMBERS, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.CALL_PHONE, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.ADD_VOICEMAIL, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.USE_SIP, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.ANSWER_PHONE_CALLS, PHONE); - PLATFORM_PERMISSIONS.put(Manifest.permission.ACCEPT_HANDOVER, PHONE); - - PLATFORM_PERMISSIONS.put(Manifest.permission.RECORD_AUDIO, MICROPHONE); -// if (SDK_INT >= Build.VERSION_CODES.S) { - PLATFORM_PERMISSIONS.put(Manifest.permission.RECORD_BACKGROUND_AUDIO, MICROPHONE); -// } - - PLATFORM_PERMISSIONS.put(Manifest.permission.ACTIVITY_RECOGNITION, ACTIVITY_RECOGNITION); - - PLATFORM_PERMISSIONS.put(Manifest.permission.CAMERA, CAMERA); -// if (SDK_INT >= Build.VERSION_CODES.S) { - PLATFORM_PERMISSIONS.put(Manifest.permission.BACKGROUND_CAMERA, CAMERA); -// } - - PLATFORM_PERMISSIONS.put(Manifest.permission.BODY_SENSORS, SENSORS); - -// if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - PLATFORM_PERMISSIONS.put(Manifest.permission.POST_NOTIFICATIONS, NOTIFICATIONS); - PLATFORM_PERMISSIONS.put(Manifest.permission.BODY_SENSORS_BACKGROUND, SENSORS); -// } - - PLATFORM_PERMISSION_GROUPS = new ArrayMap<>(); - int numPlatformPermissions = PLATFORM_PERMISSIONS.size(); - for (int i = 0; i < numPlatformPermissions; i++) { - String permission = PLATFORM_PERMISSIONS.keyAt(i); - String permissionGroup = PLATFORM_PERMISSIONS.valueAt(i); - - ArrayList<String> permissionsOfThisGroup = PLATFORM_PERMISSION_GROUPS.get( - permissionGroup); - if (permissionsOfThisGroup == null) { - permissionsOfThisGroup = new ArrayList<>(); - PLATFORM_PERMISSION_GROUPS.put(permissionGroup, permissionsOfThisGroup); - } - - permissionsOfThisGroup.add(permission); - } - - ONE_TIME_PERMISSION_GROUPS = new ArraySet<>(); - ONE_TIME_PERMISSION_GROUPS.add(LOCATION); - ONE_TIME_PERMISSION_GROUPS.add(CAMERA); - ONE_TIME_PERMISSION_GROUPS.add(MICROPHONE); - - PERM_SENSOR_CODES = new ArrayMap<>(); -// if (SDK_INT >= Build.VERSION_CODES.S) { - PERM_SENSOR_CODES.put(CAMERA, SensorPrivacyManager.Sensors.CAMERA); - PERM_SENSOR_CODES.put(MICROPHONE, SensorPrivacyManager.Sensors.MICROPHONE); -// } - - } - - private Utils() { - /* do nothing - hide constructor */ - } - - private static ArrayMap<UserHandle, Context> sUserContexts = new ArrayMap<>(); - - /** - * Creates and caches a PackageContext for the requested user, or returns the previously cached - * value. The package of the PackageContext is the application's package. - * - * @param app The currently running application - * @param user The desired user for the context - * - * @return The generated or cached Context for the requested user - * - * @throws PackageManager.NameNotFoundException If the app has no package name attached - */ - public static @NonNull Context getUserContext(Application app, UserHandle user) throws - PackageManager.NameNotFoundException { - if (!sUserContexts.containsKey(user)) { - sUserContexts.put(user, app.getApplicationContext() - .createPackageContextAsUser(app.getPackageName(), 0, user)); - } - return sUserContexts.get(user); - } - - /** - * Returns true if a permission is dangerous, installed, and not removed - * @param permissionInfo The permission we wish to check - * @return If all of the conditions are met - */ - public static boolean isPermissionDangerousInstalledNotRemoved(PermissionInfo permissionInfo) { - return permissionInfo != null - && permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS - && (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) != 0 - && (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) == 0; - } - - /** - * Get permission group a platform permission belongs to, or null if the permission is not a - * platform permission. - * - * @param permission the permission to resolve - * - * @return The group the permission belongs to - */ - public static @Nullable String getGroupOfPlatformPermission(@NonNull String permission) { - return PLATFORM_PERMISSIONS.get(permission); - } - - /** - * Get name of the permission group a permission belongs to. - * - * @param permission the {@link PermissionInfo info} of the permission to resolve - * - * @return The group the permission belongs to - */ - public static @Nullable String getGroupOfPermission(@NonNull PermissionInfo permission) { - String groupName = Utils.getGroupOfPlatformPermission(permission.name); - if (groupName == null) { - groupName = permission.group; - } - - return groupName; - } - - /** - * Get the names for all platform permissions belonging to a group. - * - * @param group the group - * - * @return The permission names or an empty list if the - * group is not does not have platform runtime permissions - */ - public static @NonNull List<String> getPlatformPermissionNamesOfGroup(@NonNull String group) { - final ArrayList<String> permissions = PLATFORM_PERMISSION_GROUPS.get(group); - return (permissions != null) ? permissions : Collections.emptyList(); - } - - /** - * Get the {@link PermissionInfo infos} for all platform permissions belonging to a group. - * - * @param pm Package manager to use to resolve permission infos - * @param group the group - * - * @return The infos for platform permissions belonging to the group or an empty list if the - * group is not does not have platform runtime permissions - */ - public static @NonNull List<PermissionInfo> getPlatformPermissionsOfGroup( - @NonNull PackageManager pm, @NonNull String group) { - ArrayList<PermissionInfo> permInfos = new ArrayList<>(); - - ArrayList<String> permissions = PLATFORM_PERMISSION_GROUPS.get(group); - if (permissions == null) { - return Collections.emptyList(); - } - - int numPermissions = permissions.size(); - for (int i = 0; i < numPermissions; i++) { - String permName = permissions.get(i); - PermissionInfo permInfo; - try { - permInfo = pm.getPermissionInfo(permName, 0); - } catch (PackageManager.NameNotFoundException e) { - throw new IllegalStateException(permName + " not defined by platform", e); - } - - permInfos.add(permInfo); - } - - return permInfos; - } - - /** - * Get the {@link PermissionInfo infos} for all permission infos belonging to a group. - * - * @param pm Package manager to use to resolve permission infos - * @param group the group - * - * @return The infos of permissions belonging to the group or an empty list if the group - * does not have runtime permissions - */ - public static @NonNull List<PermissionInfo> getPermissionInfosForGroup( - @NonNull PackageManager pm, @NonNull String group) - throws PackageManager.NameNotFoundException { - List<PermissionInfo> permissions = pm.queryPermissionsByGroup(group, 0); - permissions.addAll(getPlatformPermissionsOfGroup(pm, group)); - - /* - * If the undefined group is requested, the package manager will return all platform - * permissions, since they are marked as Undefined in the manifest. Do not return these - * permissions. - */ - if (group.equals(Manifest.permission_group.UNDEFINED)) { - List<PermissionInfo> undefinedPerms = new ArrayList<>(); - for (PermissionInfo permissionInfo : permissions) { - String permGroup = getGroupOfPlatformPermission(permissionInfo.name); - if (permGroup == null || permGroup.equals(Manifest.permission_group.UNDEFINED)) { - undefinedPerms.add(permissionInfo); - } - } - return undefinedPerms; - } - - return permissions; - } - - /** - * Get the {@link PermissionInfo infos} for all runtime installed permission infos belonging to - * a group. - * - * @param pm Package manager to use to resolve permission infos - * @param group the group - * - * @return The infos of installed runtime permissions belonging to the group or an empty list - * if the group does not have runtime permissions - */ - public static @NonNull List<PermissionInfo> getInstalledRuntimePermissionInfosForGroup( - @NonNull PackageManager pm, @NonNull String group) - throws PackageManager.NameNotFoundException { - List<PermissionInfo> permissions = pm.queryPermissionsByGroup(group, 0); - permissions.addAll(getPlatformPermissionsOfGroup(pm, group)); - - List<PermissionInfo> installedRuntime = new ArrayList<>(); - for (PermissionInfo permissionInfo: permissions) { - if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS - && (permissionInfo.flags & PermissionInfo.FLAG_INSTALLED) != 0 - && (permissionInfo.flags & PermissionInfo.FLAG_REMOVED) == 0) { - installedRuntime.add(permissionInfo); - } - } - - /* - * If the undefined group is requested, the package manager will return all platform - * permissions, since they are marked as Undefined in the manifest. Do not return these - * permissions. - */ - if (group.equals(Manifest.permission_group.UNDEFINED)) { - List<PermissionInfo> undefinedPerms = new ArrayList<>(); - for (PermissionInfo permissionInfo : installedRuntime) { - String permGroup = getGroupOfPlatformPermission(permissionInfo.name); - if (permGroup == null || permGroup.equals(Manifest.permission_group.UNDEFINED)) { - undefinedPerms.add(permissionInfo); - } - } - return undefinedPerms; - } - - return installedRuntime; - } - - /** - * Get the {@link PackageItemInfo infos} for the given permission group. - * - * @param groupName the group - * @param context the {@code Context} to retrieve {@code PackageManager} - * - * @return The info of permission group or null if the group does not have runtime permissions. - */ - public static @Nullable PackageItemInfo getGroupInfo(@NonNull String groupName, - @NonNull Context context) { - try { - return context.getPackageManager().getPermissionGroupInfo(groupName, 0); - } catch (NameNotFoundException e) { - /* ignore */ - } - try { - return context.getPackageManager().getPermissionInfo(groupName, 0); - } catch (NameNotFoundException e) { - /* ignore */ - } - return null; - } - - /** - * Get the {@link PermissionInfo infos} for all permission infos belonging to a group. - * - * @param groupName the group - * @param context the {@code Context} to retrieve {@code PackageManager} - * - * @return The infos of permissions belonging to the group or null if the group does not have - * runtime permissions. - */ - public static @Nullable List<PermissionInfo> getGroupPermissionInfos(@NonNull String groupName, - @NonNull Context context) { - try { - return Utils.getPermissionInfosForGroup(context.getPackageManager(), groupName); - } catch (NameNotFoundException e) { - /* ignore */ - } - try { - PermissionInfo permissionInfo = context.getPackageManager() - .getPermissionInfo(groupName, 0); - List<PermissionInfo> permissions = new ArrayList<>(); - permissions.add(permissionInfo); - return permissions; - } catch (NameNotFoundException e) { - /* ignore */ - } - return null; - } - - /** - * Get the names of the platform permission groups. - * - * @return the names of the platform permission groups. - */ - public static List<String> getPlatformPermissionGroups() { - return new ArrayList<>(PLATFORM_PERMISSION_GROUPS.keySet()); - } - - /** - * Get the names of the runtime platform permissions - * - * @return the names of the runtime platform permissions. - */ - public static List<String> getRuntimePlatformPermissionNames() { - return new ArrayList<>(PLATFORM_PERMISSIONS.keySet()); - } - - /** - * Is the permissions a platform runtime permission - * - * @return the names of the runtime platform permissions. - */ - public static boolean isRuntimePlatformPermission(@NonNull String permission) { - return PLATFORM_PERMISSIONS.containsKey(permission); - } - - /** - * Is the group or background group user sensitive? - * - * @param group The group that might be user sensitive - * - * @return {@code true} if the group (or it's subgroup) is user sensitive. - */ - public static boolean isGroupOrBgGroupUserSensitive(AppPermissionGroup group) { - return group.isUserSensitive() || (group.getBackgroundPermissions() != null - && group.getBackgroundPermissions().isUserSensitive()); - } - - /** - * Whether or not the given package has non-isolated storage permissions - * @param context The current context - * @param packageName The package name to check - * @return True if the package has access to non-isolated storage, false otherwise - * @throws NameNotFoundException - */ - public static boolean isNonIsolatedStorage(@NonNull Context context, - @NonNull String packageName) throws NameNotFoundException { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0); - AppOpsManager manager = context.getSystemService(AppOpsManager.class); - - - return packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.P - || (packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.R - && manager.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, - packageInfo.applicationInfo.uid, packageInfo.packageName) == MODE_ALLOWED); - } - - /** - * Build a string representing the given time if it happened on the current day and the date - * otherwise. - * - * @param context the context. - * @param lastAccessTime the time in milliseconds. - * - * @return a string representing the time or date of the given time or null if the time is 0. - */ - public static @Nullable String getAbsoluteTimeString(@NonNull Context context, - long lastAccessTime) { - if (lastAccessTime == 0) { - return null; - } - if (isToday(lastAccessTime)) { - return DateFormat.getTimeFormat(context).format(lastAccessTime); - } else { - return DateFormat.getMediumDateFormat(context).format(lastAccessTime); - } - } - - /** - * Check whether the given time (in milliseconds) is in the current day. - * - * @param time the time in milliseconds - * - * @return whether the given time is in the current day. - */ - private static boolean isToday(long time) { - Calendar today = Calendar.getInstance(Locale.getDefault()); - today.setTimeInMillis(System.currentTimeMillis()); - today.set(Calendar.HOUR_OF_DAY, 0); - today.set(Calendar.MINUTE, 0); - today.set(Calendar.SECOND, 0); - today.set(Calendar.MILLISECOND, 0); - - Calendar date = Calendar.getInstance(Locale.getDefault()); - date.setTimeInMillis(time); - return !date.before(today); - } - - /** - * Whether the Location Access Check is enabled. - * - * @return {@code true} iff the Location Access Check is enabled. - */ - public static boolean isLocationAccessCheckEnabled() { - return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, - PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, true); - } - - /** - * Get one time permissions timeout - */ - public static long getOneTimePermissionsTimeout() { - return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, - PROPERTY_ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS, ONE_TIME_PERMISSIONS_TIMEOUT_MILLIS); - } - - /** - * Returns the delay in milliseconds before revoking permissions at the end of a one-time - * permission session if all processes have been killed. - * If the session was triggered by a self-revocation, then revocation should happen - * immediately. For a regular one-time permission session, a grace period allows a quick - * app restart without losing the permission. - * @param isSelfRevoked If true, return the delay for a self-revocation session. Otherwise, - * return delay for a regular one-time permission session. - */ - public static long getOneTimePermissionsKilledDelay(boolean isSelfRevoked) { - if (isSelfRevoked) { - // For a self-revoked session, we revoke immediately when the process dies. - return 0; - } - return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, - PROPERTY_ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS, - ONE_TIME_PERMISSIONS_KILLED_DELAY_MILLIS); - } - - /** - * Whether the permission group supports one-time - * @param permissionGroup The permission group to check - * @return {@code true} iff the group supports one-time - */ - public static boolean supportsOneTimeGrant(String permissionGroup) { - return ONE_TIME_PERMISSION_GROUPS.contains(permissionGroup); - } - - /** - * Checks whether a package has an active one-time permission according to the system server's - * flags - * - * @param context the {@code Context} to retrieve {@code PackageManager} - * @param packageName The package to check for - * @return Whether a package has an active one-time permission - */ - public static boolean hasOneTimePermissions(Context context, String packageName) { - String[] permissions; - PackageManager pm = context.getPackageManager(); - try { - permissions = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) - .requestedPermissions; - } catch (NameNotFoundException e) { - Log.w(LOG_TAG, "Checking for one-time permissions in nonexistent package"); - return false; - } - if (permissions == null) { - return false; - } - for (String permissionName : permissions) { - if ((pm.getPermissionFlags(permissionName, packageName, Process.myUserHandle()) - & PackageManager.FLAG_PERMISSION_ONE_TIME) != 0 - && pm.checkPermission(permissionName, packageName) - == PackageManager.PERMISSION_GRANTED) { - return true; - } - } - return false; - } - - /** - * Gets the label of the Settings application - * - * @param pm The packageManager used to get the activity resolution - * - * @return The CharSequence title of the settings app - */ - @Nullable - public static CharSequence getSettingsLabelForNotifications(PackageManager pm) { - // We pretend we're the Settings app sending the notification, so figure out its name. - Intent openSettingsIntent = new Intent(Settings.ACTION_SETTINGS); - ResolveInfo resolveInfo = pm.resolveActivity(openSettingsIntent, MATCH_SYSTEM_ONLY); - if (resolveInfo == null) { - return null; - } - return pm.getApplicationLabel(resolveInfo.activityInfo.applicationInfo); - } - - /** - * Get all the exempted packages. - */ - public static Set<String> getExemptedPackages(@NonNull RoleManager roleManager) { - Set<String> exemptedPackages = new HashSet<>(); - - exemptedPackages.add(SYSTEM_PKG); - for (int i = 0; i < EXEMPTED_ROLES.length; i++) { - exemptedPackages.addAll(roleManager.getRoleHolders(EXEMPTED_ROLES[i])); - } - - return exemptedPackages; - } - - /** - * Returns if the permission group is Camera or Microphone (status bar indicators). - **/ - public static boolean isStatusBarIndicatorPermission(@NonNull String permissionGroupName) { - return CAMERA.equals(permissionGroupName) || MICROPHONE.equals(permissionGroupName); - } - - /** - * Navigate to notification settings for all apps - * @param context The current Context - */ - public static void navigateToNotificationSettings(@NonNull Context context) { - Intent notificationIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS); - context.startActivity(notificationIntent); - } - - /** - * Navigate to notification settings for an app - * @param context The current Context - * @param packageName The package to navigate to - * @param user Specifies the user of the package which should be navigated to. If null, the - * current user is used. - */ - public static void navigateToAppNotificationSettings(@NonNull Context context, - @NonNull String packageName, @NonNull UserHandle user) { - Intent notificationIntent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - notificationIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); - context.startActivityAsUser(notificationIntent, user); - } - - /** - * Returns if a card should be shown if the sensor is blocked - **/ - public static boolean shouldDisplayCardIfBlocked(@NonNull String permissionGroupName) { - return DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_WARNING_BANNER_DISPLAY_ENABLED, true) && ( - CAMERA.equals(permissionGroupName) || MICROPHONE.equals(permissionGroupName) - || LOCATION.equals(permissionGroupName)); - } -} diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index a01942d0dfa8..bb23d89d218f 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1346,7 +1346,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private String getDeviceOwnerDeletedPackageMsg() { DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); return dpm.getResources().getString(PACKAGE_DELETED_BY_DO, - () -> mContext.getString(R.string.package_updated_device_owner)); + () -> mContext.getString(R.string.package_deleted_device_owner)); } @Override diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index f242fda15f06..c80547ce61c0 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -213,7 +213,7 @@ public class VirtualDeviceManagerServiceTest { mContext.getSystemService(WindowManager.class), threadVerifier); mAssociationInfo = new AssociationInfo(1, 0, null, - MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0); + MacAddress.BROADCAST_ADDRESS, "", null, true, false, false, 0, 0); VirtualDeviceParams params = new VirtualDeviceParams .Builder() diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java index 0ddd52dfc76d..64a86db38396 100644 --- a/telecomm/java/android/telecom/InCallService.java +++ b/telecomm/java/android/telecom/InCallService.java @@ -22,6 +22,7 @@ import android.annotation.SystemApi; import android.app.Service; import android.app.UiModeManager; import android.bluetooth.BluetoothDevice; +import android.content.ComponentName; import android.content.Intent; import android.hardware.camera2.CameraManager; import android.net.Uri; @@ -47,7 +48,7 @@ import java.util.List; * in a call. It also provides the user with a means to initiate calls and see a history of calls * on their device. A device is bundled with a system provided default dialer/phone app. The user * may choose a single app to take over this role from the system app. An app which wishes to - * fulfill one this role uses the {@link android.app.role.RoleManager} to request that they fill the + * fulfill this role uses the {@link android.app.role.RoleManager} to request that they fill the * {@link android.app.role.RoleManager#ROLE_DIALER} role. * <p> * The default phone app provides a user interface while the device is in a call, and the device is @@ -63,13 +64,23 @@ import java.util.List; * UI, as well as an ongoing call UI.</li> * </ul> * <p> - * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} crashes during - * {@link InCallService} binding, the Telecom framework will automatically fall back to using the - * dialer app pre-loaded on the device. The system will display a notification to the user to let - * them know that the app has crashed and that their call was continued using the pre-loaded dialer - * app. + * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} returns a + * {@code null} {@link InCallService} during binding, the Telecom framework will automatically fall + * back to using the dialer app preloaded on the device. The system will display a notification to + * the user to let them know that their call was continued using the preloaded dialer app. Your + * app should never return a {@code null} binding; doing so means it does not fulfil the + * requirements of {@link android.app.role.RoleManager#ROLE_DIALER}. * <p> - * The pre-loaded dialer will ALWAYS be used when the user places an emergency call, even if your + * Note: If your app fills {@link android.app.role.RoleManager#ROLE_DIALER} and makes changes at + * runtime which cause it to no longer fulfil the requirements of this role, + * {@link android.app.role.RoleManager} will automatically remove your app from the role and close + * your app. For example, if you use + * {@link android.content.pm.PackageManager#setComponentEnabledSetting(ComponentName, int, int)} to + * programmatically disable the {@link InCallService} your app declares in its manifest, your app + * will no longer fulfil the requirements expected of + * {@link android.app.role.RoleManager#ROLE_DIALER}. + * <p> + * The preloaded dialer will ALWAYS be used when the user places an emergency call, even if your * app fills the {@link android.app.role.RoleManager#ROLE_DIALER} role. To ensure an optimal * experience when placing an emergency call, the default dialer should ALWAYS use * {@link android.telecom.TelecomManager#placeCall(Uri, Bundle)} to place calls (including diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index 0e5a1775bd6a..315c40ffa9ba 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -21,8 +21,10 @@ import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.traces.region.RegionSubject import com.android.server.wm.traces.common.FlickerComponentName -val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher", - "com.google.android.apps.nexuslauncher.NexusLauncherActivity") +val LAUNCHER_COMPONENT = FlickerComponentName( + "com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity" +) /** * Checks that [FlickerComponentName.STATUS_BAR] window is visible and above the app windows in @@ -110,9 +112,9 @@ fun FlickerTestParameter.statusBarLayerIsVisible() { fun FlickerTestParameter.navBarLayerPositionStart() { assertLayersStart { val display = this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") + ?: throw RuntimeException("There is no display!") this.visibleRegion(FlickerComponentName.NAV_BAR) - .coversExactly(WindowUtils.getNavigationBarPosition(display)) + .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation)) } } @@ -123,9 +125,9 @@ fun FlickerTestParameter.navBarLayerPositionStart() { fun FlickerTestParameter.navBarLayerPositionEnd() { assertLayersEnd { val display = this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") + ?: throw RuntimeException("There is no display!") this.visibleRegion(FlickerComponentName.NAV_BAR) - .coversExactly(WindowUtils.getNavigationBarPosition(display)) + .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation)) } } @@ -244,11 +246,11 @@ fun FlickerTestParameter.replacesLayer( assertLayersStart { this.isVisible(originalLayer) - .isInvisible(newLayer) + .isInvisible(newLayer) } assertLayersEnd { this.isInvisible(originalLayer) - .isVisible(newLayer) + .isVisible(newLayer) } } |