diff options
120 files changed, 4757 insertions, 5412 deletions
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 32bdf7962273..bb22920b7c65 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1294,7 +1294,7 @@ public class DreamService extends Service implements Window.Callback { if (!mWindowless) { Intent i = new Intent(this, DreamActivity.class); i.setPackage(getApplicationContext().getPackageName()); - i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken)); final ServiceInfo serviceInfo = fetchServiceInfo(this, new ComponentName(this, getClass())); diff --git a/core/java/android/service/voice/HotwordAudioStream.aidl b/core/java/android/service/voice/HotwordAudioStream.aidl new file mode 100644 index 000000000000..9550c830aecc --- /dev/null +++ b/core/java/android/service/voice/HotwordAudioStream.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.service.voice; + +parcelable HotwordAudioStream; diff --git a/core/java/android/service/voice/HotwordAudioStream.java b/core/java/android/service/voice/HotwordAudioStream.java new file mode 100644 index 000000000000..5442860df007 --- /dev/null +++ b/core/java/android/service/voice/HotwordAudioStream.java @@ -0,0 +1,444 @@ +/* + * 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 android.service.voice; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.PersistableBundle; + +import java.util.Objects; + +/** + * Represents an audio stream supporting the hotword detection. + * + * @hide + */ +public final class HotwordAudioStream implements Parcelable { + + /** + * The {@link AudioFormat} of the audio stream. + */ + @NonNull + @UnsupportedAppUsage + private final AudioFormat mAudioFormat; + + /** + * This stream starts with the audio bytes used for hotword detection, but continues streaming + * the audio until the stream is shutdown by the {@link HotwordDetectionService}. + */ + @NonNull + @UnsupportedAppUsage + private final ParcelFileDescriptor mAudioStreamParcelFileDescriptor; + + /** + * The timestamp when the audio stream was captured by the Audio platform. + * + * <p> + * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying + * AudioRecord. The {@link HotwordDetectionService} is expected to optionally populate this + * field by {@link AudioRecord#getTimestamp}. + * </p> + * + * <p> + * This timestamp can be used in conjunction with the + * {@link HotwordDetectedResult#getHotwordOffsetMillis()} and + * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to + * timestamps. + * </p> + * + * @see #getAudioStreamParcelFileDescriptor() + */ + @Nullable + @UnsupportedAppUsage + private final AudioTimestamp mTimestamp; + + private static AudioTimestamp defaultTimestamp() { + return null; + } + + /** + * The metadata associated with the audio stream. + */ + @NonNull + @UnsupportedAppUsage + private final PersistableBundle mMetadata; + + private static PersistableBundle defaultMetadata() { + return new PersistableBundle(); + } + + private String timestampToString() { + if (mTimestamp == null) { + return ""; + } + return "TimeStamp:" + + " framePos=" + mTimestamp.framePosition + + " nanoTime=" + mTimestamp.nanoTime; + } + + private void parcelTimestamp(Parcel dest, int flags) { + if (mTimestamp != null) { + // mTimestamp is not null, we write it to the parcel, set true. + dest.writeBoolean(true); + dest.writeLong(mTimestamp.framePosition); + dest.writeLong(mTimestamp.nanoTime); + } else { + // mTimestamp is null, we don't write any value out, set false. + dest.writeBoolean(false); + } + } + + @Nullable + private static AudioTimestamp unparcelTimestamp(Parcel in) { + // If it is true, it means we wrote the value to the parcel before, parse it. + // Otherwise, return null. + if (in.readBoolean()) { + final AudioTimestamp timeStamp = new AudioTimestamp(); + timeStamp.framePosition = in.readLong(); + timeStamp.nanoTime = in.readLong(); + return timeStamp; + } else { + return null; + } + } + + /** + * Provides an instance of {@link Builder} with state corresponding to this instance. + * @hide + */ + public Builder buildUpon() { + return new Builder(mAudioFormat, mAudioStreamParcelFileDescriptor) + .setTimestamp(mTimestamp) + .setMetadata(mMetadata); + } + + /* package-private */ + HotwordAudioStream( + @NonNull AudioFormat audioFormat, + @NonNull ParcelFileDescriptor audioStreamParcelFileDescriptor, + @Nullable AudioTimestamp timestamp, + @NonNull PersistableBundle metadata) { + this.mAudioFormat = audioFormat; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioFormat); + this.mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioStreamParcelFileDescriptor); + this.mTimestamp = timestamp; + this.mMetadata = metadata; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMetadata); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * The {@link AudioFormat} of the audio stream. + */ + @UnsupportedAppUsage + @NonNull + public AudioFormat getAudioFormat() { + return mAudioFormat; + } + + /** + * This stream starts with the audio bytes used for hotword detection, but continues streaming + * the audio until the stream is shutdown by the {@link HotwordDetectionService}. + */ + @UnsupportedAppUsage + @NonNull + public ParcelFileDescriptor getAudioStreamParcelFileDescriptor() { + return mAudioStreamParcelFileDescriptor; + } + + /** + * The timestamp when the audio stream was captured by the Audio platform. + * + * <p> + * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying + * AudioRecord. The {@link HotwordDetectionService} is expected to optionally populate this + * field by {@link AudioRecord#getTimestamp}. + * </p> + * + * <p> + * This timestamp can be used in conjunction with the + * {@link HotwordDetectedResult#getHotwordOffsetMillis()} and + * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to + * timestamps. + * </p> + * + * @see #getAudioStreamParcelFileDescriptor() + */ + @UnsupportedAppUsage + @Nullable + public AudioTimestamp getTimestamp() { + return mTimestamp; + } + + /** + * The metadata associated with the audio stream. + */ + @UnsupportedAppUsage + @NonNull + public PersistableBundle getMetadata() { + return mMetadata; + } + + @Override + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "HotwordAudioStream { " + + "audioFormat = " + mAudioFormat + ", " + + "audioStreamParcelFileDescriptor = " + mAudioStreamParcelFileDescriptor + ", " + + "timestamp = " + timestampToString() + ", " + + "metadata = " + mMetadata + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(HotwordAudioStream other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + HotwordAudioStream that = (HotwordAudioStream) o; + //noinspection PointlessBooleanExpression + return Objects.equals(mAudioFormat, that.mAudioFormat) + && Objects.equals(mAudioStreamParcelFileDescriptor, + that.mAudioStreamParcelFileDescriptor) + && Objects.equals(mTimestamp, that.mTimestamp) + && Objects.equals(mMetadata, that.mMetadata); + } + + @Override + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + Objects.hashCode(mAudioFormat); + _hash = 31 * _hash + Objects.hashCode(mAudioStreamParcelFileDescriptor); + _hash = 31 * _hash + Objects.hashCode(mTimestamp); + _hash = 31 * _hash + Objects.hashCode(mMetadata); + return _hash; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mTimestamp != null) flg |= 0x4; + dest.writeByte(flg); + dest.writeTypedObject(mAudioFormat, flags); + dest.writeTypedObject(mAudioStreamParcelFileDescriptor, flags); + parcelTimestamp(dest, flags); + dest.writeTypedObject(mMetadata, flags); + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + /* package-private */ + HotwordAudioStream(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + AudioFormat audioFormat = (AudioFormat) in.readTypedObject(AudioFormat.CREATOR); + ParcelFileDescriptor audioStreamParcelFileDescriptor = + (ParcelFileDescriptor) in.readTypedObject(ParcelFileDescriptor.CREATOR); + AudioTimestamp timestamp = unparcelTimestamp(in); + PersistableBundle metadata = (PersistableBundle) in.readTypedObject( + PersistableBundle.CREATOR); + + this.mAudioFormat = audioFormat; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioFormat); + this.mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioStreamParcelFileDescriptor); + this.mTimestamp = timestamp; + this.mMetadata = metadata; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMetadata); + + // onConstructed(); // You can define this method to get a callback + } + + @NonNull + public static final Parcelable.Creator<HotwordAudioStream> CREATOR = + new Parcelable.Creator<HotwordAudioStream>() { + @Override + public HotwordAudioStream[] newArray(int size) { + return new HotwordAudioStream[size]; + } + + @Override + public HotwordAudioStream createFromParcel(@NonNull Parcel in) { + return new HotwordAudioStream(in); + } + }; + + /** + * A builder for {@link HotwordAudioStream} + */ + @SuppressWarnings("WeakerAccess") + public static final class Builder { + + @NonNull + private AudioFormat mAudioFormat; + @NonNull + private ParcelFileDescriptor mAudioStreamParcelFileDescriptor; + @Nullable + private AudioTimestamp mTimestamp; + @NonNull + private PersistableBundle mMetadata; + + private long mBuilderFieldsSet = 0L; + + /** + * Creates a new Builder. + * + * @param audioFormat The {@link AudioFormat} of the audio stream. + * @param audioStreamParcelFileDescriptor This stream starts with the audio bytes used for + * hotword detection, but continues streaming + * the audio until the stream is shutdown by the + * {@link HotwordDetectionService}. + */ + @UnsupportedAppUsage + public Builder( + @NonNull AudioFormat audioFormat, + @NonNull ParcelFileDescriptor audioStreamParcelFileDescriptor) { + mAudioFormat = audioFormat; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioFormat); + mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioStreamParcelFileDescriptor); + } + + /** + * The {@link AudioFormat} of the audio stream. + */ + @UnsupportedAppUsage + @NonNull + public Builder setAudioFormat(@NonNull AudioFormat value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mAudioFormat = value; + return this; + } + + /** + * This stream starts with the audio bytes used for hotword detection, but continues + * streaming + * the audio until the stream is shutdown by the {@link HotwordDetectionService}. + */ + @UnsupportedAppUsage + @NonNull + public Builder setAudioStreamParcelFileDescriptor(@NonNull ParcelFileDescriptor value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mAudioStreamParcelFileDescriptor = value; + return this; + } + + /** + * The timestamp when the audio stream was captured by the Audio platform. + * + * <p> + * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying + * AudioRecord. The {@link HotwordDetectionService} is expected to optionally populate this + * field by {@link AudioRecord#getTimestamp}. + * </p> + * + * <p> + * This timestamp can be used in conjunction with the + * {@link HotwordDetectedResult#getHotwordOffsetMillis()} and + * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to + * timestamps. + * </p> + * + * @see #getAudioStreamParcelFileDescriptor() + */ + @UnsupportedAppUsage + @NonNull + public Builder setTimestamp(@NonNull AudioTimestamp value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mTimestamp = value; + return this; + } + + /** + * The metadata associated with the audio stream. + */ + @UnsupportedAppUsage + @NonNull + public Builder setMetadata(@NonNull PersistableBundle value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mMetadata = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + @UnsupportedAppUsage + @NonNull + public HotwordAudioStream build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; // Mark builder used + + if ((mBuilderFieldsSet & 0x4) == 0) { + mTimestamp = defaultTimestamp(); + } + if ((mBuilderFieldsSet & 0x8) == 0) { + mMetadata = defaultMetadata(); + } + HotwordAudioStream o = new HotwordAudioStream( + mAudioFormat, + mAudioStreamParcelFileDescriptor, + mTimestamp, + mMetadata); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x10) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } +} diff --git a/core/java/android/service/voice/HotwordDetectedResult.java b/core/java/android/service/voice/HotwordDetectedResult.java index ab71459ed51e..990e136197d9 100644 --- a/core/java/android/service/voice/HotwordDetectedResult.java +++ b/core/java/android/service/voice/HotwordDetectedResult.java @@ -20,6 +20,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; import android.content.res.Resources; import android.media.AudioRecord; import android.media.MediaSyncEvent; @@ -31,6 +32,9 @@ import com.android.internal.R; import com.android.internal.util.DataClass; import com.android.internal.util.Preconditions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -196,6 +200,17 @@ public final class HotwordDetectedResult implements Parcelable { } /** + * The list of the audio streams containing audio bytes that were used for hotword detection. + * + * @hide + */ + @NonNull + private final List<HotwordAudioStream> mAudioStreams; + private static List<HotwordAudioStream> defaultAudioStreams() { + return Collections.emptyList(); + } + + /** * App-specific extras to support trigger. * * <p>The size of this bundle will be limited to {@link #getMaxBundleSize}. Results will larger @@ -353,6 +368,54 @@ public final class HotwordDetectedResult implements Parcelable { } } + /** + * The list of the audio streams containing audio bytes that were used for hotword detection. + * + * @hide + */ + @UnsupportedAppUsage + public @NonNull List<HotwordAudioStream> getAudioStreams() { + return List.copyOf(mAudioStreams); + } + + @DataClass.Suppress("addAudioStreams") + abstract static class BaseBuilder { + /** + * The list of the audio streams containing audio bytes that were used for hotword + * detection. + * + * @hide + */ + @UnsupportedAppUsage + public @NonNull Builder setAudioStreams(@NonNull List<HotwordAudioStream> value) { + Objects.requireNonNull(value, "value should not be null"); + final Builder builder = (Builder) this; + // If the code gen flag in build() is changed, we must update the flag e.g. 0x200 here. + builder.mBuilderFieldsSet |= 0x200; + builder.mAudioStreams = List.copyOf(value); + return builder; + } + } + + /** + * Provides an instance of {@link Builder} with state corresponding to this instance. + * @hide + */ + public Builder buildUpon() { + return new Builder() + .setConfidenceLevel(mConfidenceLevel) + .setMediaSyncEvent(mMediaSyncEvent) + .setHotwordOffsetMillis(mHotwordOffsetMillis) + .setHotwordDurationMillis(mHotwordDurationMillis) + .setAudioChannel(mAudioChannel) + .setHotwordDetectionPersonalized(mHotwordDetectionPersonalized) + .setScore(mScore) + .setPersonalizedScore(mPersonalizedScore) + .setHotwordPhraseId(mHotwordPhraseId) + .setAudioStreams(mAudioStreams) + .setExtras(mExtras); + } + // Code below generated by codegen v1.0.23. @@ -436,6 +499,7 @@ public final class HotwordDetectedResult implements Parcelable { int score, int personalizedScore, int hotwordPhraseId, + @NonNull List<HotwordAudioStream> audioStreams, @NonNull PersistableBundle extras) { this.mConfidenceLevel = confidenceLevel; com.android.internal.util.AnnotationValidations.validate( @@ -448,6 +512,9 @@ public final class HotwordDetectedResult implements Parcelable { this.mScore = score; this.mPersonalizedScore = personalizedScore; this.mHotwordPhraseId = hotwordPhraseId; + this.mAudioStreams = audioStreams; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioStreams); this.mExtras = extras; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mExtras); @@ -578,6 +645,7 @@ public final class HotwordDetectedResult implements Parcelable { "score = " + mScore + ", " + "personalizedScore = " + mPersonalizedScore + ", " + "hotwordPhraseId = " + mHotwordPhraseId + ", " + + "audioStreams = " + mAudioStreams + ", " + "extras = " + mExtras + " }"; } @@ -604,6 +672,7 @@ public final class HotwordDetectedResult implements Parcelable { && mScore == that.mScore && mPersonalizedScore == that.mPersonalizedScore && mHotwordPhraseId == that.mHotwordPhraseId + && Objects.equals(mAudioStreams, that.mAudioStreams) && Objects.equals(mExtras, that.mExtras); } @@ -623,6 +692,7 @@ public final class HotwordDetectedResult implements Parcelable { _hash = 31 * _hash + mScore; _hash = 31 * _hash + mPersonalizedScore; _hash = 31 * _hash + mHotwordPhraseId; + _hash = 31 * _hash + Objects.hashCode(mAudioStreams); _hash = 31 * _hash + Objects.hashCode(mExtras); return _hash; } @@ -645,6 +715,7 @@ public final class HotwordDetectedResult implements Parcelable { dest.writeInt(mScore); dest.writeInt(mPersonalizedScore); dest.writeInt(mHotwordPhraseId); + dest.writeParcelableList(mAudioStreams, flags); dest.writeTypedObject(mExtras, flags); } @@ -669,6 +740,8 @@ public final class HotwordDetectedResult implements Parcelable { int score = in.readInt(); int personalizedScore = in.readInt(); int hotwordPhraseId = in.readInt(); + List<HotwordAudioStream> audioStreams = new ArrayList<>(); + in.readParcelableList(audioStreams, HotwordAudioStream.class.getClassLoader()); PersistableBundle extras = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR); this.mConfidenceLevel = confidenceLevel; @@ -682,6 +755,9 @@ public final class HotwordDetectedResult implements Parcelable { this.mScore = score; this.mPersonalizedScore = personalizedScore; this.mHotwordPhraseId = hotwordPhraseId; + this.mAudioStreams = audioStreams; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mAudioStreams); this.mExtras = extras; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mExtras); @@ -708,7 +784,7 @@ public final class HotwordDetectedResult implements Parcelable { */ @SuppressWarnings("WeakerAccess") @DataClass.Generated.Member - public static final class Builder { + public static final class Builder extends BaseBuilder { private @HotwordConfidenceLevelValue int mConfidenceLevel; private @Nullable MediaSyncEvent mMediaSyncEvent; @@ -719,6 +795,7 @@ public final class HotwordDetectedResult implements Parcelable { private int mScore; private int mPersonalizedScore; private int mHotwordPhraseId; + private @NonNull List<HotwordAudioStream> mAudioStreams; private @NonNull PersistableBundle mExtras; private long mBuilderFieldsSet = 0L; @@ -868,7 +945,7 @@ public final class HotwordDetectedResult implements Parcelable { @DataClass.Generated.Member public @NonNull Builder setExtras(@NonNull PersistableBundle value) { checkNotUsed(); - mBuilderFieldsSet |= 0x200; + mBuilderFieldsSet |= 0x400; mExtras = value; return this; } @@ -876,7 +953,7 @@ public final class HotwordDetectedResult implements Parcelable { /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull HotwordDetectedResult build() { checkNotUsed(); - mBuilderFieldsSet |= 0x400; // Mark builder used + mBuilderFieldsSet |= 0x800; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mConfidenceLevel = defaultConfidenceLevel(); @@ -906,6 +983,9 @@ public final class HotwordDetectedResult implements Parcelable { mHotwordPhraseId = defaultHotwordPhraseId(); } if ((mBuilderFieldsSet & 0x200) == 0) { + mAudioStreams = defaultAudioStreams(); + } + if ((mBuilderFieldsSet & 0x400) == 0) { mExtras = defaultExtras(); } HotwordDetectedResult o = new HotwordDetectedResult( @@ -918,12 +998,13 @@ public final class HotwordDetectedResult implements Parcelable { mScore, mPersonalizedScore, mHotwordPhraseId, + mAudioStreams, mExtras); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x400) != 0) { + if ((mBuilderFieldsSet & 0x800) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -931,10 +1012,10 @@ public final class HotwordDetectedResult implements Parcelable { } @DataClass.Generated( - time = 1658357814396L, + time = 1668466781144L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/service/voice/HotwordDetectedResult.java", - inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\npublic @android.compat.annotation.UnsupportedAppUsage @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.compat.annotation.UnsupportedAppUsage @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.compat.annotation.UnsupportedAppUsage @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []") @Deprecated private void __metadata() {} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index cfad1afe1b5b..fbf8d8bf9fe4 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -467,6 +467,23 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Sets whether a container is being drag-resized. + * When {@code true}, the client will reuse a single (larger) surface size to avoid + * continuous allocations on every size change. + * + * @param container WindowContainerToken of the task that changed its drag resizing state + * @hide + */ + @NonNull + public WindowContainerTransaction setDragResizing(@NonNull WindowContainerToken container, + boolean dragResizing) { + final Change change = getOrCreateChange(container.asBinder()); + change.mChangeMask |= Change.CHANGE_DRAG_RESIZING; + change.mDragResizing = dragResizing; + return this; + } + + /** * Sends a pending intent in sync. * @param sender The PendingIntent sender. * @param intent The fillIn intent to patch over the sender's base intent. @@ -906,12 +923,14 @@ public final class WindowContainerTransaction implements Parcelable { public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 5; public static final int CHANGE_FORCE_NO_PIP = 1 << 6; public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 7; + public static final int CHANGE_DRAG_RESIZING = 1 << 8; private final Configuration mConfiguration = new Configuration(); private boolean mFocusable = true; private boolean mHidden = false; private boolean mIgnoreOrientationRequest = false; private boolean mForceTranslucent = false; + private boolean mDragResizing = false; private int mChangeMask = 0; private @ActivityInfo.Config int mConfigSetMask = 0; @@ -932,6 +951,7 @@ public final class WindowContainerTransaction implements Parcelable { mHidden = in.readBoolean(); mIgnoreOrientationRequest = in.readBoolean(); mForceTranslucent = in.readBoolean(); + mDragResizing = in.readBoolean(); mChangeMask = in.readInt(); mConfigSetMask = in.readInt(); mWindowSetMask = in.readInt(); @@ -980,6 +1000,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((other.mChangeMask & CHANGE_FORCE_TRANSLUCENT) != 0) { mForceTranslucent = other.mForceTranslucent; } + if ((other.mChangeMask & CHANGE_DRAG_RESIZING) != 0) { + mDragResizing = other.mDragResizing; + } mChangeMask |= other.mChangeMask; if (other.mActivityWindowingMode >= 0) { mActivityWindowingMode = other.mActivityWindowingMode; @@ -1039,6 +1062,15 @@ public final class WindowContainerTransaction implements Parcelable { return mForceTranslucent; } + /** Gets the requested drag resizing state. */ + public boolean getDragResizing() { + if ((mChangeMask & CHANGE_DRAG_RESIZING) == 0) { + throw new RuntimeException("Drag resizing not set. " + + "Check CHANGE_DRAG_RESIZING first"); + } + return mDragResizing; + } + public int getChangeMask() { return mChangeMask; } @@ -1100,6 +1132,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((mChangeMask & CHANGE_FOCUSABLE) != 0) { sb.append("focusable:" + mFocusable + ","); } + if ((mChangeMask & CHANGE_DRAG_RESIZING) != 0) { + sb.append("dragResizing:" + mDragResizing + ","); + } if (mBoundsChangeTransaction != null) { sb.append("hasBoundsTransaction,"); } @@ -1117,6 +1152,7 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeBoolean(mHidden); dest.writeBoolean(mIgnoreOrientationRequest); dest.writeBoolean(mForceTranslucent); + dest.writeBoolean(mDragResizing); dest.writeInt(mChangeMask); dest.writeInt(mConfigSetMask); dest.writeInt(mWindowSetMask); diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index a352063aa6ee..f998a690f5a0 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -2402,7 +2402,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind return; } final ThreadedRenderer renderer = getThreadedRenderer(); - if (renderer != null) { + if (renderer != null && !CAPTION_ON_SHELL) { loadBackgroundDrawablesIfNeeded(); WindowInsets rootInsets = getRootWindowInsets(); mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer, diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index abf7e9911086..42c892a240b6 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -1667,6 +1667,9 @@ public class Canvas extends BaseCanvas { * effectively treating them as zeros. In API level {@value Build.VERSION_CODES#P} and above * these parameters will be respected. * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param bitmap The bitmap to draw using the mesh * @param meshWidth The number of columns in the mesh. Nothing is drawn if this is 0 * @param meshHeight The number of rows in the mesh. Nothing is drawn if this is 0 @@ -1678,7 +1681,7 @@ public class Canvas extends BaseCanvas { * null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values * in the array. * @param colorOffset Number of color elements to skip before drawing - * @param paint May be null. The paint used to draw the bitmap + * @param paint May be null. The paint used to draw the bitmap. Antialiasing is not supported. */ public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @@ -1832,9 +1835,12 @@ public class Canvas extends BaseCanvas { /** * Draws the specified bitmap as an N-patch (most often, a 9-patch.) * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param patch The ninepatch object to render * @param dst The destination rectangle. - * @param paint The paint to draw the bitmap with. may be null + * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported. */ public void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, @Nullable Paint paint) { super.drawPatch(patch, dst, paint); @@ -1843,9 +1849,12 @@ public class Canvas extends BaseCanvas { /** * Draws the specified bitmap as an N-patch (most often, a 9-patch.) * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param patch The ninepatch object to render * @param dst The destination rectangle. - * @param paint The paint to draw the bitmap with. may be null + * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported. */ public void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, @Nullable Paint paint) { super.drawPatch(patch, dst, paint); @@ -2278,6 +2287,9 @@ public class Canvas extends BaseCanvas { * array is optional, but if it is present, then it is used to specify the index of each * triangle, rather than just walking through the arrays in order. * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param mode How to interpret the array of vertices * @param vertexCount The number of values in the vertices array (and corresponding texs and * colors arrays if non-null). Each logical vertex is two values (x, y), vertexCount @@ -2292,8 +2304,9 @@ public class Canvas extends BaseCanvas { * @param colorOffset Number of values in colors to skip before drawing. * @param indices If not null, array of indices to reference into the vertex (texs, colors) * array. - * @param indexCount number of entries in the indices array (if not null). - * @param paint Specifies the shader to use if the texs array is non-null. + * @param indexCount Number of entries in the indices array (if not null). + * @param paint Specifies the shader to use if the texs array is non-null. Antialiasing is not + * supported. */ public void drawVertices(@NonNull VertexMode mode, int vertexCount, @NonNull float[] verts, int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors, diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 451b99ea7550..f438a03b1434 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -137,6 +137,13 @@ public class Paint { * <p>Enabling this flag will cause all draw operations that support * antialiasing to use it.</p> * + * <p>Notable draw operations that do <b>not</b> support antialiasing include:</p> + * <ul> + * <li>{@link android.graphics.Canvas#drawBitmapMesh}</li> + * <li>{@link android.graphics.Canvas#drawPatch}</li> + * <li>{@link android.graphics.Canvas#drawVertices}</li> + * </ul> + * * @see #Paint(int) * @see #setFlags(int) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 43679364b443..e58e785850fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -16,14 +16,12 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; @@ -48,7 +46,6 @@ import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; -import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -567,6 +564,22 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Return list of {@link RunningTaskInfo}s for the given display. + * + * @return filtered list of tasks or empty list + */ + public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) { + ArrayList<RunningTaskInfo> result = new ArrayList<>(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if (taskInfo.displayId == displayId) { + result.add(taskInfo); + } + } + return result; + } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ @Nullable public RunningTaskInfo getRunningTaskInfo(int taskId) { @@ -693,57 +706,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements taskListener.reparentChildSurfaceToTask(taskId, sc, t); } - /** - * Create a {@link WindowContainerTransaction} to clear task bounds. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have bounds cleared - * @return {@link WindowContainerTransaction} with pending operations to clear bounds - */ - public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType() - == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", - taskInfo.token, taskInfo); - wct.setBounds(taskInfo.token, null); - } - } - return wct; - } - - /** - * Create a {@link WindowContainerTransaction} to clear task level freeform setting. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have windowing mode reset to {@link - * WindowConfiguration#WINDOWING_MODE_UNDEFINED} - * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode - */ - public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if (taskInfo.displayId == displayId - && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, - "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, - taskInfo); - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); - } - } - return wct; - } - private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, int event) { ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 34ff6d814c8d..abc4024bc290 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -16,8 +16,11 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -151,21 +154,18 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll int displayId = mContext.getDisplayId(); + ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId); + WindowContainerTransaction wct = new WindowContainerTransaction(); - // Reset freeform windowing mode that is set per task level (tasks should inherit - // container value) - wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId), - true /* transfer */); - int targetWindowingMode; + // Reset freeform windowing mode that is set per task level so tasks inherit it + clearFreeformForStandardTasks(runningTasks, wct); if (active) { - targetWindowingMode = WINDOWING_MODE_FREEFORM; + moveHomeBehindVisibleTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct); } else { - targetWindowingMode = WINDOWING_MODE_FULLSCREEN; - // Clear any resized bounds - wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId), - true /* transfer */); + clearBoundsForStandardTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct); } - prepareWindowingModeChange(wct, displayId, targetWindowingMode); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitions.startTransition(TRANSIT_CHANGE, wct, null); } else { @@ -173,17 +173,69 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } } - private void prepareWindowingModeChange(WindowContainerTransaction wct, - int displayId, @WindowConfiguration.WindowingMode int windowingMode) { - DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer - .getDisplayAreaInfo(displayId); + private WindowContainerTransaction clearBoundsForStandardTasks( + ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", + taskInfo.token, taskInfo); + wct.setBounds(taskInfo.token, null); + } + } + return wct; + } + + private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, + "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, + taskInfo); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + } + } + + private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks"); + RunningTaskInfo homeTask = null; + ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>(); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + homeTask = taskInfo; + } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.isVisible()) { + visibleTasks.add(taskInfo); + } + } + if (homeTask == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found"); + } else { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d", + visibleTasks.size()); + wct.reorder(homeTask.getToken(), true /* onTop */); + for (RunningTaskInfo task : visibleTasks) { + wct.reorder(task.getToken(), true /* onTop */); + } + } + } + + private void setDisplayAreaWindowingMode(int displayId, + @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + displayId); if (displayAreaInfo == null) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "unable to update windowing mode for display %d display not found", displayId); return; } - ProtoLog.d(WM_SHELL_DESKTOP_MODE, + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), windowingMode); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index f0f2db7ded80..a49a300995e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,9 @@ class TaskPositioner implements DragResizeCallback { private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mResizeStartPoint = new PointF(); private final Rect mResizeTaskBounds = new Rect(); + // Whether the |dragResizing| hint should be sent with the next bounds change WCT. + // Used to optimized fluid resizing of freeform tasks. + private boolean mPendingDragResizeHint = false; private int mCtrlType; private DragStartListener mDragStartListener; @@ -53,6 +56,12 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeStart(int ctrlType, float x, float y) { + if (ctrlType != CTRL_TYPE_UNDEFINED) { + // The task is being resized, send the |dragResizing| hint to core with the first + // bounds-change wct. + mPendingDragResizeHint = true; + } + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); mCtrlType = ctrlType; @@ -63,19 +72,31 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeMove(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (changeBounds(wct, x, y)) { + if (mPendingDragResizeHint) { + // This is the first bounds change since drag resize operation started. + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); + mPendingDragResizeHint = false; + } + mTaskOrganizer.applyTransaction(wct); + } } @Override public void onDragResizeEnd(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); + changeBounds(wct, x, y); + mTaskOrganizer.applyTransaction(wct); mCtrlType = 0; mTaskBoundsAtDragStart.setEmpty(); mResizeStartPoint.set(0, 0); + mPendingDragResizeHint = false; } - private void changeBounds(float x, float y) { + private boolean changeBounds(WindowContainerTransaction wct, float x, float y) { float deltaX = x - mResizeStartPoint.x; mResizeTaskBounds.set(mTaskBoundsAtDragStart); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { @@ -96,10 +117,10 @@ class TaskPositioner implements DragResizeCallback { } if (!mResizeTaskBounds.isEmpty()) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds); - mTaskOrganizer.applyTransaction(wct); + return true; } + return false; } interface DragStartListener { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 7cbace5af48f..081c8ae91bdb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,13 +16,9 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -34,8 +30,6 @@ import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIO import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -44,11 +38,9 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; -import android.app.WindowConfiguration; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -61,8 +53,6 @@ import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; -import android.window.WindowContainerTransaction.Change; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -638,130 +628,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase { verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } - @Test - public void testPrepareClearBoundsForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - assertEquals(wct.getChanges().size(), 2); - Change boundsChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(boundsChange1); - assertNotEquals( - (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty()); - - Change boundsChange2 = wct.getChanges().get(token2.binder()); - assertNotNull(boundsChange2); - assertNotEquals( - (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty()); - } - - @Test - public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - // Only clear bounds for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - - @Test - public void testPrepareClearFreeformForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only task with freeform windowing mode and the right display should be updated - assertEquals(wct.getChanges().size(), 1); - Change wmModeChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(wmModeChange1); - assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED); - } - - @Test - public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only clear freeform for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); return taskInfo; } - - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) { - RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode); - taskInfo.displayId = 1; - taskInfo.token = token.token(); - taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - return taskInfo; - } - - private static class MockToken { - private final WindowContainerToken mToken; - private final IBinder mBinder; - - MockToken() { - mToken = mock(WindowContainerToken.class); - mBinder = mock(IBinder.class); - when(mToken.asBinder()).thenReturn(mBinder); - } - - WindowContainerToken token() { - return mToken; - } - - IBinder binder() { - return mBinder; - } - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 79b520c734c8..89bafcb6b2f4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -16,10 +16,13 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; @@ -30,13 +33,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -68,6 +72,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) public class DesktopModeControllerTest extends ShellTestCase { @@ -83,9 +90,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Mock private Handler mMockHandler; @Mock - private Transitions mMockTransitions; - private TestShellExecutor mExecutor; - + private Transitions mTransitions; private DesktopModeController mController; private DesktopModeTaskRepository mDesktopModeTaskRepository; private ShellInit mShellInit; @@ -97,20 +102,19 @@ public class DesktopModeControllerTest extends ShellTestCase { when(DesktopModeStatus.isActive(any())).thenReturn(true); mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); - mExecutor = new TestShellExecutor(); mDesktopModeTaskRepository = new DesktopModeTaskRepository(); mController = new DesktopModeController(mContext, mShellInit, mShellController, - mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions, - mDesktopModeTaskRepository, mMockHandler, mExecutor); + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, + mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn( - new WindowContainerTransaction()); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>()); mShellInit.init(); clearInvocations(mShellTaskOrganizer); clearInvocations(mRootTaskDisplayAreaOrganizer); + clearInvocations(mTransitions); } @After @@ -124,113 +128,133 @@ public class DesktopModeControllerTest extends ShellTestCase { } @Test - public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWct = new WindowContainerTransaction(); - MockToken taskMockToken = new MockToken(); - taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeEnabled_rootTdaSetToFreeform() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // The test mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to freeform + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + @Test + public void testDesktopModeDisabled_rootTdaSetToFullscreen() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // WCT should have 2 changes - clear task wm mode and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(2); + mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to fullscreen + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder()); - assertThat(taskWmModeChange).isNotNull(); - assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + @Test + public void testDesktopModeEnabled_windowingModeCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // Verify executed WCT has a change for setting display windowing mode to freeform - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 2 changes: Root TDA windowing mode and 1 task + assertThat(wct.getChanges().size()).isEqualTo(2); + // No changes for tasks that are not standard or freeform + assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull(); + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard freeform task has windowing mode cleared + Change change = wct.getChanges().get(freeformTask.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); } @Test - public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWmWct = new WindowContainerTransaction(); - MockToken taskWmMockToken = new MockToken(); - taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWmWct); - - // Create a fake WCT to simulate clearing task bounds - WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction(); - MockToken taskBoundsMockToken = new MockToken(); - taskBoundsWct.setBounds(taskBoundsMockToken.token(), null); - when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks( - mContext.getDisplayId())).thenReturn(taskBoundsWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeDisabled_windowingModeAndBoundsCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // The test mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 3 changes: Root TDA windowing mode and 2 tasks + assertThat(wct.getChanges().size()).isEqualTo(3); + // No changes to home task + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard tasks have bounds cleared + assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder())); + assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder())); + // Freeform standard tasks have windowing mode cleared + assertThat(wct.getChanges().get( + freeformTask.token.asBinder()).getWindowingMode()).isEqualTo( + WINDOWING_MODE_UNDEFINED); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); - - // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(3); - - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder()); - assertThat(taskWmMode).isNotNull(); - assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); - - // Verify executed WCT has a change for clearing task bounds - Change bounds = wct.getChanges().get(taskBoundsMockToken.binder()); - assertThat(bounds).isNotNull(); - assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0); - assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); - - // Verify executed WCT has a change for setting display windowing mode to fullscreen - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + @Test + public void testDesktopModeEnabled_homeTaskBehindVisibleTask() { + createMockDisplayArea(); + RunningTaskInfo fullscreenTask1 = createFullscreenTask(); + fullscreenTask1.isVisible = true; + RunningTaskInfo fullscreenTask2 = createFullscreenTask(); + fullscreenTask2.isVisible = false; + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask))); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // Check that there are hierarchy changes for home task and visible task + assertThat(wct.getHierarchyOps()).hasSize(2); + // First show home task + WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder()); + + // Then visible task on top of it + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder()); } @Test public void testShowDesktopApps() { // Set up two active tasks on desktop - mDesktopModeTaskRepository.addActiveTask(1); - mDesktopModeTaskRepository.addActiveTask(2); - MockToken token1 = new MockToken(); - MockToken token2 = new MockToken(); - ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken( - token1.token()).setLastActiveTime(100).build(); - ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken( - token2.token()).setLastActiveTime(200).build(); - when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1); - when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2); + RunningTaskInfo freeformTask1 = createFreeformTask(); + freeformTask1.lastActiveTime = 100; + RunningTaskInfo freeformTask2 = createFreeformTask(); + freeformTask2.lastActiveTime = 200; + mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId); + mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn( + freeformTask1); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn( + freeformTask2); // Run show desktop apps logic mController.showDesktopApps(); ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass( WindowContainerTransaction.class); - verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any()); + } else { + verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + } WindowContainerTransaction wct = wctCaptor.getValue(); // Check wct has reorder calls @@ -239,12 +263,12 @@ public class DesktopModeControllerTest extends ShellTestCase { // Task 2 has activity later, must be first WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op1.getContainer()).isEqualTo(token2.binder()); + assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder()); // Task 1 should be second - WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0); + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op2.getContainer()).isEqualTo(token2.binder()); + assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder()); } @Test @@ -266,7 +290,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_notFreeform_returnsNull() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); WindowContainerTransaction wct = mController.handleRequest( new Binder(), @@ -276,7 +300,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_returnsWct() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.token = new MockToken().mToken; trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); WindowContainerTransaction wct = mController.handleRequest( @@ -285,6 +309,57 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(wct).isNotNull(); } + private DisplayAreaInfo createMockDisplayArea() { + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + return displayAreaInfo; + } + + private RunningTaskInfo createFreeformTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createFullscreenTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createHomeTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_HOME) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private WindowContainerTransaction getDesktopModeSwitchTransaction() { + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any()); + } else { + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + } + return arg.getValue(); + } + + private void assertThatBoundsCleared(Change change) { + assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue(); + assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); + } + private static class MockToken { private final WindowContainerToken mToken; private final IBinder mBinder; @@ -298,9 +373,5 @@ public class DesktopModeControllerTest extends ShellTestCase { WindowContainerToken token() { return mToken; } - - IBinder binder() { - return mBinder; - } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt new file mode 100644 index 000000000000..ac10ddb0116a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -0,0 +1,130 @@ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for [TaskPositioner]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskPositionerTest : ShellTestCase() { + + @Mock + private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock + private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock + private lateinit var mockDragStartListener: TaskPositioner.DragStartListener + + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + + private lateinit var taskPositioner: TaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + taskPositioner = TaskPositioner( + mockShellTaskOrganizer, + mockWindowDecoration, + mockDragStartListener + ) + `when`(taskToken.asBinder()).thenReturn(taskBinder) + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + } + + @Test + fun testDragResize_move_skipsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_UNDEFINED, // Move + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Move the task 10px to the right. + val newX = STARTING_BOUNDS.left.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + } + + @Test + fun testDragResize_resize_setsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_RIGHT, // Resize right + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize the task by 10px to the right. + val newX = STARTING_BOUNDS.right.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + !change.dragResizing + } + }) + } + + companion object { + private const val TASK_ID = 5 + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + } +} diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 95599bd41e8d..1183ca3b4977 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -174,6 +174,12 @@ static struct { jfieldID typeId; } gDescriptorInfo; +static struct { + jclass clazz; + jmethodID ctorId; + jmethodID setId; +} gBufferInfo; + struct fields_t { jmethodID postEventFromNativeID; jmethodID lockAndGetContextID; @@ -460,11 +466,7 @@ status_t JMediaCodec::dequeueOutputBuffer( return err; } - ScopedLocalRef<jclass> clazz( - env, env->FindClass("android/media/MediaCodec$BufferInfo")); - - jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V"); - env->CallVoidMethod(bufferInfo, method, (jint)offset, (jint)size, timeUs, flags); + env->CallVoidMethod(bufferInfo, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags); return OK; } @@ -1091,13 +1093,7 @@ void JMediaCodec::handleCallback(const sp<AMessage> &msg) { CHECK(msg->findInt64("timeUs", &timeUs)); CHECK(msg->findInt32("flags", (int32_t *)&flags)); - ScopedLocalRef<jclass> clazz( - env, env->FindClass("android/media/MediaCodec$BufferInfo")); - jmethodID ctor = env->GetMethodID(clazz.get(), "<init>", "()V"); - jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V"); - - obj = env->NewObject(clazz.get(), ctor); - + obj = env->NewObject(gBufferInfo.clazz, gBufferInfo.ctorId); if (obj == NULL) { if (env->ExceptionCheck()) { ALOGE("Could not create MediaCodec.BufferInfo."); @@ -1107,7 +1103,7 @@ void JMediaCodec::handleCallback(const sp<AMessage> &msg) { return; } - env->CallVoidMethod(obj, method, (jint)offset, (jint)size, timeUs, flags); + env->CallVoidMethod(obj, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags); break; } @@ -3235,6 +3231,16 @@ static void android_media_MediaCodec_native_init(JNIEnv *env, jclass) { gDescriptorInfo.typeId = env->GetFieldID(clazz.get(), "mType", "I"); CHECK(gDescriptorInfo.typeId != NULL); + + clazz.reset(env->FindClass("android/media/MediaCodec$BufferInfo")); + CHECK(clazz.get() != NULL); + gBufferInfo.clazz = (jclass)env->NewGlobalRef(clazz.get()); + + gBufferInfo.ctorId = env->GetMethodID(clazz.get(), "<init>", "()V"); + CHECK(gBufferInfo.ctorId != NULL); + + gBufferInfo.setId = env->GetMethodID(clazz.get(), "set", "(IIJI)V"); + CHECK(gBufferInfo.setId != NULL); } static void android_media_MediaCodec_native_setup( diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 4267ba2ff0b7..68ff116be4b0 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -195,6 +195,9 @@ <permission android:name="com.android.systemui.permission.FLAGS" android:protectionLevel="signature" /> + <permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES" + android:protectionLevel="signature|privileged" /> + <!-- Adding Quick Settings tiles --> <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" /> @@ -976,5 +979,12 @@ <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" /> </intent-filter> </receiver> + + <provider + android:authorities="com.android.systemui.keyguard.quickaffordance" + android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider" + android:exported="true" + android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES" + /> </application> </manifest> diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml index eada40e6a40d..278a89f7dba3 100644 --- a/packages/SystemUI/compose/features/AndroidManifest.xml +++ b/packages/SystemUI/compose/features/AndroidManifest.xml @@ -34,6 +34,11 @@ android:enabled="false" tools:replace="android:authorities" tools:node="remove" /> + <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider" + android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled" + android:enabled="false" + tools:replace="android:authorities" + tools:node="remove" /> <provider android:name="com.android.keyguard.clock.ClockOptionsProvider" android:authorities="com.android.systemui.test.keyguard.clock.disabled" android:enabled="false" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt deleted file mode 100644 index 4d94bab6c26c..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt +++ /dev/null @@ -1,392 +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.systemui.user.ui.compose - -import android.graphics.drawable.Drawable -import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap -import com.android.systemui.common.ui.compose.load -import com.android.systemui.compose.SysUiOutlinedButton -import com.android.systemui.compose.SysUiTextButton -import com.android.systemui.compose.features.R -import com.android.systemui.compose.theme.LocalAndroidColorScheme -import com.android.systemui.user.ui.viewmodel.UserActionViewModel -import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import com.android.systemui.user.ui.viewmodel.UserViewModel -import java.lang.Integer.min -import kotlin.math.ceil - -@Composable -fun UserSwitcherScreen( - viewModel: UserSwitcherViewModel, - onFinished: () -> Unit, - modifier: Modifier = Modifier, -) { - val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false) - val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList()) - val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1) - val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList()) - val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false) - val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false) - - UserSwitcherScreenStateless( - isFinishRequested = isFinishRequested, - users = users, - maxUserColumns = maxUserColumns, - menuActions = menuActions, - isOpenMenuButtonVisible = isOpenMenuButtonVisible, - isMenuVisible = isMenuVisible, - onMenuClosed = viewModel::onMenuClosed, - onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked, - onCancelButtonClicked = viewModel::onCancelButtonClicked, - onFinished = { - onFinished() - viewModel.onFinished() - }, - modifier = modifier, - ) -} - -@Composable -private fun UserSwitcherScreenStateless( - isFinishRequested: Boolean, - users: List<UserViewModel>, - maxUserColumns: Int, - menuActions: List<UserActionViewModel>, - isOpenMenuButtonVisible: Boolean, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - onOpenMenuButtonClicked: () -> Unit, - onCancelButtonClicked: () -> Unit, - onFinished: () -> Unit, - modifier: Modifier = Modifier, -) { - LaunchedEffect(isFinishRequested) { - if (isFinishRequested) { - onFinished() - } - } - - Box( - modifier = - modifier - .fillMaxSize() - .padding( - horizontal = 60.dp, - vertical = 40.dp, - ), - ) { - UserGrid( - users = users, - maxUserColumns = maxUserColumns, - modifier = Modifier.align(Alignment.Center), - ) - - Buttons( - menuActions = menuActions, - isOpenMenuButtonVisible = isOpenMenuButtonVisible, - isMenuVisible = isMenuVisible, - onMenuClosed = onMenuClosed, - onOpenMenuButtonClicked = onOpenMenuButtonClicked, - onCancelButtonClicked = onCancelButtonClicked, - modifier = Modifier.align(Alignment.BottomEnd), - ) - } -} - -@Composable -private fun UserGrid( - users: List<UserViewModel>, - maxUserColumns: Int, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(44.dp), - modifier = modifier, - ) { - val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt() - (0 until rowCount).forEach { rowIndex -> - Row( - horizontalArrangement = Arrangement.spacedBy(64.dp), - modifier = modifier, - ) { - val fromIndex = rowIndex * maxUserColumns - val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns) - users.subList(fromIndex, toIndex).forEach { user -> - UserItem( - viewModel = user, - ) - } - } - } - } -} - -@Composable -private fun UserItem( - viewModel: UserViewModel, -) { - val onClicked = viewModel.onClicked - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - if (onClicked != null) { - Modifier.clickable { onClicked() } - } else { - Modifier - } - .alpha(viewModel.alpha), - ) { - Box { - UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp)) - - UserItemIcon( - image = viewModel.image, - isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible, - modifier = Modifier.align(Alignment.Center).size(222.dp) - ) - } - - // User name - val text = viewModel.name.load() - if (text != null) { - // We use the box to center-align the text vertically as that is not possible with Text - // alone. - Box( - modifier = Modifier.size(width = 222.dp, height = 48.dp), - ) { - Text( - text = text, - style = MaterialTheme.typography.titleLarge, - color = colorResource(com.android.internal.R.color.system_neutral1_50), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } -} - -@Composable -private fun UserItemBackground( - modifier: Modifier = Modifier, -) { - Image( - painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground), - contentDescription = null, - modifier = modifier.clip(CircleShape), - ) -} - -@Composable -private fun UserItemIcon( - image: Drawable, - isSelectionMarkerVisible: Boolean, - modifier: Modifier = Modifier, -) { - Image( - bitmap = image.toBitmap().asImageBitmap(), - contentDescription = null, - modifier = - if (isSelectionMarkerVisible) { - // Draws a ring - modifier.border( - width = 8.dp, - color = LocalAndroidColorScheme.current.colorAccentPrimary, - shape = CircleShape, - ) - } else { - modifier - } - .padding(16.dp) - .clip(CircleShape) - ) -} - -@Composable -private fun Buttons( - menuActions: List<UserActionViewModel>, - isOpenMenuButtonVisible: Boolean, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - onOpenMenuButtonClicked: () -> Unit, - onCancelButtonClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - ) { - // Cancel button. - SysUiTextButton( - onClick = onCancelButtonClicked, - ) { - Text(stringResource(R.string.cancel)) - } - - // "Open menu" button. - if (isOpenMenuButtonVisible) { - Spacer(modifier = Modifier.width(8.dp)) - // To properly use a DropdownMenu in Compose, we need to wrap the button that opens it - // and the menu itself in a Box. - Box { - SysUiOutlinedButton( - onClick = onOpenMenuButtonClicked, - ) { - Text(stringResource(R.string.add)) - } - Menu( - viewModel = menuActions, - isMenuVisible = isMenuVisible, - onMenuClosed = onMenuClosed, - ) - } - } - } -} - -@Composable -private fun Menu( - viewModel: List<UserActionViewModel>, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - modifier: Modifier = Modifier, -) { - val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4 - DropdownMenu( - expanded = isMenuVisible, - onDismissRequest = onMenuClosed, - modifier = - modifier.background( - color = MaterialTheme.colorScheme.inverseOnSurface, - ), - ) { - viewModel.forEachIndexed { index, action -> - MenuItem( - viewModel = action, - onClicked = { action.onClicked() }, - topPadding = - if (index == 0) { - 16.dp - } else { - 0.dp - }, - bottomPadding = - if (index == viewModel.size - 1) { - 16.dp - } else { - 0.dp - }, - modifier = Modifier.sizeIn(maxWidth = maxItemWidth), - ) - } - } -} - -@Composable -private fun MenuItem( - viewModel: UserActionViewModel, - onClicked: () -> Unit, - topPadding: Dp, - bottomPadding: Dp, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - val density = LocalDensity.current - - val icon = - remember(viewModel.iconResourceId) { - val drawable = - checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId)) - val size = with(density) { 20.dp.toPx() }.toInt() - drawable - .toBitmap( - width = size, - height = size, - ) - .asImageBitmap() - } - - DropdownMenuItem( - text = { - Text( - text = stringResource(viewModel.textResourceId), - style = MaterialTheme.typography.bodyMedium, - ) - }, - onClick = onClicked, - leadingIcon = { - Spacer(modifier = Modifier.width(10.dp)) - Image( - bitmap = icon, - contentDescription = null, - ) - }, - modifier = - modifier - .heightIn( - min = 56.dp, - ) - .padding( - start = 18.dp, - end = 65.dp, - top = topPadding, - bottom = bottomPadding, - ), - ) -} diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 9f211c92c7c0..553b86bb833f 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -16,7 +16,6 @@ -packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt -packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt -packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt --packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt -packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt -packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt -packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt 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 89f5c2c80e29..66e44b9005de 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -70,10 +70,10 @@ interface ClockController { } /** Optional method for dumping debug information */ - fun dump(pw: PrintWriter) { } + fun dump(pw: PrintWriter) {} /** Optional method for debug logging */ - fun setLogBuffer(logBuffer: LogBuffer) { } + fun setLogBuffer(logBuffer: LogBuffer) {} } /** Interface for a specific clock face version rendered by the clock */ @@ -88,40 +88,37 @@ interface ClockFaceController { /** Events that should call when various rendering parameters change */ interface ClockEvents { /** Call every time tick */ - fun onTimeTick() { } + fun onTimeTick() {} /** Call whenever timezone changes */ - fun onTimeZoneChanged(timeZone: TimeZone) { } + fun onTimeZoneChanged(timeZone: TimeZone) {} /** Call whenever the text time format changes (12hr vs 24hr) */ - fun onTimeFormatChanged(is24Hr: Boolean) { } + fun onTimeFormatChanged(is24Hr: Boolean) {} /** Call whenever the locale changes */ - fun onLocaleChanged(locale: Locale) { } - - /** Call whenever font settings change */ - fun onFontSettingChanged() { } + fun onLocaleChanged(locale: Locale) {} /** Call whenever the color palette should update */ - fun onColorPaletteChanged(resources: Resources) { } + fun onColorPaletteChanged(resources: Resources) {} } /** Methods which trigger various clock animations */ interface ClockAnimations { /** Runs an enter animation (if any) */ - fun enter() { } + fun enter() {} /** Sets how far into AOD the device currently is. */ - fun doze(fraction: Float) { } + fun doze(fraction: Float) {} /** Sets how far into the folding animation the device is. */ - fun fold(fraction: Float) { } + fun fold(fraction: Float) {} /** Runs the battery animation (if any). */ - fun charge() { } + fun charge() {} /** Move the clock, for example, if the notification tray appears in split-shade mode. */ - fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { } + fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {} /** * Whether this clock has a custom position update animation. If true, the keyguard will call @@ -135,11 +132,26 @@ interface ClockAnimations { /** Events that have specific data about the related face */ interface ClockFaceEvents { /** Region Darkness specific to the clock face */ - fun onRegionDarknessChanged(isDark: Boolean) { } + fun onRegionDarknessChanged(isDark: Boolean) {} + + /** + * Call whenever font settings change. Pass in a target font size in pixels. The specific clock + * design is allowed to ignore this target size on a case-by-case basis. + */ + fun onFontSettingChanged(fontSizePx: Float) {} + + /** + * Target region information for the clock face. For small clock, this will match the bounds of + * the parent view mostly, but have a target height based on the height of the default clock. + * For large clocks, the parent view is the entire device size, but most clocks will want to + * render within the centered targetRect to avoid obstructing other elements. The specified + * targetRegion is relative to the parent view. + */ + fun onTargetRegionChanged(targetRegion: Rect?) {} } /** Some data about a clock design */ data class ClockMetadata( val clockId: ClockId, - val name: String + val name: String, ) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index c29714957318..b49afeef09f3 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -37,7 +37,6 @@ android:id="@+id/lockscreen_clock_view_large" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" android:clipChildren="false" android:visibility="gone" /> diff --git a/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml new file mode 100644 index 000000000000..8f6b4c246ba4 --- /dev/null +++ b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/white"/> + <corners android:radius="?android:attr/buttonCornerRadius"/> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index ae2537fe29f6..f14be410bf75 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -312,22 +312,15 @@ <LinearLayout android:id="@+id/see_all_layout" - android:layout_width="match_parent" + style="@style/InternetDialog.Network" android:layout_height="64dp" - android:clickable="true" - android:focusable="true" - android:background="?android:attr/selectableItemBackground" - android:gravity="center_vertical|center_horizontal" - android:orientation="horizontal" - android:paddingStart="22dp" - android:paddingEnd="22dp"> + android:paddingStart="20dp"> <FrameLayout android:layout_width="24dp" android:layout_height="24dp" android:clickable="false" - android:layout_gravity="center_vertical|start" - android:layout_marginStart="@dimen/internet_dialog_network_layout_margin"> + android:layout_gravity="center_vertical|start"> <ImageView android:id="@+id/arrow_forward" android:src="@drawable/ic_arrow_forward" diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 9b8b611558fe..530db0d0304a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -44,7 +44,7 @@ android:background="@drawable/qs_media_outline_album_bg" /> - <com.android.systemui.ripple.MultiRippleView + <com.android.systemui.surfaceeffects.ripple.MultiRippleView android:id="@+id/touch_ripple_view" android:layout_width="match_parent" android:layout_height="@dimen/qs_media_session_height_expanded" @@ -53,6 +53,15 @@ app:layout_constraintTop_toTopOf="@id/album_art" app:layout_constraintBottom_toBottomOf="@id/album_art" /> + <com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_expanded" + app:layout_constraintStart_toStartOf="@id/album_art" + app:layout_constraintEnd_toEndOf="@id/album_art" + app:layout_constraintTop_toTopOf="@id/album_art" + app:layout_constraintBottom_toBottomOf="@id/album_art" /> + <!-- Guideline for output switcher --> <androidx.constraintlayout.widget.Guideline android:id="@+id/center_vertical_guideline" diff --git a/packages/SystemUI/res/layout/wireless_charging_layout.xml b/packages/SystemUI/res/layout/wireless_charging_layout.xml index 887e3e715369..f1bc88370071 100644 --- a/packages/SystemUI/res/layout/wireless_charging_layout.xml +++ b/packages/SystemUI/res/layout/wireless_charging_layout.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <com.android.systemui.ripple.RippleView + <com.android.systemui.surfaceeffects.ripple.RippleView android:id="@+id/wireless_charging_ripple" android:layout_width="match_parent" android:layout_height="match_parent"/> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ff29039a962f..dea06b7a00e6 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1095,7 +1095,7 @@ <item name="android:orientation">horizontal</item> <item name="android:focusable">true</item> <item name="android:clickable">true</item> - <item name="android:background">?android:attr/selectableItemBackground</item> + <item name="android:background">@drawable/internet_dialog_selected_effect</item> </style> <style name="InternetDialog.NetworkTitle"> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 148e5ec1606f..1eb621e0368b 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -44,6 +44,16 @@ app:layout_constraintTop_toTopOf="@+id/album_art" app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <!-- Turbulence noise must have the same constraint as the album art. --> + <Constraint + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_collapsed" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintTop_toTopOf="@+id/album_art" + app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <Constraint android:id="@+id/header_title" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index ac484d7dde8e..64c2ef1fc915 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -37,6 +37,16 @@ app:layout_constraintTop_toTopOf="@+id/album_art" app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <!-- Turbulence noise must have the same constraint as the album art. --> + <Constraint + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_expanded" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintTop_toTopOf="@+id/album_art" + app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <Constraint android:id="@+id/header_title" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index e07a6c1dbc46..5d3650ccc8e6 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -59,6 +59,7 @@ <Layout android:layout_width="wrap_content" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constrainedWidth="true" app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 9d3a293dd662..b679cfa145d4 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -58,9 +58,6 @@ android_library { resource_dirs: [ "res", ], - optimize: { - proguard_flags_files: ["proguard.flags"], - }, min_sdk_version: "current", plugins: ["dagger2-compiler"], } diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags deleted file mode 100644 index 5eda04500190..000000000000 --- a/packages/SystemUI/shared/proguard.flags +++ /dev/null @@ -1,4 +0,0 @@ -# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry --keepattributes Signature --keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken --keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index ca780c8dd3c9..599cd23f6616 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.systemui.plugins.ClockAnimations @@ -80,7 +81,7 @@ class DefaultClockController( } override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { - largeClock.recomputePadding() + largeClock.recomputePadding(null) animations = DefaultClockAnimations(dozeFraction, foldFraction) events.onColorPaletteChanged(resources) events.onTimeZoneChanged(TimeZone.getDefault()) @@ -101,6 +102,7 @@ class DefaultClockController( // MAGENTA is a placeholder, and will be assigned correctly in initialize private var currentColor = Color.MAGENTA private var isRegionDark = false + protected var targetRegion: Rect? = null init { view.setColors(currentColor, currentColor) @@ -112,8 +114,20 @@ class DefaultClockController( this@DefaultClockFaceController.isRegionDark = isRegionDark updateColor() } + + override fun onTargetRegionChanged(targetRegion: Rect?) { + this@DefaultClockFaceController.targetRegion = targetRegion + recomputePadding(targetRegion) + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) + recomputePadding(targetRegion) + } } + open fun recomputePadding(targetRegion: Rect?) {} + fun updateColor() { val color = if (isRegionDark) { @@ -135,9 +149,16 @@ class DefaultClockController( inner class LargeClockFaceController( view: AnimatableClockView, ) : DefaultClockFaceController(view) { - fun recomputePadding() { + override fun recomputePadding(targetRegion: Rect?) { + // We center the view within the targetRegion instead of within the parent + // view by computing the difference and adding that to the padding. + val parent = view.parent + val yDiff = + if (targetRegion != null && parent is View && parent.isLaidOut()) + targetRegion.centerY() - parent.height / 2f + else 0f val lp = view.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = (-0.5f * view.bottom).toInt() + lp.topMargin = (-0.5f * view.bottom + yDiff).toInt() view.setLayoutParams(lp) } @@ -155,18 +176,6 @@ class DefaultClockController( override fun onTimeZoneChanged(timeZone: TimeZone) = clocks.forEach { it.onTimeZoneChanged(timeZone) } - override fun onFontSettingChanged() { - smallClock.view.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() - ) - largeClock.view.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() - ) - largeClock.recomputePadding() - } - override fun onColorPaletteChanged(resources: Resources) { largeClock.updateColor() smallClock.updateColor() diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt new file mode 100644 index 000000000000..c2658a9e61b1 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -0,0 +1,111 @@ +/* + * 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.shared.keyguard.data.content + +import android.content.ContentResolver +import android.net.Uri + +/** Contract definitions for querying content about keyguard quick affordances. */ +object KeyguardQuickAffordanceProviderContract { + + const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance" + const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES" + + private val BASE_URI: Uri = + Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build() + + /** + * Table for slots. + * + * Slots are positions where affordances can be placed on the lock screen. Affordances that are + * placed on slots are said to be "selected". The system supports the idea of multiple + * affordances per slot, though the implementation may limit the number of affordances on each + * slot. + * + * Supported operations: + * - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set + * will contain rows with the [SlotTable.Columns] columns. + */ + object SlotTable { + const val TABLE_NAME = "slots" + val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build() + + object Columns { + /** String. Unique ID for this slot. */ + const val ID = "id" + /** Integer. The maximum number of affordances that can be placed in the slot. */ + const val CAPACITY = "capacity" + } + } + + /** + * Table for affordances. + * + * Affordances are actions/buttons that the user can execute. They are placed on slots on the + * lock screen. + * + * Supported operations: + * - Query - to know about all the affordances that are available on the device, regardless of + * which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will + * contain rows, each with the columns specified in [AffordanceTable.Columns]. + */ + object AffordanceTable { + const val TABLE_NAME = "affordances" + val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build() + + object Columns { + /** String. Unique ID for this affordance. */ + const val ID = "id" + /** String. User-visible name for this affordance. */ + const val NAME = "name" + /** + * Integer. Resource ID for the drawable to load for this affordance. This is a resource + * ID from the system UI package. + */ + const val ICON = "icon" + } + } + + /** + * Table for selections. + * + * Selections are pairs of slot and affordance IDs. + * + * Supported operations: + * - Insert - to insert an affordance and place it in a slot, insert values for the columns into + * the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system. + * Selecting a new affordance for a slot that is already full will automatically remove the + * oldest affordance from the slot. + * - Query - to know which affordances are set on which slots, query the [SelectionTable.URI] + * [Uri]. The result set will contain rows, each of which with the columns from + * [SelectionTable.Columns]. + * - Delete - to unselect an affordance, removing it from a slot, delete from the + * [SelectionTable.URI] [Uri], passing in values for each column. + */ + object SelectionTable { + const val TABLE_NAME = "selections" + val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build() + + object Columns { + /** String. Unique ID for the slot. */ + const val SLOT_ID = "slot_id" + /** String. Unique ID for the selected affordance. */ + const val AFFORDANCE_ID = "affordance_id" + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index c9b8712bdde9..87e9d5630b74 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -26,6 +26,7 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -43,6 +44,11 @@ import com.android.systemui.shared.regionsampling.RegionSampler 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 java.util.concurrent.Executor +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job @@ -50,11 +56,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import java.io.PrintWriter -import java.util.Locale -import java.util.TimeZone -import java.util.concurrent.Executor -import javax.inject.Inject /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -84,6 +85,7 @@ open class ClockEventController @Inject constructor( value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) + updateFontSizes() } } @@ -150,7 +152,7 @@ open class ClockEventController @Inject constructor( mainExecutor, bgExecutor, regionSamplingEnabled, - updateFun = { updateColors() } ) + updateColors) } var smallRegionSampler: RegionSampler? = null @@ -166,7 +168,7 @@ open class ClockEventController @Inject constructor( } override fun onDensityOrFontScaleChanged() { - clock?.events?.onFontSettingChanged() + updateFontSizes() } } @@ -251,6 +253,13 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.stopRegionSampler() } + private fun updateFontSizes() { + clock?.smallClock?.events?.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()) + clock?.largeClock?.events?.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()) + } + /** * Dump information for debugging */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 8ebad6c0fdbf..40423cd9ac2c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -22,6 +23,7 @@ import com.android.systemui.plugins.ClockController; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; + /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -46,6 +48,7 @@ public class KeyguardClockSwitch extends RelativeLayout { */ private FrameLayout mSmallClockFrame; private FrameLayout mLargeClockFrame; + private ClockController mClock; private View mStatusArea; private int mSmartspaceTopOffset; @@ -95,6 +98,8 @@ public class KeyguardClockSwitch extends RelativeLayout { } void setClock(ClockController clock, int statusBarState) { + mClock = clock; + // Disconnect from existing plugin. mSmallClockFrame.removeAllViews(); mLargeClockFrame.removeAllViews(); @@ -108,6 +113,35 @@ public class KeyguardClockSwitch extends RelativeLayout { Log.i(TAG, "Attached new clock views to switch"); mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); + updateClockTargetRegions(); + } + + void updateClockTargetRegions() { + if (mClock != null) { + if (mSmallClockFrame.isLaidOut()) { + int targetHeight = getResources() + .getDimensionPixelSize(R.dimen.small_clock_text_size); + mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect( + mSmallClockFrame.getLeft(), + mSmallClockFrame.getTop(), + mSmallClockFrame.getRight(), + mSmallClockFrame.getTop() + targetHeight)); + } + + if (mLargeClockFrame.isLaidOut()) { + int largeClockTopMargin = getResources() + .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); + int targetHeight = getResources() + .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; + int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2 + + largeClockTopMargin / 2; + mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect( + mLargeClockFrame.getLeft(), + top, + mLargeClockFrame.getRight(), + top + targetHeight)); + } + } } private void updateClockViews(boolean useLargeClock, boolean animate) { @@ -214,6 +248,10 @@ public class KeyguardClockSwitch extends RelativeLayout { protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); + if (changed) { + post(() -> updateClockTargetRegions()); + } + if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index d3cc7ed08a82..789f6218caf9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -77,7 +77,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @KeyguardClockSwitch.ClockSize private int mCurrentClockSize = SMALL; - private int mKeyguardClockTopMargin = 0; + private int mKeyguardSmallClockTopMargin = 0; private final ClockRegistry.ClockChangeListener mClockChangedListener; private ViewGroup mStatusArea; @@ -162,7 +162,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); mClockEventController.registerListeners(mView); - mKeyguardClockTopMargin = + mKeyguardSmallClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); if (mOnlyClock) { @@ -244,10 +244,12 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS */ public void onDensityOrFontScaleChanged() { mView.onDensityOrFontScaleChanged(); - mKeyguardClockTopMargin = + mKeyguardSmallClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); + mView.updateClockTargetRegions(); } + /** * Set which clock should be displayed on the keyguard. The other one will be automatically * hidden. @@ -327,7 +329,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return frameHeight / 2 + clockHeight / 2; } else { int clockHeight = clock.getSmallClock().getView().getHeight(); - return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin; + return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index c93fe6ac9f34..4b57d455a137 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -29,7 +29,7 @@ import android.view.View import android.view.animation.PathInterpolator import com.android.internal.graphics.ColorUtils import com.android.systemui.animation.Interpolators -import com.android.systemui.ripple.RippleShader +import com.android.systemui.surfaceeffects.ripple.RippleShader private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt index 616e49c0b709..1454210ad798 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt @@ -31,7 +31,7 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java index e82d0ea85490..3808ab742419 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java @@ -30,7 +30,7 @@ import android.view.WindowManager; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.ripple.RippleShader.RippleShape; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; /** * A WirelessChargingAnimation is a view containing view + animation for wireless charging. diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java index 145569919e8e..36103f8db8d8 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java @@ -33,9 +33,9 @@ import android.widget.TextView; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; -import com.android.systemui.ripple.RippleAnimationConfig; -import com.android.systemui.ripple.RippleShader.RippleShape; -import com.android.systemui.ripple.RippleView; +import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; +import com.android.systemui.surfaceeffects.ripple.RippleView; import java.text.NumberFormat; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index beaccbaf9a70..e8e1f2e95f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -231,7 +231,8 @@ public class BrightLineFalsingManager implements FalsingManager { // check for false tap if it is a seekbar interaction if (interactionType == MEDIA_SEEKBAR) { - localResult[0] &= isFalseTap(LOW_PENALTY); + localResult[0] &= isFalseTap(mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : FalsingManager.LOW_PENALTY); } logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 139a8b769583..b8030b2a0013 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -17,6 +17,7 @@ package com.android.systemui.dagger; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AlarmManager; @@ -68,6 +69,7 @@ import android.os.PowerManager; import android.os.ServiceManager; import android.os.UserManager; import android.os.Vibrator; +import android.os.storage.StorageManager; import android.permission.PermissionManager; import android.safetycenter.SafetyCenterManager; import android.service.dreams.DreamService; @@ -109,6 +111,7 @@ import dagger.Provides; /** * Provides Non-SystemUI, Framework-Owned instances to the dependency graph. */ +@SuppressLint("NonInjectedService") @Module public class FrameworkServicesModule { @Provides @@ -462,7 +465,13 @@ public class FrameworkServicesModule { @Provides @Singleton - static SubscriptionManager provideSubcriptionManager(Context context) { + static StorageManager provideStorageManager(Context context) { + return context.getSystemService(StorageManager.class); + } + + @Provides + @Singleton + static SubscriptionManager provideSubscriptionManager(Context context) { return context.getSystemService(SubscriptionManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index 7ab36e84178e..d3555eec0243 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -16,6 +16,7 @@ package com.android.systemui.dagger; +import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider; import com.android.systemui.statusbar.QsFrameTranslateModule; import dagger.Subcomponent; @@ -42,4 +43,9 @@ public interface ReferenceSysUIComponent extends SysUIComponent { interface Builder extends SysUIComponent.Builder { ReferenceSysUIComponent build(); } + + /** + * Member injection into the supplied argument. + */ + void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index a4509012866f..ce9a1fc2778e 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -120,28 +120,6 @@ object Flags { @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer") /** - * Whether the user interactor and repository should use `UserSwitcherController`. - * - * If this is `false`, the interactor and repo skip the controller and directly access the - * framework APIs. - */ - // TODO(b/254513286): Tracking Bug - val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = - unreleasedFlag(210, "user_interactor_and_repo_use_controller") - - /** - * Whether `UserSwitcherController` should use the user interactor. - * - * When this is `true`, the controller does not directly access framework APIs. Instead, it goes - * through the interactor. - * - * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it - * would created a cycle between controller -> interactor -> controller. - */ - // TODO(b/254513102): Tracking Bug - val USER_CONTROLLER_USES_INTERACTOR = releasedFlag(211, "user_controller_uses_interactor") - - /** * Whether the clock on a wide lock screen should use the new "stepping" animation for moving * the digits when the clock moves. */ @@ -302,6 +280,8 @@ object Flags { // TODO(b/254513168): Tracking Bug @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple") + @JvmField val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media") + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt new file mode 100644 index 000000000000..0f4581ce3e61 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -0,0 +1,297 @@ +/* + * 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.keyguard + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.UriMatcher +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.util.Log +import com.android.systemui.SystemUIAppComponentFactoryBase +import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +class KeyguardQuickAffordanceProvider : + ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { + + @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor + + private lateinit var contextAvailableCallback: ContextAvailableCallback + + private val uriMatcher = + UriMatcher(UriMatcher.NO_MATCH).apply { + addURI( + Contract.AUTHORITY, + Contract.SlotTable.TABLE_NAME, + MATCH_CODE_ALL_SLOTS, + ) + addURI( + Contract.AUTHORITY, + Contract.AffordanceTable.TABLE_NAME, + MATCH_CODE_ALL_AFFORDANCES, + ) + addURI( + Contract.AUTHORITY, + Contract.SelectionTable.TABLE_NAME, + MATCH_CODE_ALL_SELECTIONS, + ) + } + + override fun onCreate(): Boolean { + return true + } + + override fun attachInfo(context: Context?, info: ProviderInfo?) { + contextAvailableCallback.onContextAvailable(checkNotNull(context)) + super.attachInfo(context, info) + } + + override fun setContextAvailableCallback(callback: ContextAvailableCallback) { + contextAvailableCallback = callback + } + + override fun getType(uri: Uri): String? { + val prefix = + when (uriMatcher.match(uri)) { + MATCH_CODE_ALL_SLOTS, + MATCH_CODE_ALL_AFFORDANCES, + MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd." + else -> null + } + + val tableName = + when (uriMatcher.match(uri)) { + MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME + MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME + MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME + else -> null + } + + if (prefix == null || tableName == null) { + return null + } + + return "$prefix${Contract.AUTHORITY}.$tableName" + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) { + throw UnsupportedOperationException() + } + + return insertSelection(values) + } + + override fun query( + uri: Uri, + projection: Array<out String>?, + selection: String?, + selectionArgs: Array<out String>?, + sortOrder: String?, + ): Cursor? { + return when (uriMatcher.match(uri)) { + MATCH_CODE_ALL_AFFORDANCES -> queryAffordances() + MATCH_CODE_ALL_SLOTS -> querySlots() + MATCH_CODE_ALL_SELECTIONS -> querySelections() + else -> null + } + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<out String>?, + ): Int { + Log.e(TAG, "Update is not supported!") + return 0 + } + + override fun delete( + uri: Uri, + selection: String?, + selectionArgs: Array<out String>?, + ): Int { + if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) { + throw UnsupportedOperationException() + } + + return deleteSelection(uri, selectionArgs) + } + + private fun insertSelection(values: ContentValues?): Uri? { + if (values == null) { + throw IllegalArgumentException("Cannot insert selection, no values passed in!") + } + + if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) { + throw IllegalArgumentException( + "Cannot insert selection, " + + "\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!" + ) + } + + if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) { + throw IllegalArgumentException( + "Cannot insert selection, " + + "\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!" + ) + } + + val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID) + + if (slotId.isNullOrEmpty()) { + throw IllegalArgumentException("Cannot insert selection, slot ID was empty!") + } + + if (affordanceId.isNullOrEmpty()) { + throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!") + } + + val success = runBlocking { + interactor.select( + slotId = slotId, + affordanceId = affordanceId, + ) + } + + return if (success) { + Log.d(TAG, "Successfully selected $affordanceId for slot $slotId") + context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null) + Contract.SelectionTable.URI + } else { + Log.d(TAG, "Failed to select $affordanceId for slot $slotId") + null + } + } + + private fun querySelections(): Cursor { + return MatrixCursor( + arrayOf( + Contract.SelectionTable.Columns.SLOT_ID, + Contract.SelectionTable.Columns.AFFORDANCE_ID, + ) + ) + .apply { + val affordanceIdsBySlotId = runBlocking { interactor.getSelections() } + affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) -> + affordanceIds.forEach { affordanceId -> + addRow( + arrayOf( + slotId, + affordanceId, + ) + ) + } + } + } + } + + private fun queryAffordances(): Cursor { + return MatrixCursor( + arrayOf( + Contract.AffordanceTable.Columns.ID, + Contract.AffordanceTable.Columns.NAME, + Contract.AffordanceTable.Columns.ICON, + ) + ) + .apply { + interactor.getAffordancePickerRepresentations().forEach { representation -> + addRow( + arrayOf( + representation.id, + representation.name, + representation.iconResourceId, + ) + ) + } + } + } + + private fun querySlots(): Cursor { + return MatrixCursor( + arrayOf( + Contract.SlotTable.Columns.ID, + Contract.SlotTable.Columns.CAPACITY, + ) + ) + .apply { + interactor.getSlotPickerRepresentations().forEach { representation -> + addRow( + arrayOf( + representation.id, + representation.maxSelectedAffordances, + ) + ) + } + } + } + + private fun deleteSelection( + uri: Uri, + selectionArgs: Array<out String>?, + ): Int { + if (selectionArgs == null) { + throw IllegalArgumentException( + "Cannot delete selection, selection arguments not included!" + ) + } + + val (slotId, affordanceId) = + when (selectionArgs.size) { + 1 -> Pair(selectionArgs[0], null) + 2 -> Pair(selectionArgs[0], selectionArgs[1]) + else -> + throw IllegalArgumentException( + "Cannot delete selection, selection arguments has wrong size, expected to" + + " have 1 or 2 arguments, had ${selectionArgs.size} instead!" + ) + } + + val deleted = runBlocking { + interactor.unselect( + slotId = slotId, + affordanceId = affordanceId, + ) + } + + return if (deleted) { + Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId") + context?.contentResolver?.notifyChange(uri, null) + 1 + } else { + Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId") + 0 + } + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceProvider" + private const val MATCH_CODE_ALL_SLOTS = 1 + private const val MATCH_CODE_ALL_AFFORDANCES = 2 + private const val MATCH_CODE_ALL_SELECTIONS = 3 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a7f1b95555ba..a8f39fa9a456 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -26,7 +26,8 @@ import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder -import com.android.systemui.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout private const val TAG = "MediaViewHolder" @@ -38,6 +39,8 @@ class MediaViewHolder constructor(itemView: View) { // Player information val albumView = itemView.requireViewById<ImageView>(R.id.album_art) val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view) + val turbulenceNoiseView = + itemView.requireViewById<TurbulenceNoiseView>(R.id.turbulence_noise_view) val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt index 918417fcd9a9..93be6a78ccd5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -29,7 +29,8 @@ import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme -import com.android.systemui.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController /** * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] @@ -102,13 +103,21 @@ internal constructor( private val context: Context, private val mediaViewHolder: MediaViewHolder, private val multiRippleController: MultiRippleController, + private val turbulenceNoiseController: TurbulenceNoiseController, animatingColorTransitionFactory: AnimatingColorTransitionFactory ) { constructor( context: Context, mediaViewHolder: MediaViewHolder, multiRippleController: MultiRippleController, - ) : this(context, mediaViewHolder, multiRippleController, ::AnimatingColorTransition) + turbulenceNoiseController: TurbulenceNoiseController + ) : this( + context, + mediaViewHolder, + multiRippleController, + turbulenceNoiseController, + ::AnimatingColorTransition + ) val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) val surfaceColor = @@ -129,6 +138,7 @@ internal constructor( mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) multiRippleController.updateColor(accentPrimary) + turbulenceNoiseController.updateNoiseColor(accentPrimary) } val accentSecondary = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 215fa03c8c59..21e64e28ff19 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -64,6 +65,7 @@ import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.settingslib.widget.AdaptiveIcon; @@ -97,13 +99,16 @@ import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.ripple.MultiRippleController; -import com.android.systemui.ripple.RippleAnimation; -import com.android.systemui.ripple.RippleAnimationConfig; -import com.android.systemui.ripple.RippleShader; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.surfaceeffects.ripple.MultiRippleController; +import com.android.systemui.surfaceeffects.ripple.MultiRippleView; +import com.android.systemui.surfaceeffects.ripple.RippleAnimation; +import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; +import com.android.systemui.surfaceeffects.ripple.RippleShader; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.time.SystemClock; @@ -216,7 +221,9 @@ public class MediaControlPanel { private boolean mShowBroadcastDialogButton = false; private String mSwitchBroadcastApp; private MultiRippleController mMultiRippleController; + private TurbulenceNoiseController mTurbulenceNoiseController; private FeatureFlags mFeatureFlags; + private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null; /** * Initialize a new control panel @@ -394,9 +401,20 @@ public class MediaControlPanel { AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); - mMultiRippleController = new MultiRippleController(vh.getMultiRippleView()); + MultiRippleView multiRippleView = vh.getMultiRippleView(); + mMultiRippleController = new MultiRippleController(multiRippleView); + mTurbulenceNoiseController = new TurbulenceNoiseController(vh.getTurbulenceNoiseView()); + multiRippleView.addRipplesFinishedListener( + () -> { + if (mTurbulenceNoiseAnimationConfig == null) { + mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation(); + } + // Color will be correctly updated in ColorSchemeTransition. + mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig); + } + ); mColorSchemeTransition = new ColorSchemeTransition( - mContext, mMediaViewHolder, mMultiRippleController); + mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController); mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter); } @@ -571,7 +589,10 @@ public class MediaControlPanel { seamlessView.setContentDescription(deviceString); seamlessView.setOnClickListener( v -> { - if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + if (mFalsingManager.isFalseTap( + mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : + FalsingManager.LOW_PENALTY)) { return; } @@ -994,7 +1015,10 @@ public class MediaControlPanel { } else { button.setEnabled(true); button.setOnClickListener(v -> { - if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + if (!mFalsingManager.isFalseTap( + mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : + FalsingManager.LOW_PENALTY)) { mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId); logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT); action.run(); @@ -1027,7 +1051,7 @@ public class MediaControlPanel { /* maxWidth= */ maxSize, /* maxHeight= */ maxSize, /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density, - mColorSchemeTransition.getAccentPrimary().getTargetColor(), + mColorSchemeTransition.getAccentPrimary().getCurrentColor(), /* opacity= */ 100, /* shouldFillRipple= */ false, /* sparkleStrength= */ 0f, @@ -1036,6 +1060,26 @@ public class MediaControlPanel { ); } + private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() { + return new TurbulenceNoiseAnimationConfig( + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT, + TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + /* noiseMoveSpeedX= */ 0f, + /* noiseMoveSpeedY= */ 0f, + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, + /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), + // We want to add (BlendMode.PLUS) the turbulence noise on top of the album art. + // Thus, set the background color with alpha 0. + /* backgroundColor= */ ColorUtils.setAlphaComponent(Color.BLACK, 0), + TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY, + /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(), + /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(), + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_DURATION_IN_MILLIS, + this.getContext().getResources().getDisplayMetrics().density, + BlendMode.PLUS, + /* onAnimationEnd= */ null + ); + } private void clearButton(final ImageButton button) { button.setImageDrawable(null); button.setContentDescription(null); diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index e354a03f1725..1ea202582f83 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -18,8 +18,8 @@ package com.android.systemui.media.taptotransfer.receiver import android.content.Context import android.util.AttributeSet -import com.android.systemui.ripple.RippleShader -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleShader +import com.android.systemui.surfaceeffects.ripple.RippleView /** * An expanding ripple effect for the media tap-to-transfer receiver chip. diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 0697133a02f9..f92bbf75d027 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -364,13 +364,18 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private void distributeTiles() { emptyAndInflateOrRemovePages(); - final int tileCount = mPages.get(0).maxTiles(); - if (DEBUG) Log.d(TAG, "Distributing tiles"); + final int tilesPerPageCount = mPages.get(0).maxTiles(); int index = 0; - final int NT = mTiles.size(); - for (int i = 0; i < NT; i++) { + final int totalTilesCount = mTiles.size(); + if (DEBUG) { + Log.d(TAG, "Distributing tiles: " + + "[tilesPerPageCount=" + tilesPerPageCount + "]" + + "[totalTilesCount=" + totalTilesCount + "]" + ); + } + for (int i = 0; i < totalTilesCount; i++) { TileRecord tile = mTiles.get(i); - if (mPages.get(index).mRecords.size() == tileCount) index++; + if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++; if (DEBUG) { Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to " + index); @@ -577,8 +582,8 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { }); setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated. int dx = getWidth() * lastPageNumber; - mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, - REVEAL_SCROLL_DURATION_MILLIS); + mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, + REVEAL_SCROLL_DURATION_MILLIS); postInvalidateOnAnimation(); } @@ -738,6 +743,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { public interface PageListener { int INVALID_PAGE = -1; + void onPageChanged(boolean isFirst, int pageNumber); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 3d00dd49cb1c..7ee404756633 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -123,7 +123,6 @@ public class TileLayout extends ViewGroup implements QSTileLayout { public boolean updateResources() { final Resources res = mContext.getResources(); mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); - updateColumns(); mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId); mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal); mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index cf10c7940871..79fcc7d81372 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -82,12 +82,12 @@ public class QSCustomizer extends LinearLayout { DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setMoveDuration(TileAdapter.MOVE_DURATION); mRecyclerView.setItemAnimator(animator); + + updateTransparentViewHeight(); } void updateResources() { - LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams(); - lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); - mTransparentView.setLayoutParams(lp); + updateTransparentViewHeight(); mRecyclerView.getAdapter().notifyItemChanged(0); } @@ -236,4 +236,10 @@ public class QSCustomizer extends LinearLayout { public boolean isOpening() { return mOpening; } + + private void updateTransparentViewHeight() { + LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams(); + lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); + mTransparentView.setLayoutParams(lp); + } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java index 27ad86f0ae12..ee3b13091d00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java @@ -249,15 +249,7 @@ public class InternetDialog extends SystemUIDialog implements mBackgroundOn = mContext.getDrawable(R.drawable.settingslib_switch_bar_bg_on); mInternetDialogTitle.setText(getDialogTitleText()); mInternetDialogTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); - - TypedArray typedArray = mContext.obtainStyledAttributes( - new int[]{android.R.attr.selectableItemBackground}); - try { - mBackgroundOff = typedArray.getDrawable(0 /* index */); - } finally { - typedArray.recycle(); - } - + mBackgroundOff = mContext.getDrawable(R.drawable.internet_dialog_selected_effect); setOnClickListener(); mTurnWifiOnLayout.setBackground(null); mAirplaneModeButton.setVisibility( diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt deleted file mode 100644 index 6de46483892b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt +++ /dev/null @@ -1,53 +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.systemui.ripple - -/** A common utility functions that are used for computing [RippleShader]. */ -class RippleShaderUtilLibrary { - //language=AGSL - companion object { - const val SHADER_LIB = """ - float triangleNoise(vec2 n) { - n = fract(n * vec2(5.3987, 5.4421)); - n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); - float xy = n.x * n.y; - return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; - } - const float PI = 3.1415926535897932384626; - - float sparkles(vec2 uv, float t) { - float n = triangleNoise(uv); - float s = 0.0; - for (float i = 0; i < 4; i += 1) { - float l = i * 0.01; - float h = l + 0.1; - float o = smoothstep(n - l, h, n); - o *= abs(sin(PI * o * (t + 0.55 * i))); - s += o; - } - return s; - } - - vec2 distort(vec2 p, float time, float distort_amount_radial, - float distort_amount_xy) { - float angle = atan(p.y, p.x); - return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), - cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial - + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), - cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; - }""" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt index 954534d42fdd..5011227ad2cc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt @@ -51,6 +51,8 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END) connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END) constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT) + constrainedWidth(R.id.date, true) + constrainedWidth(R.id.statusIcons, true) } ) } @@ -92,6 +94,8 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain centerEnd, ConstraintSet.END ) + constrainedWidth(R.id.date, true) + constrainedWidth(R.id.statusIcons, true) }, qsConstraintsChanges = { setGuidelineBegin(centerStart, offsetFromEdge) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 4e0d7ac1512a..46199613d58d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -174,7 +174,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanelController; import com.android.systemui.recents.ScreenPinningRequest; -import com.android.systemui.ripple.RippleShader.RippleShape; import com.android.systemui.scrim.ScrimView; import com.android.systemui.settings.brightness.BrightnessSliderController; import com.android.systemui.shade.CameraLauncher; @@ -234,6 +233,7 @@ import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowStateController; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.WallpaperController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -1148,7 +1148,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. mNotificationIconAreaController.setupShelf(mNotificationShelfController); mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); - mUserSwitcherController.init(mNotificationShadeWindowView); // Allow plugins to reference DarkIconDispatcher and StatusBarStateController mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); @@ -4285,7 +4284,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } // TODO: Bring these out of CentralSurfaces. mUserInfoControllerImpl.onDensityOrFontScaleChanged(); - mUserSwitcherController.onDensityOrFontScaleChanged(); mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext); mHeadsUpManager.onDensityOrFontScaleChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index d54a8638f2e9..c527f30c424c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -53,7 +53,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -205,7 +204,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private KeyguardViewMediator mKeyguardViewMediator; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -275,8 +273,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump @Main Executor mainExecutor, ScreenOffAnimationController screenOffAnimationController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, - KeyguardViewMediator keyguardViewMediator) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager) { mScrimStateListener = lightBarController::setScrimState; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; @@ -315,8 +312,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } }); mColors = new GradientColors(); - - mKeyguardViewMediator = keyguardViewMediator; } /** @@ -812,13 +807,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindTint, interpolatedFraction); } - - // If we're unlocked but still playing the occlude animation, remain at the keyguard - // alpha temporarily. - if (mKeyguardViewMediator.isOccludeAnimationPlaying() - || mState.mLaunchingAffordanceWithPreview) { - mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA; - } } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) { mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f); } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index cf4106c508cb..68d30d3f3d1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -21,7 +21,6 @@ import android.graphics.ColorFilter import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.drawable.Drawable -import android.os.UserHandle import android.widget.BaseAdapter import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord @@ -84,7 +83,7 @@ protected constructor( } fun refresh() { - controller.refreshUsers(UserHandle.USER_NULL) + controller.refreshUsers() } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt index 146b222c94ce..bdb656b9d2d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -14,35 +14,74 @@ * limitations under the License. * */ + package com.android.systemui.statusbar.policy -import android.annotation.UserIdInt +import android.content.Context import android.content.Intent import android.view.View -import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper +import dagger.Lazy +import java.io.PrintWriter import java.lang.ref.WeakReference -import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** Access point into multi-user switching logic. */ +@Deprecated("Use UserInteractor or GuestUserInteractor instead.") +@SysUISingleton +class UserSwitcherController +@Inject +constructor( + @Application private val applicationContext: Context, + private val userInteractorLazy: Lazy<UserInteractor>, + private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, + private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, + private val activityStarter: ActivityStarter, +) { -/** Defines interface for a class that provides user switching functionality and state. */ -interface UserSwitcherController : Dumpable { + /** Defines interface for classes that can be called back when the user is switched. */ + fun interface UserSwitchCallback { + /** Notifies that the user has switched. */ + fun onUserSwitched() + } + + private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } + private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } + private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } + + private val callbackCompatMap = mutableMapOf<UserSwitchCallback, UserInteractor.UserCallback>() /** The current list of [UserRecord]. */ val users: ArrayList<UserRecord> + get() = userInteractor.userRecords.value /** Whether the user switcher experience should use the simple experience. */ val isSimpleUserSwitcher: Boolean - - /** Require a view for jank detection */ - fun init(view: View) + get() = userInteractor.isSimpleUserSwitcher /** The [UserRecord] of the current user or `null` when none. */ val currentUserRecord: UserRecord? + get() = userInteractor.selectedUserRecord.value /** The name of the current user of the device or `null`, when none is selected. */ val currentUserName: String? + get() = + currentUserRecord?.let { + LegacyUserUiHelper.getUserRecordName( + context = applicationContext, + record = it, + isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, + isGuestUserResetting = userInteractor.isGuestUserResetting, + ) + } /** * Notifies that a user has been selected. @@ -55,34 +94,40 @@ interface UserSwitcherController : Dumpable { * @param userId The ID of the user to switch to. * @param dialogShower An optional [DialogShower] in case we need to show dialogs. */ - fun onUserSelected(userId: Int, dialogShower: DialogShower?) - - /** Whether it is allowed to add users while the device is locked. */ - val isAddUsersFromLockScreenEnabled: Flow<Boolean> + fun onUserSelected(userId: Int, dialogShower: DialogShower?) { + userInteractor.selectUser(userId, dialogShower) + } /** Whether the guest user is configured to always be present on the device. */ val isGuestUserAutoCreated: Boolean + get() = userInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean - - /** Creates and switches to the guest user. */ - fun createAndSwitchToGuestUser(dialogShower: DialogShower?) - - /** Shows the add user dialog. */ - fun showAddUserDialog(dialogShower: DialogShower?) - - /** Starts an activity to add a supervised user to the device. */ - fun startSupervisedUserActivity() - - /** Notifies when the display density or font scale has changed. */ - fun onDensityOrFontScaleChanged() + get() = userInteractor.isGuestUserResetting /** Registers an adapter to notify when the users change. */ - fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) + fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { + userInteractor.addCallback( + object : UserInteractor.UserCallback { + override fun isEvictable(): Boolean { + return adapter.get() == null + } + + override fun onUserStateChanged() { + adapter.get()?.notifyDataSetChanged() + } + } + ) + } /** Notifies the item for a user has been clicked. */ - fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?) + fun onUserListItemClicked( + record: UserRecord, + dialogShower: DialogShower?, + ) { + userInteractor.onRecordSelected(record, dialogShower) + } /** * Removes guest user and switches to target user. The guest must be the current user and its id @@ -103,7 +148,12 @@ interface UserSwitcherController : Dumpable { * @param targetUserId id of the user to switch to after guest is removed. If * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user. */ - fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int) + fun removeGuestUser(guestUserId: Int, targetUserId: Int) { + userInteractor.removeGuestUser( + guestUserId = guestUserId, + targetUserId = targetUserId, + ) + } /** * Exits guest user and switches to previous non-guest user. The guest must be the current user. @@ -114,43 +164,58 @@ interface UserSwitcherController : Dumpable { * @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest * only if its ephemeral, else keep guest */ - fun exitGuestUser( - @UserIdInt guestUserId: Int, - @UserIdInt targetUserId: Int, - forceRemoveGuestOnExit: Boolean - ) + fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) { + userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) + } /** * Guarantee guest is present only if the device is provisioned. Otherwise, create a content * observer to wait until the device is provisioned, then schedule the guest creation. */ - fun schedulePostBootGuestCreation() + fun schedulePostBootGuestCreation() { + guestUserInteractor.onDeviceBootCompleted() + } /** Whether keyguard is showing. */ val isKeyguardShowing: Boolean + get() = keyguardInteractor.isKeyguardShowing() /** Starts an activity with the given [Intent]. */ - fun startActivity(intent: Intent) + fun startActivity(intent: Intent) { + activityStarter.startActivity(intent, /* dismissShade= */ true) + } /** * Refreshes users from UserManager. * * The pictures are only loaded if they have not been loaded yet. - * - * @param forcePictureLoadForId forces the picture of the given user to be reloaded. */ - fun refreshUsers(forcePictureLoadForId: Int) + fun refreshUsers() { + userInteractor.refreshUsers() + } /** Adds a subscriber to when user switches. */ - fun addUserSwitchCallback(callback: UserSwitchCallback) + fun addUserSwitchCallback(callback: UserSwitchCallback) { + val interactorCallback = + object : UserInteractor.UserCallback { + override fun onUserStateChanged() { + callback.onUserSwitched() + } + } + callbackCompatMap[callback] = interactorCallback + userInteractor.addCallback(interactorCallback) + } /** Removes a previously-added subscriber. */ - fun removeUserSwitchCallback(callback: UserSwitchCallback) + fun removeUserSwitchCallback(callback: UserSwitchCallback) { + val interactorCallback = callbackCompatMap.remove(callback) + if (interactorCallback != null) { + userInteractor.removeCallback(interactorCallback) + } + } - /** Defines interface for classes that can be called back when the user is switched. */ - fun interface UserSwitchCallback { - /** Notifies that the user has switched. */ - fun onUserSwitched() + fun dump(pw: PrintWriter, args: Array<out String>) { + userInteractor.dump(pw) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt deleted file mode 100644 index 935fc7f10198..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ /dev/null @@ -1,299 +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.systemui.statusbar.policy - -import android.content.Context -import android.content.Intent -import android.view.View -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.domain.interactor.GuestUserInteractor -import com.android.systemui.user.domain.interactor.UserInteractor -import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper -import dagger.Lazy -import java.io.PrintWriter -import java.lang.ref.WeakReference -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow - -/** Implementation of [UserSwitcherController]. */ -@SysUISingleton -class UserSwitcherControllerImpl -@Inject -constructor( - @Application private val applicationContext: Context, - flags: FeatureFlags, - @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, - private val userInteractorLazy: Lazy<UserInteractor>, - private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, - private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, - private val activityStarter: ActivityStarter, -) : UserSwitcherController { - - private val useInteractor: Boolean = - flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && - !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val _oldImpl: UserSwitcherControllerOldImpl - get() = oldImpl.get() - private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } - private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } - private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } - - private val callbackCompatMap = - mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>() - - private fun notSupported(): Nothing { - error("Not supported in the new implementation!") - } - - override val users: ArrayList<UserRecord> - get() = - if (useInteractor) { - userInteractor.userRecords.value - } else { - _oldImpl.users - } - - override val isSimpleUserSwitcher: Boolean - get() = - if (useInteractor) { - userInteractor.isSimpleUserSwitcher - } else { - _oldImpl.isSimpleUserSwitcher - } - - override fun init(view: View) { - if (!useInteractor) { - _oldImpl.init(view) - } - } - - override val currentUserRecord: UserRecord? - get() = - if (useInteractor) { - userInteractor.selectedUserRecord.value - } else { - _oldImpl.currentUserRecord - } - - override val currentUserName: String? - get() = - if (useInteractor) { - currentUserRecord?.let { - LegacyUserUiHelper.getUserRecordName( - context = applicationContext, - record = it, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, - ) - } - } else { - _oldImpl.currentUserName - } - - override fun onUserSelected( - userId: Int, - dialogShower: UserSwitchDialogController.DialogShower? - ) { - if (useInteractor) { - userInteractor.selectUser(userId, dialogShower) - } else { - _oldImpl.onUserSelected(userId, dialogShower) - } - } - - override val isAddUsersFromLockScreenEnabled: Flow<Boolean> - get() = - if (useInteractor) { - notSupported() - } else { - _oldImpl.isAddUsersFromLockScreenEnabled - } - - override val isGuestUserAutoCreated: Boolean - get() = - if (useInteractor) { - userInteractor.isGuestUserAutoCreated - } else { - _oldImpl.isGuestUserAutoCreated - } - - override val isGuestUserResetting: Boolean - get() = - if (useInteractor) { - userInteractor.isGuestUserResetting - } else { - _oldImpl.isGuestUserResetting - } - - override fun createAndSwitchToGuestUser( - dialogShower: UserSwitchDialogController.DialogShower?, - ) { - if (useInteractor) { - notSupported() - } else { - _oldImpl.createAndSwitchToGuestUser(dialogShower) - } - } - - override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (useInteractor) { - notSupported() - } else { - _oldImpl.showAddUserDialog(dialogShower) - } - } - - override fun startSupervisedUserActivity() { - if (useInteractor) { - notSupported() - } else { - _oldImpl.startSupervisedUserActivity() - } - } - - override fun onDensityOrFontScaleChanged() { - if (!useInteractor) { - _oldImpl.onDensityOrFontScaleChanged() - } - } - - override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { - if (useInteractor) { - userInteractor.addCallback( - object : UserInteractor.UserCallback { - override fun isEvictable(): Boolean { - return adapter.get() == null - } - - override fun onUserStateChanged() { - adapter.get()?.notifyDataSetChanged() - } - } - ) - } else { - _oldImpl.addAdapter(adapter) - } - } - - override fun onUserListItemClicked( - record: UserRecord, - dialogShower: UserSwitchDialogController.DialogShower?, - ) { - if (useInteractor) { - userInteractor.onRecordSelected(record, dialogShower) - } else { - _oldImpl.onUserListItemClicked(record, dialogShower) - } - } - - override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (useInteractor) { - userInteractor.removeGuestUser( - guestUserId = guestUserId, - targetUserId = targetUserId, - ) - } else { - _oldImpl.removeGuestUser(guestUserId, targetUserId) - } - } - - override fun exitGuestUser( - guestUserId: Int, - targetUserId: Int, - forceRemoveGuestOnExit: Boolean - ) { - if (useInteractor) { - userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) - } else { - _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) - } - } - - override fun schedulePostBootGuestCreation() { - if (useInteractor) { - guestUserInteractor.onDeviceBootCompleted() - } else { - _oldImpl.schedulePostBootGuestCreation() - } - } - - override val isKeyguardShowing: Boolean - get() = - if (useInteractor) { - keyguardInteractor.isKeyguardShowing() - } else { - _oldImpl.isKeyguardShowing - } - - override fun startActivity(intent: Intent) { - if (useInteractor) { - activityStarter.startActivity(intent, /* dismissShade= */ true) - } else { - _oldImpl.startActivity(intent) - } - } - - override fun refreshUsers(forcePictureLoadForId: Int) { - if (useInteractor) { - userInteractor.refreshUsers() - } else { - _oldImpl.refreshUsers(forcePictureLoadForId) - } - } - - override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (useInteractor) { - val interactorCallback = - object : UserInteractor.UserCallback { - override fun onUserStateChanged() { - callback.onUserSwitched() - } - } - callbackCompatMap[callback] = interactorCallback - userInteractor.addCallback(interactorCallback) - } else { - _oldImpl.addUserSwitchCallback(callback) - } - } - - override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (useInteractor) { - val interactorCallback = callbackCompatMap.remove(callback) - if (interactorCallback != null) { - userInteractor.removeCallback(interactorCallback) - } - } else { - _oldImpl.removeUserSwitchCallback(callback) - } - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - if (useInteractor) { - userInteractor.dump(pw) - } else { - _oldImpl.dump(pw, args) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java deleted file mode 100644 index c294c370a601..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ /dev/null @@ -1,1063 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.statusbar.policy; - -import static android.os.UserManager.SWITCHABILITY_STATUS_OK; - -import android.annotation.UserIdInt; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.IActivityManager; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.UserInfo; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.os.Handler; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.telephony.TelephonyCallback; -import android.text.TextUtils; -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.view.View; -import android.view.WindowManagerGlobal; -import android.widget.Toast; - -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.util.LatencyTracker; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.settingslib.users.UserCreatingDialog; -import com.android.systemui.GuestResetOrExitSessionReceiver; -import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.SystemUISecondaryUserService; -import com.android.systemui.animation.DialogCuj; -import com.android.systemui.animation.DialogLaunchAnimator; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.LongRunning; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.qs.QSUserSwitcherEvent; -import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.data.source.UserRecord; -import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; -import com.android.systemui.user.shared.model.UserActionModel; -import com.android.systemui.user.ui.dialog.AddUserDialog; -import com.android.systemui.user.ui.dialog.ExitGuestDialog; -import com.android.systemui.util.settings.GlobalSettings; -import com.android.systemui.util.settings.SecureSettings; - -import java.io.PrintWriter; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -import javax.inject.Inject; - -import kotlinx.coroutines.flow.Flow; -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlowKt; - -/** - * Old implementation. Keeps a list of all users on the device for user switching. - * - * @deprecated This is the old implementation. Please depend on {@link UserSwitcherController} - * instead. - */ -@Deprecated -@SysUISingleton -public class UserSwitcherControllerOldImpl implements UserSwitcherController { - - private static final String TAG = "UserSwitcherController"; - private static final boolean DEBUG = false; - private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING = - "lockscreenSimpleUserSwitcher"; - private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000; - - private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; - private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000L; - - private static final String INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"; - private static final String INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"; - - protected final Context mContext; - protected final UserTracker mUserTracker; - protected final UserManager mUserManager; - private final ContentObserver mSettingsObserver; - private final ArrayList<WeakReference<BaseUserSwitcherAdapter>> mAdapters = new ArrayList<>(); - @VisibleForTesting - final GuestResumeSessionReceiver mGuestResumeSessionReceiver; - @VisibleForTesting - final GuestResetOrExitSessionReceiver mGuestResetOrExitSessionReceiver; - private final KeyguardStateController mKeyguardStateController; - private final DeviceProvisionedController mDeviceProvisionedController; - private final DevicePolicyManager mDevicePolicyManager; - protected final Handler mHandler; - private final ActivityStarter mActivityStarter; - private final BroadcastDispatcher mBroadcastDispatcher; - private final BroadcastSender mBroadcastSender; - private final TelephonyListenerManager mTelephonyListenerManager; - private final InteractionJankMonitor mInteractionJankMonitor; - private final LatencyTracker mLatencyTracker; - private final DialogLaunchAnimator mDialogLaunchAnimator; - - private ArrayList<UserRecord> mUsers = new ArrayList<>(); - @VisibleForTesting - AlertDialog mExitGuestDialog; - @VisibleForTesting - Dialog mAddUserDialog; - private int mLastNonGuestUser = UserHandle.USER_SYSTEM; - private boolean mSimpleUserSwitcher; - // When false, there won't be any visual affordance to add a new user from the keyguard even if - // the user is unlocked - private final MutableStateFlow<Boolean> mAddUsersFromLockScreen = - StateFlowKt.MutableStateFlow(false); - private boolean mUserSwitcherEnabled; - @VisibleForTesting - boolean mPauseRefreshUsers; - private int mSecondaryUser = UserHandle.USER_NULL; - private Intent mSecondaryUserServiceIntent; - private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2); - private final UiEventLogger mUiEventLogger; - private final IActivityManager mActivityManager; - private final Executor mBgExecutor; - private final Executor mUiExecutor; - private final Executor mLongRunningExecutor; - private final boolean mGuestUserAutoCreated; - private final AtomicBoolean mGuestIsResetting; - private final AtomicBoolean mGuestCreationScheduled; - private FalsingManager mFalsingManager; - @Nullable - private View mView; - private String mCreateSupervisedUserPackage; - private GlobalSettings mGlobalSettings; - private List<UserSwitchCallback> mUserSwitchCallbacks = - Collections.synchronizedList(new ArrayList<>()); - - @Inject - public UserSwitcherControllerOldImpl( - Context context, - IActivityManager activityManager, - UserManager userManager, - UserTracker userTracker, - KeyguardStateController keyguardStateController, - DeviceProvisionedController deviceProvisionedController, - DevicePolicyManager devicePolicyManager, - @Main Handler handler, - ActivityStarter activityStarter, - BroadcastDispatcher broadcastDispatcher, - BroadcastSender broadcastSender, - UiEventLogger uiEventLogger, - FalsingManager falsingManager, - TelephonyListenerManager telephonyListenerManager, - SecureSettings secureSettings, - GlobalSettings globalSettings, - @Background Executor bgExecutor, - @LongRunning Executor longRunningExecutor, - @Main Executor uiExecutor, - InteractionJankMonitor interactionJankMonitor, - LatencyTracker latencyTracker, - DumpManager dumpManager, - DialogLaunchAnimator dialogLaunchAnimator, - GuestResumeSessionReceiver guestResumeSessionReceiver, - GuestResetOrExitSessionReceiver guestResetOrExitSessionReceiver) { - mContext = context; - mActivityManager = activityManager; - mUserTracker = userTracker; - mBroadcastDispatcher = broadcastDispatcher; - mBroadcastSender = broadcastSender; - mTelephonyListenerManager = telephonyListenerManager; - mUiEventLogger = uiEventLogger; - mFalsingManager = falsingManager; - mInteractionJankMonitor = interactionJankMonitor; - mLatencyTracker = latencyTracker; - mGlobalSettings = globalSettings; - mGuestResumeSessionReceiver = guestResumeSessionReceiver; - mGuestResetOrExitSessionReceiver = guestResetOrExitSessionReceiver; - mBgExecutor = bgExecutor; - mLongRunningExecutor = longRunningExecutor; - mUiExecutor = uiExecutor; - mGuestResumeSessionReceiver.register(); - mGuestResetOrExitSessionReceiver.register(); - mGuestUserAutoCreated = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_guestUserAutoCreated); - mGuestIsResetting = new AtomicBoolean(); - mGuestCreationScheduled = new AtomicBoolean(); - mKeyguardStateController = keyguardStateController; - mDeviceProvisionedController = deviceProvisionedController; - mDevicePolicyManager = devicePolicyManager; - mHandler = handler; - mActivityStarter = activityStarter; - mUserManager = userManager; - mDialogLaunchAnimator = dialogLaunchAnimator; - - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_ADDED); - filter.addAction(Intent.ACTION_USER_REMOVED); - filter.addAction(Intent.ACTION_USER_INFO_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); - filter.addAction(Intent.ACTION_USER_STOPPED); - filter.addAction(Intent.ACTION_USER_UNLOCKED); - mBroadcastDispatcher.registerReceiver( - mReceiver, filter, null /* executor */, - UserHandle.SYSTEM, Context.RECEIVER_EXPORTED, null /* permission */); - - mSimpleUserSwitcher = shouldUseSimpleUserSwitcher(); - - mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class); - - filter = new IntentFilter(); - mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter, - PERMISSION_SELF, null /* scheduler */, - Context.RECEIVER_EXPORTED_UNAUDITED); - - mSettingsObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - mSimpleUserSwitcher = shouldUseSimpleUserSwitcher(); - mAddUsersFromLockScreen.setValue( - mGlobalSettings.getIntForUser( - Settings.Global.ADD_USERS_WHEN_LOCKED, - 0, - UserHandle.USER_SYSTEM) != 0); - mUserSwitcherEnabled = mGlobalSettings.getIntForUser( - Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0; - refreshUsers(UserHandle.USER_NULL); - }; - }; - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.USER_SWITCHER_ENABLED), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor( - Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED), - true, mSettingsObserver); - // Fetch initial values. - mSettingsObserver.onChange(false); - - keyguardStateController.addCallback(mCallback); - listenForCallState(); - - mCreateSupervisedUserPackage = mContext.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage); - - dumpManager.registerDumpable(getClass().getSimpleName(), this); - - refreshUsers(UserHandle.USER_NULL); - } - - @Override - @SuppressWarnings("unchecked") - public void refreshUsers(int forcePictureLoadForId) { - if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId + ")"); - if (forcePictureLoadForId != UserHandle.USER_NULL) { - mForcePictureLoadForUserId.put(forcePictureLoadForId, true); - } - - if (mPauseRefreshUsers) { - return; - } - - boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL); - SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size()); - final int userCount = mUsers.size(); - for (int i = 0; i < userCount; i++) { - UserRecord r = mUsers.get(i); - if (r == null || r.picture == null || r.info == null || forceAllUsers - || mForcePictureLoadForUserId.get(r.info.id)) { - continue; - } - bitmaps.put(r.info.id, r.picture); - } - mForcePictureLoadForUserId.clear(); - - mBgExecutor.execute(() -> { - List<UserInfo> infos = mUserManager.getAliveUsers(); - if (infos == null) { - return; - } - ArrayList<UserRecord> records = new ArrayList<>(infos.size()); - int currentId = mUserTracker.getUserId(); - // Check user switchability of the foreground user since SystemUI is running in - // User 0 - boolean canSwitchUsers = mUserManager.getUserSwitchability( - UserHandle.of(mUserTracker.getUserId())) == SWITCHABILITY_STATUS_OK; - UserRecord guestRecord = null; - - for (UserInfo info : infos) { - boolean isCurrent = currentId == info.id; - if (!mUserSwitcherEnabled && !info.isPrimary()) { - continue; - } - - if (info.isEnabled()) { - if (info.isGuest()) { - // Tapping guest icon triggers remove and a user switch therefore - // the icon shouldn't be enabled even if the user is current - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - mUserManager, - null /* picture */, - info, - isCurrent, - canSwitchUsers); - } else if (info.supportsSwitchToByUser()) { - records.add( - LegacyUserDataHelper.createRecord( - mContext, - mUserManager, - bitmaps.get(info.id), - info, - isCurrent, - canSwitchUsers)); - } - } - } - - if (guestRecord == null) { - if (mGuestUserAutoCreated) { - // If mGuestIsResetting=true, the switch should be disabled since - // we will just use it as an indicator for "Resetting guest...". - // Otherwise, default to canSwitchUsers. - boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ENTER_GUEST_MODE, - false /* isRestricted */, - isSwitchToGuestEnabled); - records.add(guestRecord); - } else if (canCreateGuest(guestRecord != null)) { - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ENTER_GUEST_MODE, - false /* isRestricted */, - canSwitchUsers); - records.add(guestRecord); - } - } else { - records.add(guestRecord); - } - - if (canCreateUser()) { - final UserRecord userRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ADD_USER, - createIsRestricted(), - canSwitchUsers); - records.add(userRecord); - } - - if (canCreateSupervisedUser()) { - final UserRecord userRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ADD_SUPERVISED_USER, - createIsRestricted(), - canSwitchUsers); - records.add(userRecord); - } - - if (canManageUsers()) { - records.add(LegacyUserDataHelper.createRecord( - mContext, - KeyguardUpdateMonitor.getCurrentUser(), - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - /* isRestricted= */ false, - /* isSwitchToEnabled= */ true - )); - } - - mUiExecutor.execute(() -> { - if (records != null) { - mUsers = records; - notifyAdapters(); - } - }); - }); - } - - private boolean systemCanCreateUsers() { - return !mUserManager.hasBaseUserRestriction( - UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM); - } - - private boolean currentUserCanCreateUsers() { - UserInfo currentUser = mUserTracker.getUserInfo(); - return currentUser != null - && (currentUser.isAdmin() || mUserTracker.getUserId() == UserHandle.USER_SYSTEM) - && systemCanCreateUsers(); - } - - private boolean anyoneCanCreateUsers() { - return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue(); - } - - @VisibleForTesting - boolean canCreateGuest(boolean hasExistingGuest) { - return mUserSwitcherEnabled - && (currentUserCanCreateUsers() || anyoneCanCreateUsers()) - && !hasExistingGuest; - } - - @VisibleForTesting - boolean canCreateUser() { - return mUserSwitcherEnabled - && (currentUserCanCreateUsers() || anyoneCanCreateUsers()) - && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY); - } - - @VisibleForTesting - boolean canManageUsers() { - UserInfo currentUser = mUserTracker.getUserInfo(); - return mUserSwitcherEnabled - && ((currentUser != null && currentUser.isAdmin()) - || mAddUsersFromLockScreen.getValue()); - } - - private boolean createIsRestricted() { - return !mAddUsersFromLockScreen.getValue(); - } - - @VisibleForTesting - boolean canCreateSupervisedUser() { - return !TextUtils.isEmpty(mCreateSupervisedUserPackage) && canCreateUser(); - } - - private void pauseRefreshUsers() { - if (!mPauseRefreshUsers) { - mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS); - mPauseRefreshUsers = true; - } - } - - private void notifyAdapters() { - for (int i = mAdapters.size() - 1; i >= 0; i--) { - BaseUserSwitcherAdapter adapter = mAdapters.get(i).get(); - if (adapter != null) { - adapter.notifyDataSetChanged(); - } else { - mAdapters.remove(i); - } - } - } - - @Override - public boolean isSimpleUserSwitcher() { - return mSimpleUserSwitcher; - } - - /** - * Returns whether the current user is a system user. - */ - @VisibleForTesting - boolean isSystemUser() { - return mUserTracker.getUserId() == UserHandle.USER_SYSTEM; - } - - @Override - public @Nullable UserRecord getCurrentUserRecord() { - for (int i = 0; i < mUsers.size(); ++i) { - UserRecord userRecord = mUsers.get(i); - if (userRecord.isCurrent) { - return userRecord; - } - } - return null; - } - - @Override - public void onUserSelected(int userId, @Nullable DialogShower dialogShower) { - UserRecord userRecord = mUsers.stream() - .filter(x -> x.resolveId() == userId) - .findFirst() - .orElse(null); - if (userRecord == null) { - return; - } - - onUserListItemClicked(userRecord, dialogShower); - } - - @Override - public Flow<Boolean> isAddUsersFromLockScreenEnabled() { - return mAddUsersFromLockScreen; - } - - @Override - public boolean isGuestUserAutoCreated() { - return mGuestUserAutoCreated; - } - - @Override - public boolean isGuestUserResetting() { - return mGuestIsResetting.get(); - } - - @Override - public void onUserListItemClicked(UserRecord record, DialogShower dialogShower) { - if (record.isGuest && record.info == null) { - createAndSwitchToGuestUser(dialogShower); - } else if (record.isAddUser) { - showAddUserDialog(dialogShower); - } else if (record.isAddSupervisedUser) { - startSupervisedUserActivity(); - } else if (record.isManageUsers) { - startActivity(new Intent(Settings.ACTION_USER_SETTINGS)); - } else { - onUserListItemClicked(record.info.id, record, dialogShower); - } - } - - private void onUserListItemClicked(int id, UserRecord record, DialogShower dialogShower) { - int currUserId = mUserTracker.getUserId(); - // If switching from guest and guest is ephemeral, then follow the flow - // of showExitGuestDialog to remove current guest, - // and switch to selected user - UserInfo currUserInfo = mUserTracker.getUserInfo(); - if (currUserId == id) { - if (record.isGuest) { - showExitGuestDialog(id, currUserInfo.isEphemeral(), dialogShower); - } - return; - } - - if (currUserInfo != null && currUserInfo.isGuest()) { - showExitGuestDialog(currUserId, currUserInfo.isEphemeral(), - record.resolveId(), dialogShower); - return; - } - - if (dialogShower != null) { - // If we haven't morphed into another dialog, it means we have just switched users. - // Then, dismiss the dialog. - dialogShower.dismiss(); - } - switchToUserId(id); - } - - private void switchToUserId(int id) { - try { - if (mView != null) { - mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder - .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView) - .setTimeout(MULTI_USER_JOURNEY_TIMEOUT)); - } - mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH); - pauseRefreshUsers(); - mActivityManager.switchUser(id); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't switch user.", e); - } - } - - private void showExitGuestDialog(int id, boolean isGuestEphemeral, DialogShower dialogShower) { - int newId = UserHandle.USER_SYSTEM; - if (mLastNonGuestUser != UserHandle.USER_SYSTEM) { - UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); - if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) { - newId = info.id; - } - } - showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); - } - - private void showExitGuestDialog( - int id, - boolean isGuestEphemeral, - int targetId, - DialogShower dialogShower) { - if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { - mExitGuestDialog.cancel(); - } - mExitGuestDialog = new ExitGuestDialog( - mContext, - id, - isGuestEphemeral, - targetId, - mKeyguardStateController.isShowing(), - mFalsingManager, - mDialogLaunchAnimator, - this::exitGuestUser); - if (dialogShower != null) { - dialogShower.showDialog(mExitGuestDialog, new DialogCuj( - InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, - INTERACTION_JANK_EXIT_GUEST_MODE_TAG)); - } else { - mExitGuestDialog.show(); - } - } - - @Override - public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) { - createGuestAsync(guestId -> { - // guestId may be USER_NULL if we haven't reloaded the user list yet. - if (guestId != UserHandle.USER_NULL) { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD); - onUserListItemClicked(guestId, UserRecord.createForGuest(), dialogShower); - } - }); - } - - @Override - public void showAddUserDialog(@Nullable DialogShower dialogShower) { - if (mAddUserDialog != null && mAddUserDialog.isShowing()) { - mAddUserDialog.cancel(); - } - final UserInfo currentUser = mUserTracker.getUserInfo(); - mAddUserDialog = new AddUserDialog( - mContext, - currentUser.getUserHandle(), - mKeyguardStateController.isShowing(), - /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), - mFalsingManager, - mBroadcastSender, - mDialogLaunchAnimator); - if (dialogShower != null) { - dialogShower.showDialog(mAddUserDialog, - new DialogCuj( - InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, - INTERACTION_JANK_ADD_NEW_USER_TAG - )); - } else { - mAddUserDialog.show(); - } - } - - @Override - public void startSupervisedUserActivity() { - final Intent intent = new Intent() - .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(mCreateSupervisedUserPackage) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - mContext.startActivity(intent); - } - - private void listenForCallState() { - mTelephonyListenerManager.addCallStateListener(mPhoneStateListener); - } - - private final TelephonyCallback.CallStateListener mPhoneStateListener = - new TelephonyCallback.CallStateListener() { - private int mCallState; - - @Override - public void onCallStateChanged(int state) { - if (mCallState == state) return; - if (DEBUG) Log.v(TAG, "Call state changed: " + state); - mCallState = state; - refreshUsers(UserHandle.USER_NULL); - } - }; - - private BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) { - Log.v(TAG, "Broadcast: a=" + intent.getAction() - + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)); - } - - boolean unpauseRefreshUsers = false; - int forcePictureLoadForId = UserHandle.USER_NULL; - - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { - mExitGuestDialog.cancel(); - mExitGuestDialog = null; - } - - final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - final UserInfo userInfo = mUserManager.getUserInfo(currentId); - final int userCount = mUsers.size(); - for (int i = 0; i < userCount; i++) { - UserRecord record = mUsers.get(i); - if (record.info == null) continue; - boolean shouldBeCurrent = record.info.id == currentId; - if (record.isCurrent != shouldBeCurrent) { - mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent)); - } - if (shouldBeCurrent && !record.isGuest) { - mLastNonGuestUser = record.info.id; - } - if ((userInfo == null || !userInfo.isAdmin()) && record.isRestricted) { - // Immediately remove restricted records in case the AsyncTask is too slow. - mUsers.remove(i); - i--; - } - } - notifyUserSwitchCallbacks(); - notifyAdapters(); - - // Disconnect from the old secondary user's service - if (mSecondaryUser != UserHandle.USER_NULL) { - context.stopServiceAsUser(mSecondaryUserServiceIntent, - UserHandle.of(mSecondaryUser)); - mSecondaryUser = UserHandle.USER_NULL; - } - // Connect to the new secondary user's service (purely to ensure that a persistent - // SystemUI application is created for that user) - if (userInfo != null && userInfo.id != UserHandle.USER_SYSTEM) { - context.startServiceAsUser(mSecondaryUserServiceIntent, - UserHandle.of(userInfo.id)); - mSecondaryUser = userInfo.id; - } - unpauseRefreshUsers = true; - if (mGuestUserAutoCreated) { - // Guest user must be scheduled for creation AFTER switching to the target user. - // This avoids lock contention which will produce UX bugs on the keyguard - // (b/193933686). - // TODO(b/191067027): Move guest user recreation to system_server - guaranteeGuestPresent(); - } - } else if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) { - forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL); - } else if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { - // Unlocking the system user may require a refresh - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId != UserHandle.USER_SYSTEM) { - return; - } - } - refreshUsers(forcePictureLoadForId); - if (unpauseRefreshUsers) { - mUnpauseRefreshUsers.run(); - } - } - }; - - private final Runnable mUnpauseRefreshUsers = new Runnable() { - @Override - public void run() { - mHandler.removeCallbacks(this); - mPauseRefreshUsers = false; - refreshUsers(UserHandle.USER_NULL); - } - }; - - @Override - public void dump(PrintWriter pw, String[] args) { - pw.println("UserSwitcherController state:"); - pw.println(" mLastNonGuestUser=" + mLastNonGuestUser); - pw.print(" mUsers.size="); pw.println(mUsers.size()); - for (int i = 0; i < mUsers.size(); i++) { - final UserRecord u = mUsers.get(i); - pw.print(" "); pw.println(u.toString()); - } - pw.println("mSimpleUserSwitcher=" + mSimpleUserSwitcher); - pw.println("mGuestUserAutoCreated=" + mGuestUserAutoCreated); - } - - @Override - public String getCurrentUserName() { - if (mUsers.isEmpty()) return null; - UserRecord item = mUsers.stream().filter(x -> x.isCurrent).findFirst().orElse(null); - if (item == null || item.info == null) return null; - if (item.isGuest) return mContext.getString(com.android.internal.R.string.guest_name); - return item.info.name; - } - - @Override - public void onDensityOrFontScaleChanged() { - refreshUsers(UserHandle.USER_ALL); - } - - @Override - public void addAdapter(WeakReference<BaseUserSwitcherAdapter> adapter) { - mAdapters.add(adapter); - } - - @Override - public ArrayList<UserRecord> getUsers() { - return mUsers; - } - - @Override - public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) { - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser.id != guestUserId) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not current user (" + currentUser.id + ")"); - return; - } - if (!currentUser.isGuest()) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not a guest"); - return; - } - - boolean marked = mUserManager.markGuestForDeletion(currentUser.id); - if (!marked) { - Log.w(TAG, "Couldn't mark the guest for deletion for user " + guestUserId); - return; - } - - if (targetUserId == UserHandle.USER_NULL) { - // Create a new guest in the foreground, and then immediately switch to it - createGuestAsync(newGuestId -> { - if (newGuestId == UserHandle.USER_NULL) { - Log.e(TAG, "Could not create new guest, switching back to system user"); - switchToUserId(UserHandle.USER_SYSTEM); - mUserManager.removeUser(currentUser.id); - try { - WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't remove guest because ActivityManager " - + "or WindowManager is dead"); - } - return; - } - switchToUserId(newGuestId); - mUserManager.removeUser(currentUser.id); - }); - } else { - if (mGuestUserAutoCreated) { - mGuestIsResetting.set(true); - } - switchToUserId(targetUserId); - mUserManager.removeUser(currentUser.id); - } - } - - @Override - public void exitGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId, - boolean forceRemoveGuestOnExit) { - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser.id != guestUserId) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not current user (" + currentUser.id + ")"); - return; - } - if (!currentUser.isGuest()) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not a guest"); - return; - } - - int newUserId = UserHandle.USER_SYSTEM; - if (targetUserId == UserHandle.USER_NULL) { - // when target user is not specified switch to last non guest user - if (mLastNonGuestUser != UserHandle.USER_SYSTEM) { - UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); - if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) { - newUserId = info.id; - } - } - } else { - newUserId = targetUserId; - } - - if (currentUser.isEphemeral() || forceRemoveGuestOnExit) { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); - removeGuestUser(currentUser.id, newUserId); - } else { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); - switchToUserId(newUserId); - } - } - - private void scheduleGuestCreation() { - if (!mGuestCreationScheduled.compareAndSet(false, true)) { - return; - } - - mLongRunningExecutor.execute(() -> { - int newGuestId = createGuest(); - mGuestCreationScheduled.set(false); - mGuestIsResetting.set(false); - if (newGuestId == UserHandle.USER_NULL) { - Log.w(TAG, "Could not create new guest while exiting existing guest"); - // Refresh users so that we still display "Guest" if - // config_guestUserAutoCreated=true - refreshUsers(UserHandle.USER_NULL); - } - }); - - } - - @Override - public void schedulePostBootGuestCreation() { - if (isDeviceAllowedToAddGuest()) { - guaranteeGuestPresent(); - } else { - mDeviceProvisionedController.addCallback(mGuaranteeGuestPresentAfterProvisioned); - } - } - - private boolean isDeviceAllowedToAddGuest() { - return mDeviceProvisionedController.isDeviceProvisioned() - && !mDevicePolicyManager.isDeviceManaged(); - } - - /** - * If there is no guest on the device, schedule creation of a new guest user in the background. - */ - private void guaranteeGuestPresent() { - if (isDeviceAllowedToAddGuest() && mUserManager.findCurrentGuestUser() == null) { - scheduleGuestCreation(); - } - } - - private void createGuestAsync(Consumer<Integer> callback) { - final Dialog guestCreationProgressDialog = - new UserCreatingDialog(mContext, /* isGuest= */true); - guestCreationProgressDialog.show(); - - // userManager.createGuest will block the thread so post is needed for the dialog to show - mBgExecutor.execute(() -> { - final int guestId = createGuest(); - mUiExecutor.execute(() -> { - guestCreationProgressDialog.dismiss(); - if (guestId == UserHandle.USER_NULL) { - Toast.makeText(mContext, - com.android.settingslib.R.string.add_guest_failed, - Toast.LENGTH_SHORT).show(); - } - callback.accept(guestId); - }); - }); - } - - /** - * Creates a guest user and return its multi-user user ID. - * - * This method does not check if a guest already exists before it makes a call to - * {@link UserManager} to create a new one. - * - * @return The multi-user user ID of the newly created guest user, or - * {@link UserHandle#USER_NULL} if the guest couldn't be created. - */ - private @UserIdInt int createGuest() { - UserInfo guest; - try { - guest = mUserManager.createGuest(mContext); - } catch (UserManager.UserOperationException e) { - Log.e(TAG, "Couldn't create guest user", e); - return UserHandle.USER_NULL; - } - if (guest == null) { - Log.e(TAG, "Couldn't create guest, most likely because there already exists one"); - return UserHandle.USER_NULL; - } - return guest.id; - } - - @Override - public void init(View view) { - mView = view; - } - - @Override - public boolean isKeyguardShowing() { - return mKeyguardStateController.isShowing(); - } - - private boolean shouldUseSimpleUserSwitcher() { - int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; - return mGlobalSettings.getIntForUser(SIMPLE_USER_SWITCHER_GLOBAL_SETTING, - defaultSimpleUserSwitcher, UserHandle.USER_SYSTEM) != 0; - } - - @Override - public void startActivity(Intent intent) { - mActivityStarter.startActivity(intent, /* dismissShade= */ true); - } - - @Override - public void addUserSwitchCallback(UserSwitchCallback callback) { - mUserSwitchCallbacks.add(callback); - } - - @Override - public void removeUserSwitchCallback(UserSwitchCallback callback) { - mUserSwitchCallbacks.remove(callback); - } - - /** - * Notify user switch callbacks that user has switched. - */ - private void notifyUserSwitchCallbacks() { - List<UserSwitchCallback> temp; - synchronized (mUserSwitchCallbacks) { - temp = new ArrayList<>(mUserSwitchCallbacks); - } - for (UserSwitchCallback callback : temp) { - callback.onUserSwitched(); - } - } - - private final KeyguardStateController.Callback mCallback = - new KeyguardStateController.Callback() { - @Override - public void onKeyguardShowingChanged() { - - // When Keyguard is going away, we don't need to update our items immediately - // which - // helps making the transition faster. - if (!mKeyguardStateController.isShowing()) { - mHandler.post(UserSwitcherControllerOldImpl.this::notifyAdapters); - } else { - notifyAdapters(); - } - } - }; - - private final DeviceProvisionedController.DeviceProvisionedListener - mGuaranteeGuestPresentAfterProvisioned = - new DeviceProvisionedController.DeviceProvisionedListener() { - @Override - public void onDeviceProvisionedChanged() { - if (isDeviceAllowedToAddGuest()) { - mBgExecutor.execute( - () -> mDeviceProvisionedController.removeCallback( - mGuaranteeGuestPresentAfterProvisioned)); - guaranteeGuestPresent(); - } - } - }; -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index b1b45b51d8e4..1b7353923ada 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -58,8 +58,6 @@ import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; -import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.statusbar.policy.UserSwitcherControllerImpl; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.statusbar.policy.WalletControllerImpl; import com.android.systemui.statusbar.policy.ZenModeController; @@ -198,8 +196,4 @@ public interface StatusBarPolicyModule { static DataSaverController provideDataSaverController(NetworkController networkController) { return networkController.getDataSaverController(); } - - /** Binds {@link UserSwitcherController} to its implementation. */ - @Binds - UserSwitcherController bindUserSwitcherController(UserSwitcherControllerImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt index 48df15c78ea5..93e78acc63fd 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import androidx.annotation.VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt index c7f0b7e0056e..f558fee776e6 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.content.Context import android.graphics.Canvas @@ -31,11 +31,21 @@ import android.view.View class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { internal val ripples = ArrayList<RippleAnimation>() + private val listeners = ArrayList<RipplesFinishedListener>() private val ripplePaint = Paint() private var isWarningLogged = false companion object { - const val TAG = "MultiRippleView" + private const val TAG = "MultiRippleView" + + interface RipplesFinishedListener { + /** Triggered when all the ripples finish running. */ + fun onRipplesFinish() + } + } + + fun addRipplesFinishedListener(listener: RipplesFinishedListener) { + listeners.add(listener) } override fun onDraw(canvas: Canvas?) { @@ -62,6 +72,10 @@ class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, a shouldInvalidate = shouldInvalidate || anim.isPlaying() } - if (shouldInvalidate) invalidate() + if (shouldInvalidate) { + invalidate() + } else { // Nothing is playing. + listeners.forEach { listener -> listener.onRipplesFinish() } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt index aca9e254e4c3..b2f8994ebf51 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.animation.Animator import android.animation.AnimatorListenerAdapter diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt index 88122544c7cd..ae73df201f8d 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt @@ -1,4 +1,4 @@ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt index d2f3a6a7bee1..a950d34513ee 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.PointF import android.graphics.RuntimeShader import android.util.MathUtils +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary /** * Shader class that renders an expanding ripple effect. The ripple contains three elements: @@ -31,7 +33,7 @@ import android.util.MathUtils * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. */ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) : - RuntimeShader(buildShader(rippleShape)) { + RuntimeShader(buildShader(rippleShape)) { /** Shapes that the [RippleShader] supports. */ enum class RippleShape { @@ -39,25 +41,30 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C ROUNDED_BOX, ELLIPSE } - //language=AGSL + // language=AGSL companion object { - private const val SHADER_UNIFORMS = """uniform vec2 in_center; - uniform vec2 in_size; - uniform float in_progress; - uniform float in_cornerRadius; - uniform float in_thickness; - uniform float in_time; - uniform float in_distort_radial; - uniform float in_distort_xy; - uniform float in_fadeSparkle; - uniform float in_fadeFill; - uniform float in_fadeRing; - uniform float in_blur; - uniform float in_pixelDensity; - layout(color) uniform vec4 in_color; - uniform float in_sparkle_strength;""" - - private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) { + private const val SHADER_UNIFORMS = + """ + uniform vec2 in_center; + uniform vec2 in_size; + uniform float in_progress; + uniform float in_cornerRadius; + uniform float in_thickness; + uniform float in_time; + uniform float in_distort_radial; + uniform float in_distort_xy; + uniform float in_fadeSparkle; + uniform float in_fadeFill; + uniform float in_fadeRing; + uniform float in_blur; + uniform float in_pixelDensity; + layout(color) uniform vec4 in_color; + uniform float in_sparkle_strength; + """ + + private const val SHADER_CIRCLE_MAIN = + """ + vec4 main(vec2 p) { vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); float radius = in_size.x * 0.5; float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); @@ -73,7 +80,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) { + private const val SHADER_ROUNDED_BOX_MAIN = + """ + vec4 main(vec2 p) { float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius, in_thickness), in_blur); float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius), @@ -89,7 +98,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val SHADER_ELLIPSE_MAIN = """vec4 main(vec2 p) { + private const val SHADER_ELLIPSE_MAIN = + """ + vec4 main(vec2 p) { vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur); @@ -105,22 +116,31 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + - SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF + + private const val CIRCLE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.CIRCLE_SDF + SHADER_CIRCLE_MAIN - private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS + - RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + - SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN - private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + - SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF + + private const val ROUNDED_BOX_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ROUNDED_BOX_SDF + + SHADER_ROUNDED_BOX_MAIN + private const val ELLIPSE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ELLIPSE_SDF + SHADER_ELLIPSE_MAIN private fun buildShader(rippleShape: RippleShape): String = - when (rippleShape) { - RippleShape.CIRCLE -> CIRCLE_SHADER - RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER - RippleShape.ELLIPSE -> ELLIPSE_SHADER - } + when (rippleShape) { + RippleShape.CIRCLE -> CIRCLE_SHADER + RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER + RippleShape.ELLIPSE -> ELLIPSE_SHADER + } private fun subProgress(start: Float, end: Float, progress: Float): Float { val min = Math.min(start, end) @@ -130,9 +150,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } } - /** - * Sets the center position of the ripple. - */ + /** Sets the center position of the ripple. */ fun setCenter(x: Float, y: Float) { setFloatUniform("in_center", x, y) } @@ -144,21 +162,21 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C maxSize.y = height } - /** - * Progress of the ripple. Float value between [0, 1]. - */ + /** Progress of the ripple. Float value between [0, 1]. */ var progress: Float = 0.0f set(value) { field = value setFloatUniform("in_progress", value) val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value) - setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg, - /* height= */ maxSize.y * curvedProg) + setFloatUniform( + "in_size", + /* width= */ maxSize.x * curvedProg, + /* height= */ maxSize.y * curvedProg + ) setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f) // radius should not exceed width and height values. - setFloatUniform("in_cornerRadius", - Math.min(maxSize.x, maxSize.y) * curvedProg) + setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg) setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value)) @@ -175,18 +193,14 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple)) } - /** - * Play time since the start of the effect. - */ + /** Play time since the start of the effect. */ var time: Float = 0.0f set(value) { field = value setFloatUniform("in_time", value) } - /** - * A hex value representing the ripple color, in the format of ARGB - */ + /** A hex value representing the ripple color, in the format of ARGB */ var color: Int = 0xffffff set(value) { field = value @@ -194,9 +208,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } /** - * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus - * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 - * it's opaque white and looks the most grainy. + * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with + * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's + * opaque white and looks the most grainy. */ var sparkleStrength: Float = 0.0f set(value) { @@ -204,9 +218,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C setFloatUniform("in_sparkle_strength", value) } - /** - * Distortion strength of the ripple. Expected value between[0, 1]. - */ + /** Distortion strength of the ripple. Expected value between[0, 1]. */ var distortionStrength: Float = 0.0f set(value) { field = value diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt index a6d79303962f..299469494295 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -26,7 +26,7 @@ import android.graphics.Paint import android.util.AttributeSet import android.view.View import androidx.core.graphics.ColorUtils -import com.android.systemui.ripple.RippleShader.RippleShape +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape /** * A generic expanding ripple effect. @@ -98,15 +98,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a rippleShader.time = now.toFloat() invalidate() } - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - onAnimationEnd?.run() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + onAnimationEnd?.run() + } } - }) + ) animator.start() } - /** Set the color to be used for the ripple. + /** + * Set the color to be used for the ripple. * * The alpha value of the color will be applied to the ripple. The alpha range is [0-100]. */ @@ -123,9 +126,7 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a rippleShader.rippleFill = rippleFill } - /** - * Set the intensity of the sparkles. - */ + /** Set the intensity of the sparkles. */ fun setSparkleStrength(strength: Float) { rippleShader.sparkleStrength = strength } @@ -143,20 +144,30 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a // active effect area. Values here should be kept in sync with the animation implementation // in the ripple shader. if (rippleShape == RippleShape.CIRCLE) { - val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxWidth + val maskRadius = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxWidth canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint) } else { - val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxWidth * 2 - val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxHeight * 2 + val maskWidth = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxWidth * 2 + val maskHeight = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxHeight * 2 canvas.drawRect( - /* left= */ centerX - maskWidth, - /* top= */ centerY - maskHeight, - /* right= */ centerX + maskWidth, - /* bottom= */ centerY + maskHeight, - ripplePaint) + /* left= */ centerX - maskWidth, + /* top= */ centerY - maskHeight, + /* right= */ centerX + maskWidth, + /* bottom= */ centerY + maskHeight, + ripplePaint + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt index 5e256c653992..8b2f46648fef 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.shaderutil /** Library class that contains 2D signed distance functions. */ class SdfShaderLibrary { - //language=AGSL + // language=AGSL companion object { - const val CIRCLE_SDF = """ + const val CIRCLE_SDF = + """ float sdCircle(vec2 p, float r) { return (length(p)-r) / r; } @@ -34,7 +35,8 @@ class SdfShaderLibrary { } """ - const val ROUNDED_BOX_SDF = """ + const val ROUNDED_BOX_SDF = + """ float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) { size *= 0.5; cornerRadius *= 0.5; @@ -58,7 +60,8 @@ class SdfShaderLibrary { // Used non-trigonometry parametrization and Halley's method (iterative) for root finding. // This is more expensive than the regular circle SDF, recommend to use the circle SDF if // possible. - const val ELLIPSE_SDF = """float sdEllipse(vec2 p, vec2 wh) { + const val ELLIPSE_SDF = + """float sdEllipse(vec2 p, vec2 wh) { wh *= 0.5; // symmetry @@ -98,7 +101,8 @@ class SdfShaderLibrary { } """ - const val SHADER_SDF_OPERATION_LIB = """ + const val SHADER_SDF_OPERATION_LIB = + """ float soften(float d, float blur) { float blurHalf = blur * 0.5; return smoothstep(-blurHalf, blurHalf, d); diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt new file mode 100644 index 000000000000..d78e0c153db8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt @@ -0,0 +1,148 @@ +/* + * 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.surfaceeffects.shaderutil + +/** A common utility functions that are used for computing shaders. */ +class ShaderUtilLibrary { + // language=AGSL + companion object { + const val SHADER_LIB = + """ + float triangleNoise(vec2 n) { + n = fract(n * vec2(5.3987, 5.4421)); + n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); + float xy = n.x * n.y; + // compute in [0..2[ and remap to [-1.0..1.0[ + return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; + } + + const float PI = 3.1415926535897932384626; + + float sparkles(vec2 uv, float t) { + float n = triangleNoise(uv); + float s = 0.0; + for (float i = 0; i < 4; i += 1) { + float l = i * 0.01; + float h = l + 0.1; + float o = smoothstep(n - l, h, n); + o *= abs(sin(PI * o * (t + 0.55 * i))); + s += o; + } + return s; + } + + vec2 distort(vec2 p, float time, float distort_amount_radial, + float distort_amount_xy) { + float angle = atan(p.y, p.x); + return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), + cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial + + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), + cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; + } + + // Return range [-1, 1]. + vec3 hash(vec3 p) { + p = fract(p * vec3(.3456, .1234, .9876)); + p += dot(p, p.yxz + 43.21); + p = (p.xxy + p.yxx) * p.zyx; + return (fract(sin(p) * 4567.1234567) - .5) * 2.; + } + + // Skew factors (non-uniform). + const float SKEW = 0.3333333; // 1/3 + const float UNSKEW = 0.1666667; // 1/6 + + // Return range roughly [-1,1]. + // It's because the hash function (that returns a random gradient vector) returns + // different magnitude of vectors. Noise doesn't have to be in the precise range thus + // skipped normalize. + float simplex3d(vec3 p) { + // Skew the input coordinate, so that we get squashed cubical grid + vec3 s = floor(p + (p.x + p.y + p.z) * SKEW); + + // Unskew back + vec3 u = s - (s.x + s.y + s.z) * UNSKEW; + + // Unskewed coordinate that is relative to p, to compute the noise contribution + // based on the distance. + vec3 c0 = p - u; + + // We have six simplices (in this case tetrahedron, since we are in 3D) that we + // could possibly in. + // Here, we are finding the correct tetrahedron (simplex shape), and traverse its + // four vertices (c0..3) when computing noise contribution. + // The way we find them is by comparing c0's x,y,z values. + // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in + // by comparing x and y values. i.e. x>y lower, x<y, upper triangle. + // Same applies in 3D. + // + // Below indicates the offsets (or offset directions) when c0=(x0,y0,z0) + // x0>y0>z0: (1,0,0), (1,1,0), (1,1,1) + // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1) + // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1) + // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1) + // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1) + // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1) + // + // The rule is: + // * For offset1, set 1 at the max component, otherwise 0. + // * For offset2, set 0 at the min component, otherwise 1. + // * For offset3, set 1 for all. + // + // Encode x0-y0, y0-z0, z0-x0 in a vec3 + vec3 en = c0 - c0.yzx; + // Each represents whether x0>y0, y0>z0, z0>x0 + en = step(vec3(0.), en); + // en.zxy encodes z0>x0, x0>y0, y0>x0 + vec3 offset1 = en * (1. - en.zxy); // find max + vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min) + vec3 offset3 = vec3(1.); + + vec3 c1 = c0 - offset1 + UNSKEW; + vec3 c2 = c0 - offset2 + UNSKEW * 2.; + vec3 c3 = c0 - offset3 + UNSKEW * 3.; + + // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution) + // + // First compute d^2, squared distance to the point. + vec4 w; // w = max(0, r^2 - d^2)) + w.x = dot(c0, c0); + w.y = dot(c1, c1); + w.z = dot(c2, c2); + w.w = dot(c3, c3); + + // Noise contribution should decay to zero before they cross the simplex boundary. + // Usually r^2 is 0.5 or 0.6; + // 0.5 ensures continuity but 0.6 increases the visual quality for the application + // where discontinuity isn't noticeable. + w = max(0.6 - w, 0.); + + // Noise contribution from each point. + vec4 nc; + nc.x = dot(hash(s), c0); + nc.y = dot(hash(s + offset1), c1); + nc.z = dot(hash(s + offset2), c2); + nc.w = dot(hash(s + offset3), c3); + + nc *= w*w*w*w; + + // Add all the noise contributions. + // Should multiply by the possible max contribution to adjust the range in [-1,1]. + return dot(vec4(32.), nc); + } + """ + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt new file mode 100644 index 000000000000..5ac3aad749fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -0,0 +1,67 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.graphics.BlendMode +import android.graphics.Color + +/** Turbulence noise animation configuration. */ +data class TurbulenceNoiseAnimationConfig( + /** The number of grids that is used to generate noise. */ + val gridCount: Float = DEFAULT_NOISE_GRID_COUNT, + + /** Multiplier for the noise luma matte. Increase this for brighter effects. */ + val luminosityMultiplier: Float = DEFAULT_LUMINOSITY_MULTIPLIER, + + /** + * Noise move speed variables. + * + * Its sign determines the direction; magnitude determines the speed. <ul> + * ``` + * <li> [noiseMoveSpeedX] positive: right to left; negative: left to right. + * <li> [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom. + * <li> [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it + * to add turbulence in place. + * ``` + * </ul> + */ + val noiseMoveSpeedX: Float = 0f, + val noiseMoveSpeedY: Float = 0f, + val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z, + + /** Color of the effect. */ + var color: Int = DEFAULT_COLOR, + /** Background color of the effect. */ + val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR, + val opacity: Int = DEFAULT_OPACITY, + val width: Float = 0f, + val height: Float = 0f, + val duration: Float = DEFAULT_NOISE_DURATION_IN_MILLIS, + val pixelDensity: Float = 1f, + val blendMode: BlendMode = DEFAULT_BLEND_MODE, + val onAnimationEnd: Runnable? = null +) { + companion object { + const val DEFAULT_NOISE_DURATION_IN_MILLIS = 7500F + const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f + const val DEFAULT_NOISE_GRID_COUNT = 1.2f + const val DEFAULT_NOISE_SPEED_Z = 0.3f + const val DEFAULT_OPACITY = 150 // full opacity is 255. + const val DEFAULT_COLOR = Color.WHITE + const val DEFAULT_BACKGROUND_COLOR = Color.BLACK + val DEFAULT_BLEND_MODE = BlendMode.SRC_OVER + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt new file mode 100644 index 000000000000..4c7e5f4c7093 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt @@ -0,0 +1,30 @@ +/* + * 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.surfaceeffects.turbulencenoise + +/** A controller that plays [TurbulenceNoiseView]. */ +class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) { + /** Updates the color of the noise. */ + fun updateNoiseColor(color: Int) { + turbulenceNoiseView.updateColor(color) + } + + // TODO: add cancel and/ or pause once design requirements become clear. + /** Plays [TurbulenceNoiseView] with the given config. */ + fun play(turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig) { + turbulenceNoiseView.play(turbulenceNoiseAnimationConfig) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt new file mode 100644 index 000000000000..19c114d2693c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -0,0 +1,121 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary +import java.lang.Float.max + +/** Shader that renders turbulence simplex noise, with no octave. */ +class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) { + // language=AGSL + companion object { + private const val UNIFORMS = + """ + uniform float in_gridNum; + uniform vec3 in_noiseMove; + uniform vec2 in_size; + uniform float in_aspectRatio; + uniform float in_opacity; + uniform float in_pixelDensity; + layout(color) uniform vec4 in_color; + layout(color) uniform vec4 in_backgroundColor; + """ + + private const val SHADER_LIB = + """ + float getLuminosity(vec3 c) { + return 0.3*c.r + 0.59*c.g + 0.11*c.b; + } + + vec3 maskLuminosity(vec3 dest, float lum) { + dest.rgb *= vec3(lum); + // Clip back into the legal range + dest = clamp(dest, vec3(0.), vec3(1.0)); + return dest; + } + """ + + private const val MAIN_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + float luma = simplex3d(noiseP) * in_opacity; + vec3 mask = maskLuminosity(in_color.rgb, luma); + vec3 color = in_backgroundColor.rgb + mask * 0.6; + + // Add dither with triangle distribution to avoid color banding. Ok to dither in the + // shader here as we are in gamma space. + float dither = triangleNoise(p * in_pixelDensity) / 255.; + + // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to + // multiply rgb with a to get the correct result. + color = (color + dither.rrr) * in_color.a; + return vec4(color, in_color.a); + } + """ + + private const val TURBULENCE_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SHADER_LIB + MAIN_SHADER + } + + /** Sets the number of grid for generating noise. */ + fun setGridCount(gridNumber: Float = 1.0f) { + setFloatUniform("in_gridNum", gridNumber) + } + + /** + * Sets the pixel density of the screen. + * + * Used it for noise dithering. + */ + fun setPixelDensity(pixelDensity: Float) { + setFloatUniform("in_pixelDensity", pixelDensity) + } + + /** Sets the noise color of the effect. */ + fun setColor(color: Int) { + setColorUniform("in_color", color) + } + + /** Sets the background color of the effect. */ + fun setBackgroundColor(color: Int) { + setColorUniform("in_backgroundColor", color) + } + + /** + * Sets the opacity to achieve fade in/ out of the animation. + * + * Expected value range is [1, 0]. + */ + fun setOpacity(opacity: Float) { + setFloatUniform("in_opacity", opacity) + } + + /** Sets the size of the shader. */ + fun setSize(width: Float, height: Float) { + setFloatUniform("in_size", width, height) + setFloatUniform("in_aspectRatio", width / max(height, 0.001f)) + } + + /** Sets noise move speed in x, y, and z direction. */ + fun setNoiseMove(x: Float, y: Float, z: Float) { + setFloatUniform("in_noiseMove", x, y, z) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt new file mode 100644 index 000000000000..8649d5924587 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt @@ -0,0 +1,123 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.graphics.ColorUtils +import java.util.Random +import kotlin.math.sin + +/** View that renders turbulence noise effect. */ +class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + companion object { + private const val MS_TO_SEC = 0.001f + private const val TWO_PI = Math.PI.toFloat() * 2f + } + + @VisibleForTesting val turbulenceNoiseShader = TurbulenceNoiseShader() + private val paint = Paint().apply { this.shader = turbulenceNoiseShader } + private val random = Random() + private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + private var config: TurbulenceNoiseAnimationConfig? = null + + val isPlaying: Boolean + get() = animator.isRunning + + init { + // Only visible during the animation. + visibility = INVISIBLE + } + + /** Updates the color during the animation. No-op if there's no animation playing. */ + fun updateColor(color: Int) { + config?.let { + it.color = color + applyConfig(it) + } + } + + override fun onDraw(canvas: Canvas?) { + if (canvas == null || !canvas.isHardwareAccelerated) { + // Drawing with the turbulence noise shader requires hardware acceleration, so skip + // if it's unsupported. + return + } + + canvas.drawPaint(paint) + } + + internal fun play(config: TurbulenceNoiseAnimationConfig) { + if (isPlaying) { + return // Ignore if the animation is playing. + } + visibility = VISIBLE + applyConfig(config) + + // Add random offset to avoid same patterned noise. + val offsetX = random.nextFloat() + val offsetY = random.nextFloat() + + animator.duration = config.duration.toLong() + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + // Remap [0,1] to [0, 2*PI] + val progress = TWO_PI * updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + offsetX + timeInSec * config.noiseMoveSpeedX, + offsetY + timeInSec * config.noiseMoveSpeedY, + timeInSec * config.noiseMoveSpeedZ + ) + + // Fade in and out the noise as the animation progress. + // TODO: replace it with a better curve + turbulenceNoiseShader.setOpacity(sin(TWO_PI - progress) * config.luminosityMultiplier) + + invalidate() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + visibility = INVISIBLE + config.onAnimationEnd?.run() + } + } + ) + animator.start() + } + + private fun applyConfig(config: TurbulenceNoiseAnimationConfig) { + this.config = config + with(turbulenceNoiseShader) { + setGridCount(config.gridCount) + setColor(ColorUtils.setAlphaComponent(config.color, config.opacity)) + setBackgroundColor(config.backgroundColor) + setSize(config.width, config.height) + setPixelDensity(config.pixelDensity) + } + paint.blendMode = config.blendMode + } +} diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index bf706735d531..e8a22ec4fbe7 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -46,6 +46,7 @@ import com.android.internal.R; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.CoreStartable; import com.android.systemui.SystemUIApplication; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.util.NotificationChannels; @@ -61,15 +62,24 @@ public class StorageNotification implements CoreStartable { private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME"; private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD"; private final Context mContext; + private final BroadcastDispatcher mBroadcastDispatcher; // TODO: delay some notifications to avoid bumpy fast operations - private NotificationManager mNotificationManager; - private StorageManager mStorageManager; + private final NotificationManager mNotificationManager; + private final StorageManager mStorageManager; @Inject - public StorageNotification(Context context) { + public StorageNotification( + Context context, + BroadcastDispatcher broadcastDispatcher, + NotificationManager notificationManager, + StorageManager storageManager + ) { mContext = context; + mBroadcastDispatcher = broadcastDispatcher; + mNotificationManager = notificationManager; + mStorageManager = storageManager; } private static class MoveInfo { @@ -168,17 +178,22 @@ public class StorageNotification implements CoreStartable { @Override public void start() { - mNotificationManager = mContext.getSystemService(NotificationManager.class); - - mStorageManager = mContext.getSystemService(StorageManager.class); mStorageManager.registerListener(mListener); - mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME), - android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null, - Context.RECEIVER_EXPORTED_UNAUDITED); - mContext.registerReceiver(mFinishReceiver, new IntentFilter(ACTION_FINISH_WIZARD), - android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null, - Context.RECEIVER_EXPORTED_UNAUDITED); + mBroadcastDispatcher.registerReceiver( + mSnoozeReceiver, + new IntentFilter(ACTION_SNOOZE_VOLUME), + null, + null, + Context.RECEIVER_EXPORTED_UNAUDITED, + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + mBroadcastDispatcher.registerReceiver( + mFinishReceiver, + new IntentFilter(ACTION_FINISH_WIZARD), + null, + null, + Context.RECEIVER_EXPORTED_UNAUDITED, + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); // Kick current state into place final List<DiskInfo> disks = mStorageManager.getDisks(); diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index ffaf524bb0d1..ed53de7dbee7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -19,31 +19,18 @@ package com.android.systemui.user.data.repository import android.content.Context import android.content.pm.UserInfo -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting -import androidx.appcompat.content.res.AppCompatResources -import com.android.internal.util.UserIcons -import com.android.systemui.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import java.util.concurrent.atomic.AtomicBoolean @@ -55,7 +42,6 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -72,15 +58,6 @@ import kotlinx.coroutines.withContext * upstream changes. */ interface UserRepository { - /** List of all users on the device. */ - val users: Flow<List<UserModel>> - - /** The currently-selected user. */ - val selectedUser: Flow<UserModel> - - /** List of available user-related actions. */ - val actions: Flow<List<UserActionModel>> - /** User switcher related settings. */ val userSwitcherSettings: Flow<UserSwitcherSettingsModel> @@ -93,9 +70,6 @@ interface UserRepository { /** User ID of the last non-guest selected user. */ val lastSelectedNonGuestUserId: Int - /** Whether actions are available even when locked. */ - val isActionableWhenLocked: Flow<Boolean> - /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean @@ -125,18 +99,13 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - private val controller: UserSwitcherController, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, private val globalSettings: GlobalSettings, private val tracker: UserTracker, - private val featureFlags: FeatureFlags, ) : UserRepository { - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() }) override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings.asStateFlow().filterNotNull() @@ -150,58 +119,11 @@ constructor( override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM private set - private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow { - fun send() { - trySendWithFailureLogging( - controller.users, - TAG, - ) - } - - val callback = UserSwitcherController.UserSwitchCallback { send() } - - controller.addUserSwitchCallback(callback) - send() - - awaitClose { controller.removeUserSwitchCallback(callback) } - } - - override val users: Flow<List<UserModel>> = - userRecords.map { records -> records.filter { it.isUser() }.map { it.toUserModel() } } - - override val selectedUser: Flow<UserModel> = - users.map { users -> users.first { user -> user.isSelected } } - - override val actions: Flow<List<UserActionModel>> = - userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - - override val isActionableWhenLocked: Flow<Boolean> = - if (isNewImpl) { - emptyFlow() - } else { - controller.isAddUsersFromLockScreenEnabled - } - override val isGuestUserAutoCreated: Boolean = - if (isNewImpl) { - appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) - } else { - controller.isGuestUserAutoCreated - } + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) private var _isGuestUserResetting: Boolean = false - override var isGuestUserResetting: Boolean = - if (isNewImpl) { - _isGuestUserResetting - } else { - controller.isGuestUserResetting - } - set(value) = - if (isNewImpl) { - _isGuestUserResetting = value - } else { - error("Not supported in the old implementation!") - } + override var isGuestUserResetting: Boolean = _isGuestUserResetting override val isGuestUserCreationScheduled = AtomicBoolean() @@ -210,10 +132,8 @@ constructor( override var isRefreshUsersPaused: Boolean = false init { - if (isNewImpl) { - observeSelectedUser() - observeUserSettings() - } + observeSelectedUser() + observeUserSettings() } override fun refreshUsers() { @@ -327,64 +247,6 @@ constructor( } } - private fun UserRecord.isUser(): Boolean { - return when { - isAddUser -> false - isAddSupervisedUser -> false - isManageUsers -> false - isGuest -> info != null - else -> true - } - } - - private fun UserRecord.isNotUser(): Boolean { - return !isUser() - } - - private fun UserRecord.toUserModel(): UserModel { - return UserModel( - id = resolveId(), - name = getUserName(this), - image = getUserImage(this), - isSelected = isCurrent, - isSelectable = isSwitchToEnabled || isGuest, - isGuest = isGuest, - ) - } - - private fun UserRecord.toActionModel(): UserActionModel { - return when { - isAddUser -> UserActionModel.ADD_USER - isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER - isGuest -> UserActionModel.ENTER_GUEST_MODE - isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT - else -> error("Don't know how to convert to UserActionModel: $this") - } - } - - private fun getUserName(record: UserRecord): Text { - val resourceId: Int? = LegacyUserUiHelper.getGuestUserRecordNameResourceId(record) - return if (resourceId != null) { - Text.Resource(resourceId) - } else { - Text.Loaded(checkNotNull(record.info).name) - } - } - - private fun getUserImage(record: UserRecord): Drawable { - if (record.isGuest) { - return checkNotNull( - AppCompatResources.getDrawable(appContext, R.drawable.ic_account_circle) - ) - } - - val userId = checkNotNull(record.info?.id) - return manager.getUserIcon(userId)?.let { userSelectedIcon -> - BitmapDrawable(userSelectedIcon) - } - ?: UserIcons.getDefaultUserIcon(appContext.resources, userId, /* light= */ false) - } - companion object { private const val TAG = "UserRepository" @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index dda78aad54c6..6b81bf2cfb08 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -39,12 +39,9 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -64,8 +61,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -82,10 +77,8 @@ class UserInteractor constructor( @Application private val applicationContext: Context, private val repository: UserRepository, - private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, private val keyguardInteractor: KeyguardInteractor, - private val featureFlags: FeatureFlags, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, @@ -107,9 +100,6 @@ constructor( fun onUserStateChanged() } - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val supervisedUserPackageName: String? get() = applicationContext.getString( @@ -122,181 +112,146 @@ constructor( /** List of current on-device users to select from. */ val users: Flow<List<UserModel>> get() = - if (isNewImpl) { - combine( - repository.userInfos, - repository.selectedUserInfo, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, settings -> - toUserModels( - userInfos = userInfos, - selectedUserId = selectedUserInfo.id, - isUserSwitcherEnabled = settings.isUserSwitcherEnabled, - ) - } - } else { - repository.users + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) } /** The currently-selected user. */ val selectedUser: Flow<UserModel> get() = - if (isNewImpl) { - combine( - repository.selectedUserInfo, - repository.userSwitcherSettings, - ) { selectedUserInfo, settings -> - val selectedUserId = selectedUserInfo.id - checkNotNull( - toUserModel( - userInfo = selectedUserInfo, - selectedUserId = selectedUserId, - canSwitchUsers = canSwitchUsers(selectedUserId), - isUserSwitcherEnabled = settings.isUserSwitcherEnabled, - ) + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, ) - } - } else { - repository.selectedUser + ) } /** List of user-switcher related actions that are available. */ val actions: Flow<List<UserActionModel>> get() = - if (isNewImpl) { - combine( - repository.selectedUserInfo, - repository.userInfos, - repository.userSwitcherSettings, - keyguardInteractor.isKeyguardShowing, - ) { _, userInfos, settings, isDeviceLocked -> - buildList { - val hasGuestUser = userInfos.any { it.isGuest } - if ( - !hasGuestUser && - (guestUserInteractor.isGuestUserAutoCreated || - UserActionsUtil.canCreateGuest( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - )) - ) { - add(UserActionModel.ENTER_GUEST_MODE) - } - - if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { - // The device is locked and our setting to allow actions that add users - // from the lock-screen is not enabled. The guest action from above is - // always allowed, even when the device is locked, but the various "add - // user" actions below are not. We can finish building the list here. - - val canCreateUsers = - UserActionsUtil.canCreateUser( + combine( + repository.selectedUserInfo, + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { _, userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( manager, repository, settings.isUserSwitcherEnabled, settings.isAddUsersFromLockscreen, - ) + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } - if (canCreateUsers) { - add(UserActionModel.ADD_USER) - } + if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. - if ( - UserActionsUtil.canCreateSupervisedUser( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - supervisedUserPackageName, - ) - ) { - add(UserActionModel.ADD_SUPERVISED_USER) - } + val canCreateUsers = + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + + if (canCreateUsers) { + add(UserActionModel.ADD_USER) } if ( - UserActionsUtil.canManageUsers( + UserActionsUtil.canCreateSupervisedUser( + manager, repository, settings.isUserSwitcherEnabled, settings.isAddUsersFromLockscreen, + supervisedUserPackageName, ) ) { - add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + add(UserActionModel.ADD_SUPERVISED_USER) } } - } - } else { - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions - } else { - // If not actionable it means that we're not allowed to show actions - // when - // locked and we are locked. Therefore, we should show no actions. - flowOf(emptyList()) - } + + if ( + UserActionsUtil.canManageUsers( + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) } + } } val userRecords: StateFlow<ArrayList<UserRecord>> = - if (isNewImpl) { - combine( - repository.userInfos, - repository.selectedUserInfo, - actions, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, actionModels, settings -> - ArrayList( - userInfos.map { + combine( + repository.userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { toRecord( - userInfo = it, + action = it, selectedUserId = selectedUserInfo.id, + isRestricted = + it != UserActionModel.ENTER_GUEST_MODE && + it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && + !settings.isAddUsersFromLockscreen, ) - } + - actionModels.map { - toRecord( - action = it, - selectedUserId = selectedUserInfo.id, - isRestricted = - it != UserActionModel.ENTER_GUEST_MODE && - it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && - !settings.isAddUsersFromLockscreen, - ) - } - ) - } - .onEach { notifyCallbacks() } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = ArrayList(), + } ) - } else { - MutableStateFlow(ArrayList()) - } + } + .onEach { notifyCallbacks() } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) val selectedUserRecord: StateFlow<UserRecord?> = - if (isNewImpl) { - repository.selectedUserInfo - .map { selectedUserInfo -> - toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - } else { - MutableStateFlow(null) - } + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated @@ -311,44 +266,37 @@ constructor( val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow() val isSimpleUserSwitcher: Boolean - get() = - if (isNewImpl) { - repository.isSimpleUserSwitcher() - } else { - error("Not supported in the old implementation!") - } + get() = repository.isSimpleUserSwitcher() init { - if (isNewImpl) { - refreshUsersScheduler.refreshIfNotPaused() - telephonyInteractor.callState - .distinctUntilChanged() - .onEach { refreshUsersScheduler.refreshIfNotPaused() } - .launchIn(applicationScope) - - combine( - broadcastDispatcher.broadcastFlow( - filter = - IntentFilter().apply { - addAction(Intent.ACTION_USER_ADDED) - addAction(Intent.ACTION_USER_REMOVED) - addAction(Intent.ACTION_USER_INFO_CHANGED) - addAction(Intent.ACTION_USER_SWITCHED) - addAction(Intent.ACTION_USER_STOPPED) - addAction(Intent.ACTION_USER_UNLOCKED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> intent }, - ), - repository.selectedUserInfo.pairwise(null), - ) { intent, selectedUserChange -> - Pair(intent, selectedUserChange.previousValue) - } - .onEach { (intent, previousSelectedUser) -> - onBroadcastReceived(intent, previousSelectedUser) - } - .launchIn(applicationScope) - } + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) } fun addCallback(callback: UserCallback) { @@ -414,48 +362,43 @@ constructor( newlySelectedUserId: Int, dialogShower: UserSwitchDialogController.DialogShower? = null, ) { - if (isNewImpl) { - val currentlySelectedUserInfo = repository.getSelectedUserInfo() - if ( - newlySelectedUserId == currentlySelectedUserInfo.id && - currentlySelectedUserInfo.isGuest - ) { - // Here when clicking on the currently-selected guest user to leave guest mode - // and return to the previously-selected non-guest user. - showDialog( - ShowDialogRequestModel.ShowExitGuestDialog( - guestUserId = currentlySelectedUserInfo.id, - targetUserId = repository.lastSelectedNonGuestUserId, - isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - onExitGuestUser = this::exitGuestUser, - dialogShower = dialogShower, - ) + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) - return - } + ) + return + } - if (currentlySelectedUserInfo.isGuest) { - // Here when switching from guest to a non-guest user. - showDialog( - ShowDialogRequestModel.ShowExitGuestDialog( - guestUserId = currentlySelectedUserInfo.id, - targetUserId = newlySelectedUserId, - isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - onExitGuestUser = this::exitGuestUser, - dialogShower = dialogShower, - ) + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) - return - } + ) + return + } - dialogShower?.dismiss() + dialogShower?.dismiss() - switchUser(newlySelectedUserId) - } else { - controller.onUserSelected(newlySelectedUserId, dialogShower) - } + switchUser(newlySelectedUserId) } /** Executes the given action. */ @@ -463,51 +406,38 @@ constructor( action: UserActionModel, dialogShower: UserSwitchDialogController.DialogShower? = null, ) { - if (isNewImpl) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> - guestUserInteractor.createAndSwitchTo( - this::showDialog, - this::dismissDialog, - ) { userId -> - selectUser(userId, dialogShower) - } - UserActionModel.ADD_USER -> { - val currentUser = repository.getSelectedUserInfo() - showDialog( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = currentUser.userHandle, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, - dialogShower = dialogShower, - ) - ) + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + ) { userId -> + selectUser(userId, dialogShower) } - UserActionModel.ADD_SUPERVISED_USER -> - activityStarter.startActivity( - Intent() - .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(supervisedUserPackageName) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - /* dismissShade= */ true, - ) - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ true, - ) - } - } else { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + dialogShower = dialogShower, ) + ) } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ true, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ true, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index e9217209530b..58a4473186b3 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -27,15 +27,12 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.FalsingManager import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -50,16 +47,11 @@ constructor( private val broadcastSender: Lazy<BroadcastSender>, private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, private val interactor: Lazy<UserInteractor>, - private val featureFlags: Lazy<FeatureFlags>, ) : CoreStartable { private var currentDialog: Dialog? = null override fun start() { - if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { - return - } - startHandlingDialogShowRequests() startHandlingDialogDismissRequests() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index d857e85bac53..0910ea36b7ff 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -20,8 +20,6 @@ package com.android.systemui.user.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.systemui.common.ui.drawable.CircularDrawable -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor @@ -41,12 +39,8 @@ private constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, - private val featureFlags: FeatureFlags, ) : ViewModel() { - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -216,7 +210,6 @@ private constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, - private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") @@ -224,7 +217,6 @@ private constructor( userInteractor = userInteractor, guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, - featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index ea0b03d1ebfd..78c28ea23be0 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -140,6 +140,12 @@ tools:replace="android:authorities" tools:node="remove" /> + <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider" + android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled" + android:enabled="false" + tools:replace="android:authorities" + tools:node="remove" /> + <provider android:name="androidx.core.content.FileProvider" android:authorities="com.android.systemui.test.fileprovider" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 52b6b38ca8ef..e8f8e25364b3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -155,7 +155,8 @@ class ClockEventControllerTest : SysuiTestCase() { verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() - verify(events).onFontSettingChanged() + verify(smallClockEvents, times(2)).onFontSettingChanged(anyFloat()) + verify(largeClockEvents, times(2)).onFontSettingChanged(anyFloat()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt index 2af055783c22..d1597149ce0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt @@ -24,7 +24,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index f8579fff488b..0fadc138637a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -120,6 +120,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue(); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); + mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt index 7cd8e749a6e9..56c91bc4525d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt @@ -42,6 +42,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -464,7 +465,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun onFalseTapOrTouch() { whenever(mockController.getTransportControls()).thenReturn(mockTransport) whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true) - whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true) + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) viewModel.updateController(mockController) val pos = 169 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt index a8f413848009..a94374680b91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt @@ -25,7 +25,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme -import com.android.systemui.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController import junit.framework.Assert.assertEquals import org.junit.After import org.junit.Before @@ -62,6 +63,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { @Mock private lateinit var mediaViewHolder: MediaViewHolder @Mock private lateinit var gutsViewHolder: GutsViewHolder @Mock private lateinit var multiRippleController: MultiRippleController + @Mock private lateinit var turbulenceNoiseController: TurbulenceNoiseController @JvmField @Rule val mockitoRule = MockitoJUnit.rule() @@ -76,6 +78,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { context, mediaViewHolder, multiRippleController, + turbulenceNoiseController, animatingColorTransitionFactory ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 1ad2ca9b9db3..761773b1a345 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -78,9 +78,10 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager -import com.android.systemui.ripple.MultiRippleView import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.KotlinArgumentCaptor @@ -178,6 +179,7 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var dismiss: FrameLayout private lateinit var dismissText: TextView private lateinit var multiRippleView: MultiRippleView + private lateinit var turbulenceNoiseView: TurbulenceNoiseView private lateinit var session: MediaSession private lateinit var device: MediaDeviceData @@ -210,7 +212,10 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var recSubtitle3: TextView private var shouldShowBroadcastButton: Boolean = false private val fakeFeatureFlag = - FakeFeatureFlags().apply { this.set(Flags.UMO_SURFACE_RIPPLE, false) } + FakeFeatureFlags().apply { + this.set(Flags.UMO_SURFACE_RIPPLE, false) + this.set(Flags.MEDIA_FALSING_PENALTY, true) + } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -382,6 +387,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } multiRippleView = MultiRippleView(context, null) + turbulenceNoiseView = TurbulenceNoiseView(context, null) whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.appIcon).thenReturn(appIcon) @@ -425,6 +431,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) + whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) } /** Initialize elements for the recommendation view holder */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java index 5abc0e1f9c89..35c8cc70953d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.content.Context; +import android.content.res.Resources; import android.test.suitebuilder.annotation.SmallTest; import android.view.accessibility.AccessibilityNodeInfo; @@ -42,16 +44,22 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; @SmallTest @RunWith(AndroidJUnit4.class) public class TileLayoutTest extends SysuiTestCase { - private TileLayout mTileLayout; + private Resources mResources; private int mLayoutSizeForOneTile; + private TileLayout mTileLayout; // under test @Before public void setUp() throws Exception { - mTileLayout = new TileLayout(mContext); + Context context = Mockito.spy(mContext); + mResources = Mockito.spy(context.getResources()); + Mockito.when(mContext.getResources()).thenReturn(mResources); + + mTileLayout = new TileLayout(context); // Layout needs to leave space for the tile margins. Three times the margin size is // sufficient for any number of columns. mLayoutSizeForOneTile = @@ -203,4 +211,21 @@ public class TileLayoutTest extends SysuiTestCase { verify(tileRecord1.tileView).setPosition(0); verify(tileRecord2.tileView).setPosition(1); } + + @Test + public void resourcesChanged_updateResources_returnsTrue() { + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1); + mTileLayout.updateResources(); // setup with 1 + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2); + + assertEquals(true, mTileLayout.updateResources()); + } + + @Test + public void resourcesSame_updateResources_returnsFalse() { + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1); + mTileLayout.updateResources(); // setup with 1 + + assertEquals(false, mTileLayout.updateResources()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index 3131f60893c7..08a90b79089e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -42,27 +42,21 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class UserDetailViewAdapterTest : SysuiTestCase() { - @Mock - private lateinit var mUserSwitcherController: UserSwitcherController - @Mock - private lateinit var mParent: ViewGroup - @Mock - private lateinit var mUserDetailItemView: UserDetailItemView - @Mock - private lateinit var mOtherView: View - @Mock - private lateinit var mInflatedUserDetailItemView: UserDetailItemView - @Mock - private lateinit var mLayoutInflater: LayoutInflater + @Mock private lateinit var mUserSwitcherController: UserSwitcherController + @Mock private lateinit var mParent: ViewGroup + @Mock private lateinit var mUserDetailItemView: UserDetailItemView + @Mock private lateinit var mOtherView: View + @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView + @Mock private lateinit var mLayoutInflater: LayoutInflater private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake() private lateinit var adapter: UserDetailView.Adapter private lateinit var uiEventLogger: UiEventLoggerFake @@ -77,10 +71,13 @@ class UserDetailViewAdapterTest : SysuiTestCase() { `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean())) .thenReturn(mInflatedUserDetailItemView) `when`(mParent.context).thenReturn(mContext) - adapter = UserDetailView.Adapter( - mContext, mUserSwitcherController, uiEventLogger, - falsingManagerFake - ) + adapter = + UserDetailView.Adapter( + mContext, + mUserSwitcherController, + uiEventLogger, + falsingManagerFake + ) mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt index d7eb337efd3b..bc17c19df8f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt @@ -363,6 +363,22 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { } } + @Test + fun testEmptyCutoutDateIconsAreConstrainedWidth() { + CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()() + + assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue() + assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue() + } + + @Test + fun testCenterCutoutDateIconsAreConstrainedWidth() { + CombinedShadeHeadersConstraintManagerImpl.centerCutoutConstraints(false, 10)() + + assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue() + assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue() + } + private operator fun ConstraintsChanges.invoke() { qqsConstraintsChanges?.invoke(qqsConstraint) qsConstraintsChanges?.invoke(qsConstraint) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index 539a54b731ec..f5bed79b5e6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -139,12 +139,19 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test - fun defaultClock_events_onFontSettingChanged() { + fun defaultSmallClock_events_onFontSettingChanged() { val clock = provider.createClock(DEFAULT_CLOCK_ID) - clock.events.onFontSettingChanged() + clock.smallClock.events.onFontSettingChanged(100f) - verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat()) - verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat()) + verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(100f)) + } + + @Test + fun defaultLargeClock_events_onFontSettingChanged() { + val clock = provider.createClock(DEFAULT_CLOCK_ID) + clock.largeClock.events.onFontSettingChanged(200f) + + verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(200f)) verify(mockLargeClockView).setLayoutParams(any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 808abc8e9de5..de71e2c250c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.statusbar.phone.ScrimController.KEYGUARD_SCRIM_ALPHA; import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE; import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT; @@ -59,7 +58,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -119,7 +117,6 @@ public class ScrimControllerTest extends SysuiTestCase { // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - @Mock private KeyguardViewMediator mKeyguardViewMediator; private static class AnimatorListener implements Animator.AnimatorListener { private int mNumStarts; @@ -233,8 +230,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager, - mKeyguardViewMediator); + mStatusBarKeyguardViewManager); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -243,8 +239,6 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(false); mScrimController.transitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); - - mScrimController.setLaunchingAffordanceWithPreview(false); } @After @@ -858,8 +852,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager, - mKeyguardViewMediator); + mStatusBarKeyguardViewManager); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1638,30 +1631,6 @@ public class ScrimControllerTest extends SysuiTestCase { assertScrimAlpha(mScrimBehind, 0); } - @Test - public void keyguardAlpha_whenUnlockedForOcclusion_ifPlayingOcclusionAnimation() { - mScrimController.transitionTo(ScrimState.KEYGUARD); - - when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); - - mScrimController.transitionTo(ScrimState.UNLOCKED); - finishAnimationsImmediately(); - - assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); - } - - @Test - public void keyguardAlpha_whenUnlockedForLaunch_ifLaunchingAffordance() { - mScrimController.transitionTo(ScrimState.KEYGUARD); - when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); - mScrimController.setLaunchingAffordanceWithPreview(true); - - mScrimController.transitionTo(ScrimState.UNLOCKED); - finishAnimationsImmediately(); - - assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); - } - private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt index f3046477f4d1..0a3da0b5b029 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt @@ -237,7 +237,7 @@ class BaseUserSwitcherAdapterTest : SysuiTestCase() { fun refresh() { underTest.refresh() - verify(controller).refreshUsers(UserHandle.USER_NULL) + verify(controller).refreshUsers() } private fun createUserRecord( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt deleted file mode 100644 index 169f4fb2715b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.statusbar.policy - -import android.app.IActivityManager -import android.app.NotificationManager -import android.app.admin.DevicePolicyManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.hardware.face.FaceManager -import android.hardware.fingerprint.FingerprintManager -import android.os.Handler -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.ThreadedRenderer -import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor -import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.util.LatencyTracker -import com.android.internal.util.UserIcons -import com.android.systemui.GuestResetOrExitSessionReceiver -import com.android.systemui.GuestResumeSessionReceiver -import com.android.systemui.GuestSessionNotification -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogCuj -import com.android.systemui.animation.DialogLaunchAnimator -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.dump.DumpManager -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.QSUserSwitcherEvent -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.NotificationShadeWindowView -import com.android.systemui.telephony.TelephonyListenerManager -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.concurrency.FakeExecutor -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.kotlinArgumentCaptor -import com.android.systemui.util.mockito.nullable -import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -@SmallTest -class UserSwitcherControllerOldImplTest : SysuiTestCase() { - @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock private lateinit var activityManager: IActivityManager - @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var handler: Handler - @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var userManager: UserManager - @Mock private lateinit var activityStarter: ActivityStarter - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var broadcastSender: BroadcastSender - @Mock private lateinit var telephonyListenerManager: TelephonyListenerManager - @Mock private lateinit var secureSettings: SecureSettings - @Mock private lateinit var falsingManager: FalsingManager - @Mock private lateinit var dumpManager: DumpManager - @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor - @Mock private lateinit var latencyTracker: LatencyTracker - @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower - @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView - @Mock private lateinit var threadedRenderer: ThreadedRenderer - @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator - @Mock private lateinit var globalSettings: GlobalSettings - @Mock private lateinit var guestSessionNotification: GuestSessionNotification - @Mock private lateinit var guestResetOrExitSessionReceiver: GuestResetOrExitSessionReceiver - private lateinit var resetSessionDialogFactory: - GuestResumeSessionReceiver.ResetSessionDialog.Factory - private lateinit var guestResumeSessionReceiver: GuestResumeSessionReceiver - private lateinit var testableLooper: TestableLooper - private lateinit var bgExecutor: FakeExecutor - private lateinit var longRunningExecutor: FakeExecutor - private lateinit var uiExecutor: FakeExecutor - private lateinit var uiEventLogger: UiEventLoggerFake - private lateinit var userSwitcherController: UserSwitcherControllerOldImpl - private lateinit var picture: Bitmap - private val ownerId = UserHandle.USER_SYSTEM - private val ownerInfo = UserInfo(ownerId, "Owner", null, - UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or - UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN, - UserManager.USER_TYPE_FULL_SYSTEM) - private val guestId = 1234 - private val guestInfo = UserInfo(guestId, "Guest", null, - UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST) - private val secondaryUser = - UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - testableLooper = TestableLooper.get(this) - bgExecutor = FakeExecutor(FakeSystemClock()) - longRunningExecutor = FakeExecutor(FakeSystemClock()) - uiExecutor = FakeExecutor(FakeSystemClock()) - uiEventLogger = UiEventLoggerFake() - - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_guestUserAutoCreated, false) - - mContext.addMockSystemService(Context.FACE_SERVICE, mock(FaceManager::class.java)) - mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, - mock(NotificationManager::class.java)) - mContext.addMockSystemService(Context.FINGERPRINT_SERVICE, - mock(FingerprintManager::class.java)) - - resetSessionDialogFactory = object : GuestResumeSessionReceiver.ResetSessionDialog.Factory { - override fun create(userId: Int): GuestResumeSessionReceiver.ResetSessionDialog { - return GuestResumeSessionReceiver.ResetSessionDialog( - mContext, - mock(UserSwitcherController::class.java), - uiEventLogger, - userId - ) - } - } - - guestResumeSessionReceiver = GuestResumeSessionReceiver(userTracker, - secureSettings, - broadcastDispatcher, - guestSessionNotification, - resetSessionDialogFactory) - - `when`(userManager.canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY))) - .thenReturn(true) - `when`(notificationShadeWindowView.context).thenReturn(context) - - // Since userSwitcherController involves InteractionJankMonitor. - // Let's fulfill the dependencies. - val mockedContext = mock(Context::class.java) - doReturn(mockedContext).`when`(notificationShadeWindowView).context - doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow - doNothing().`when`(threadedRenderer).addObserver(any()) - doNothing().`when`(threadedRenderer).removeObserver(any()) - doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer - - picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) - - // Create defaults for the current user - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - setupController() - } - - private fun setupController() { - userSwitcherController = - UserSwitcherControllerOldImpl( - mContext, - activityManager, - userManager, - userTracker, - keyguardStateController, - deviceProvisionedController, - devicePolicyManager, - handler, - activityStarter, - broadcastDispatcher, - broadcastSender, - uiEventLogger, - falsingManager, - telephonyListenerManager, - secureSettings, - globalSettings, - bgExecutor, - longRunningExecutor, - uiExecutor, - interactionJankMonitor, - latencyTracker, - dumpManager, - dialogLaunchAnimator, - guestResumeSessionReceiver, - guestResetOrExitSessionReceiver - ) - userSwitcherController.init(notificationShadeWindowView) - } - - @Test - fun testSwitchUser_parentDialogDismissed() { - val otherUserRecord = UserRecord( - secondaryUser, - picture, - false /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower) - testableLooper.processAllMessages() - - verify(dialogShower).dismiss() - } - - @Test - fun testAddGuest_okButtonPressed() { - val emptyGuestUserRecord = - UserRecord( - null, - null, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`(userManager.createGuest(any())).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null) - bgExecutor.runAllReady() - uiExecutor.runAllReady() - testableLooper.processAllMessages() - verify(interactionJankMonitor).begin(any()) - verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH) - verify(activityManager).switchUser(guestInfo.id) - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0)) - } - - @Test - fun testAddGuest_parentDialogDismissed() { - val emptyGuestUserRecord = - UserRecord( - null, - null, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`(userManager.createGuest(any())).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower) - bgExecutor.runAllReady() - uiExecutor.runAllReady() - testableLooper.processAllMessages() - verify(dialogShower).dismiss() - } - - @Test - fun testRemoveGuest_removeButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_POSITIVE).performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertTrue( - QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id == uiEventLogger.eventId(0) || - QSUserSwitcherEvent.QS_USER_SWITCH.id == uiEventLogger.eventId(0) - ) - } - - @Test - fun testRemoveGuest_removeButtonPressed_dialogDismissed() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_POSITIVE).performClick() - testableLooper.processAllMessages() - assertFalse(userSwitcherController.mExitGuestDialog.isShowing) - } - - @Test - fun testRemoveGuest_dialogShowerUsed() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower) - assertNotNull(userSwitcherController.mExitGuestDialog) - testableLooper.processAllMessages() - verify(dialogShower) - .showDialog( - userSwitcherController.mExitGuestDialog, - DialogCuj(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, "exit_guest_mode")) - } - - @Test - fun testRemoveGuest_cancelButtonPressed_isNotLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_NEUTRAL).performClick() - testableLooper.processAllMessages() - assertEquals(0, uiEventLogger.numLogs()) - } - - @Test - fun testWipeGuest_startOverButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - // Simulate that guest user has already logged in - `when`(secureSettings.getIntForUser( - eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) - .thenReturn(1) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - - // Simulate a user switch event - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver) - userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog) - userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog - .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_WIPE).performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_WIPE.id, uiEventLogger.eventId(0)) - } - - @Test - fun testWipeGuest_continueButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - // Simulate that guest user has already logged in - `when`(secureSettings.getIntForUser( - eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) - .thenReturn(1) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - - // Simulate a user switch event - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver) - userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog) - userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog - .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_DONTWIPE) - .performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE.id, uiEventLogger.eventId(0)) - } - - @Test - fun test_getCurrentUserName_shouldReturnNameOfTheCurrentUser() { - fun addUser(id: Int, name: String, isCurrent: Boolean) { - userSwitcherController.users.add( - UserRecord( - UserInfo(id, name, 0), - null, false, isCurrent, false, - false, false, false - ) - ) - } - val bgUserName = "background_user" - val fgUserName = "foreground_user" - - addUser(1, bgUserName, false) - addUser(2, fgUserName, true) - - assertEquals(fgUserName, userSwitcherController.currentUserName) - } - - @Test - fun isSystemUser_currentUserIsSystemUser_shouldReturnTrue() { - `when`(userTracker.userId).thenReturn(UserHandle.USER_SYSTEM) - assertEquals(true, userSwitcherController.isSystemUser) - } - - @Test - fun isSystemUser_currentUserIsNotSystemUser_shouldReturnFalse() { - `when`(userTracker.userId).thenReturn(1) - assertEquals(false, userSwitcherController.isSystemUser) - } - - @Test - fun testCanCreateSupervisedUserWithConfiguredPackage() { - // GIVEN the supervised user creation package is configured - `when`(context.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage)) - .thenReturn("some_pkg") - - // AND the current user is allowed to create new users - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - // WHEN the controller is started with the above config - setupController() - testableLooper.processAllMessages() - - // THEN a supervised user can be constructed - assertTrue(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCannotCreateSupervisedUserWithConfiguredPackage() { - // GIVEN the supervised user creation package is NOT configured - `when`(context.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage)) - .thenReturn(null) - - // AND the current user is allowed to create new users - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - // WHEN the controller is started with the above config - setupController() - testableLooper.processAllMessages() - - // THEN a supervised user can NOT be constructed - assertFalse(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCannotCreateUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateUser()) - } - - @Test - fun testCannotCreateGuestUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateGuest(false)) - } - - @Test - fun testCannotCreateSupervisedUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - setupController() - assertTrue(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - setupController() - assertFalse(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherEnabled_isAdmin() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - setupController() - assertTrue(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherDisabled_isAdmin() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - setupController() - assertFalse(userSwitcherController.canManageUsers()) - } - - @Test - fun addUserSwitchCallback() { - val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>() - verify(broadcastDispatcher).registerReceiver( - capture(broadcastReceiverCaptor), - any(), - nullable(), nullable(), anyInt(), nullable()) - - val cb = mock(UserSwitcherController.UserSwitchCallback::class.java) - userSwitcherController.addUserSwitchCallback(cb) - - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - broadcastReceiverCaptor.value.onReceive(context, intent) - verify(cb).onUserSwitched() - } - - @Test - fun onUserItemClicked_guest_runsOnBgThread() { - val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java) - val guestUserRecord = UserRecord( - null, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - - userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower) - assertTrue(bgExecutor.numPending() > 0) - verify(userManager, never()).createGuest(context) - bgExecutor.runAllReady() - verify(userManager).createGuest(context) - } - - @Test - fun onUserItemClicked_manageUsers() { - val manageUserRecord = LegacyUserDataHelper.createRecord( - mContext, - ownerId, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - isRestricted = false, - isSwitchToEnabled = true - ) - - userSwitcherController.onUserListItemClicked(manageUserRecord, null) - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), - eq(true) - ) - Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt index 05512e5bf1ce..0d19ab1db390 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER +import com.android.systemui.surfaceeffects.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt new file mode 100644 index 000000000000..2024d53b0212 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt @@ -0,0 +1,58 @@ +/* + * 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.surfaceeffects.ripple + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MultiRippleViewTest : SysuiTestCase() { + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun onRippleFinishes_triggersRippleFinished() { + val multiRippleView = MultiRippleView(context, null) + val multiRippleController = MultiRippleController(multiRippleView) + val rippleAnimationConfig = RippleAnimationConfig(duration = 1000L) + + var isTriggered = false + val listener = + object : MultiRippleView.Companion.RipplesFinishedListener { + override fun onRipplesFinish() { + isTriggered = true + } + } + multiRippleView.addRipplesFinishedListener(listener) + + fakeExecutor.execute { + val rippleAnimation = RippleAnimation(rippleAnimationConfig) + multiRippleController.play(rippleAnimation) + + fakeSystemClock.advanceTime(rippleAnimationConfig.duration) + + assertThat(isTriggered).isTrue() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt index 7662282a04f4..756397a30e43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt index 2d2f4cc9edd2..1e5ab7e25599 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest @@ -21,12 +21,10 @@ import com.android.systemui.SysuiTestCase import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock @SmallTest @RunWith(AndroidTestingRunner::class) class RippleViewTest : SysuiTestCase() { - @Mock private lateinit var rippleView: RippleView @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt new file mode 100644 index 000000000000..d25c8c1a5899 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt @@ -0,0 +1,71 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.graphics.Color +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TurbulenceNoiseControllerTest : SysuiTestCase() { + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun play_playsTurbulenceNoise() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(turbulenceNoiseView.isPlaying).isFalse() + } + } + + @Test + fun updateColor_updatesCorrectColor() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f, color = Color.WHITE) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + val expectedColor = Color.RED + + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(config) + + turbulenceNoiseView.updateColor(expectedColor) + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(config.color).isEqualTo(expectedColor) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt new file mode 100644 index 000000000000..633aac076502 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt @@ -0,0 +1,86 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TurbulenceNoiseViewTest : SysuiTestCase() { + + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun play_viewHasCorrectVisibility() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE) + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) + } + } + + @Test + fun play_playsAnimation() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + } + } + + @Test + fun play_onEnd_triggersOnAnimationEnd() { + var animationEnd = false + val config = + TurbulenceNoiseAnimationConfig( + duration = 1000f, + onAnimationEnd = { animationEnd = true } + ) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(animationEnd).isTrue() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt deleted file mode 100644 index 7c7f0e1e0e12..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt +++ /dev/null @@ -1,248 +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.systemui.user.data.repository - -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import androidx.test.filters.SmallTest -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { - - @Before - fun setUp() { - super.setUp(isRefactored = true) - } - - @Test - fun userSwitcherSettings() = runSelfCancelingTest { - setUpGlobalSettings( - isSimpleUserSwitcher = true, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - underTest = create(this) - - var value: UserSwitcherSettingsModel? = null - underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) - - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = true, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - - setUpGlobalSettings( - isSimpleUserSwitcher = false, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = false, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - } - - @Test - fun refreshUsers() = runSelfCancelingTest { - underTest = create(this) - val initialExpectedValue = - setUpUsers( - count = 3, - selectedIndex = 0, - ) - var userInfos: List<UserInfo>? = null - var selectedUserInfo: UserInfo? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(initialExpectedValue) - assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - - val secondExpectedValue = - setUpUsers( - count = 4, - selectedIndex = 1, - ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(secondExpectedValue) - assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - - val selectedNonGuestUserId = selectedUserInfo?.id - val thirdExpectedValue = - setUpUsers( - count = 2, - isLastGuestUser = true, - selectedIndex = 1, - ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(thirdExpectedValue) - assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) - assertThat(selectedUserInfo?.isGuest).isTrue() - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) - } - - @Test - fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { - underTest = create(this) - val unsortedUsers = - setUpUsers( - count = 3, - selectedIndex = 0, - isLastGuestUser = true, - ) - unsortedUsers[0].creationTime = 999 - unsortedUsers[1].creationTime = 900 - unsortedUsers[2].creationTime = 950 - val expectedUsers = - listOf( - unsortedUsers[1], - unsortedUsers[0], - unsortedUsers[2], // last because this is the guest - ) - var userInfos: List<UserInfo>? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) - - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(expectedUsers) - } - - @Test - fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest { - underTest = create(this) - var selectedUserInfo: UserInfo? = null - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - setUpUsers( - count = 2, - selectedIndex = 0, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id == 0) - setUpUsers( - count = 2, - selectedIndex = 1, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id == 1) - } - - private fun setUpUsers( - count: Int, - isLastGuestUser: Boolean = false, - selectedIndex: Int = 0, - ): List<UserInfo> { - val userInfos = - (0 until count).map { index -> - createUserInfo( - index, - isGuest = isLastGuestUser && index == count - 1, - ) - } - whenever(manager.aliveUsers).thenReturn(userInfos) - tracker.set(userInfos, selectedIndex) - return userInfos - } - - private fun createUserInfo( - id: Int, - isGuest: Boolean, - ): UserInfo { - val flags = 0 - return UserInfo( - id, - "user_$id", - /* iconPath= */ "", - flags, - if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), - ) - } - - private fun setUpGlobalSettings( - isSimpleUserSwitcher: Boolean = false, - isAddUsersFromLockscreen: Boolean = false, - isUserSwitcherEnabled: Boolean = true, - ) { - context.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_expandLockScreenUserSwitcher, - true, - ) - globalSettings.putIntForUser( - UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, - if (isSimpleUserSwitcher) 1 else 0, - UserHandle.USER_SYSTEM, - ) - globalSettings.putIntForUser( - Settings.Global.ADD_USERS_WHEN_LOCKED, - if (isAddUsersFromLockscreen) 1 else 0, - UserHandle.USER_SYSTEM, - ) - globalSettings.putIntForUser( - Settings.Global.USER_SWITCHER_ENABLED, - if (isUserSwitcherEnabled) 1 else 0, - UserHandle.USER_SYSTEM, - ) - } - - private fun assertUserSwitcherSettings( - model: UserSwitcherSettingsModel?, - expectedSimpleUserSwitcher: Boolean, - expectedAddUsersFromLockscreen: Boolean, - expectedUserSwitcherEnabled: Boolean, - ) { - checkNotNull(model) - assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) - assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) - assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) - } - - /** - * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which - * is then automatically canceled and cleaned-up. - */ - private fun runSelfCancelingTest( - block: suspend CoroutineScope.() -> Unit, - ) = - runBlocking(Dispatchers.Main.immediate) { - val scope = CoroutineScope(coroutineContext + Job()) - block(scope) - scope.cancel() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index dcea83a55a74..2e527be1af89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,54 +17,263 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -abstract class UserRepositoryImplTest : SysuiTestCase() { +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplTest : SysuiTestCase() { - @Mock protected lateinit var manager: UserManager - @Mock protected lateinit var controller: UserSwitcherController + @Mock private lateinit var manager: UserManager - protected lateinit var underTest: UserRepositoryImpl + private lateinit var underTest: UserRepositoryImpl - protected lateinit var globalSettings: FakeSettings - protected lateinit var tracker: FakeUserTracker - protected lateinit var featureFlags: FakeFeatureFlags + private lateinit var globalSettings: FakeSettings + private lateinit var tracker: FakeUserTracker - protected fun setUp(isRefactored: Boolean) { + @Before + fun setUp() { MockitoAnnotations.initMocks(this) globalSettings = FakeSettings() tracker = FakeUserTracker() - featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } - protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + isLastGuestUser = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + @Test + fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { + underTest = create(this) + val unsortedUsers = + setUpUsers( + count = 3, + selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest + ) + var userInfos: List<UserInfo>? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(expectedUsers) + } + + private fun setUpUsers( + count: Int, + isLastGuestUser: Boolean = false, + selectedIndex: Int = 0, + ): List<UserInfo> { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = isLastGuestUser && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + @Test + fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest { + underTest = create(this) + var selectedUserInfo: UserInfo? = null + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + setUpUsers( + count = 2, + selectedIndex = 0, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(0) + setUpUsers( + count = 2, + selectedIndex = 1, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(1) + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } + + private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { return UserRepositoryImpl( appContext = context, manager = manager, - controller = controller, applicationScope = scope, mainDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE, globalSettings = globalSettings, tracker = tracker, - featureFlags = featureFlags, ) } companion object { - @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt deleted file mode 100644 index a363a037c499..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt +++ /dev/null @@ -1,209 +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.systemui.user.data.repository - -import android.content.pm.UserInfo -import androidx.test.filters.SmallTest -import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } - - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor<UserSwitcherController.UserSwitchCallback> - - @Before - fun setUp() { - super.setUp(isRefactored = false) - - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = create() - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), - ) - ) - var models: List<UserModel>? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) - - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actions - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), - ) - ) - var models: List<UserActionModel>? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(4) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - job.cancel() - } - - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) - } - - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, - isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt deleted file mode 100644 index f682e31c0547..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ /dev/null @@ -1,740 +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.systemui.user.domain.interactor - -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import androidx.test.filters.SmallTest -import com.android.internal.R.drawable.ic_account_circle -import com.android.systemui.R -import com.android.systemui.common.shared.model.Text -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.domain.model.ShowDialogRequestModel -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.advanceUntilIdle -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.verify - -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorRefactoredTest : UserInteractorTest() { - - override fun isRefactored(): Boolean { - return true - } - - @Before - override fun setUp() { - super.setUp() - - overrideResource(R.drawable.ic_account_circle, GUEST_ICON) - overrideResource(R.dimen.max_avatar_size, 10) - overrideResource( - com.android.internal.R.string.config_supervisedUserCreationPackage, - SUPERVISED_USER_CREATION_APP_PACKAGE, - ) - whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) - whenever(manager.canAddMoreUsers(any())).thenReturn(true) - } - - @Test - fun `onRecordSelected - user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) - - verify(dialogShower).dismiss() - verify(activityManager).switchUser(userInfos[1].id) - Unit - } - - @Test - fun `onRecordSelected - switch to guest user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos.last())) - - verify(activityManager).switchUser(userInfos.last().id) - Unit - } - - @Test - fun `onRecordSelected - enter guest mode`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) - whenever(manager.createGuest(any())).thenReturn(guestUserInfo) - - underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) - - verify(dialogShower).dismiss() - verify(manager).createGuest(any()) - Unit - } - - @Test - fun `onRecordSelected - action`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) - - verify(dialogShower, never()).dismiss() - verify(activityStarter).startActivity(any(), anyBoolean()) - } - - @Test - fun `users - switcher enabled`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 3, includeGuest = true) - - job.cancel() - } - - @Test - fun `users - switches to second user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - userRepository.setSelectedUserInfo(userInfos[1]) - - assertUsers(models = value, count = 2, selectedIndex = 1) - job.cancel() - } - - @Test - fun `users - switcher not enabled`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 1) - - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: UserModel? = null - val job = underTest.selectedUser.onEach { value = it }.launchIn(this) - assertUser(value, id = 0, isSelected = true) - - userRepository.setSelectedUserInfo(userInfos[1]) - assertUser(value, id = 1, isSelected = true) - - job.cancel() - } - - @Test - fun `actions - device unlocked`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - - job.cancel() - } - - @Test - fun `actions - device unlocked user not primary - empty list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(emptyList<UserActionModel>()) - - job.cancel() - } - - @Test - fun `actions - device unlocked user is guest - empty list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - assertThat(userInfos[1].isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(emptyList<UserActionModel>()) - - job.cancel() - } - - @Test - fun `actions - device locked add from lockscreen set - full list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true, - ) - ) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - - job.cancel() - } - - @Test - fun `actions - device locked - only guest action and manage user is shown`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(true) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT - ) - ) - - job.cancel() - } - - @Test - fun `executeAction - add user - dialog shown`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - val dialogShower: UserSwitchDialogController.DialogShower = mock() - - underTest.executeAction(UserActionModel.ADD_USER, dialogShower) - assertThat(dialogRequest) - .isEqualTo( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = userInfos[0].userHandle, - isKeyguardShowing = false, - showEphemeralMessage = false, - dialogShower = dialogShower, - ) - ) - - underTest.onDialogShown() - assertThat(dialogRequest).isNull() - - job.cancel() - } - - @Test - fun `executeAction - add supervised user - starts activity`() = - runBlocking(IMMEDIATE) { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action) - .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) - assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) - } - - @Test - fun `executeAction - navigate to manage users`() = - runBlocking(IMMEDIATE) { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) - } - - @Test - fun `executeAction - guest mode`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) - whenever(manager.createGuest(any())).thenReturn(guestUserInfo) - val dialogRequests = mutableListOf<ShowDialogRequestModel?>() - val showDialogsJob = - underTest.dialogShowRequests - .onEach { - dialogRequests.add(it) - if (it != null) { - underTest.onDialogShown() - } - } - .launchIn(this) - val dismissDialogsJob = - underTest.dialogDismissRequests - .onEach { - if (it != null) { - underTest.onDialogDismissed() - } - } - .launchIn(this) - - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - assertThat(dialogRequests) - .contains( - ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), - ) - verify(activityManager).switchUser(guestUserInfo.id) - - showDialogsJob.cancel() - dismissDialogsJob.cancel() - } - - @Test - fun `selectUser - already selected guest re-selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - val guestUserInfo = userInfos[1] - assertThat(guestUserInfo.isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(guestUserInfo) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser( - newlySelectedUserId = guestUserInfo.id, - dialogShower = dialogShower, - ) - - assertThat(dialogRequest) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - job.cancel() - } - - @Test - fun `selectUser - currently guest non-guest selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - val guestUserInfo = userInfos[1] - assertThat(guestUserInfo.isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(guestUserInfo) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) - - assertThat(dialogRequest) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - job.cancel() - } - - @Test - fun `selectUser - not currently guest - switches users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) - - assertThat(dialogRequest).isNull() - verify(activityManager).switchUser(userInfos[1].id) - verify(dialogShower).dismiss() - job.cancel() - } - - @Test - fun `Telephony call state changes - refreshes users`() = - runBlocking(IMMEDIATE) { - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - telephonyRepository.setCallState(1) - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `User switched broadcast`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val callback1: UserInteractor.UserCallback = mock() - val callback2: UserInteractor.UserCallback = mock() - underTest.addCallback(callback1) - underTest.addCallback(callback2) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - userRepository.setSelectedUserInfo(userInfos[1]) - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_SWITCHED) - .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), - ) - } - - verify(callback1).onUserStateChanged() - verify(callback2).onUserStateChanged() - assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `User info changed broadcast`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_INFO_CHANGED), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `System user unlocked broadcast - refresh users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_UNLOCKED) - .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `Non-system user unlocked broadcast - do not refresh users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) - } - - @Test - fun userRecords() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - testCoroutineScope.advanceUntilIdle() - - assertRecords( - records = underTest.userRecords.value, - userIds = listOf(0, 1, 2), - selectedUserIndex = 0, - includeGuest = false, - expectedActions = - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ), - ) - } - - @Test - fun selectedUserRecord() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - assertRecordForUser( - record = underTest.selectedUserRecord.value, - id = 0, - hasPicture = true, - isCurrent = true, - isSwitchToEnabled = true, - ) - } - - private fun assertUsers( - models: List<UserModel>?, - count: Int, - selectedIndex: Int = 0, - includeGuest: Boolean = false, - ) { - checkNotNull(models) - assertThat(models.size).isEqualTo(count) - models.forEachIndexed { index, model -> - assertUser( - model = model, - id = index, - isSelected = index == selectedIndex, - isGuest = includeGuest && index == count - 1 - ) - } - } - - private fun assertUser( - model: UserModel?, - id: Int, - isSelected: Boolean = false, - isGuest: Boolean = false, - ) { - checkNotNull(model) - assertThat(model.id).isEqualTo(id) - assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) - assertThat(model.isSelected).isEqualTo(isSelected) - assertThat(model.isSelectable).isTrue() - assertThat(model.isGuest).isEqualTo(isGuest) - } - - private fun assertRecords( - records: List<UserRecord>, - userIds: List<Int>, - selectedUserIndex: Int = 0, - includeGuest: Boolean = false, - expectedActions: List<UserActionModel> = emptyList(), - ) { - assertThat(records.size >= userIds.size).isTrue() - userIds.indices.forEach { userIndex -> - val record = records[userIndex] - assertThat(record.info).isNotNull() - val isGuest = includeGuest && userIndex == userIds.size - 1 - assertRecordForUser( - record = record, - id = userIds[userIndex], - hasPicture = !isGuest, - isCurrent = userIndex == selectedUserIndex, - isGuest = isGuest, - isSwitchToEnabled = true, - ) - } - - assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) - (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> - val record = records[actionIndex] - assertThat(record.info).isNull() - assertRecordForAction( - record = record, - type = expectedActions[actionIndex - userIds.size], - ) - } - } - - private fun assertRecordForUser( - record: UserRecord?, - id: Int? = null, - hasPicture: Boolean = false, - isCurrent: Boolean = false, - isGuest: Boolean = false, - isSwitchToEnabled: Boolean = false, - ) { - checkNotNull(record) - assertThat(record.info?.id).isEqualTo(id) - assertThat(record.picture != null).isEqualTo(hasPicture) - assertThat(record.isCurrent).isEqualTo(isCurrent) - assertThat(record.isGuest).isEqualTo(isGuest) - assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) - } - - private fun assertRecordForAction( - record: UserRecord, - type: UserActionModel, - ) { - assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) - assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) - assertThat(record.isAddSupervisedUser) - .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) - } - - private fun createUserInfos( - count: Int, - includeGuest: Boolean, - ): List<UserInfo> { - return (0 until count).map { index -> - val isGuest = includeGuest && index == count - 1 - createUserInfo( - id = index, - name = - if (isGuest) { - "guest" - } else { - "user_$index" - }, - isPrimary = !isGuest && index == 0, - isGuest = isGuest, - ) - } - } - - private fun createUserInfo( - id: Int, - name: String, - isPrimary: Boolean = false, - isGuest: Boolean = false, - ): UserInfo { - return UserInfo( - id, - name, - /* iconPath= */ "", - /* flags= */ if (isPrimary) { - UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN - } else { - 0 - }, - if (isGuest) { - UserManager.USER_TYPE_FULL_GUEST - } else { - UserManager.USER_TYPE_FULL_SYSTEM - }, - ) - } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - private val GUEST_ICON: Drawable = mock() - private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 58f55314c1b6..8fb98c12d6ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -19,51 +19,90 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle import com.android.internal.logging.UiEventLogger import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver +import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.common.shared.model.Text import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -abstract class UserInteractorTest : SysuiTestCase() { +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorTest : SysuiTestCase() { - @Mock protected lateinit var controller: UserSwitcherController - @Mock protected lateinit var activityStarter: ActivityStarter - @Mock protected lateinit var manager: UserManager - @Mock protected lateinit var activityManager: ActivityManager - @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock protected lateinit var devicePolicyManager: DevicePolicyManager - @Mock protected lateinit var uiEventLogger: UiEventLogger - @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var manager: UserManager + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver - protected lateinit var underTest: UserInteractor + private lateinit var underTest: UserInteractor - protected lateinit var testCoroutineScope: TestCoroutineScope - protected lateinit var userRepository: FakeUserRepository - protected lateinit var keyguardRepository: FakeKeyguardRepository - protected lateinit var telephonyRepository: FakeTelephonyRepository + private lateinit var testCoroutineScope: TestCoroutineScope + private lateinit var userRepository: FakeUserRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var telephonyRepository: FakeTelephonyRepository - abstract fun isRefactored(): Boolean - - open fun setUp() { + @Before + fun setUp() { MockitoAnnotations.initMocks(this) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() @@ -79,16 +118,11 @@ abstract class UserInteractorTest : SysuiTestCase() { UserInteractor( applicationContext = context, repository = userRepository, - controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, ), - featureFlags = - FakeFeatureFlags().apply { - set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) - }, manager = manager, applicationScope = testCoroutineScope, telephonyInteractor = @@ -117,7 +151,665 @@ abstract class UserInteractorTest : SysuiTestCase() { ) } + @Test + fun `onRecordSelected - user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) + + verify(dialogShower).dismiss() + verify(activityManager).switchUser(userInfos[1].id) + Unit + } + + @Test + fun `onRecordSelected - switch to guest user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos.last())) + + verify(activityManager).switchUser(userInfos.last().id) + Unit + } + + @Test + fun `onRecordSelected - enter guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + + underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) + + verify(dialogShower).dismiss() + verify(manager).createGuest(any()) + Unit + } + + @Test + fun `onRecordSelected - action`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) + + verify(dialogShower, never()).dismiss() + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action and manage user is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT + ) + ) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogShower: UserSwitchDialogController.DialogShower = mock() + + underTest.executeAction(UserActionModel.ADD_USER, dialogShower) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + dialogShower = dialogShower, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf<ShowDialogRequestModel?>() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser( + newlySelectedUserId = guestUserInfo.id, + dialogShower = dialogShower, + ) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + verify(dialogShower).dismiss() + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + + private fun assertUsers( + models: List<UserModel>?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun assertRecords( + records: List<UserRecord>, + userIds: List<Int>, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List<UserActionModel> = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + private fun assertRecordForAction( + record: UserRecord, + type: UserActionModel, + ) { + assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) + assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) + assertThat(record.isAddSupervisedUser) + .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List<UserInfo> { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val GUEST_ICON: Drawable = mock() + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt deleted file mode 100644 index 6a17c8ddc63d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt +++ /dev/null @@ -1,174 +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.systemui.user.domain.interactor - -import androidx.test.filters.SmallTest -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify - -@SmallTest -@RunWith(JUnit4::class) -open class UserInteractorUnrefactoredTest : UserInteractorTest() { - - override fun isRefactored(): Boolean { - return false - } - - @Before - override fun setUp() { - super.setUp() - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) - } - - private fun setActions() { - userRepository.setActions(UserActionModel.values().toList()) - } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 116023aca655..db136800a3cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.user.ui.viewmodel import android.app.ActivityManager import android.app.admin.DevicePolicyManager -import android.graphics.drawable.Drawable +import android.content.pm.UserInfo import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -27,32 +27,37 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.yield +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -60,11 +65,11 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class UserSwitcherViewModelTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var activityManager: ActivityManager @Mock private lateinit var manager: UserManager @@ -80,28 +85,47 @@ class UserSwitcherViewModelTest : SysuiTestCase() { private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var powerRepository: FakePowerRepository + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private lateinit var injectedScope: CoroutineScope + @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + whenever(manager.getUserSwitchability(any())) + .thenReturn(UserManager.SWITCHABILITY_STATUS_OK) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_PACKAGE, + ) + testDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(testDispatcher) + injectedScope = CoroutineScope(testScope.coroutineContext + SupervisorJob()) userRepository = FakeUserRepository() + runBlocking { + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + ) + ) + } + keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() - val featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) - val scope = TestCoroutineScope() val refreshUsersScheduler = RefreshUsersScheduler( - applicationScope = scope, - mainDispatcher = IMMEDIATE, + applicationScope = injectedScope, + mainDispatcher = testDispatcher, repository = userRepository, ) val guestUserInteractor = GuestUserInteractor( applicationContext = context, - applicationScope = scope, - mainDispatcher = IMMEDIATE, - backgroundDispatcher = IMMEDIATE, + applicationScope = injectedScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, manager = manager, repository = userRepository, deviceProvisionedController = deviceProvisionedController, @@ -118,21 +142,19 @@ class UserSwitcherViewModelTest : SysuiTestCase() { UserInteractor( applicationContext = context, repository = userRepository, - controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, ), - featureFlags = featureFlags, manager = manager, - applicationScope = scope, + applicationScope = injectedScope, telephonyInteractor = TelephonyInteractor( repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, - backgroundDispatcher = IMMEDIATE, + backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = guestUserInteractor, @@ -141,222 +163,216 @@ class UserSwitcherViewModelTest : SysuiTestCase() { PowerInteractor( repository = powerRepository, ), - featureFlags = featureFlags, guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @Test - fun users() = - runBlocking(IMMEDIATE) { - userRepository.setUsers( - listOf( - UserModel( - id = 0, - name = Text.Loaded("zero"), - image = USER_IMAGE, - isSelected = true, - isSelectable = true, - isGuest = false, - ), - UserModel( - id = 1, - name = Text.Loaded("one"), - image = USER_IMAGE, - isSelected = false, - isSelectable = true, - isGuest = false, - ), - UserModel( - id = 2, - name = Text.Loaded("two"), - image = USER_IMAGE, - isSelected = false, - isSelectable = false, - isGuest = false, - ), - ) + fun users() = selfCancelingTest { + val userInfos = + listOf( + UserInfo( + /* id= */ 0, + /* name= */ "zero", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 1, + /* name= */ "one", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 2, + /* name= */ "two", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_SYSTEM, + ), ) - - var userViewModels: List<UserViewModel>? = null - val job = underTest.users.onEach { userViewModels = it }.launchIn(this) - - assertThat(userViewModels).hasSize(3) - assertUserViewModel( - viewModel = userViewModels?.get(0), - viewKey = 0, - name = "zero", - isSelectionMarkerVisible = true, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA, - isClickable = true, - ) - assertUserViewModel( - viewModel = userViewModels?.get(1), - viewKey = 1, - name = "one", - isSelectionMarkerVisible = false, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA, - isClickable = true, - ) - assertUserViewModel( - viewModel = userViewModels?.get(2), - viewKey = 2, - name = "two", - isSelectionMarkerVisible = false, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA, - isClickable = false, - ) - job.cancel() - } + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + + val userViewModels = mutableListOf<List<UserViewModel>>() + val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } + + assertThat(userViewModels.last()).hasSize(3) + assertUserViewModel( + viewModel = userViewModels.last()[0], + viewKey = 0, + name = "zero", + isSelectionMarkerVisible = true, + ) + assertUserViewModel( + viewModel = userViewModels.last()[1], + viewKey = 1, + name = "one", + isSelectionMarkerVisible = false, + ) + assertUserViewModel( + viewModel = userViewModels.last()[2], + viewKey = 2, + name = "two", + isSelectionMarkerVisible = false, + ) + job.cancel() + } @Test - fun `maximumUserColumns - few users`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - var value: Int? = null - val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(4) - job.cancel() - } + fun `maximumUserColumns - few users`() = selfCancelingTest { + setUsers(count = 2) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + + assertThat(values.last()).isEqualTo(4) + + job.cancel() + } @Test - fun `maximumUserColumns - many users`() = - runBlocking(IMMEDIATE) { - setUsers(count = 5) - var value: Int? = null - val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(3) - job.cancel() - } + fun `maximumUserColumns - many users`() = selfCancelingTest { + setUsers(count = 5) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + + assertThat(values.last()).isEqualTo(3) + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - has actions - true`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) + fun `isOpenMenuButtonVisible - has actions - true`() = selfCancelingTest { + setUsers(2) - var isVisible: Boolean? = null - val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this) + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - assertThat(isVisible).isTrue() - job.cancel() - } + assertThat(isVisible.last()).isTrue() + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - no actions - false`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(emptyList()) + fun `isOpenMenuButtonVisible - no actions - false`() = selfCancelingTest { + val userInfos = setUsers(2) + userRepository.setSelectedUserInfo(userInfos[1]) + keyguardRepository.setKeyguardShowing(true) + whenever(manager.canAddMoreUsers(any())).thenReturn(false) - var isVisible: Boolean? = null - val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this) + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - assertThat(isVisible).isFalse() - job.cancel() - } + assertThat(isVisible.last()).isFalse() + job.cancel() + } @Test - fun menu() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - var isMenuVisible: Boolean? = null - val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this) - assertThat(isMenuVisible).isFalse() + fun menu() = selfCancelingTest { + val isMenuVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) } + assertThat(isMenuVisible.last()).isFalse() - underTest.onOpenMenuButtonClicked() - assertThat(isMenuVisible).isTrue() + underTest.onOpenMenuButtonClicked() + assertThat(isMenuVisible.last()).isTrue() - underTest.onMenuClosed() - assertThat(isMenuVisible).isFalse() + underTest.onMenuClosed() + assertThat(isMenuVisible.last()).isFalse() - job.cancel() - } + job.cancel() + } @Test - fun `menu actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - var actions: List<UserActionViewModel>? = null - val job = underTest.menu.onEach { actions = it }.launchIn(this) - - assertThat(actions?.map { it.viewKey }) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), - UserActionModel.ADD_USER.ordinal.toLong(), - UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), - ) + fun `menu actions`() = selfCancelingTest { + setUsers(2) + val actions = mutableListOf<List<UserActionViewModel>>() + val job = launch(testDispatcher) { underTest.menu.toList(actions) } + + assertThat(actions.last().map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), ) + ) - job.cancel() - } + job.cancel() + } @Test - fun `isFinishRequested - finishes when user is switched`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - userRepository.setSelectedUser(1) - yield() - assertThat(isFinishRequested).isTrue() - - job.cancel() - } + fun `isFinishRequested - finishes when user is switched`() = selfCancelingTest { + val userInfos = setUsers(count = 2) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + userRepository.setSelectedUserInfo(userInfos[1]) + + assertThat(isFinishRequested.last()).isTrue() + + job.cancel() + } @Test - fun `isFinishRequested - finishes when the screen turns off`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - powerRepository.setInteractive(true) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - powerRepository.setInteractive(false) - yield() - assertThat(isFinishRequested).isTrue() - - job.cancel() - } + fun `isFinishRequested - finishes when the screen turns off`() = selfCancelingTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + powerRepository.setInteractive(false) + + assertThat(isFinishRequested.last()).isTrue() + + job.cancel() + } @Test - fun `isFinishRequested - finishes when cancel button is clicked`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - powerRepository.setInteractive(true) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - underTest.onCancelButtonClicked() - yield() - assertThat(isFinishRequested).isTrue() - - underTest.onFinished() - yield() - assertThat(isFinishRequested).isFalse() - - job.cancel() - } + fun `isFinishRequested - finishes when cancel button is clicked`() = selfCancelingTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + underTest.onCancelButtonClicked() + + assertThat(isFinishRequested.last()).isTrue() + + underTest.onFinished() + + assertThat(isFinishRequested.last()).isFalse() - private suspend fun setUsers(count: Int) { - userRepository.setUsers( + job.cancel() + } + + private suspend fun setUsers(count: Int): List<UserInfo> { + val userInfos = (0 until count).map { index -> - UserModel( - id = index, - name = Text.Loaded("$index"), - image = USER_IMAGE, - isSelected = index == 0, - isSelectable = true, - isGuest = false, + UserInfo( + /* id= */ index, + /* name= */ "$index", + /* iconPath= */ "", + /* flags= */ if (index == 0) { + // This is the primary user. + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN + } else { + // This isn't the primary user. + 0 + }, + UserManager.USER_TYPE_FULL_SYSTEM, ) } - ) + userRepository.setUserInfos(userInfos) + + if (userInfos.isNotEmpty()) { + userRepository.setSelectedUserInfo(userInfos[0]) + } + return userInfos } private fun assertUserViewModel( @@ -364,19 +380,25 @@ class UserSwitcherViewModelTest : SysuiTestCase() { viewKey: Int, name: String, isSelectionMarkerVisible: Boolean, - alpha: Float, - isClickable: Boolean, ) { checkNotNull(viewModel) assertThat(viewModel.viewKey).isEqualTo(viewKey) assertThat(viewModel.name).isEqualTo(Text.Loaded(name)) assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible) - assertThat(viewModel.alpha).isEqualTo(alpha) - assertThat(viewModel.onClicked != null).isEqualTo(isClickable) + assertThat(viewModel.alpha) + .isEqualTo(LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA) + assertThat(viewModel.onClicked).isNotNull() } + private fun selfCancelingTest( + block: suspend TestScope.() -> Unit, + ): TestResult = + testScope.runTest { + block() + injectedScope.coroutineContext[Job.Key]?.cancelAndJoin() + } + companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - private val USER_IMAGE = mock<Drawable>() + private const val SUPERVISED_USER_CREATION_PACKAGE = "com.some.package" } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 4df8aa42ea2f..b7c8cbf40bea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -20,26 +20,15 @@ package com.android.systemui.user.data.repository import android.content.pm.UserInfo import android.os.UserHandle import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { - private val _users = MutableStateFlow<List<UserModel>>(emptyList()) - override val users: Flow<List<UserModel>> = _users.asStateFlow() - override val selectedUser: Flow<UserModel> = - users.map { models -> models.first { model -> model.isSelected } } - - private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) - override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() - private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings.asStateFlow() @@ -52,9 +41,6 @@ class FakeUserRepository : UserRepository { override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM - private val _isActionableWhenLocked = MutableStateFlow(false) - override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() - private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated @@ -100,35 +86,6 @@ class FakeUserRepository : UserRepository { yield() } - fun setUsers(models: List<UserModel>) { - _users.value = models - } - - suspend fun setSelectedUser(userId: Int) { - check(_users.value.find { it.id == userId } != null) { - "Cannot select a user with ID $userId - no user with that ID found!" - } - - setUsers( - _users.value.map { model -> - when { - model.isSelected && model.id != userId -> model.copy(isSelected = false) - !model.isSelected && model.id == userId -> model.copy(isSelected = true) - else -> model - } - } - ) - yield() - } - - fun setActions(models: List<UserActionModel>) { - _actions.value = models - } - - fun setActionableWhenLocked(value: Boolean) { - _isActionableWhenLocked.value = value - } - fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index c128b5ead406..4f1efd627e40 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -528,7 +528,7 @@ public class AccountManagerService private Map<Account, Integer> getAccountsAndVisibilityForPackage(String packageName, List<String> accountTypes, Integer callingUid, UserAccounts accounts) { if (!packageExistsForUser(packageName, accounts.userId)) { - Log.d(TAG, "Package not found " + packageName); + Log.w(TAG, "getAccountsAndVisibilityForPackage#Package not found " + packageName); return new LinkedHashMap<>(); } @@ -677,7 +677,7 @@ public class AccountManagerService restoreCallingIdentity(identityToken); } } catch (NameNotFoundException e) { - Log.d(TAG, "Package not found " + e.getMessage()); + Log.w(TAG, "resolveAccountVisibility#Package not found " + e.getMessage()); return AccountManager.VISIBILITY_NOT_VISIBLE; } @@ -756,7 +756,7 @@ public class AccountManagerService } return true; } catch (NameNotFoundException e) { - Log.d(TAG, "Package not found " + e.getMessage()); + Log.w(TAG, "isPreOApplication#Package not found " + e.getMessage()); return true; } } @@ -4063,7 +4063,7 @@ public class AccountManagerService int uid = mPackageManager.getPackageUidAsUser(packageName, userId); return hasAccountAccess(account, packageName, uid); } catch (NameNotFoundException e) { - Log.d(TAG, "Package not found " + e.getMessage()); + Log.w(TAG, "hasAccountAccess#Package not found " + e.getMessage()); return false; } } @@ -4195,7 +4195,7 @@ public class AccountManagerService } final long token = Binder.clearCallingIdentity(); try { - AccountAndUser[] allAccounts = getAllAccounts(); + AccountAndUser[] allAccounts = getAllAccountsForSystemProcess(); for (int i = allAccounts.length - 1; i >= 0; i--) { if (allAccounts[i].account.equals(account)) { return true; @@ -4345,10 +4345,11 @@ public class AccountManagerService /** * Returns accounts for all running users, ignores visibility values. * + * Should only be called by System process. * @hide */ @NonNull - public AccountAndUser[] getRunningAccounts() { + public AccountAndUser[] getRunningAccountsForSystem() { final int[] runningUserIds; try { runningUserIds = ActivityManager.getService().getRunningUserIds(); @@ -4356,26 +4357,34 @@ public class AccountManagerService // Running in system_server; should never happen throw new RuntimeException(e); } - return getAccounts(runningUserIds); + return getAccountsForSystem(runningUserIds); } /** * Returns accounts for all users, ignores visibility values. * + * Should only be called by system process + * * @hide */ @NonNull - public AccountAndUser[] getAllAccounts() { + public AccountAndUser[] getAllAccountsForSystemProcess() { final List<UserInfo> users = getUserManager().getAliveUsers(); final int[] userIds = new int[users.size()]; for (int i = 0; i < userIds.length; i++) { userIds[i] = users.get(i).id; } - return getAccounts(userIds); + return getAccountsForSystem(userIds); } + /** + * Returns all accounts for the given user, ignores all visibility checks. + * This should only be called by system process. + * + * @hide + */ @NonNull - private AccountAndUser[] getAccounts(int[] userIds) { + private AccountAndUser[] getAccountsForSystem(int[] userIds) { final ArrayList<AccountAndUser> runningAccounts = Lists.newArrayList(); for (int userId : userIds) { UserAccounts userAccounts = getUserAccounts(userId); @@ -4384,7 +4393,7 @@ public class AccountManagerService userAccounts, null /* type */, Binder.getCallingUid(), - null /* packageName */, + "android"/* packageName */, false /* include managed not visible*/); for (Account account : accounts) { runningAccounts.add(new AccountAndUser(account, userId)); @@ -5355,7 +5364,7 @@ public class AccountManagerService } } else { Account[] accounts = getAccountsFromCache(userAccounts, null /* type */, - Process.SYSTEM_UID, null /* packageName */, false); + Process.SYSTEM_UID, "android" /* packageName */, false); fout.println("Accounts: " + accounts.length); for (Account account : accounts) { fout.println(" " + account.toString()); @@ -5550,7 +5559,7 @@ public class AccountManagerService return true; } } catch (PackageManager.NameNotFoundException e) { - Log.d(TAG, "Package not found " + e.getMessage()); + Log.w(TAG, "isPrivileged#Package not found " + e.getMessage()); } } } finally { @@ -6074,7 +6083,7 @@ public class AccountManagerService } } } catch (NameNotFoundException e) { - Log.d(TAG, "Package not found " + e.getMessage()); + Log.w(TAG, "filterSharedAccounts#Package not found " + e.getMessage()); } Map<Account, Integer> filtered = new LinkedHashMap<>(); for (Map.Entry<Account, Integer> entry : unfiltered.entrySet()) { diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 73afa60e8510..eb81e70363d4 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -2215,7 +2215,8 @@ public class SyncManager { pw.print("Storage low: "); pw.println(storageLowIntent != null); pw.print("Clock valid: "); pw.println(mSyncStorageEngine.isClockValid()); - final AccountAndUser[] accounts = AccountManagerService.getSingleton().getAllAccounts(); + final AccountAndUser[] accounts = + AccountManagerService.getSingleton().getAllAccountsForSystemProcess(); pw.print("Accounts: "); if (accounts != INITIAL_ACCOUNTS_ARRAY) { @@ -3274,7 +3275,8 @@ public class SyncManager { private void updateRunningAccountsH(EndPoint syncTargets) { synchronized (mAccountsLock) { AccountAndUser[] oldAccounts = mRunningAccounts; - mRunningAccounts = AccountManagerService.getSingleton().getRunningAccounts(); + mRunningAccounts = + AccountManagerService.getSingleton().getRunningAccountsForSystem(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Slog.v(TAG, "Accounts list: "); for (AccountAndUser acc : mRunningAccounts) { @@ -3316,7 +3318,8 @@ public class SyncManager { } // Cancel all jobs from non-existent accounts. - AccountAndUser[] allAccounts = AccountManagerService.getSingleton().getAllAccounts(); + AccountAndUser[] allAccounts = + AccountManagerService.getSingleton().getAllAccountsForSystemProcess(); List<SyncOperation> ops = getAllPendingSyncs(); for (int i = 0, opsSize = ops.size(); i < opsSize; i++) { SyncOperation op = ops.get(i); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 725fb3fec616..377a651eb031 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1033,7 +1033,7 @@ public final class PowerManagerService extends SystemService super(context); mContext = context; - mBinderService = new BinderService(); + mBinderService = new BinderService(mContext); mLocalService = new LocalService(); mNativeWrapper = injector.createNativeWrapper(); mSystemProperties = injector.createSystemPropertiesWrapper(); @@ -5465,12 +5465,17 @@ public final class PowerManagerService extends SystemService @VisibleForTesting final class BinderService extends IPowerManager.Stub { + private final PowerManagerShellCommand mShellCommand; + + BinderService(Context context) { + mShellCommand = new PowerManagerShellCommand(context, this); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - (new PowerManagerShellCommand(this)).exec( - this, in, out, err, args, callback, resultReceiver); + mShellCommand.exec(this, in, out, err, args, callback, resultReceiver); } @Override // Binder call diff --git a/services/core/java/com/android/server/power/PowerManagerShellCommand.java b/services/core/java/com/android/server/power/PowerManagerShellCommand.java index a9b33ed58ef7..9439b762fde0 100644 --- a/services/core/java/com/android/server/power/PowerManagerShellCommand.java +++ b/services/core/java/com/android/server/power/PowerManagerShellCommand.java @@ -16,10 +16,15 @@ package com.android.server.power; +import android.content.Context; import android.content.Intent; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.ShellCommand; +import android.util.SparseArray; +import android.view.Display; import java.io.PrintWriter; import java.util.List; @@ -27,9 +32,13 @@ import java.util.List; class PowerManagerShellCommand extends ShellCommand { private static final int LOW_POWER_MODE_ON = 1; - final PowerManagerService.BinderService mService; + private final Context mContext; + private final PowerManagerService.BinderService mService; - PowerManagerShellCommand(PowerManagerService.BinderService service) { + private SparseArray<WakeLock> mProxWakelocks = new SparseArray<>(); + + PowerManagerShellCommand(Context context, PowerManagerService.BinderService service) { + mContext = context; mService = service; } @@ -52,6 +61,8 @@ class PowerManagerShellCommand extends ShellCommand { return runSuppressAmbientDisplay(); case "list-ambient-display-suppression-tokens": return runListAmbientDisplaySuppressionTokens(); + case "set-prox": + return runSetProx(); default: return handleDefaultCommands(cmd); } @@ -117,6 +128,56 @@ class PowerManagerShellCommand extends ShellCommand { return 0; } + + /** TODO: Consider updating this code to support all wakelock types. */ + private int runSetProx() throws RemoteException { + PrintWriter pw = getOutPrintWriter(); + final boolean acquire; + switch (getNextArgRequired().toLowerCase()) { + case "list": + pw.println("Wakelocks:"); + pw.println(mProxWakelocks); + return 0; + case "acquire": + acquire = true; + break; + case "release": + acquire = false; + break; + default: + pw.println("Error: Allowed options are 'list' 'enable' and 'disable'."); + return -1; + } + + int displayId = Display.INVALID_DISPLAY; + String displayOption = getNextArg(); + if ("-d".equals(displayOption)) { + String idStr = getNextArg(); + displayId = Integer.parseInt(idStr); + if (displayId < 0) { + pw.println("Error: Specified displayId (" + idStr + ") must a non-negative int."); + return -1; + } + } + + int wakelockIndex = displayId + 1; // SparseArray doesn't support negative indexes + WakeLock wakelock = mProxWakelocks.get(wakelockIndex); + if (wakelock == null) { + PowerManager pm = mContext.getSystemService(PowerManager.class); + wakelock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, + "PowerManagerShellCommand[" + displayId + "]", displayId); + mProxWakelocks.put(wakelockIndex, wakelock); + } + + if (acquire) { + wakelock.acquire(); + } else { + wakelock.release(); + } + pw.println(wakelock); + return 0; + } + @Override public void onHelp() { final PrintWriter pw = getOutPrintWriter(); @@ -138,6 +199,11 @@ class PowerManagerShellCommand extends ShellCommand { pw.println(" ambient display"); pw.println(" list-ambient-display-suppression-tokens"); pw.println(" prints the tokens used to suppress ambient display"); + pw.println(" set-prox [list|acquire|release] (-d <display_id>)"); + pw.println(" Acquires the proximity sensor wakelock. Wakelock is associated with"); + pw.println(" a specific display if specified. 'list' lists wakelocks previously"); + pw.println(" created by set-prox including their held status."); + pw.println(); Intent.printIntentArgsHelp(pw , ""); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index dc69ca6bc0cf..2387e25c270c 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2247,12 +2247,6 @@ class ActivityStarter { ? targetTask.getTopNonFinishingActivity() : targetTaskTop; - // At this point we are certain we want the task moved to the front. If we need to dismiss - // any other always-on-top root tasks, now is the time to do it. - if (targetTaskTop.canTurnScreenOn() && mService.isDreaming()) { - targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); - } - if (mMovedToFront) { // We moved the task to front, use starting window to hide initial drawn delay. targetTaskTop.showStartingWindow(true /* taskSwitch */); @@ -2264,6 +2258,12 @@ class ActivityStarter { // And for paranoia, make sure we have correctly resumed the top activity. resumeTargetRootTaskIfNeeded(); + // This is moving an existing task to front. But since dream activity has a higher z-order + // to cover normal activities, it needs the awakening event to be dismissed. + if (mService.isDreaming() && targetTaskTop.canTurnScreenOn()) { + targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); + } + mLastStartActivityRecord = targetTaskTop; return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP; } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 377c5b41fe35..93fcc202368c 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -289,6 +289,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { private final IBinder mFragmentToken; /** + * Whether to delay the call to {@link #updateOrganizedTaskFragmentSurface()} when there is a + * configuration change. + */ + private boolean mDelayOrganizedTaskFragmentSurfaceUpdate; + + /** * Whether to delay the last activity of TaskFragment being immediately removed while finishing. * This should only be set on a embedded TaskFragment, where the organizer can have the * opportunity to perform animations and finishing the adjacent TaskFragment. @@ -2273,35 +2279,41 @@ class TaskFragment extends WindowContainer<WindowContainer> { @Override public void onConfigurationChanged(Configuration newParentConfig) { - // Task will animate differently. - if (mTaskFragmentOrganizer != null) { - mTmpPrevBounds.set(getBounds()); - } - super.onConfigurationChanged(newParentConfig); - final boolean shouldStartChangeTransition = shouldStartChangeTransition(mTmpPrevBounds); - if (shouldStartChangeTransition) { - initializeChangeTransition(mTmpPrevBounds); - } if (mTaskFragmentOrganizer != null) { - if (mTransitionController.isShellTransitionsEnabled() - && !mTransitionController.isCollecting(this)) { - // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so - // update the surface here if it is not collected by Shell transition. - updateOrganizedTaskFragmentSurface(); - } else if (!mTransitionController.isShellTransitionsEnabled() - && !shouldStartChangeTransition) { - // Update the surface here instead of in the organizer so that we can make sure - // it can be synced with the surface freezer for legacy app transition. - updateOrganizedTaskFragmentSurface(); - } + updateOrganizedTaskFragmentSurface(); } sendTaskFragmentInfoChanged(); } + void deferOrganizedTaskFragmentSurfaceUpdate() { + mDelayOrganizedTaskFragmentSurfaceUpdate = true; + } + + void continueOrganizedTaskFragmentSurfaceUpdate() { + mDelayOrganizedTaskFragmentSurfaceUpdate = false; + updateOrganizedTaskFragmentSurface(); + } + private void updateOrganizedTaskFragmentSurface() { + if (mDelayOrganizedTaskFragmentSurfaceUpdate) { + return; + } + if (mTransitionController.isShellTransitionsEnabled() + && !mTransitionController.isCollecting(this)) { + // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so + // update the surface here if it is not collected by Shell transition. + updateOrganizedTaskFragmentSurfaceUnchecked(); + } else if (!mTransitionController.isShellTransitionsEnabled() && !isAnimating()) { + // Update the surface here instead of in the organizer so that we can make sure + // it can be synced with the surface freezer for legacy app transition. + updateOrganizedTaskFragmentSurfaceUnchecked(); + } + } + + private void updateOrganizedTaskFragmentSurfaceUnchecked() { final SurfaceControl.Transaction t = getSyncTransaction(); updateSurfacePosition(t); updateOrganizedTaskFragmentSurfaceSize(t, false /* forceUpdate */); @@ -2355,7 +2367,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { } /** Whether we should prepare a transition for this {@link TaskFragment} bounds change. */ - private boolean shouldStartChangeTransition(Rect startBounds) { + boolean shouldStartChangeTransition(Rect startBounds) { if (mTaskFragmentOrganizer == null || !canStartChangeTransition()) { return false; } @@ -2375,7 +2387,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { void setSurfaceControl(SurfaceControl sc) { super.setSurfaceControl(sc); if (mTaskFragmentOrganizer != null) { - updateOrganizedTaskFragmentSurface(); + updateOrganizedTaskFragmentSurfaceUnchecked(); // If the TaskFragmentOrganizer was set before we created the SurfaceControl, we need to // emit the callbacks now. sendTaskFragmentAppeared(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 32a110ea530e..007628f49651 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -48,6 +48,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANI import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; @@ -146,6 +147,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub @VisibleForTesting final ArrayMap<IBinder, TaskFragment> mLaunchTaskFragments = new ArrayMap<>(); + private final Rect mTmpBounds = new Rect(); + WindowOrganizerController(ActivityTaskManagerService atm) { mService = atm; mGlobalLock = atm.mGlobalLock; @@ -710,7 +713,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) { - int effects = 0; + int effects = applyChanges(tr, c, null /* errorCallbackToken */); final SurfaceControl.Transaction t = c.getBoundsChangeTransaction(); if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) { @@ -725,6 +728,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects = TRANSACT_EFFECTS_LIFECYCLE; } + if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING) != 0) { + tr.setDragResizing(c.getDragResizing(), DRAG_RESIZE_MODE_FREEFORM); + } + final int childWindowingMode = c.getActivityWindowingMode(); if (childWindowingMode > -1) { tr.setActivityWindowingMode(childWindowingMode); @@ -767,6 +774,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private int applyDisplayAreaChanges(DisplayArea displayArea, WindowContainerTransaction.Change c) { final int[] effects = new int[1]; + effects[0] = applyChanges(displayArea, c, null /* errorCallbackToken */); if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_IGNORE_ORIENTATION_REQUEST) != 0) { @@ -787,6 +795,27 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects[0]; } + private int applyTaskFragmentChanges(@NonNull TaskFragment taskFragment, + @NonNull WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { + if (taskFragment.isEmbeddedTaskFragmentInPip()) { + // No override from organizer for embedded TaskFragment in a PIP Task. + return 0; + } + + // When the TaskFragment is resized, we may want to create a change transition for it, for + // which we want to defer the surface update until we determine whether or not to start + // change transition. + mTmpBounds.set(taskFragment.getBounds()); + taskFragment.deferOrganizedTaskFragmentSurfaceUpdate(); + final int effects = applyChanges(taskFragment, c, errorCallbackToken); + if (taskFragment.shouldStartChangeTransition(mTmpBounds)) { + taskFragment.initializeChangeTransition(mTmpBounds); + } + taskFragment.continueOrganizedTaskFragmentSurfaceUpdate(); + mTmpBounds.set(0, 0, 0, 0); + return effects; + } + private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects, int syncId, @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, @@ -1452,20 +1481,15 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private int applyWindowContainerChange(WindowContainer wc, WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { sanitizeWindowContainer(wc); - if (wc.asTaskFragment() != null && wc.asTaskFragment().isEmbeddedTaskFragmentInPip()) { - // No override from organizer for embedded TaskFragment in a PIP Task. - return 0; - } - - int effects = applyChanges(wc, c, errorCallbackToken); - - if (wc instanceof DisplayArea) { - effects |= applyDisplayAreaChanges(wc.asDisplayArea(), c); - } else if (wc instanceof Task) { - effects |= applyTaskChanges(wc.asTask(), c); + if (wc.asDisplayArea() != null) { + return applyDisplayAreaChanges(wc.asDisplayArea(), c); + } else if (wc.asTask() != null) { + return applyTaskChanges(wc.asTask(), c); + } else if (wc.asTaskFragment() != null) { + return applyTaskFragmentChanges(wc.asTaskFragment(), c, errorCallbackToken); + } else { + return applyChanges(wc, c, errorCallbackToken); } - - return effects; } @Override diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index bf751646da37..45606f965858 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1537,10 +1537,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mWmService.makeWindowFreezingScreenIfNeededLocked(this); // If the orientation is changing, or we're starting or ending a drag resizing action, - // then we need to hold off on unfreezing the display until this window has been - // redrawn; to do that, we need to go through the process of getting informed by the - // application when it has finished drawing. - if (getOrientationChanging() || dragResizingChanged) { + // or we're resizing an embedded Activity, then we need to hold off on unfreezing the + // display until this window has been redrawn; to do that, we need to go through the + // process of getting informed by the application when it has finished drawing. + if (getOrientationChanging() || dragResizingChanged + || isEmbeddedActivityResizeChanged()) { if (dragResizingChanged) { ProtoLog.v(WM_DEBUG_RESIZE, "Resize start waiting for draw, " @@ -4147,6 +4148,20 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mActivityRecord == null || mActivityRecord.isFullyTransparentBarAllowed(frame); } + /** + * Whether this window belongs to a resizing embedded activity. + */ + private boolean isEmbeddedActivityResizeChanged() { + if (mActivityRecord == null || !isVisibleRequested()) { + // No need to update if the window is in the background. + return false; + } + + final TaskFragment embeddedTaskFragment = mActivityRecord.getOrganizedTaskFragment(); + return embeddedTaskFragment != null + && mDisplayContent.mChangingContainers.contains(embeddedTaskFragment); + } + boolean isDragResizeChanged() { return mDragResizing != computeDragResizing(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 496f6817bb08..6fe2d2cbe9d4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1131,6 +1131,26 @@ public class ActivityStarterTests extends WindowTestsBase { } @Test + public void testRecycleTaskWakeUpWhenDreaming() { + doNothing().when(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString()); + doReturn(true).when(mWm.mAtmService).isDreaming(); + final ActivityStarter starter = prepareStarter(0 /* flags */); + final ActivityRecord target = new ActivityBuilder(mAtm).setCreateTask(true).build(); + starter.mStartActivity = target; + target.mVisibleRequested = false; + target.setTurnScreenOn(true); + // Assume the flag was consumed by relayout. + target.setCurrentLaunchCanTurnScreenOn(false); + startActivityInner(starter, target, null /* source */, null /* options */, + null /* inTask */, null /* inTaskFragment */); + // The flag should be set again when resuming (from recycleTask) the target as top. + assertTrue(target.currentLaunchCanTurnScreenOn()); + // In real case, dream activity has a higher priority (TaskDisplayArea#getPriority) that + // will be put at a higher z-order. So it relies on wakeUp() to be dismissed. + verify(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString()); + } + + @Test public void testTargetTaskInSplitScreen() { final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_LAUNCH_ADJACENT, false /* mockGetRootTask */); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 83f17897eb62..3ff2c0e0d024 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -118,10 +118,13 @@ public class TaskFragmentTest extends WindowTestsBase { doReturn(true).when(mTaskFragment).isVisibleRequested(); clearInvocations(mTransaction); + mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate(); mTaskFragment.setBounds(endBounds); + assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds)); + mTaskFragment.initializeChangeTransition(startBounds); + mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate(); // Surface reset when prepare transition. - verify(mTaskFragment).initializeChangeTransition(startBounds); verify(mTransaction).setPosition(mLeash, 0, 0); verify(mTransaction).setWindowCrop(mLeash, 0, 0); @@ -166,7 +169,7 @@ public class TaskFragmentTest extends WindowTestsBase { mTaskFragment.setBounds(endBounds); - verify(mTaskFragment, never()).initializeChangeTransition(any()); + assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds)); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 9bcc1367f8ab..04d873453b2d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -95,6 +95,8 @@ import android.view.InsetsState; import android.view.InsetsVisibilities; import android.view.SurfaceControl; import android.view.WindowManager; +import android.window.ITaskFragmentOrganizer; +import android.window.TaskFragmentOrganizer; import androidx.test.filters.SmallTest; @@ -798,6 +800,39 @@ public class WindowStateTests extends WindowTestsBase { } @Test + public void testEmbeddedActivityResizing_clearAllDrawn() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + mAtm.mTaskFragmentOrganizerController.registerOrganizer( + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder())); + final Task task = createTask(mDisplayContent); + final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity(); + final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity, + "App window"); + doReturn(true).when(embeddedActivity).isVisible(); + embeddedActivity.mVisibleRequested = true; + makeWindowVisible(win); + win.mLayoutSeq = win.getDisplayContent().mLayoutSeq; + // Set the bounds twice: + // 1. To make sure there is no orientation change after #reportResized, which can also cause + // #clearAllDrawn. + // 2. Make #isLastConfigReportedToClient to be false after #reportResized, so it can process + // to check if we need redraw. + embeddedTf.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + embeddedTf.setBounds(0, 0, 1000, 2000); + win.reportResized(); + embeddedTf.setBounds(500, 0, 1000, 2000); + + // Clear all drawn when the embedded TaskFragment is in mDisplayContent.mChangingContainers. + win.updateResizingWindowIfNeeded(); + verify(embeddedActivity, never()).clearAllDrawn(); + + mDisplayContent.mChangingContainers.add(embeddedTf); + win.updateResizingWindowIfNeeded(); + verify(embeddedActivity).clearAllDrawn(); + } + + @Test public void testCantReceiveTouchWhenAppTokenHiddenRequested() { final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0"); win0.mActivityRecord.mVisibleRequested = false; diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java new file mode 100644 index 000000000000..d5eea1f3ff35 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java @@ -0,0 +1,233 @@ +/* + * 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.voiceinteraction; + +import static android.app.AppOpsManager.MODE_ALLOWED; + +import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG; + +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.media.permission.Identity; +import android.os.ParcelFileDescriptor; +import android.service.voice.HotwordAudioStream; +import android.service.voice.HotwordDetectedResult; +import android.util.Pair; +import android.util.Slog; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +final class HotwordAudioStreamManager { + + private static final String TAG = "HotwordAudioStreamManager"; + private static final String OP_MESSAGE = "Streaming hotword audio to VoiceInteractionService"; + private static final String TASK_ID_PREFIX = "HotwordDetectedResult@"; + private static final String THREAD_NAME_PREFIX = "Copy-"; + + private final AppOpsManager mAppOpsManager; + private final Identity mVoiceInteractorIdentity; + private final ExecutorService mExecutorService = Executors.newCachedThreadPool(); + + HotwordAudioStreamManager(@NonNull AppOpsManager appOpsManager, + @NonNull Identity voiceInteractorIdentity) { + mAppOpsManager = appOpsManager; + mVoiceInteractorIdentity = voiceInteractorIdentity; + } + + /** + * Starts copying the audio streams in the given {@link HotwordDetectedResult}. + * <p> + * The returned {@link HotwordDetectedResult} is identical the one that was passed in, except + * that the {@link ParcelFileDescriptor}s within {@link HotwordDetectedResult#getAudioStreams()} + * are replaced with descriptors from pipes managed by {@link HotwordAudioStreamManager}. The + * returned value should be passed on to the client (i.e., the voice interactor). + * </p> + * + * @throws IOException If there was an error creating the managed pipe. + */ + @NonNull + public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result) + throws IOException { + List<HotwordAudioStream> audioStreams = result.getAudioStreams(); + if (audioStreams.isEmpty()) { + return result; + } + + List<HotwordAudioStream> newAudioStreams = new ArrayList<>(audioStreams.size()); + List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> sourcesAndSinks = new ArrayList<>( + audioStreams.size()); + for (HotwordAudioStream audioStream : audioStreams) { + ParcelFileDescriptor[] clientPipe = ParcelFileDescriptor.createReliablePipe(); + ParcelFileDescriptor clientAudioSource = clientPipe[0]; + ParcelFileDescriptor clientAudioSink = clientPipe[1]; + HotwordAudioStream newAudioStream = + audioStream.buildUpon().setAudioStreamParcelFileDescriptor( + clientAudioSource).build(); + newAudioStreams.add(newAudioStream); + + ParcelFileDescriptor serviceAudioSource = + audioStream.getAudioStreamParcelFileDescriptor(); + sourcesAndSinks.add(new Pair<>(serviceAudioSource, clientAudioSink)); + } + + String resultTaskId = TASK_ID_PREFIX + System.identityHashCode(result); + mExecutorService.execute(new HotwordDetectedResultCopyTask(resultTaskId, sourcesAndSinks)); + + return result.buildUpon().setAudioStreams(newAudioStreams).build(); + } + + private class HotwordDetectedResultCopyTask implements Runnable { + private final String mResultTaskId; + private final List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> mSourcesAndSinks; + private final ExecutorService mExecutorService = Executors.newCachedThreadPool(); + + HotwordDetectedResultCopyTask(String resultTaskId, + List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> sourcesAndSinks) { + mResultTaskId = resultTaskId; + mSourcesAndSinks = sourcesAndSinks; + } + + @Override + public void run() { + Thread.currentThread().setName(THREAD_NAME_PREFIX + mResultTaskId); + int size = mSourcesAndSinks.size(); + List<SingleAudioStreamCopyTask> tasks = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Pair<ParcelFileDescriptor, ParcelFileDescriptor> sourceAndSink = + mSourcesAndSinks.get(i); + ParcelFileDescriptor serviceAudioSource = sourceAndSink.first; + ParcelFileDescriptor clientAudioSink = sourceAndSink.second; + String streamTaskId = mResultTaskId + "@" + i; + tasks.add(new SingleAudioStreamCopyTask(streamTaskId, serviceAudioSource, + clientAudioSink)); + } + + if (mAppOpsManager.startOpNoThrow(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, + mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName, + mVoiceInteractorIdentity.attributionTag, OP_MESSAGE) == MODE_ALLOWED) { + try { + // TODO(b/244599891): Set timeout, close after inactivity + mExecutorService.invokeAll(tasks); + } catch (InterruptedException e) { + Slog.e(TAG, mResultTaskId + ": Task was interrupted", e); + bestEffortPropagateError(e.getMessage()); + } finally { + mAppOpsManager.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, + mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName, + mVoiceInteractorIdentity.attributionTag); + } + } else { + bestEffortPropagateError( + "Failed to obtain RECORD_AUDIO_HOTWORD permission for " + + SoundTriggerSessionPermissionsDecorator.toString( + mVoiceInteractorIdentity)); + } + } + + private void bestEffortPropagateError(@NonNull String errorMessage) { + try { + for (Pair<ParcelFileDescriptor, ParcelFileDescriptor> sourceAndSink : + mSourcesAndSinks) { + ParcelFileDescriptor serviceAudioSource = sourceAndSink.first; + ParcelFileDescriptor clientAudioSink = sourceAndSink.second; + serviceAudioSource.closeWithError(errorMessage); + clientAudioSink.closeWithError(errorMessage); + } + } catch (IOException e) { + Slog.e(TAG, mResultTaskId + ": Failed to propagate error", e); + } + } + } + + private static class SingleAudioStreamCopyTask implements Callable<Void> { + // TODO: Make this buffer size customizable from updateState() + private static final int COPY_BUFFER_LENGTH = 2_560; + + private final String mStreamTaskId; + private final ParcelFileDescriptor mAudioSource; + private final ParcelFileDescriptor mAudioSink; + + SingleAudioStreamCopyTask(String streamTaskId, ParcelFileDescriptor audioSource, + ParcelFileDescriptor audioSink) { + mStreamTaskId = streamTaskId; + mAudioSource = audioSource; + mAudioSink = audioSink; + } + + @Override + public Void call() throws Exception { + Thread.currentThread().setName(THREAD_NAME_PREFIX + mStreamTaskId); + + // Note: We are intentionally NOT using try-with-resources here. If we did, + // the ParcelFileDescriptors will be automatically closed WITHOUT errors before we go + // into the IOException-catch block. We want to propagate the error while closing the + // PFDs. + InputStream fis = null; + OutputStream fos = null; + try { + fis = new ParcelFileDescriptor.AutoCloseInputStream(mAudioSource); + fos = new ParcelFileDescriptor.AutoCloseOutputStream(mAudioSink); + byte[] buffer = new byte[COPY_BUFFER_LENGTH]; + while (true) { + if (Thread.interrupted()) { + Slog.e(TAG, + mStreamTaskId + ": SingleAudioStreamCopyTask task was interrupted"); + break; + } + + int bytesRead = fis.read(buffer); + if (bytesRead < 0) { + Slog.i(TAG, mStreamTaskId + ": Reached end of audio stream"); + break; + } + if (bytesRead > 0) { + if (DEBUG) { + // TODO(b/244599440): Add proper logging + Slog.d(TAG, mStreamTaskId + ": Copied " + bytesRead + + " bytes from audio stream. First 20 bytes=" + Arrays.toString( + Arrays.copyOfRange(buffer, 0, 20))); + } + fos.write(buffer, 0, bytesRead); + } + // TODO(b/244599891): Close PFDs after inactivity + } + } catch (IOException e) { + mAudioSource.closeWithError(e.getMessage()); + mAudioSink.closeWithError(e.getMessage()); + Slog.e(TAG, mStreamTaskId + ": Failed to copy audio stream", e); + } finally { + if (fis != null) { + fis.close(); + } + if (fos != null) { + fos.close(); + } + } + + return null; + } + } + +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index a6e1a3256cb6..ee8070888725 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -59,6 +59,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppOpsManager; import android.attention.AttentionManagerInternal; import android.content.ComponentName; import android.content.ContentCaptureOptions; @@ -137,6 +138,7 @@ final class HotwordDetectionConnection { // The error codes are used for onError callback private static final int HOTWORD_DETECTION_SERVICE_DIED = -1; private static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2; + private static final int CALLBACK_ONDETECTED_STREAM_COPY_ERROR = -4; // Hotword metrics private static final int METRICS_INIT_UNKNOWN_TIMEOUT = @@ -168,6 +170,8 @@ final class HotwordDetectionConnection { // TODO: This may need to be a Handler(looper) private final ScheduledExecutorService mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + private final AppOpsManager mAppOpsManager; + private final HotwordAudioStreamManager mHotwordAudioStreamManager; @Nullable private final ScheduledFuture<?> mCancellationTaskFuture; private final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false); private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied; @@ -228,6 +232,9 @@ final class HotwordDetectionConnection { mContext = context; mVoiceInteractionServiceUid = voiceInteractionServiceUid; mVoiceInteractorIdentity = voiceInteractorIdentity; + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); + mHotwordAudioStreamManager = new HotwordAudioStreamManager(mAppOpsManager, + mVoiceInteractorIdentity); mDetectionComponentName = serviceName; mUser = userId; mCallback = callback; @@ -482,13 +489,19 @@ final class HotwordDetectionConnection { return; } saveProximityMetersToBundle(result); - mSoftwareCallback.onDetected(result, null, null); - if (result != null) { - Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, "Egressed detected result: " + result); - } + HotwordDetectedResult newResult; + try { + newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result); + } catch (IOException e) { + // TODO: Write event + mSoftwareCallback.onError(); + return; + } + mSoftwareCallback.onDetected(newResult, null, null); + Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult) + + " bits from hotword trusted process"); + if (mDebugHotwordLogging) { + Slog.i(TAG, "Egressed detected result: " + newResult); } } } @@ -660,6 +673,7 @@ final class HotwordDetectionConnection { try { enforcePermissionsForDataDelivery(); } catch (SecurityException e) { + Slog.i(TAG, "Ignoring #onDetected due to a SecurityException", e); HotwordMetricsLogger.writeKeyphraseTriggerEvent( mDetectorType, METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION); @@ -667,13 +681,19 @@ final class HotwordDetectionConnection { return; } saveProximityMetersToBundle(result); - externalCallback.onKeyphraseDetected(recognitionEvent, result); - if (result != null) { - Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, "Egressed detected result: " + result); - } + HotwordDetectedResult newResult; + try { + newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result); + } catch (IOException e) { + // TODO: Write event + externalCallback.onError(CALLBACK_ONDETECTED_STREAM_COPY_ERROR); + return; + } + externalCallback.onKeyphraseDetected(recognitionEvent, newResult); + Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult) + + " bits from hotword trusted process"); + if (mDebugHotwordLogging) { + Slog.i(TAG, "Egressed detected result: " + newResult); } } } @@ -757,6 +777,7 @@ final class HotwordDetectionConnection { } private void restartProcessLocked() { + // TODO(b/244598068): Check HotwordAudioStreamManager first Slog.v(TAG, "Restarting hotword detection process"); ServiceConnection oldConnection = mRemoteHotwordDetectionService; HotwordDetectionServiceIdentity previousIdentity = mIdentity; @@ -991,16 +1012,24 @@ final class HotwordDetectionConnection { callback.onError(); return; } - callback.onDetected(triggerResult, null /* audioFormat */, + HotwordDetectedResult newResult; + try { + newResult = + mHotwordAudioStreamManager.startCopyingAudioStreams( + triggerResult); + } catch (IOException e) { + // TODO: Write event + callback.onError(); + return; + } + callback.onDetected(newResult, null /* audioFormat */, null /* audioStream */); - if (triggerResult != null) { - Slog.i(TAG, "Egressed " - + HotwordDetectedResult.getUsageSize(triggerResult) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, - "Egressed detected result: " + triggerResult); - } + Slog.i(TAG, "Egressed " + + HotwordDetectedResult.getUsageSize(newResult) + + " bits from hotword trusted process"); + if (mDebugHotwordLogging) { + Slog.i(TAG, + "Egressed detected result: " + newResult); } } }); |