diff options
111 files changed, 4847 insertions, 740 deletions
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java index 238c028fa0cf..9eac10846319 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java @@ -18,6 +18,7 @@ package android.libcore.regression; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; +import android.util.Log; import androidx.test.filters.LargeTest; @@ -47,6 +48,8 @@ import javax.crypto.spec.IvParameterSpec; @RunWith(JUnitParamsRunner.class) @LargeTest public class CipherPerfTest { + private static final String TAG = "android.libcore.regression.CipherPerfTest"; + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); public static Collection getCases() { @@ -71,6 +74,10 @@ public class CipherPerfTest { } for (int keySize : keySizes) { for (int inputSize : inputSizes) { + Log.i(TAG, + "param[" + params.size() + "] = " + mode.name() + ", " + + padding.name() + ", " + keySize + ", " + inputSize + + ", " + implementation.name()); params.add( new Object[] { mode, padding, keySize, inputSize, implementation diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1a1f3548fa74..4b6c62ecb032 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -91,6 +91,7 @@ package android { field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE"; field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT"; field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE"; + field @FlaggedApi("android.content.pm.verification_service") public static final String BIND_VERIFICATION_AGENT = "android.permission.BIND_VERIFICATION_AGENT"; field public static final String BIND_VISUAL_QUERY_DETECTION_SERVICE = "android.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE"; field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE"; field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE"; @@ -412,6 +413,7 @@ package android { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE"; field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED"; + field @FlaggedApi("android.content.pm.verification_service") public static final String VERIFICATION_AGENT = "android.permission.VERIFICATION_AGENT"; field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS"; field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; @@ -4303,6 +4305,7 @@ package android.content.pm { method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>); field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS"; field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER"; + field @FlaggedApi("android.content.pm.verification_service") public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE"; field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1 field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2 field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3 @@ -4616,6 +4619,61 @@ package android.content.pm.verify.domain { } +package android.content.pm.verify.pkg { + + @FlaggedApi("android.content.pm.verification_service") public final class VerificationSession implements android.os.Parcelable { + method public int describeContents(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long extendTimeRemaining(long); + method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getDeclaredLibraries(); + method @NonNull public android.os.PersistableBundle getExtensionParams(); + method public int getId(); + method public int getInstallSessionId(); + method @NonNull public String getPackageName(); + method @NonNull public android.content.pm.SigningInfo getSigningInfo(); + method @NonNull public android.net.Uri getStagedPackageUri(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime(); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR; + field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2 + field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1 + field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0 + } + + @FlaggedApi("android.content.pm.verification_service") public final class VerificationStatus implements android.os.Parcelable { + method public int describeContents(); + method public int getAslStatus(); + method @NonNull public String getFailureMessage(); + method public boolean isVerified(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationStatus> CREATOR; + field public static final int VERIFIER_STATUS_ASL_BAD = 2; // 0x2 + field public static final int VERIFIER_STATUS_ASL_GOOD = 1; // 0x1 + field public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; // 0x0 + } + + public static final class VerificationStatus.Builder { + ctor public VerificationStatus.Builder(); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus build(); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setAslStatus(int); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setFailureMessage(@NonNull String); + method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setVerified(boolean); + } + + @FlaggedApi("android.content.pm.verification_service") public abstract class VerifierService extends android.app.Service { + ctor public VerifierService(); + method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); + method public abstract void onPackageNameAvailable(@NonNull String); + method public abstract void onVerificationCancelled(@NonNull String); + method public abstract void onVerificationRequired(@NonNull android.content.pm.verify.pkg.VerificationSession); + method public abstract void onVerificationRetry(@NonNull android.content.pm.verify.pkg.VerificationSession); + method public abstract void onVerificationTimeout(int); + } + +} + package android.content.rollback { public final class PackageRollbackInfo implements android.os.Parcelable { diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index fb2655c771c4..e985f88f38fc 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -5039,6 +5039,25 @@ public abstract class PackageManager { "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER"; /** + * Used by the system to query a {@link android.content.pm.verify.pkg.VerifierService} provider, + * which registers itself via an intent-filter handling this action. + * + * <p class="note">Only the system can bind to such a verifier service. This is protected by the + * {@link android.Manifest.permission#BIND_VERIFICATION_AGENT} permission. The verifier service + * app should protect the service by adding this permission in the service declaration in its + * manifest. + * <p> + * A verifier service must be a privileged app and hold the + * {@link android.Manifest.permission#VERIFICATION_AGENT} permission. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_VERIFICATION_SERVICE) + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE"; + + /** * The names of the requested permissions. * <p> * <strong>Type:</strong> String[] diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 160cbdffe5bb..300740e84c60 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -309,3 +309,11 @@ flag { description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently." bug: "361776825" } + +flag { + name: "verification_service" + namespace: "package_manager_service" + description: "Feature flag to enable the new verification service." + bug: "360129103" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl new file mode 100644 index 000000000000..38a7956603ae --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +import android.content.pm.verify.pkg.VerificationStatus; +import android.os.PersistableBundle; + +/** + * Oneway interface that allows the verifier to send response or verification results back to + * the system. + * @hide + */ +oneway interface IVerificationSessionCallback { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationIncomplete(int verificationId, int reason); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationComplete(int verificationId, in VerificationStatus status); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + void reportVerificationCompleteWithExtensionResponse(int verificationId, in VerificationStatus status, in PersistableBundle response); +} diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl new file mode 100644 index 000000000000..7a9484abd1b1 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +/** + * Non-oneway interface that allows the verifier to retrieve information from the system. + * @hide + */ +interface IVerificationSessionInterface { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + long getTimeoutTime(int verificationId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + long extendTimeRemaining(int verificationId, long additionalMs); +}
\ No newline at end of file diff --git a/core/java/android/content/pm/verify/pkg/IVerifierService.aidl b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl new file mode 100644 index 000000000000..d3071fd67b9d --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +import android.content.pm.verify.pkg.VerificationSession; + +/** + * Oneway interface that allows the system to communicate to the verifier service agent. + * @hide + */ +oneway interface IVerifierService { + void onPackageNameAvailable(in String packageName); + void onVerificationCancelled(in String packageName); + void onVerificationRequired(in VerificationSession session); + void onVerificationRetry(in VerificationSession session); + void onVerificationTimeout(int verificationId); +}
\ No newline at end of file diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.aidl b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl new file mode 100644 index 000000000000..ac855850a86c --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +/** @hide */ +parcelable VerificationSession; diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java new file mode 100644 index 000000000000..70b4a022f521 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.pm.Flags; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.SigningInfo; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; + +/** + * This class is used by the system to describe the details about a verification request sent to the + * verification agent, aka the verifier. It includes the interfaces for the verifier to communicate + * back to the system. + * @hide + */ +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +@SystemApi +public final class VerificationSession implements Parcelable { + /** + * The verification cannot be completed because of unknown reasons. + */ + public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; + /** + * The verification cannot be completed because the network is unavailable. + */ + public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; + /** + * The verification cannot be completed because the network is limited. + */ + public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; + + /** + * @hide + */ + @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = { + VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE, + VERIFICATION_INCOMPLETE_NETWORK_LIMITED, + VERIFICATION_INCOMPLETE_UNKNOWN, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerificationIncompleteReason { + } + + private final int mId; + private final int mInstallSessionId; + @NonNull + private final String mPackageName; + @NonNull + private final Uri mStagedPackageUri; + @NonNull + private final SigningInfo mSigningInfo; + @NonNull + private final List<SharedLibraryInfo> mDeclaredLibraries; + @NonNull + private final PersistableBundle mExtensionParams; + @NonNull + private final IVerificationSessionInterface mSession; + @NonNull + private final IVerificationSessionCallback mCallback; + + /** + * Constructor used by the system to describe the details of a verification session. + * @hide + */ + public VerificationSession(int id, int installSessionId, @NonNull String packageName, + @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo, + @NonNull List<SharedLibraryInfo> declaredLibraries, + @NonNull PersistableBundle extensionParams, + @NonNull IVerificationSessionInterface session, + @NonNull IVerificationSessionCallback callback) { + mId = id; + mInstallSessionId = installSessionId; + mPackageName = packageName; + mStagedPackageUri = stagedPackageUri; + mSigningInfo = signingInfo; + mDeclaredLibraries = declaredLibraries; + mExtensionParams = extensionParams; + mSession = session; + mCallback = callback; + } + + /** + * A unique identifier tied to this specific verification session. + */ + public int getId() { + return mId; + } + + /** + * The package name of the app that is to be verified. + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * The id of the installation session associated with the verification. + */ + public int getInstallSessionId() { + return mInstallSessionId; + } + + /** + * The Uri of the path where the package's code files are located. + */ + public @NonNull Uri getStagedPackageUri() { + return mStagedPackageUri; + } + + /** + * Signing info of the package to be verified. + */ + public @NonNull SigningInfo getSigningInfo() { + return mSigningInfo; + } + + /** + * Returns a mapping of any shared libraries declared in the manifest + * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty + * map if no shared libraries are declared by the package. + */ + @NonNull + public List<SharedLibraryInfo> getDeclaredLibraries() { + return Collections.unmodifiableList(mDeclaredLibraries); + } + + /** + * Returns any extension params associated with the verification request. + */ + @NonNull + public PersistableBundle getExtensionParams() { + return mExtensionParams; + } + + /** + * Get the value of Clock.elapsedRealtime() at which time this verification + * will timeout as incomplete if no other verification response is provided. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public long getTimeoutTime() { + try { + return mSession.getTimeoutTime(mId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Extend the timeout for this session by the provided additionalMs to + * fetch relevant information over the network or wait for the network. + * This may be called multiple times. If the request would bypass any max + * duration by the system, the method will return a lower value than the + * requested amount that indicates how much the time was extended. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public long extendTimeRemaining(long additionalMs) { + try { + return mSession.extendTimeRemaining(mId, additionalMs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report to the system that verification could not be completed along + * with an approximate reason to pass on to the installer. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationIncomplete(@VerificationIncompleteReason int reason) { + try { + mCallback.reportVerificationIncomplete(mId, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report to the system that the verification has completed and the + * install process may act on that status to either block in the case + * of failure or continue to process the install in the case of success. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationComplete(@NonNull VerificationStatus status) { + try { + mCallback.reportVerificationComplete(mId, status); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Same as {@link #reportVerificationComplete(VerificationStatus)}, but also provide + * a result to the extension params provided in the request, which will be passed to the + * installer in the installation result. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public void reportVerificationComplete(@NonNull VerificationStatus status, + @NonNull PersistableBundle response) { + try { + mCallback.reportVerificationCompleteWithExtensionResponse(mId, status, response); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private VerificationSession(@NonNull Parcel in) { + mId = in.readInt(); + mInstallSessionId = in.readInt(); + mPackageName = in.readString8(); + mStagedPackageUri = Uri.CREATOR.createFromParcel(in); + mSigningInfo = SigningInfo.CREATOR.createFromParcel(in); + mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR); + mExtensionParams = in.readPersistableBundle(getClass().getClassLoader()); + mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder()); + mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mId); + dest.writeInt(mInstallSessionId); + dest.writeString8(mPackageName); + Uri.writeToParcel(dest, mStagedPackageUri); + mSigningInfo.writeToParcel(dest, flags); + dest.writeTypedList(mDeclaredLibraries); + dest.writePersistableBundle(mExtensionParams); + dest.writeStrongBinder(mSession.asBinder()); + dest.writeStrongBinder(mCallback.asBinder()); + } + + @NonNull + public static final Creator<VerificationSession> CREATOR = new Creator<>() { + @Override + public VerificationSession createFromParcel(@NonNull Parcel in) { + return new VerificationSession(in); + } + + @Override + public VerificationSession[] newArray(int size) { + return new VerificationSession[size]; + } + }; +} diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl new file mode 100644 index 000000000000..6a1cb4f2d1d2 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +/** @hide */ +parcelable VerificationStatus; diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.java b/core/java/android/content/pm/verify/pkg/VerificationStatus.java new file mode 100644 index 000000000000..4d0379d79773 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.pm.Flags; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This class is used by the verifier to describe the status of the verification request, whether + * it's successful or it has failed along with any relevant details. + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +public final class VerificationStatus implements Parcelable { + /** + * The ASL status has not been determined. This happens in situations where the verification + * service is not monitoring ASLs, and means the ASL data in the app is not necessarily bad but + * can't be trusted. + */ + public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; + + /** + * The app's ASL data is considered to be in a good state. + */ + public static final int VERIFIER_STATUS_ASL_GOOD = 1; + + /** + * There is something bad in the app's ASL data; the user should be warned about this when shown + * the ASL data and/or appropriate decisions made about the use of this data by the platform. + */ + public static final int VERIFIER_STATUS_ASL_BAD = 2; + + /** @hide */ + @IntDef(prefix = {"VERIFIER_STATUS_ASL_"}, value = { + VERIFIER_STATUS_ASL_UNDEFINED, + VERIFIER_STATUS_ASL_GOOD, + VERIFIER_STATUS_ASL_BAD, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerifierStatusAsl {} + + private boolean mIsVerified; + private @VerifierStatusAsl int mAslStatus; + @NonNull + private String mFailuresMessage = ""; + + private VerificationStatus() {} + + /** + * @return whether the status is set to verified or not. + */ + public boolean isVerified() { + return mIsVerified; + } + + /** + * @return the failure message associated with the failure status. + */ + @NonNull + public String getFailureMessage() { + return mFailuresMessage; + } + + /** + * @return the asl status. + */ + public @VerifierStatusAsl int getAslStatus() { + return mAslStatus; + } + + /** + * Builder to construct a {@link VerificationStatus} object. + */ + public static final class Builder { + final VerificationStatus mStatus = new VerificationStatus(); + + /** + * Set in the status whether the verification has succeeded or failed. + */ + @NonNull + public Builder setVerified(boolean verified) { + mStatus.mIsVerified = verified; + return this; + } + + /** + * Set a developer-facing failure message to include in the verification failure status. + */ + @NonNull + public Builder setFailureMessage(@NonNull String failureMessage) { + mStatus.mFailuresMessage = failureMessage; + return this; + } + + /** + * Set the ASL status, as defined in {@link VerifierStatusAsl}. + */ + @NonNull + public Builder setAslStatus(@VerifierStatusAsl int aslStatus) { + mStatus.mAslStatus = aslStatus; + return this; + } + + /** + * Build the status object. + */ + @NonNull + public VerificationStatus build() { + return mStatus; + } + } + + private VerificationStatus(Parcel in) { + mIsVerified = in.readBoolean(); + mAslStatus = in.readInt(); + mFailuresMessage = in.readString8(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mIsVerified); + dest.writeInt(mAslStatus); + dest.writeString8(mFailuresMessage); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<VerificationStatus> CREATOR = new Creator<>() { + @Override + public VerificationStatus createFromParcel(@NonNull Parcel in) { + return new VerificationStatus(in); + } + + @Override + public VerificationStatus[] newArray(int size) { + return new VerificationStatus[size]; + } + }; +} diff --git a/core/java/android/content/pm/verify/pkg/VerifierService.java b/core/java/android/content/pm/verify/pkg/VerifierService.java new file mode 100644 index 000000000000..ccf211915326 --- /dev/null +++ b/core/java/android/content/pm/verify/pkg/VerifierService.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.content.pm.verify.pkg; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.Flags; +import android.content.pm.PackageManager; +import android.os.IBinder; + +/** + * A base service implementation for the verifier agent to implement. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) +public abstract class VerifierService extends Service { + /** + * Called when a package name is available for a pending verification, + * giving the verifier opportunity to pre-fetch any relevant information + * that may be needed should a verification for the package be required. + */ + public abstract void onPackageNameAvailable(@NonNull String packageName); + + /** + * Called when a package recently provided via {@link #onPackageNameAvailable} + * is no longer expected to be installed. This is a hint that any pre-fetch or + * cache created as a result of the previous call may be be cleared. + * <p>This method will never be called after {@link #onVerificationRequired} is called for the + * same package. Once a verification is officially requested by + * {@link #onVerificationRequired}, it cannot be cancelled. + * </p> + */ + public abstract void onVerificationCancelled(@NonNull String packageName); + + /** + * Called when an application needs to be verified. Details about the + * verification and actions that can be taken on it will be encapsulated in + * the provided {@link VerificationSession} parameter. + */ + public abstract void onVerificationRequired(@NonNull VerificationSession session); + + /** + * Called when a verification needs to be retried. This can be encountered + * when a prior verification was marked incomplete and the user has indicated + * that they've resolved the issue, or when a timeout is reached, but the + * the system is attempting to retry. Details about the + * verification and actions that can be taken on it will be encapsulated in + * the provided {@link VerificationSession} parameter. + */ + public abstract void onVerificationRetry(@NonNull VerificationSession session); + + /** + * Called in the case that an active verification has failed. Any APIs called + * on the {@link VerificationSession} instance associated with this {@code verificationId} will + * throw an {@link IllegalStateException}. + */ + public abstract void onVerificationTimeout(int verificationId); + + /** + * Called when the verifier service is bound to the system. + */ + public @Nullable IBinder onBind(@Nullable Intent intent) { + if (intent == null || !PackageManager.ACTION_VERIFY_PACKAGE.equals(intent.getAction())) { + return null; + } + return new IVerifierService.Stub() { + @Override + public void onPackageNameAvailable(@NonNull String packageName) { + VerifierService.this.onPackageNameAvailable(packageName); + } + + @Override + public void onVerificationCancelled(@NonNull String packageName) { + VerifierService.this.onVerificationCancelled(packageName); + } + + @Override + public void onVerificationRequired(@NonNull VerificationSession session) { + VerifierService.this.onVerificationRequired(session); + } + + @Override + public void onVerificationRetry(@NonNull VerificationSession session) { + VerifierService.this.onVerificationRetry(session); + } + + @Override + public void onVerificationTimeout(int verificationId) { + VerifierService.this.onVerificationTimeout(verificationId); + } + }; + } +} diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index a9c57909e1c1..c9f464716e72 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -25,6 +25,7 @@ import android.util.ArraySet; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.LinkedHashMap; import java.util.Objects; import java.util.Set; @@ -63,6 +64,7 @@ public class ZenModeDiff { public static class FieldDiff<T> { private final T mFrom; private final T mTo; + private final BaseDiff mDetailedDiff; /** * Constructor to create a FieldDiff object with the given values. @@ -72,6 +74,19 @@ public class ZenModeDiff { public FieldDiff(@Nullable T from, @Nullable T to) { mFrom = from; mTo = to; + mDetailedDiff = null; + } + + /** + * Constructor to create a FieldDiff object with the given values, and that has a + * detailed BaseDiff. + * @param from from (old) value + * @param to to (new) value + */ + public FieldDiff(@Nullable T from, @Nullable T to, @Nullable BaseDiff detailedDiff) { + mFrom = from; + mTo = to; + mDetailedDiff = detailedDiff; } /** @@ -93,6 +108,9 @@ public class ZenModeDiff { */ @Override public String toString() { + if (mDetailedDiff != null) { + return mDetailedDiff.toString(); + } return mFrom + "->" + mTo; } @@ -100,6 +118,9 @@ public class ZenModeDiff { * Returns whether this represents an actual diff. */ public boolean hasDiff() { + if (mDetailedDiff != null) { + return mDetailedDiff.hasDiff(); + } // note that Objects.equals handles null values gracefully. return !Objects.equals(mFrom, mTo); } @@ -115,7 +136,8 @@ public class ZenModeDiff { @ExistenceChange private int mExists = NONE; // Map from field name to diffs for any standalone fields in the object. - private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>(); + // LinkedHashMap is specifically chosen here to show insertion order when keys are fetched. + private LinkedHashMap<String, FieldDiff> mFields = new LinkedHashMap<>(); // Functions for actually diffing objects and string representations have to be implemented // by subclasses. @@ -550,8 +572,16 @@ public class ZenModeDiff { if (!Objects.equals(from.enabler, to.enabler)) { addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler)); } - if (!Objects.equals(from.zenPolicy, to.zenPolicy)) { - addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy)); + if (android.app.Flags.modesApi()) { + PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy); + if (policyDiff.hasDiff()) { + addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy, + policyDiff)); + } + } else { + if (!Objects.equals(from.zenPolicy, to.zenPolicy)) { + addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy)); + } } if (from.modified != to.modified) { addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified)); @@ -560,9 +590,12 @@ public class ZenModeDiff { addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg)); } if (android.app.Flags.modesApi()) { - if (!Objects.equals(from.zenDeviceEffects, to.zenDeviceEffects)) { + DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects, + to.zenDeviceEffects); + if (deviceEffectsDiff.hasDiff()) { addField(FIELD_ZEN_DEVICE_EFFECTS, - new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects)); + new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects, + deviceEffectsDiff)); } if (!Objects.equals(from.triggerDescription, to.triggerDescription)) { addField(FIELD_TRIGGER_DESCRIPTION, @@ -630,7 +663,7 @@ public class ZenModeDiff { sb.append(key); sb.append(":"); - sb.append(diff); + sb.append(diff.toString()); } if (becameActive()) { diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java index 2db4285f0820..7f3858e842ed 100644 --- a/core/java/android/view/autofill/AutofillStateFingerprint.java +++ b/core/java/android/view/autofill/AutofillStateFingerprint.java @@ -97,7 +97,6 @@ public final class AutofillStateFingerprint { if (sDebug) { Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size()); } -// ArrayList<Integer> hashes = getFingerprintIds(autofillableViews); ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews); for (Map.Entry<Integer, View> entry : hashes.entrySet()) { @@ -123,7 +122,6 @@ public final class AutofillStateFingerprint { if (view != null) { int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */); AutofillId autofillId = view.getAutofillId(); - autofillId.setSessionId(mSessionId); mHashToAutofillIdMap.put(id, autofillId); } else { if (sDebug) { diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index fbc30ed3d8f5..70ac12f07a23 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -306,3 +306,10 @@ flag { description: "Allow entering desktop mode by default on freeform displays" bug: "361419732" } + +flag { + name: "enable_desktop_app_launch_alttab_transitions" + namespace: "lse_desktop_experience" + description: "Enables custom transitions for alt-tab app launches in Desktop Mode." + bug: "370735595" +}
\ No newline at end of file diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d35c66ed719e..ed33edecb333 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8449,6 +8449,29 @@ <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE" android:protectionLevel="signature"/> + <!-- @SystemApi + @FlaggedApi("android.content.pm.verification_service") + Allows app to be the verification agent to verify packages. + <p>Protection level: signature|privileged + @hide + --> + <permission android:name="android.permission.VERIFICATION_AGENT" + android:protectionLevel="signature|privileged" + android:featureFlag="android.content.pm.verification_service" /> + + <!-- @SystemApi + @FlaggedApi("android.content.pm.verification_service") + Must be required by a privileged {@link android.content.pm.verify.pkg.VerifierService} + to ensure that only the system can bind to it. + This permission should not be held by anything other than the system. + <p>Not for use by third-party applications. </p> + <p>Protection level: signature + @hide + --> + <permission android:name="android.permission.BIND_VERIFICATION_AGENT" + android:protectionLevel="internal" + android:featureFlag="android.content.pm.verification_service" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 5c0dca2104af..1a3a30d9c36d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1250,6 +1250,7 @@ a watch, setting this config is no-op. 0 - Nothing 1 - Switch to the recent app + 2 - Launch the default fitness app --> <integer name="config_doublePressOnStemPrimaryBehavior">0</integer> @@ -2309,10 +2310,6 @@ spatial audio is enabled for a newly connected audio device --> <bool name="config_spatial_audio_head_tracking_enabled_default">false</bool> - <!-- Flag indicating whether platform level volume adjustments are enabled for remote sessions - on grouped devices. --> - <bool name="config_volumeAdjustmentForRemoteGroupSessions">true</bool> - <!-- Flag indicating current media Output Switcher version. --> <integer name="config_mediaOutputSwitchDialogVersion">1</integer> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 807df1be7fb5..0ccef9123e10 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5102,8 +5102,6 @@ <java-symbol type="dimen" name="config_wallpaperDimAmount" /> - <java-symbol type="bool" name="config_volumeAdjustmentForRemoteGroupSessions" /> - <java-symbol type="integer" name="config_mediaOutputSwitchDialogVersion" /> <!-- List of shared library packages that should be loaded by the classloader after the diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 9821d433500f..56e18e6c443f 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -250,7 +250,7 @@ android_ravenwood_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.test.uiautomator_uiautomator", - "compatibility-device-util-axt", + "compatibility-device-util-axt-ravenwood", "flag-junit", "platform-test-annotations", "flag-junit", diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java new file mode 100644 index 000000000000..987f68d4f9e1 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.pm.SharedLibraryInfo; +import android.content.pm.SigningInfo; +import android.content.pm.VersionedPackage; +import android.content.pm.verify.pkg.IVerificationSessionCallback; +import android.content.pm.verify.pkg.IVerificationSessionInterface; +import android.content.pm.verify.pkg.VerificationSession; +import android.content.pm.verify.pkg.VerificationStatus; +import android.net.Uri; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerificationSessionTest { + private static final int TEST_ID = 100; + private static final int TEST_INSTALL_SESSION_ID = 33; + private static final String TEST_PACKAGE_NAME = "com.foo"; + private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test"); + private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo(); + private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 = + new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME, + Collections.singletonList("path1"), "sharedLib1", 101, + SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1), + null, null, false); + private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 = + new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME, + Collections.singletonList("path2"), "sharedLib2", 102, + SharedLibraryInfo.TYPE_DYNAMIC, + new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false); + private static final long TEST_TIMEOUT_TIME = System.currentTimeMillis(); + private static final long TEST_EXTEND_TIME = 2000L; + private static final String TEST_KEY = "test key"; + private static final String TEST_VALUE = "test value"; + + private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>(); + private final PersistableBundle mTestExtensionParams = new PersistableBundle(); + @Mock + private IVerificationSessionInterface mTestSessionInterface; + @Mock + private IVerificationSessionCallback mTestCallback; + private VerificationSession mTestSession; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1); + mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2); + mTestExtensionParams.putString(TEST_KEY, TEST_VALUE); + mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, + TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries, + mTestExtensionParams, mTestSessionInterface, mTestCallback); + } + + @Test + public void testParcel() { + Parcel parcel = Parcel.obtain(); + mTestSession.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + VerificationSession sessionFromParcel = + VerificationSession.CREATOR.createFromParcel(parcel); + assertThat(sessionFromParcel.getId()).isEqualTo(TEST_ID); + assertThat(sessionFromParcel.getInstallSessionId()).isEqualTo(TEST_INSTALL_SESSION_ID); + assertThat(sessionFromParcel.getPackageName()).isEqualTo(TEST_PACKAGE_NAME); + assertThat(sessionFromParcel.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI); + assertThat(sessionFromParcel.getSigningInfo().getSigningDetails()) + .isEqualTo(TEST_SIGNING_INFO.getSigningDetails()); + List<SharedLibraryInfo> declaredLibrariesFromParcel = + sessionFromParcel.getDeclaredLibraries(); + assertThat(declaredLibrariesFromParcel).hasSize(2); + // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly + assertThat(declaredLibrariesFromParcel.getFirst().toString()) + .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString()); + assertThat(declaredLibrariesFromParcel.get(1).toString()) + .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString()); + // We can't directly test with PersistableBundle.equals() because the parceled bundle's + // structure is different, but all the key/value pairs should be preserved as before. + assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY)) + .isEqualTo(mTestExtensionParams.getString(TEST_KEY)); + } + + @Test + public void testInterface() throws Exception { + when(mTestSessionInterface.getTimeoutTime(anyInt())).thenAnswer(i -> TEST_TIMEOUT_TIME); + when(mTestSessionInterface.extendTimeRemaining(anyInt(), anyLong())).thenAnswer( + i -> i.getArguments()[1]); + + assertThat(mTestSession.getTimeoutTime()).isEqualTo(TEST_TIMEOUT_TIME); + verify(mTestSessionInterface, times(1)).getTimeoutTime(eq(TEST_ID)); + assertThat(mTestSession.extendTimeRemaining(TEST_EXTEND_TIME)).isEqualTo(TEST_EXTEND_TIME); + verify(mTestSessionInterface, times(1)).extendTimeRemaining( + eq(TEST_ID), eq(TEST_EXTEND_TIME)); + } + + @Test + public void testCallback() throws Exception { + PersistableBundle response = new PersistableBundle(); + response.putString("test key", "test value"); + final VerificationStatus status = + new VerificationStatus.Builder().setVerified(true).build(); + mTestSession.reportVerificationComplete(status); + verify(mTestCallback, times(1)).reportVerificationComplete( + eq(TEST_ID), eq(status)); + mTestSession.reportVerificationComplete(status, response); + verify(mTestCallback, times(1)) + .reportVerificationCompleteWithExtensionResponse( + eq(TEST_ID), eq(status), eq(response)); + + final int reason = VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN; + mTestSession.reportVerificationIncomplete(reason); + verify(mTestCallback, times(1)).reportVerificationIncomplete( + eq(TEST_ID), eq(reason)); + } +} diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java new file mode 100644 index 000000000000..67d407a72925 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.pm.verify.pkg.VerificationStatus; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerificationStatusTest { + private static final boolean TEST_VERIFIED = true; + private static final int TEST_ASL_STATUS = VerificationStatus.VERIFIER_STATUS_ASL_GOOD; + private static final String TEST_FAILURE_MESSAGE = "test test"; + private static final String TEST_KEY = "test key"; + private static final String TEST_VALUE = "test value"; + private final PersistableBundle mTestExtras = new PersistableBundle(); + private VerificationStatus mStatus; + + @Before + public void setUpWithBuilder() { + mTestExtras.putString(TEST_KEY, TEST_VALUE); + mStatus = new VerificationStatus.Builder() + .setAslStatus(TEST_ASL_STATUS) + .setFailureMessage(TEST_FAILURE_MESSAGE) + .setVerified(TEST_VERIFIED) + .build(); + } + + @Test + public void testGetters() { + assertThat(mStatus.isVerified()).isEqualTo(TEST_VERIFIED); + assertThat(mStatus.getAslStatus()).isEqualTo(TEST_ASL_STATUS); + assertThat(mStatus.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE); + } + + @Test + public void testParcel() { + Parcel parcel = Parcel.obtain(); + mStatus.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + VerificationStatus statusFromParcel = VerificationStatus.CREATOR.createFromParcel(parcel); + assertThat(statusFromParcel.isVerified()).isEqualTo(TEST_VERIFIED); + assertThat(statusFromParcel.getAslStatus()).isEqualTo(TEST_ASL_STATUS); + assertThat(statusFromParcel.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE); + } +} diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java new file mode 100644 index 000000000000..7f73a1eb4b48 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.content.pm.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.SigningInfo; +import android.content.pm.verify.pkg.IVerifierService; +import android.content.pm.verify.pkg.VerificationSession; +import android.content.pm.verify.pkg.VerifierService; +import android.net.Uri; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mockito; + +import java.util.ArrayList; + +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VerifierServiceTest { + private static final int TEST_ID = 100; + private static final int TEST_INSTALL_SESSION_ID = 33; + private static final String TEST_PACKAGE_NAME = "com.foo"; + private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test"); + private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo(); + private VerifierService mService; + private VerificationSession mSession; + + @Before + public void setUp() { + mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS); + mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, + TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, + new ArrayList<>(), + new PersistableBundle(), null, null); + } + + @Test + public void testBind() throws Exception { + Intent intent = Mockito.mock(Intent.class); + when(intent.getAction()).thenReturn(PackageManager.ACTION_VERIFY_PACKAGE); + IVerifierService binder = + (IVerifierService) mService.onBind(intent); + assertThat(binder).isNotNull(); + binder.onPackageNameAvailable(TEST_PACKAGE_NAME); + verify(mService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME)); + binder.onVerificationCancelled(TEST_PACKAGE_NAME); + verify(mService).onVerificationCancelled(eq(TEST_PACKAGE_NAME)); + binder.onVerificationRequired(mSession); + verify(mService).onVerificationRequired(eq(mSession)); + binder.onVerificationRetry(mSession); + verify(mService).onVerificationRetry(eq(mSession)); + binder.onVerificationTimeout(TEST_ID); + verify(mService).onVerificationTimeout(eq(TEST_ID)); + } + + @Test + public void testBindFailsWithWrongIntent() { + Intent intent = Mockito.mock(Intent.class); + when(intent.getAction()).thenReturn(Intent.ACTION_SEND); + assertThat(mService.onBind(intent)).isNull(); + } +} diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp index cdc8a9e06d0b..7cf49ab5c376 100644 --- a/core/tests/utiltests/Android.bp +++ b/core/tests/utiltests/Android.bp @@ -61,7 +61,7 @@ android_ravenwood_test { "androidx.annotation_annotation", "androidx.test.rules", "frameworks-base-testutils", - "servicestests-utils", + "servicestests-utils-ravenwood", ], srcs: [ "src/android/util/IRemoteMemoryIntArray.aidl", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 955fe83d34ee..985224e30a51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -350,8 +350,17 @@ class DesktopModeTaskRepository ( /** Minimizes the task for [taskId] and [displayId] */ fun minimizeTask(displayId: Int, taskId: Int) { - logD("Minimize Task: display=%d, task=%d", displayId, taskId) - desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) + if (displayId == INVALID_DISPLAY) { + // When a task vanishes it doesn't have a displayId. Find the display of the task and + // mark it as minimized. + getDisplayIdForTask(taskId)?.let { + minimizeTask(it, taskId) + } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId) + } else { + logD("Minimize Task: display=%d, task=%d", displayId, taskId) + desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) + } + if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 125805c14321..b8bb73ba4148 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -44,7 +44,6 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE -import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT @@ -550,7 +549,29 @@ class DesktopTasksController( /** Move a task to the front */ fun moveTaskToFront(taskId: Int) { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) } + val task = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task) + } + + /** + * Launch a background task in desktop. Note that this should be used when we are already in + * desktop. If outside of desktop and want to launch a background task in desktop, use + * [moveBackgroundTaskToDesktop] instead. + */ + private fun moveBackgroundTaskToFront(taskId: Int) { + logV("moveBackgroundTaskToFront taskId=%s", taskId) + val wct = WindowContainerTransaction() + // TODO: b/342378842 - Instead of using default display, support multiple displays + val taskToMinimize: RunningTaskInfo? = + addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) + wct.startTask( + taskId, + ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + }.toBundle(), + ) + val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) + addPendingMinimizeTransition(transition, taskToMinimize) } /** Move a task to the front */ @@ -558,7 +579,8 @@ class DesktopTasksController( logV("moveTaskToFront taskId=%s", taskInfo.taskId) val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) - val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) + val taskToMinimize = + addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId) if (Transitions.ENABLE_SHELL_TRANSITIONS) { val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) addPendingMinimizeTransition(transition, taskToMinimize) @@ -1254,7 +1276,7 @@ class DesktopTasksController( } // Desktop Mode is showing and we're launching a new Task - we might need to minimize // a Task. - val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId) if (taskToMinimize != null) { addPendingMinimizeTransition(transition, taskToMinimize) return wct @@ -1280,7 +1302,8 @@ class DesktopTasksController( // Desktop Mode is already showing and we're launching a new Task - we might need to // minimize another Task. - val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + val taskToMinimize = + addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId) addPendingMinimizeTransition(transition, taskToMinimize) } } @@ -1313,14 +1336,11 @@ class DesktopTasksController( // Remove wallpaper activity when the last active task is removed removeWallpaperActivity(wct) } - taskRepository.addClosingTask(task.displayId, task.taskId) - // If a CLOSE is triggered on a desktop task, remove the task. - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && - taskRepository.isVisibleTask(task.taskId) && - transitionType == TRANSIT_CLOSE - ) { - wct.removeTask(task.token) + + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + taskRepository.addClosingTask(task.displayId, task.taskId) } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding( task.displayId, @@ -1425,12 +1445,12 @@ class DesktopTasksController( private fun addAndGetMinimizeChangesIfNeeded( displayId: Int, wct: WindowContainerTransaction, - newTaskInfo: RunningTaskInfo + newTaskId: Int ): RunningTaskInfo? { if (!desktopTasksLimiter.isPresent) return null return desktopTasksLimiter .get() - .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo) + .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskId) } private fun addPendingMinimizeTransition( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index d84349b1ce1f..7e0741f1f859 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -208,15 +208,15 @@ class DesktopTasksLimiter ( fun addAndGetMinimizeTaskChangesIfNeeded( displayId: Int, wct: WindowContainerTransaction, - newFrontTaskInfo: RunningTaskInfo, + newFrontTaskId: Int, ): RunningTaskInfo? { ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", - newFrontTaskInfo.taskId) + newFrontTaskId) val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( taskRepository.getActiveNonMinimizedOrderedTasks(displayId), - newFrontTaskInfo.taskId) + newFrontTaskId) val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) if (taskToMinimize != null) { wct.reorder(taskToMinimize.token, false /* onTop */) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 83cc18baf6cc..7f7f10519bb1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -24,6 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; +import android.window.flags.DesktopModeFlags; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; @@ -121,7 +122,16 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { - repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId); + // TODO: b/370038902 - Handle Activity#finishAndRemoveTask. + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() + || repository.isClosingTask(taskInfo.taskId)) { + // A task that's vanishing should be removed: + // - If it's closed by the X button which means it's marked as a closing task. + repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId); + } else { + repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false); + repository.minimizeTask(taskInfo.displayId, taskInfo.taskId); + } }); } mWindowDecorationViewModel.onTaskVanished(taskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 1b9bf2acbb96..dc0bc7816859 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -304,54 +304,28 @@ public class PipTransition extends PipTransitionController implements if (pipChange == null) { return false; } - WindowContainerToken pipTaskToken = pipChange.getContainer(); SurfaceControl pipLeash = pipChange.getLeash(); + Preconditions.checkNotNull(pipLeash, "Leash is null for swipe-up transition."); - if (pipTaskToken == null || pipLeash == null) { - return false; - } - - SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); - PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; - - Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds(); - Rect destinationBounds = pipChange.getEndAbsBounds(); - - float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat(); - - // We fake the source rect hint when the one prvided by the app is invalid for - // the animation with an app icon overlay. - Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint() - : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio); - - WindowContainerTransaction finishWct = new WindowContainerTransaction(); - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - - final float scale = (float) destinationBounds.width() / animationSrcRectHint.width(); - startTransaction.setWindowCrop(pipLeash, animationSrcRectHint); - startTransaction.setPosition(pipLeash, - destinationBounds.left - animationSrcRectHint.left * scale, - destinationBounds.top - animationSrcRectHint.top * scale); - startTransaction.setScale(pipLeash, scale, scale); - - if (overlayLeash != null) { + final Rect destinationBounds = pipChange.getEndAbsBounds(); + final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay(); + if (swipePipToHomeOverlay != null) { final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); - - // Overlay needs to be adjusted once a new draw comes in resetting surface transform. - tx.setScale(overlayLeash, 1f, 1f); - tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f, - (destinationBounds.height() - overlaySize) / 2f); + // It is possible we reparent the PIP activity to a new PIP task (in multi-activity + // apps), so we should also reparent the overlay to the final PIP task. + startTransaction.reparent(swipePipToHomeOverlay, pipLeash) + .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE) + .setScale(swipePipToHomeOverlay, 1f, 1f) + .setPosition(swipePipToHomeOverlay, + (destinationBounds.width() - overlaySize) / 2f, + (destinationBounds.height() - overlaySize) / 2f); } - startTransaction.apply(); - tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), - this::onClientDrawAtTransitionEnd); - finishWct.setBoundsChangeTransaction(pipTaskToken, tx); - - // Note that finishWct should be free of any actual WM state changes; we are using - // it for syncing with the client draw after delayed configuration changes are dispatched. - finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct); + startTransaction.merge(finishTransaction); + startTransaction.apply(); + finishCallback.onTransitionFinished(null /* finishWct */); + onClientDrawAtTransitionEnd(); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 8077aeebf27f..f7ed1dd4606b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -52,7 +52,6 @@ import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Slog; -import android.view.Display; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.PictureInPictureSurfaceTransaction; @@ -910,6 +909,14 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "task #" + taskInfo.taskId + " is always_on_top"); return; } + if (TransitionUtil.isClosingType(change.getMode()) + && taskInfo != null && taskInfo.lastParentTaskIdBeforePip > 0) { + // Pinned task is closing as a side effect of the removal of its original Task, + // such transition should be handled by PiP. So cancel the merge here. + cancel(false /* toHome */, false /* withScreenshots */, + "task #" + taskInfo.taskId + " is removed with its original parent"); + return; + } final boolean isRootTask = taskInfo != null && TransitionInfo.isIndependent(change, info); final boolean isRecentsTask = mRecentsTask != null diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 794f9d819f4b..97ceecc3662d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -820,6 +820,18 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun minimizeTask_withInvalidDisplay_minimizesCorrectTask() { + repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 0) + repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 0) + + repo.minimizeTask(displayId = INVALID_DISPLAY, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isTrue() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test fun unminimizeTask_unminimizesTask() { repo.minimizeTask(displayId = 0, taskId = 0) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 8870846efed4..2ddb1acf7300 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -1350,6 +1350,32 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveTaskToFront_backgroundTask_launchesTask() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + + controller.moveTaskToFront(task.taskId) + + val wct = getLatestWct(type = TRANSIT_OPEN) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + val task = createTaskInfo(1001) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + + controller.moveTaskToFront(task.taskId) + + val wct = getLatestWct(type = TRANSIT_OPEN) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = false) + wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2075,11 +2101,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, - ) - fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -2112,8 +2135,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() { + fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -2122,11 +2144,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { val task = setUpFreeformTask() taskRepository.wallpaperActivityToken = MockToken().token() @@ -2149,11 +2168,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2165,7 +2181,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { + fun handleRequest_backTransition_multipleTasks_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2211,11 +2227,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -2231,11 +2244,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -2244,22 +2254,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() { + fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -2268,11 +2264,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { val task = setUpFreeformTask() taskRepository.wallpaperActivityToken = MockToken().token() @@ -2282,26 +2275,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() { + fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -2313,11 +2288,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2328,25 +2300,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - assertNotNull(result, "Should handle request") - result.assertRemoveAt(index = 0, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() { + fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2357,28 +2312,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -2392,28 +2327,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { + fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -2427,11 +2342,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 045e07796cb8..bc5ae97a55ea 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -291,7 +291,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( displayId = DEFAULT_DISPLAY, wct = wct, - newFrontTaskInfo = setUpFreeformTask()) + newFrontTaskId = setUpFreeformTask().taskId) assertThat(minimizedTaskId).isNull() assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added @@ -307,7 +307,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( displayId = DEFAULT_DISPLAY, wct = wct, - newFrontTaskInfo = setUpFreeformTask()) + newFrontTaskId = setUpFreeformTask().taskId) assertThat(minimizedTaskId).isEqualTo(tasks.first()) assertThat(wct.hierarchyOps.size).isEqualTo(1) @@ -325,7 +325,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( displayId = 0, wct = wct, - newFrontTaskInfo = setUpFreeformTask()) + newFrontTaskId = setUpFreeformTask().taskId) assertThat(minimizedTaskId).isNull() assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 763d0153071e..3b2c7e6eaf99 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -18,15 +18,19 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.INVALID_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -139,6 +143,40 @@ public final class FreeformTaskListenerTests extends ShellTestCase { verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(true); } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + public void onTaskVanished_nonClosingTask_isMinimized() { + ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + task.isVisible = false; + task.displayId = INVALID_DISPLAY; + mFreeformTaskListener.onTaskVanished(task); + + verify(mDesktopModeTaskRepository).minimizeTask(task.displayId, task.taskId); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + public void onTaskVanished_closingTask_isNotMinimized() { + ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + when(mDesktopModeTaskRepository.isClosingTask(task.taskId)).thenReturn(true); + task.isVisible = false; + task.displayId = INVALID_DISPLAY; + mFreeformTaskListener.onTaskVanished(task); + + verify(mDesktopModeTaskRepository, never()).minimizeTask(task.displayId, task.taskId); + verify(mDesktopModeTaskRepository).removeFreeformTask(task.displayId, task.taskId); + } + @After public void tearDown() { mMockitoSession.finishMocking(); diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 9899e4ec388d..83a4dd5a682a 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -22,7 +22,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.res.Resources; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -57,8 +56,6 @@ public final class RoutingSessionInfo implements Parcelable { } }; - private static final String TAG = "RoutingSessionInfo"; - private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE"; private static final String KEY_VOLUME_HANDLING = "volumeHandling"; @@ -142,15 +139,7 @@ public final class RoutingSessionInfo implements Parcelable { mVolume = builder.mVolume; mIsSystemSession = builder.mIsSystemSession; - - boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean( - com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); - mVolumeHandling = - defineVolumeHandling( - mIsSystemSession, - builder.mVolumeHandling, - mSelectedRoutes, - volumeAdjustmentForRemoteGroupSessions); + mVolumeHandling = builder.mVolumeHandling; mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling); mTransferReason = builder.mTransferReason; @@ -207,20 +196,6 @@ public final class RoutingSessionInfo implements Parcelable { return controlHints; } - @MediaRoute2Info.PlaybackVolume - private static int defineVolumeHandling( - boolean isSystemSession, - @MediaRoute2Info.PlaybackVolume int volumeHandling, - List<String> selectedRoutes, - boolean volumeAdjustmentForRemoteGroupSessions) { - if (!isSystemSession - && !volumeAdjustmentForRemoteGroupSessions - && selectedRoutes.size() > 1) { - return MediaRoute2Info.PLAYBACK_VOLUME_FIXED; - } - return volumeHandling; - } - @NonNull private static String ensureString(@Nullable String str) { return str != null ? str : ""; diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java index 3955ff068d94..5f5058d79545 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java @@ -18,8 +18,6 @@ package com.android.mediaroutertest; import static com.google.common.truth.Truth.assertThat; -import android.content.res.Resources; -import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -95,24 +93,4 @@ public class RoutingSessionInfoTest { assertThat(sessionInfoWithProviderId2.getTransferableRoutes()) .isEqualTo(sessionInfoWithProviderId.getTransferableRoutes()); } - - @Test - public void testGetVolumeHandlingGroupSession() { - RoutingSessionInfo sessionInfo = new RoutingSessionInfo.Builder( - TEST_ID, TEST_CLIENT_PACKAGE_NAME) - .setName(TEST_NAME) - .addSelectedRoute(TEST_ROUTE_ID_0) - .addSelectedRoute(TEST_ROUTE_ID_2) - .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) - .build(); - - boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean( - com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); - - int expectedResult = volumeAdjustmentForRemoteGroupSessions - ? MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE : - MediaRoute2Info.PLAYBACK_VOLUME_FIXED; - - assertThat(sessionInfo.getVolumeHandling()).isEqualTo(expectedResult); - } } diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml deleted file mode 100644 index a1761e55f1e0..000000000000 --- a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2024 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources> - <declare-styleable name="ButtonPreference"> - <attr name="buttonType" format="enum"> - <enum name="filled" value="0"/> - <enum name="tonal" value="1"/> - <enum name="outline" value="2"/> - </attr> - <attr name="buttonSize" format="enum"> - <enum name="normal" value="0"/> - <enum name="large" value="1"/> - <enum name="extra" value="2"/> - </attr> - </declare-styleable> -</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml index 9c1e503f1372..970eeb2bfa72 100644 --- a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml +++ b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml @@ -18,12 +18,12 @@ <resources> <declare-styleable name="ButtonPreference"> <attr name="android:gravity" /> - <attr name="buttonType" format="enum"> + <attr name="buttonPreferenceType" format="enum"> <enum name="filled" value="0"/> <enum name="tonal" value="1"/> <enum name="outline" value="2"/> </attr> - <attr name="buttonSize" format="enum"> + <attr name="buttonPreferenceSize" format="enum"> <enum name="normal" value="0"/> <enum name="large" value="1"/> <enum name="extra" value="2"/> diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java index 0041eb2c7072..979ff96be3f7 100644 --- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java +++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java @@ -137,8 +137,8 @@ public class ButtonPreference extends Preference { mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START); if (SettingsThemeHelper.isExpressiveTheme(context)) { - int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0); - int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0); + int type = a.getInt(R.styleable.ButtonPreference_buttonPreferenceType, 0); + int size = a.getInt(R.styleable.ButtonPreference_buttonPreferenceSize, 0); resId = ButtonStyle.getLayoutId(type, size); } a.recycle(); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index f6e10570e656..0773bd701d5c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -186,7 +186,7 @@ public class GlobalSettingsValidators { VALIDATORS.put( Global.STEM_PRIMARY_BUTTON_SHORT_PRESS, new InclusiveIntegerRangeValidator(0, 1)); VALIDATORS.put( - Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 1)); + Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 2)); VALIDATORS.put( Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS, new InclusiveIntegerRangeValidator(0, 1)); VALIDATORS.put( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index dc9e267cb0e1..56de096effce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -132,7 +132,6 @@ fun SceneContainer( state = state, modifier = modifier.fillMaxSize(), swipeSourceDetector = viewModel.edgeDetector, - gestureFilter = viewModel::shouldFilterGesture, ) { sceneByKey.forEach { (sceneKey, scene) -> scene( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index c8adac064690..5fa5db880cce 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -124,10 +124,6 @@ internal class DraggableHandlerImpl( overSlop: Float, pointersDown: Int, ): DragController { - if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) { - return NoOpDragController - } - if (overSlop == 0f) { val oldDragController = dragController check(oldDragController != null && oldDragController.isDrivingTransition) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index dc3135ddbf54..5ddc28485ac6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -42,10 +42,8 @@ import androidx.compose.ui.input.pointer.util.addPointerInputChange import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.node.currentValueOf -import androidx.compose.ui.node.observeReads import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity @@ -79,7 +77,6 @@ import kotlinx.coroutines.launch @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, - enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, onFirstPointerDown: () -> Unit = {}, @@ -89,7 +86,6 @@ internal fun Modifier.multiPointerDraggable( this.then( MultiPointerDraggableElement( orientation, - enabled, startDragImmediately, onDragStarted, onFirstPointerDown, @@ -100,7 +96,6 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, - private val enabled: () -> Boolean, private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, @@ -111,7 +106,6 @@ private data class MultiPointerDraggableElement( override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( orientation = orientation, - enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, onFirstPointerDown = onFirstPointerDown, @@ -121,7 +115,6 @@ private data class MultiPointerDraggableElement( override fun update(node: MultiPointerDraggableNode) { node.orientation = orientation - node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted node.onFirstPointerDown = onFirstPointerDown @@ -131,7 +124,6 @@ private data class MultiPointerDraggableElement( internal class MultiPointerDraggableNode( orientation: Orientation, - enabled: () -> Boolean, var startDragImmediately: (startedPosition: Offset) -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, @@ -142,21 +134,10 @@ internal class MultiPointerDraggableNode( DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode, - ObserverModifierNode, SpaceVectorConverter { private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() }) private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() }) private val velocityTracker = VelocityTracker() - private var previousEnabled: Boolean = false - - var enabled: () -> Boolean = enabled - set(value) { - // Reset the pointer input whenever enabled changed. - if (value != field) { - field = value - pointerInput.resetPointerInputHandler() - } - } private var converter = SpaceVectorConverter(orientation) @@ -178,21 +159,6 @@ internal class MultiPointerDraggableNode( } } - override fun onAttach() { - previousEnabled = enabled() - onObservedReadsChanged() - } - - override fun onObservedReadsChanged() { - observeReads { - val newEnabled = enabled() - if (newEnabled != previousEnabled) { - pointerInput.resetPointerInputHandler() - } - previousEnabled = newEnabled - } - } - override fun onCancelPointerInput() { pointerTracker.onCancelPointerInput() pointerInput.onCancelPointerInput() @@ -254,9 +220,7 @@ internal class MultiPointerDraggableNode( velocityTracker.resetTracking() velocityTracker.addPointerInputChange(firstPointerDown) startedPosition = firstPointerDown.position - if (enabled()) { - onFirstPointerDown() - } + onFirstPointerDown() } // Changes with at least one pointer @@ -295,10 +259,6 @@ internal class MultiPointerDraggableNode( } private suspend fun PointerInputScope.pointerInput() { - if (!enabled()) { - return - } - val currentContext = currentCoroutineContext() awaitPointerEventScope { while (currentContext.isActive) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 6e89814a2dc2..cec888380513 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -47,9 +47,6 @@ import androidx.compose.ui.unit.LayoutDirection * @param state the state of this layout. * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from, * if any. - * @param gestureFilter decides whether a drag gesture that started at the given start position - * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it - * returns `false`, the drag gesture will be handled. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param builder the configuration of the different scenes and overlays of this layout. @@ -60,7 +57,6 @@ fun SceneTransitionLayout( modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, swipeDetector: SwipeDetector = DefaultSwipeDetector, - gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f, builder: SceneTransitionLayoutScope.() -> Unit, ) { @@ -69,7 +65,6 @@ fun SceneTransitionLayout( modifier, swipeSourceDetector, swipeDetector, - gestureFilter, transitionInterceptionThreshold, onLayoutImpl = null, builder, @@ -621,7 +616,6 @@ internal fun SceneTransitionLayoutForTesting( modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, swipeDetector: SwipeDetector = DefaultSwipeDetector, - gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, builder: SceneTransitionLayoutScope.() -> Unit, @@ -638,7 +632,6 @@ internal fun SceneTransitionLayoutForTesting( transitionInterceptionThreshold = transitionInterceptionThreshold, builder = builder, animationScope = animationScope, - gestureFilter = gestureFilter, ) .also { onLayoutImpl?.invoke(it) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 9e7be37523f4..65c404387734 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.ApproachLayoutModifierNode import androidx.compose.ui.layout.ApproachMeasureScope import androidx.compose.ui.layout.LookaheadScope @@ -71,7 +70,6 @@ internal class SceneTransitionLayoutImpl( * animations. */ internal val animationScope: CoroutineScope, - internal val gestureFilter: (startedPosition: Offset) -> Boolean, ) { /** * The map of [Scene]s. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt index f758102fee47..54ee78366875 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt @@ -17,7 +17,6 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Stable -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerInputChange /** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */ @@ -32,8 +31,6 @@ interface SwipeDetector { val DefaultSwipeDetector = PassthroughSwipeDetector() -val DefaultGestureFilter = { _: Offset -> false } - /** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */ class PassthroughSwipeDetector : SwipeDetector { override fun detectSwipe(change: PointerInputChange): Boolean { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 98d4aaa91458..d201be9acc12 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -41,7 +41,28 @@ internal fun Modifier.swipeToScene( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): Modifier { - return this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) + return if (draggableHandler.enabled()) { + this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) + } else { + this + } +} + +private fun DraggableHandlerImpl.enabled(): Boolean { + return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation) +} + +private fun DraggableHandlerImpl.contentForSwipes(): Content { + return layoutImpl.contentForUserActions() +} + +/** Whether swipe should be enabled in the given [orientation]. */ +private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { + if (userActions.isEmpty()) { + return false + } + + return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } private data class SwipeToSceneElement( @@ -64,7 +85,6 @@ private class SwipeToSceneNode( delegate( MultiPointerDraggableNode( orientation = draggableHandler.orientation, - enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, onFirstPointerDown = ::onFirstPointerDown, @@ -124,22 +144,6 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() - private fun enabled(): Boolean { - return draggableHandler.isDrivingTransition || - contentForSwipes().shouldEnableSwipes(multiPointerDraggableNode.orientation) - } - - private fun contentForSwipes(): Content { - return draggableHandler.layoutImpl.contentForUserActions() - } - - /** Whether swipe should be enabled in the given [orientation]. */ - private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { - return userActions.keys.any { - it is Swipe.Resolved && it.direction.orientation == orientation - } - } - private fun startDragImmediately(startedPosition: Offset): Boolean { // Immediately start the drag if the user can't swipe in the other direction and the gesture // handler can intercept it. @@ -152,7 +156,7 @@ private class SwipeToSceneNode( Orientation.Vertical -> Orientation.Horizontal Orientation.Horizontal -> Orientation.Vertical } - return contentForSwipes().shouldEnableSwipes(oppositeOrientation) + return draggableHandler.contentForSwipes().shouldEnableSwipes(oppositeOrientation) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 16dc0d544fd2..dd4f99f5c64e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -109,8 +109,6 @@ class DraggableHandlerTest { val transitionInterceptionThreshold = 0.05f - var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter - private val layoutImpl = SceneTransitionLayoutImpl( state = layoutState, @@ -123,7 +121,6 @@ class DraggableHandlerTest { // Use testScope and not backgroundScope here because backgroundScope does not // work well with advanceUntilIdle(), which is used by some tests. animationScope = testScope, - gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) }, ) .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) } @@ -352,13 +349,6 @@ class DraggableHandlerTest { } @Test - fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest { - gestureFilter = { _ -> true } - onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f) - assertIdle(currentScene = SceneA) - } - - @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index 493f3a1377cc..c8f6e6d99933 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Velocity import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.modifiers.thenIf import com.android.compose.nestedscroll.SuspendedValue import com.google.common.truth.Truth.assertThat import kotlin.properties.Delegates @@ -94,19 +95,20 @@ class MultiPointerDraggableTest { Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - enabled = { enabled }, - startDragImmediately = { false }, - onDragStarted = { _, _, _ -> - started = true - SimpleDragController( - onDrag = { dragged = true }, - onStop = { stopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) + .thenIf(enabled) { + Modifier.multiPointerDraggable( + orientation = Orientation.Vertical, + startDragImmediately = { false }, + onDragStarted = { _, _, _ -> + started = true + SimpleDragController( + onDrag = { dragged = true }, + onStop = { stopped = true }, + ) + }, + dispatcher = defaultDispatcher, + ) + } ) } @@ -164,7 +166,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, // We want to start a drag gesture immediately startDragImmediately = { true }, onDragStarted = { _, _, _ -> @@ -238,7 +239,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> started = true @@ -358,7 +358,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> started = true @@ -464,7 +463,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> verticalStarted = true @@ -477,7 +475,6 @@ class MultiPointerDraggableTest { ) .multiPointerDraggable( orientation = Orientation.Horizontal, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> horizontalStarted = true @@ -570,7 +567,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, swipeDetector = object : SwipeDetector { @@ -672,7 +668,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> SimpleDragController( @@ -744,7 +739,6 @@ class MultiPointerDraggableTest { .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, - enabled = { true }, startDragImmediately = { false }, onDragStarted = { _, _, _ -> SimpleDragController( diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 25e87132eb0e..ce64628a863c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -22,11 +22,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -36,8 +40,11 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertPositionInRootIsEqualTo +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Density @@ -844,4 +851,29 @@ class SwipeToSceneTest { assertThat(transition.progress).isEqualTo(1f) assertThat(availableOnPostScroll).isEqualTo(ovescrollPx) } + + @Test + fun sceneWithoutSwipesDoesNotConsumeGestures() { + val buttonTag = "button" + + rule.setContent { + Box { + var count by remember { mutableStateOf(0) } + Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { + Text("Count: $count") + } + + SceneTransitionLayout(remember { MutableSceneTransitionLayoutState(SceneA) }) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + } + } + } + + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") + + // Click on the root at its center, where the button is located. Clicks should go through + // the STL and reach the button given that there is no swipes for the current scene. + repeat(3) { rule.onRoot().performClick() } + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") + } } diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp index c399abc7d81d..81d92faa40af 100644 --- a/packages/SystemUI/customization/Android.bp +++ b/packages/SystemUI/customization/Android.bp @@ -36,6 +36,7 @@ android_library { "SystemUIPluginLib", "SystemUIUnfoldLib", "kotlinx_coroutines", + "monet", "dagger2", "jsr330", ], diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml index 5eafbfc1f0b1..ec466f041179 100644 --- a/packages/SystemUI/customization/res/values/ids.xml +++ b/packages/SystemUI/customization/res/values/ids.xml @@ -6,4 +6,13 @@ <item type="id" name="weather_clock_weather_icon" /> <item type="id" name="weather_clock_temperature" /> <item type="id" name="weather_clock_alarm_dnd" /> + + <item type="id" name="HOUR_DIGIT_PAIR"/> + <item type="id" name="MINUTE_DIGIT_PAIR"/> + <item type="id" name="HOUR_FIRST_DIGIT"/> + <item type="id" name="HOUR_SECOND_DIGIT"/> + <item type="id" name="MINUTE_FIRST_DIGIT"/> + <item type="id" name="MINUTE_SECOND_DIGIT"/> + <item type="id" name="TIME_FULL_FORMAT"/> + <item type="id" name="DATE_FORMAT"/> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 1863cd861c05..9877406eeac2 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -62,6 +62,7 @@ constructor( // implement the get method and ensure a value is returned before initialization is complete. private var logger = DEFAULT_LOGGER get() = field ?: DEFAULT_LOGGER + var messageBuffer: MessageBuffer get() = logger.buffer set(value) { @@ -123,24 +124,24 @@ constructor( attrs, R.styleable.AnimatableClockView, defStyleAttr, - defStyleRes + defStyleRes, ) try { dozingWeightInternal = animatableClockViewAttributes.getInt( R.styleable.AnimatableClockView_dozeWeight, - /* default = */ 100 + /* default = */ 100, ) lockScreenWeightInternal = animatableClockViewAttributes.getInt( R.styleable.AnimatableClockView_lockScreenWeight, - /* default = */ 300 + /* default = */ 300, ) chargeAnimationDelay = animatableClockViewAttributes.getInt( R.styleable.AnimatableClockView_chargeAnimationDelay, - /* default = */ 200 + /* default = */ 200, ) } finally { animatableClockViewAttributes.recycle() @@ -151,14 +152,14 @@ constructor( attrs, android.R.styleable.TextView, defStyleAttr, - defStyleRes + defStyleRes, ) try { isSingleLineInternal = textViewAttributes.getBoolean( android.R.styleable.TextView_singleLine, - /* default = */ false + /* default = */ false, ) } finally { textViewAttributes.recycle() @@ -280,7 +281,7 @@ constructor( text: CharSequence, start: Int, lengthBefore: Int, - lengthAfter: Int + lengthAfter: Int, ) { logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() } super.onTextChanged(text, start, lengthBefore, lengthAfter) @@ -305,7 +306,7 @@ constructor( interpolator = null, duration = 0, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) setTextStyle( weight = lockScreenWeight, @@ -314,7 +315,7 @@ constructor( interpolator = null, duration = COLOR_ANIM_DURATION, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) } @@ -327,7 +328,7 @@ constructor( interpolator = null, duration = 0, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) setTextStyle( weight = lockScreenWeight, @@ -336,7 +337,7 @@ constructor( duration = APPEAR_ANIM_DURATION, interpolator = Interpolators.EMPHASIZED_DECELERATE, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) } @@ -353,7 +354,7 @@ constructor( interpolator = null, duration = 0, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) setTextStyle( weight = dozingWeightInternal, @@ -362,7 +363,7 @@ constructor( interpolator = Interpolators.EMPHASIZED_DECELERATE, duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(), delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) } @@ -381,7 +382,7 @@ constructor( interpolator = null, duration = CHARGE_ANIM_DURATION_PHASE_1, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) } setTextStyle( @@ -391,7 +392,7 @@ constructor( interpolator = null, duration = CHARGE_ANIM_DURATION_PHASE_0, delay = chargeAnimationDelay.toLong(), - onAnimationEnd = startAnimPhase2 + onAnimationEnd = startAnimPhase2, ) } @@ -404,7 +405,7 @@ constructor( interpolator = null, duration = DOZE_ANIM_DURATION, delay = 0, - onAnimationEnd = null + onAnimationEnd = null, ) } @@ -444,7 +445,7 @@ constructor( interpolator: TimeInterpolator?, duration: Long, delay: Long, - onAnimationEnd: Runnable? + onAnimationEnd: Runnable?, ) { textAnimator?.let { it.setTextStyle( @@ -454,7 +455,7 @@ constructor( duration = duration, interpolator = interpolator, delay = delay, - onAnimationEnd = onAnimationEnd + onAnimationEnd = onAnimationEnd, ) it.glyphFilter = glyphFilter } @@ -468,7 +469,7 @@ constructor( duration = duration, interpolator = interpolator, delay = delay, - onAnimationEnd = onAnimationEnd + onAnimationEnd = onAnimationEnd, ) textAnimator.glyphFilter = glyphFilter } @@ -476,6 +477,7 @@ constructor( } fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context)) + fun refreshFormat(use24HourFormat: Boolean) { Patterns.update(context) @@ -560,18 +562,11 @@ constructor( * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means * it finished moving. */ - fun offsetGlyphsForStepClockAnimation( - distance: Float, - fraction: Float, - ) { + fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) { for (i in 0 until NUM_DIGITS) { val dir = if (isLayoutRtl) -1 else 1 val digitFraction = - getDigitFraction( - digit = i, - isMovingToCenter = distance > 0, - fraction = fraction, - ) + getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction) val moveAmountForDigit = dir * distance * digitFraction glyphOffsets[i] = moveAmountForDigit diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt new file mode 100644 index 000000000000..d001ef966c13 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2024 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.clocks + +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.util.TypedValue +import com.android.internal.graphics.ColorUtils +import com.android.internal.graphics.cam.Cam +import com.android.internal.graphics.cam.CamUtils +import com.android.internal.policy.SystemBarUtils +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.monet.ColorScheme +import com.android.systemui.monet.Style as MonetStyle +import com.android.systemui.monet.TonalPalette +import java.io.IOException +import kotlin.math.abs + +class AssetLoader +private constructor( + private val pluginCtx: Context, + private val sysuiCtx: Context, + private val baseDir: String, + var colorScheme: ColorScheme?, + var seedColor: Int?, + var overrideChroma: Float?, + val typefaceCache: TypefaceCache, + val getThemeSeedColor: (Context) -> Int, + messageBuffer: MessageBuffer, +) { + val logger = Logger(messageBuffer, TAG) + private val resources = + listOf( + Pair(pluginCtx.resources, pluginCtx.packageName), + Pair(sysuiCtx.resources, sysuiCtx.packageName), + ) + + constructor( + pluginCtx: Context, + sysuiCtx: Context, + baseDir: String, + messageBuffer: MessageBuffer, + getThemeSeedColor: ((Context) -> Int)? = null, + ) : this( + pluginCtx, + sysuiCtx, + baseDir, + colorScheme = null, + seedColor = null, + overrideChroma = null, + typefaceCache = + TypefaceCache(messageBuffer) { Typeface.createFromAsset(pluginCtx.assets, it) }, + getThemeSeedColor = getThemeSeedColor ?: Companion::getThemeSeedColor, + messageBuffer = messageBuffer, + ) + + fun listAssets(path: String): List<String> { + return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList() + } + + fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString) + + fun readString(resStr: String): String { + val resPair = resolveResourceId(resStr) + if (resPair == null) { + throw IOException("Failed to parse string: $resStr") + } + + val (res, id) = resPair + return res.getString(id) + } + + fun tryReadColor(resStr: String): Int? = tryRead(resStr, ::readColor) + + fun readColor(resStr: String): Int { + if (resStr.startsWith("#")) { + return Color.parseColor(resStr) + } + + val schemeColor = tryParseColorFromScheme(resStr) + if (schemeColor != null) { + logColor("ColorScheme: $resStr", schemeColor) + return checkChroma(schemeColor) + } + + val result = resolveColorResourceId(resStr) + if (result == null) { + throw IOException("Failed to parse color: $resStr") + } + + val (res, colorId, targetTone) = result + val color = res.getColor(colorId) + if (targetTone == null || TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) { + logColor("Resources: $resStr", color) + return checkChroma(color) + } else { + val interpolatedColor = + ColorStateList.valueOf(color) + .withLStar((1000f - targetTone) / 10f) + .getDefaultColor() + logColor("Resources (interpolated tone): $resStr", interpolatedColor) + return checkChroma(interpolatedColor) + } + } + + private fun checkChroma(color: Int): Int { + return overrideChroma?.let { + val cam = Cam.fromInt(color) + val tone = CamUtils.lstarFromInt(color) + val result = ColorUtils.CAMToColor(cam.hue, it, tone) + logColor("Chroma override", result) + result + } ?: color + } + + private fun tryParseColorFromScheme(resStr: String): Int? { + val colorScheme = this.colorScheme + if (colorScheme == null) { + logger.w("No color scheme available") + return null + } + + val (packageName, category, name) = parseResourceId(resStr) + if (packageName != "android" || category != "color") { + logger.w("Failed to parse package from $resStr") + return null + } + + var parts = name.split('_') + if (parts.size != 3) { + logger.w("Failed to find palette and shade from $name") + return null + } + val (_, paletteKey, shadeKeyStr) = parts + + val palette = + when (paletteKey) { + "accent1" -> colorScheme.accent1 + "accent2" -> colorScheme.accent2 + "accent3" -> colorScheme.accent3 + "neutral1" -> colorScheme.neutral1 + "neutral2" -> colorScheme.neutral2 + else -> return null + } + + if (shadeKeyStr.contains("+") || shadeKeyStr.contains("-")) { + val signIndex = shadeKeyStr.indexOfLast { it == '-' || it == '+' } + // Use the tone of the seed color if it was set explicitly. + var baseTone = + if (seedColor != null) colorScheme.seedTone.toFloat() + else shadeKeyStr.substring(0, signIndex).toFloatOrNull() + val diff = shadeKeyStr.substring(signIndex).toFloatOrNull() + + if (baseTone == null) { + logger.w("Failed to parse base tone from $shadeKeyStr") + return null + } + + if (diff == null) { + logger.w("Failed to parse relative tone from $shadeKeyStr") + return null + } + return palette.getAtTone(baseTone + diff) + } else { + val shadeKey = shadeKeyStr.toIntOrNull() + if (shadeKey == null) { + logger.w("Failed to parse tone from $shadeKeyStr") + return null + } + return palette.allShadesMapped.get(shadeKey) ?: palette.getAtTone(shadeKey.toFloat()) + } + } + + fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr) + + fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset) + + fun readTextAsset(path: String): String { + return pluginCtx.resources.assets.open("$baseDir$path").use { stream -> + val buffer = ByteArray(stream.available()) + stream.read(buffer) + String(buffer) + } + } + + fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset) + + fun readDrawableAsset(path: String): Drawable { + var result: Drawable? + + if (path.startsWith("@")) { + val pair = resolveResourceId(path) + if (pair == null) { + throw IOException("Failed to parse $path to an id") + } + val (res, id) = pair + result = res.getDrawable(id) + } else if (path.endsWith("xml")) { + // TODO(b/248609434): Support xml files in assets + throw IOException("Cannot load xml files from assets") + } else { + // Attempt to load as if it's a bitmap and directly loadable + result = + pluginCtx.resources.assets.open("$baseDir$path").use { stream -> + Drawable.createFromResourceStream( + pluginCtx.resources, + TypedValue(), + stream, + null, + ) + } + } + + return result ?: throw IOException("Failed to load: $baseDir$path") + } + + fun parseResourceId(resStr: String): Triple<String?, String, String> { + if (!resStr.startsWith("@")) { + throw IOException("Invalid resource id: $resStr; Must start with '@'") + } + + // Parse out resource string + val parts = resStr.drop(1).split('/', ':') + return when (parts.size) { + 2 -> Triple(null, parts[0], parts[1]) + 3 -> Triple(parts[0], parts[1], parts[2]) + else -> throw IOException("Failed to parse resource string: $resStr") + } + } + + fun resolveColorResourceId(resStr: String): Triple<Resources, Int, Float?>? { + var (packageName, category, name) = parseResourceId(resStr) + + // Convert relative tonal specifiers to standard + val relIndex = name.indexOfLast { it == '_' } + val isToneRelative = name.contains("-") || name.contains("+") + val targetTone = + if (packageName != "android") { + null + } else if (isToneRelative) { + val signIndex = name.indexOfLast { it == '-' || it == '+' } + val baseTone = name.substring(relIndex + 1, signIndex).toFloatOrNull() + var diff = name.substring(signIndex).toFloatOrNull() + if (baseTone == null || diff == null) { + logger.w("Failed to parse relative tone from $name") + return null + } + baseTone + diff + } else { + val absTone = name.substring(relIndex + 1).toFloatOrNull() + if (absTone == null) { + logger.w("Failed to parse absolute tone from $name") + return null + } + absTone + } + + if ( + targetTone != null && + (isToneRelative || !TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) + ) { + val closeTone = TonalPalette.SHADE_KEYS.minBy { abs(it - targetTone) } + val prevName = name + name = name.substring(0, relIndex + 1) + closeTone + logger.i("Converted $prevName to $name") + } + + val result = resolveResourceId(packageName, category, name) + if (result == null) { + return null + } + + val (res, resId) = result + return Triple(res, resId, targetTone) + } + + fun resolveResourceId(resStr: String): Pair<Resources, Int>? { + val (packageName, category, name) = parseResourceId(resStr) + return resolveResourceId(packageName, category, name) + } + + fun resolveResourceId( + packageName: String?, + category: String, + name: String, + ): Pair<Resources, Int>? { + for ((res, ctxPkgName) in resources) { + val result = res.getIdentifier(name, category, packageName ?: ctxPkgName) + if (result != 0) { + return Pair(res, result) + } + } + return null + } + + private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? { + try { + if (arg == null) { + return null + } + return fn(arg) + } catch (ex: IOException) { + logger.w("Failed to read $arg", ex) + return null + } + } + + fun assetExists(path: String): Boolean { + try { + if (path.startsWith("@")) { + val pair = resolveResourceId(path) + val colorPair = resolveColorResourceId(path) + return pair != null || colorPair != null + } else { + val stream = pluginCtx.resources.assets.open("$baseDir$path") + if (stream == null) { + return false + } + + stream.close() + return true + } + } catch (ex: IOException) { + return false + } + } + + fun copy(messageBuffer: MessageBuffer? = null): AssetLoader = + AssetLoader( + pluginCtx, + sysuiCtx, + baseDir, + colorScheme, + seedColor, + overrideChroma, + typefaceCache, + getThemeSeedColor, + messageBuffer ?: logger.buffer, + ) + + fun setSeedColor(seedColor: Int?, style: MonetStyle?) { + this.seedColor = seedColor + refreshColorPalette(style) + } + + fun refreshColorPalette(style: MonetStyle?) { + val seedColor = + this.seedColor ?: getThemeSeedColor(sysuiCtx).also { logColor("Theme Seed Color", it) } + this.colorScheme = + ColorScheme( + seedColor, + false, // darkTheme is not used for palette generation + style ?: MonetStyle.CLOCK, + ) + + // Enforce low chroma on output colors if low chroma theme is selected + this.overrideChroma = run { + val cam = colorScheme?.seed?.let { Cam.fromInt(it) } + if (cam != null && cam.chroma < LOW_CHROMA_LIMIT) { + return@run cam.chroma * LOW_CHROMA_SCALE + } + return@run null + } + } + + fun getClockPaddingStart(): Int { + val result = resolveResourceId(null, "dimen", "clock_padding_start") + if (result != null) { + val (res, id) = result + return res.getDimensionPixelSize(id) + } + return -1 + } + + fun getStatusBarHeight(): Int { + val display = pluginCtx.getDisplayNoVerify() + if (display != null) { + return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout) + } + + logger.w("No display available; falling back to android.R.dimen.status_bar_height") + val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height") + if (statusBarHeight != null) { + val (res, resId) = statusBarHeight + return res.getDimensionPixelSize(resId) + } + + throw Exception("Could not fetch StatusBarHeight") + } + + fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id } + + fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize) + + fun getString(name: String): String = getResource("string", name, Resources::getString) + + private fun <T> getResource( + category: String, + name: String, + getter: (res: Resources, id: Int) -> T, + ): T { + val result = resolveResourceId(null, category, name) + if (result != null) { + val (res, id) = result + if (id == -1) throw Exception("Cannot find id of $id from $TAG") + return getter(res, id) + } + throw Exception("Cannot find id of $name from $TAG") + } + + private fun logColor(name: String, color: Int) { + if (DEBUG_COLOR) { + val cam = Cam.fromInt(color) + val tone = CamUtils.lstarFromInt(color) + logger.i("$name -> (hue: ${cam.hue}, chroma: ${cam.chroma}, tone: $tone)") + } + } + + companion object { + private val DEBUG_COLOR = true + private val LOW_CHROMA_LIMIT = 15 + private val LOW_CHROMA_SCALE = 1.5f + private val TAG = AssetLoader::class.simpleName!! + + private fun getThemeSeedColor(ctx: Context): Int { + return ctx.resources.getColor(android.R.color.system_palette_key_color_primary_light) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt new file mode 100644 index 000000000000..5a041691b06a --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 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.clocks + +object ClockAnimation { + const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt new file mode 100644 index 000000000000..f5e843234095 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2024 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.clocks + +import android.graphics.Point +import android.view.animation.Interpolator +import com.android.app.animation.Interpolators +import com.android.internal.annotations.Keep +import com.android.systemui.monet.Style as MonetStyle +import com.android.systemui.shared.clocks.view.HorizontalAlignment +import com.android.systemui.shared.clocks.view.VerticalAlignment + +/** Data format for a simple asset-defined clock */ +@Keep +data class ClockDesign( + val id: String, + val name: String? = null, + val description: String? = null, + val thumbnail: String? = null, + val large: ClockFace? = null, + val small: ClockFace? = null, + val colorPalette: MonetStyle? = null, +) + +/** Describes a clock using layers */ +@Keep +data class ClockFace( + val layers: List<ClockLayer> = listOf<ClockLayer>(), + val layerBounds: LayerBounds = LayerBounds.FIT, + val wallpaper: String? = null, + val faceLayout: DigitalFaceLayout? = null, + val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f), +) + +@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float) + +/** Base Type for a Clock Layer */ +@Keep +interface ClockLayer { + /** Override of face LayerBounds setting for this layer */ + val layerBounds: LayerBounds? +} + +/** Clock layer that renders a static asset */ +@Keep +data class AssetLayer( + /** Asset to render in this layer */ + val asset: AssetReference, + override val layerBounds: LayerBounds? = null, +) : ClockLayer + +/** Clock layer that renders the time (or a component of it) using numerals */ +@Keep +data class DigitalHandLayer( + /** See SimpleDateFormat for timespec format info */ + val timespec: DigitalTimespec, + val style: TextStyle, + // adoStyle concrete type must match style, + // cause styles will transition between style and aodStyle + val aodStyle: TextStyle?, + val timer: Int? = null, + override val layerBounds: LayerBounds? = null, + var faceLayout: DigitalFaceLayout? = null, + // we pass 12-hour format from json, which will be converted to 24-hour format in codes + val dateTimeFormat: String, + val alignment: DigitalAlignment?, + // ratio of margins to measured size, currently used for handwritten clocks + val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(), +) : ClockLayer + +/** Clock layer that renders the time (or a component of it) using numerals */ +@Keep +data class ComposedDigitalHandLayer( + val customizedView: String? = null, + /** See SimpleDateFormat for timespec format info */ + val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(), + override val layerBounds: LayerBounds? = null, +) : ClockLayer + +@Keep +data class DigitalAlignment( + val horizontalAlignment: HorizontalAlignment?, + val verticalAlignment: VerticalAlignment?, +) + +@Keep +data class DigitalMarginRatio( + val left: Float = 0F, + val top: Float = 0F, + val right: Float = 0F, + val bottom: Float = 0F, +) + +/** Clock layer which renders a component of the time using an analog hand */ +@Keep +data class AnalogHandLayer( + val timespec: AnalogTimespec, + val tickMode: AnalogTickMode, + val asset: AssetReference, + val timer: Int? = null, + val clock_pivot: Point = Point(0, 0), + val asset_pivot: Point? = null, + val length: Float = 1f, + override val layerBounds: LayerBounds? = null, +) : ClockLayer + +/** Clock layer which renders the time using an AVD */ +@Keep +data class AnimatedHandLayer( + val timespec: AnalogTimespec, + val asset: AssetReference, + val timer: Int? = null, + override val layerBounds: LayerBounds? = null, +) : ClockLayer + +/** A collection of asset references for use in different device modes */ +@Keep +data class AssetReference( + val light: String, + val dark: String, + val doze: String? = null, + val lightTint: String? = null, + val darkTint: String? = null, + val dozeTint: String? = null, +) + +/** + * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to + * either subtype. + */ +@Keep +interface TextStyle { + // fontSizeScale is a scale factor applied to the default clock's font size. + val fontSizeScale: Float? +} + +/** + * This specifies a font and styling parameters for that font. This is rendered using a text view + * and the text animation classes used by the default clock. To ensure default value take effects, + * all parameters MUST have a default value + */ +@Keep +data class FontTextStyle( + // Font to load and use in the TextView + val fontFamily: String? = null, + val lineHeight: Float? = null, + val borderWidth: String? = null, + // ratio of borderWidth / fontSize + val borderWidthScale: Float? = null, + // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100` + val fillColorLight: String? = null, + // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100` + val fillColorDark: String? = null, + override val fontSizeScale: Float? = null, + /** + * use `wdth` for width, `wght` for weight, 'opsz' for optical size single quote for tag name, + * and no quote for value separate different axis with `,` e.g. "'wght' 1000, 'wdth' 108, 'opsz' + * 90" + */ + var fontVariation: String? = null, + // used when alternate in one font file is needed + var fontFeatureSettings: String? = null, + val renderType: RenderType = RenderType.STROKE_TEXT, + val outlineColor: String? = null, + val transitionDuration: Long = -1L, + val transitionInterpolator: InterpolatorEnum? = null, +) : TextStyle + +/** + * As an alternative to using a font, we can instead render a digital clock using a set of drawables + * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing + * and placing them. This may be easier than generating a font file in some cases, and is provided + * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese). + */ +@Keep +data class LottieTextStyle( + val numbers: List<String> = listOf(), + // Spacing between numbers, dimension string + val spacing: String = "0dp", + // Colon drawable may be omitted if unused in format spec + val colon: String? = null, + // key is keypath name to get strokes from lottie, value is the color name to query color in + // palette, e.g. @android:color/system_accent1_100 + val fillColorLightMap: Map<String, String>? = null, + val fillColorDarkMap: Map<String, String>? = null, + override val fontSizeScale: Float? = null, + val paddingVertical: String = "0dp", + val paddingHorizontal: String = "0dp", +) : TextStyle + +/** Layer sizing mode for the clockface or layer */ +enum class LayerBounds { + /** + * Sized so the larger dimension matches the allocated space. This results in some of the + * allocated space being unused. + */ + FIT, + + /** + * Sized so the smaller dimension matches the allocated space. This will clip some content to + * the edges of the space. + */ + FILL, + + /** Fills the allocated space exactly by stretching the layer */ + STRETCH, +} + +/** Ticking mode for analog hands. */ +enum class AnalogTickMode { + SWEEP, + TICK, +} + +/** Timspec options for Analog Hands. Named for tick interval. */ +enum class AnalogTimespec { + SECONDS, + MINUTES, + HOURS, + HOURS_OF_DAY, + DAY_OF_WEEK, + DAY_OF_MONTH, + DAY_OF_YEAR, + WEEK, + MONTH, + TIMER, +} + +enum class DigitalTimespec { + TIME_FULL_FORMAT, + DIGIT_PAIR, + FIRST_DIGIT, + SECOND_DIGIT, + DATE_FORMAT, +} + +enum class DigitalFaceLayout { + // can only use HH_PAIR, MM_PAIR from DigitalTimespec + TWO_PAIRS_VERTICAL, + TWO_PAIRS_HORIZONTAL, + // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT + // from DigitalTimespec, used for tabular layout when the font doesn't support tnum + FOUR_DIGITS_ALIGN_CENTER, + FOUR_DIGITS_HORIZONTAL, +} + +enum class RenderType { + CHANGE_WEIGHT, + HOLLOW_TEXT, + STROKE_TEXT, + OUTER_OUTLINE_TEXT, +} + +enum class InterpolatorEnum(factory: () -> Interpolator) { + STANDARD({ Interpolators.STANDARD }), + EMPHASIZED({ Interpolators.EMPHASIZED }); + + val interpolator: Interpolator by lazy(factory) +} + +fun generateDigitalLayerIdString(layer: DigitalHandLayer): String { + return if ( + layer.timespec == DigitalTimespec.TIME_FULL_FORMAT || + layer.timespec == DigitalTimespec.DATE_FORMAT + ) { + layer.timespec.toString() + } else { + if ("h" in layer.dateTimeFormat) { + "HOUR" + "_" + layer.timespec.toString() + } else { + "MINUTE" + "_" + layer.timespec.toString() + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 954155d16b05..9da3022fc0d8 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -65,7 +65,7 @@ private val KNOWN_PLUGINS = private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut( key: TKey, value: TVal, - onNew: (TVal) -> Unit + onNew: (TVal) -> Unit, ): TVal { val result = this.putIfAbsent(key, value) if (result == null) { @@ -110,7 +110,7 @@ open class ClockRegistry( selfChange: Boolean, uris: Collection<Uri>, flags: Int, - userId: Int + userId: Int, ) { scope.launch(bgDispatcher) { querySettings() } } @@ -180,7 +180,7 @@ open class ClockRegistry( override fun onPluginLoaded( plugin: ClockProviderPlugin, pluginContext: Context, - manager: PluginLifecycleManager<ClockProviderPlugin> + manager: PluginLifecycleManager<ClockProviderPlugin>, ) { plugin.initialize(clockBuffers) @@ -218,7 +218,7 @@ open class ClockRegistry( override fun onPluginUnloaded( plugin: ClockProviderPlugin, - manager: PluginLifecycleManager<ClockProviderPlugin> + manager: PluginLifecycleManager<ClockProviderPlugin>, ) { for (clock in plugin.getClocks()) { val id = clock.clockId @@ -290,12 +290,12 @@ open class ClockRegistry( Settings.Secure.getStringForUser( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, - ActivityManager.getCurrentUser() + ActivityManager.getCurrentUser(), ) } else { Settings.Secure.getString( context.contentResolver, - Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, ) } @@ -320,13 +320,13 @@ open class ClockRegistry( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json, - ActivityManager.getCurrentUser() + ActivityManager.getCurrentUser(), ) } else { Settings.Secure.putString( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, - json + json, ) } } catch (ex: Exception) { @@ -418,7 +418,7 @@ open class ClockRegistry( pluginManager.addPluginListener( pluginListener, ClockProviderPlugin::class.java, - /*allowMultiple=*/ true + /*allowMultiple=*/ true, ) scope.launch(bgDispatcher) { querySettings() } @@ -427,7 +427,7 @@ open class ClockRegistry( Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), /*notifyForDescendants=*/ false, settingObserver, - UserHandle.USER_ALL + UserHandle.USER_ALL, ) ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG) @@ -435,7 +435,7 @@ open class ClockRegistry( context.contentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), /*notifyForDescendants=*/ false, - settingObserver + settingObserver, ) } } @@ -504,7 +504,7 @@ open class ClockRegistry( val isCurrent = currentClockId == info.metadata.clockId logger.log( if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, - { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" } + { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, ) { str1 = info.metadata.clockId str2 = info.manager.toString() @@ -516,7 +516,7 @@ open class ClockRegistry( val isCurrent = currentClockId == info.metadata.clockId logger.log( if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, - { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" } + { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, ) { str1 = info.metadata.clockId str2 = info.manager.toString() @@ -532,7 +532,7 @@ open class ClockRegistry( val isCurrent = currentClockId == info.metadata.clockId logger.log( if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG, - { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" } + { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, ) { str1 = info.metadata.clockId str2 = info.manager.toString() @@ -548,7 +548,7 @@ open class ClockRegistry( val isCurrent = currentClockId == info.metadata.clockId logger.log( if (isCurrent) LogLevel.INFO else LogLevel.DEBUG, - { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" } + { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }, ) { str1 = info.metadata.clockId str2 = info.manager.toString() diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 4802e3447eb2..07191c671a34 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -34,7 +34,7 @@ class DefaultClockProvider( val layoutInflater: LayoutInflater, val resources: Resources, val hasStepClockAnimation: Boolean = false, - val migratedClocks: Boolean = false + val migratedClocks: Boolean = false, ) : ClockProvider { private var messageBuffers: ClockMessageBuffers? = null diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt new file mode 100644 index 000000000000..38697063bea5 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.clocks + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.graphics.Point + +class DigitTranslateAnimator(val updateCallback: () -> Unit) { + val DEFAULT_ANIMATION_DURATION = 500L + val updatedTranslate = Point(0, 0) + + val baseTranslation = Point(0, 0) + var targetTranslation: Point? = null + val bounceAnimator: ValueAnimator = + ValueAnimator.ofFloat(1f).apply { + duration = DEFAULT_ANIMATION_DURATION + addUpdateListener { + updateTranslation(it.animatedFraction, updatedTranslate) + updateCallback() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rebase() + } + + override fun onAnimationCancel(animation: Animator) { + rebase() + } + } + ) + } + + fun rebase() { + baseTranslation.x = updatedTranslate.x + baseTranslation.y = updatedTranslate.y + } + + fun animatePosition( + animate: Boolean = true, + delay: Long = 0, + duration: Long = -1L, + interpolator: TimeInterpolator? = null, + targetTranslation: Point? = null, + onAnimationEnd: Runnable? = null, + ) { + this.targetTranslation = targetTranslation ?: Point(0, 0) + if (animate) { + bounceAnimator.cancel() + bounceAnimator.startDelay = delay + bounceAnimator.duration = + if (duration == -1L) { + DEFAULT_ANIMATION_DURATION + } else { + duration + } + interpolator?.let { bounceAnimator.interpolator = it } + if (onAnimationEnd != null) { + val listener = + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd.run() + bounceAnimator.removeListener(this) + } + + override fun onAnimationCancel(animation: Animator) { + bounceAnimator.removeListener(this) + } + } + bounceAnimator.addListener(listener) + } + bounceAnimator.start() + } else { + // No animation is requested, thus set base and target state to the same state. + updateTranslation(1F, updatedTranslate) + rebase() + updateCallback() + } + } + + fun updateTranslation(progress: Float, outPoint: Point) { + outPoint.x = + (baseTranslation.x + progress * (targetTranslation!!.x - baseTranslation.x)).toInt() + outPoint.y = + (baseTranslation.y + progress * (targetTranslation!!.y - baseTranslation.y)).toInt() + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt new file mode 100644 index 000000000000..2be6c6573ebe --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 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.clocks + +import android.content.Context +import android.util.TypedValue +import java.util.regex.Pattern + +class DimensionParser(private val ctx: Context) { + fun convert(dimension: String?): Float? { + if (dimension == null) { + return null + } + return convert(dimension) + } + + fun convert(dimension: String): Float { + val metrics = ctx.resources.displayMetrics + val (value, unit) = parse(dimension) + return TypedValue.applyDimension(unit, value, metrics) + } + + fun parse(dimension: String): Pair<Float, Int> { + val matcher = parserPattern.matcher(dimension) + if (!matcher.matches()) { + throw NumberFormatException("Failed to parse '$dimension'") + } + + val value = + matcher.group(1)?.toFloat() ?: throw NumberFormatException("Bad value in '$dimension'") + val unit = + dimensionMap.get(matcher.group(3) ?: "") + ?: throw NumberFormatException("Bad unit in '$dimension'") + return Pair(value, unit) + } + + private companion object { + val parserPattern = Pattern.compile("(\\d+(\\.\\d+)?)([a-z]+)") + val dimensionMap = + mapOf( + "dp" to TypedValue.COMPLEX_UNIT_DIP, + "dip" to TypedValue.COMPLEX_UNIT_DIP, + "sp" to TypedValue.COMPLEX_UNIT_SP, + "px" to TypedValue.COMPLEX_UNIT_PX, + "pt" to TypedValue.COMPLEX_UNIT_PT, + "mm" to TypedValue.COMPLEX_UNIT_MM, + "in" to TypedValue.COMPLEX_UNIT_IN, + ) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt new file mode 100644 index 000000000000..34cb4ef7089d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.clocks + +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.LogcatOnlyMessageBuffer +import com.android.systemui.log.core.Logger + +object LogUtil { + // Used when MessageBuffers are not provided by the host application + val DEFAULT_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.INFO) + + // Only intended for use during initialization steps where the correct logger doesn't exist yet + val FALLBACK_INIT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.ERROR), "CLOCK_INIT") + + // Debug is primarially used for tests, but can also be used for tracking down hard issues. + val DEBUG_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.DEBUG) +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt new file mode 100644 index 000000000000..f5a9375122b5 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 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.clocks + +import android.graphics.Typeface +import com.android.systemui.animation.TypefaceVariantCache +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference + +class TypefaceCache(messageBuffer: MessageBuffer, val typefaceFactory: (String) -> Typeface) { + private val logger = Logger(messageBuffer, this::class.simpleName!!) + + private data class CacheKey(val res: String, val fvar: String?) + + private inner class WeakTypefaceRef(val key: CacheKey, typeface: Typeface) : + WeakReference<Typeface>(typeface, queue) + + private var totalHits = 0 + + private var totalMisses = 0 + + private var totalEvictions = 0 + + // We use a map of WeakRefs here instead of an LruCache. This prevents needing to resize the + // cache depending on the number of distinct fonts used by a clock, as different clocks have + // different numbers of simultaneously loaded and configured fonts. Because our clocks tend to + // initialize a number of parallel views and animators, our usages of Typefaces overlap. As a + // result, once a typeface is no longer being used, it is unlikely to be recreated immediately. + private val cache = mutableMapOf<CacheKey, WeakTypefaceRef>() + private val queue = ReferenceQueue<Typeface>() + + fun getTypeface(res: String): Typeface { + checkQueue() + val key = CacheKey(res, null) + cache.get(key)?.get()?.let { + logHit(key) + return it + } + + logMiss(key) + val result = typefaceFactory(res) + cache.put(key, WeakTypefaceRef(key, result)) + return result + } + + fun getVariantCache(res: String): TypefaceVariantCache { + val baseTypeface = getTypeface(res) + return object : TypefaceVariantCache { + override fun getTypefaceForVariant(fvar: String?): Typeface? { + checkQueue() + val key = CacheKey(res, fvar) + cache.get(key)?.get()?.let { + logHit(key) + return it + } + + logMiss(key) + return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also { + cache.put(key, WeakTypefaceRef(key, it)) + } + } + } + } + + private fun logHit(key: CacheKey) { + totalHits++ + if (DEBUG_HITS) + logger.i({ "HIT: $str1; Total: $int1" }) { + str1 = key.toString() + int1 = totalHits + } + } + + private fun logMiss(key: CacheKey) { + totalMisses++ + logger.w({ "MISS: $str1; Total: $int1" }) { + str1 = key.toString() + int1 = totalMisses + } + } + + private fun logEviction(key: CacheKey) { + totalEvictions++ + logger.i({ "EVICTED: $str1; Total: $int1" }) { + str1 = key.toString() + int1 = totalEvictions + } + } + + private fun checkQueue() = + generateSequence { queue.poll() } + .filterIsInstance<WeakTypefaceRef>() + .forEach { + logEviction(it.key) + cache.remove(it.key) + } + + companion object { + private val DEBUG_HITS = false + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt new file mode 100644 index 000000000000..eb7234646a64 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2024 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.clocks.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Point +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.AssetLoader +import com.android.systemui.shared.clocks.LogUtil +import java.util.Locale + +abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) { + protected val logger = Logger(messageBuffer, this::class.simpleName!!) + get() = field ?: LogUtil.FALLBACK_INIT_LOGGER + + abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView> + + @VisibleForTesting + var isAnimationEnabled = true + set(value) { + field = value + digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value } + } + + var dozeFraction: Float = 0F + set(value) { + field = value + digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field } + } + + val dozeControlState = DozeControlState() + + var isReactiveTouchInteractionEnabled = false + set(value) { + field = value + } + + open val text: String? + get() = null + + open fun refreshTime() = logger.d("refreshTime()") + + override fun invalidate() { + logger.d("invalidate()") + super.invalidate() + } + + override fun requestLayout() { + logger.d("requestLayout()") + super.requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + logger.d("onMeasure()") + calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) } + ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) } + calculateLeftTopPosition() + dozeControlState.animateReady = true + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + logger.d("onLayout()") + super.onLayout(changed, left, top, right, bottom) + } + + override fun onDraw(canvas: Canvas) { + text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") } + super.onDraw(canvas) + } + + /* + * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null + * result causes the normal view measuring logic to execute. + */ + protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null + + protected open fun calculateLeftTopPosition() {} + + override fun addView(child: View?) { + if (child == null) return + logger.d({ "addView($str1 @$int1)" }) { + str1 = child::class.simpleName!! + int1 = child.id + } + super.addView(child) + if (child is SimpleDigitalClockTextView) { + digitalClockTextViewMap[child.id] = child + } + child.setWillNotDraw(true) + } + + open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { + digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) } + } + + open fun animateCharge() { + digitalClockTextViewMap.forEach { _, view -> view.animateCharge() } + } + + open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} + + fun updateColors(assets: AssetLoader, isRegionDark: Boolean) { + digitalClockTextViewMap.forEach { _, view -> view.updateColors(assets, isRegionDark) } + invalidate() + } + + fun onFontSettingChanged(fontSizePx: Float) { + digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) } + } + + open val hasCustomWeatherDataDisplay + get() = false + + open val hasCustomPositionUpdatedAnimation + get() = false + + /** True if it's large weather clock, will use weatherBlueprint in compose */ + open val useCustomClockScene + get() = false + + // TODO: implement ClockEventUnion? + open fun onLocaleChanged(locale: Locale) {} + + open fun onWeatherDataChanged(data: WeatherData) {} + + open fun onAlarmDataChanged(data: AlarmData) {} + + open fun onZenDataChanged(data: ZenData) {} + + open fun onPickerCarouselSwiping(swipingFraction: Float) {} + + open fun isAlignedWithScreen(): Boolean = false + + /** + * animateDoze needs correct translate value, which is calculated in onMeasure so we need to + * delay this animation when we get correct values + */ + class DozeControlState { + var animateDoze: () -> Unit = {} + set(value) { + if (animateReady) { + value() + field = {} + } else { + field = value + } + } + + var animateReady = false + set(value) { + if (value) { + animateDoze() + animateDoze = {} + } + field = value + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt new file mode 100644 index 000000000000..c29c8dac8ba6 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2024 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.clocks.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Point +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import com.android.app.animation.Interpolators +import com.android.systemui.customization.R +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.shared.clocks.AssetLoader +import com.android.systemui.shared.clocks.DigitTranslateAnimator +import com.android.systemui.shared.clocks.FontTextStyle +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal) + +class FlexClockView(context: Context, val assetLoader: AssetLoader, messageBuffer: MessageBuffer) : + DigitalClockFaceView(context, messageBuffer) { + override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>() + val digitLeftTopMap = mutableMapOf<Int, Point>() + var maxSingleDigitHeight = -1 + var maxSingleDigitWidth = -1 + val lockscreenTranslate = Point(0, 0) + val aodTranslate = Point(0, 0) + + init { + setWillNotDraw(false) + layoutParams = + RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + + private var prevX = 0f + private var prevY = 0f + private var isDown = false + + // TODO(b/340253296): Genericize; json spec + private var wght = 603f + private var wdth = 100f + + // TODO(b/340253296): Json spec + private val MAX_WGHT = 950f + private val MIN_WGHT = 50f + private val WGHT_SCALE = 0.5f + + private val MAX_WDTH = 150f + private val MIN_WDTH = 0f + private val WDTH_SCALE = 0.2f + + override fun onTouchEvent(evt: MotionEvent): Boolean { + // TODO(b/340253296): implement on DigitalClockFaceView? + if (!isReactiveTouchInteractionEnabled) { + return super.onTouchEvent(evt) + } + + when (evt.action) { + MotionEvent.ACTION_DOWN -> { + isDown = true + prevX = evt.x + prevY = evt.y + return true + } + + MotionEvent.ACTION_MOVE -> { + if (!isDown) { + return super.onTouchEvent(evt) + } + + wdth = clamp(wdth + (evt.x - prevX) * WDTH_SCALE, MIN_WDTH, MAX_WDTH) + wght = clamp(wght + (evt.y - prevY) * WGHT_SCALE, MIN_WGHT, MAX_WGHT) + prevX = evt.x + prevY = evt.y + + // TODO(b/340253296): Genericize; json spec + val fvar = "'wght' $wght, 'wdth' $wdth, 'opsz' 144, 'ROND' 100" + digitalClockTextViewMap.forEach { (_, view) -> + val textStyle = view.textStyle as FontTextStyle + textStyle.fontVariation = fvar + view.applyStyles(assetLoader, textStyle, view.aodStyle) + } + + requestLayout() + invalidate() + return true + } + + MotionEvent.ACTION_UP -> { + isDown = false + return true + } + } + + return super.onTouchEvent(evt) + } + + override fun addView(child: View?) { + super.addView(child) + (child as SimpleDigitalClockTextView).digitTranslateAnimator = + DigitTranslateAnimator(::invalidate) + } + + protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point { + digitalClockTextViewMap.forEach { (_, textView) -> + textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + } + val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!! + maxSingleDigitHeight = textView.measuredHeight + maxSingleDigitWidth = textView.measuredWidth + aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt() + aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt() + return Point( + ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2), + ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2), + ) + } + + protected override fun calculateLeftTopPosition() { + digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0) + digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0) + digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight) + digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight) + digitLeftTopMap.forEach { _, point -> + point.x += abs(aodTranslate.x) + point.y += abs(aodTranslate.y) + } + } + + override fun refreshTime() { + super.refreshTime() + digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + digitalClockTextViewMap.forEach { (id, _) -> + val textView = digitalClockTextViewMap[id]!! + canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat()) + textView.draw(canvas) + canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat()) + } + } + + override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { + dozeControlState.animateDoze = { + super.animateDoze(isDozing, isAnimated) + if (maxSingleDigitHeight == -1) { + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + } + digitalClockTextViewMap.forEach { (id, textView) -> + textView.digitTranslateAnimator?.let { + if (!isDozing) { + it.animatePosition( + animate = isAnimated && isAnimationEnabled, + interpolator = Interpolators.EMPHASIZED, + duration = AOD_TRANSITION_DURATION, + targetTranslation = + updateDirectionalTargetTranslate(id, lockscreenTranslate), + ) + } else { + it.animatePosition( + animate = isAnimated && isAnimationEnabled, + interpolator = Interpolators.EMPHASIZED, + duration = AOD_TRANSITION_DURATION, + onAnimationEnd = null, + targetTranslation = updateDirectionalTargetTranslate(id, aodTranslate), + ) + } + } + } + } + } + + override fun animateCharge() { + super.animateCharge() + digitalClockTextViewMap.forEach { (id, textView) -> + textView.digitTranslateAnimator?.let { + it.animatePosition( + animate = isAnimationEnabled, + interpolator = Interpolators.EMPHASIZED, + duration = CHARGING_TRANSITION_DURATION, + onAnimationEnd = { + it.animatePosition( + animate = isAnimationEnabled, + interpolator = Interpolators.EMPHASIZED, + duration = CHARGING_TRANSITION_DURATION, + targetTranslation = + updateDirectionalTargetTranslate( + id, + if (dozeFraction == 1F) aodTranslate else lockscreenTranslate, + ), + ) + }, + targetTranslation = + updateDirectionalTargetTranslate( + id, + if (dozeFraction == 1F) lockscreenTranslate else aodTranslate, + ), + ) + } + } + } + + companion object { + val AOD_TRANSITION_DURATION = 750L + val CHARGING_TRANSITION_DURATION = 300L + + val AOD_HORIZONTAL_TRANSLATE_RATIO = 0.15F + val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F + + // Use the sign of targetTranslation to control the direction of digit translation + fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point { + val outPoint = Point(targetTranslation) + when (id) { + R.id.HOUR_FIRST_DIGIT -> { + outPoint.x *= -1 + outPoint.y *= -1 + } + + R.id.HOUR_SECOND_DIGIT -> { + outPoint.x *= 1 + outPoint.y *= -1 + } + + R.id.MINUTE_FIRST_DIGIT -> { + outPoint.x *= -1 + outPoint.y *= 1 + } + + R.id.MINUTE_SECOND_DIGIT -> { + outPoint.x *= 1 + outPoint.y *= 1 + } + } + return outPoint + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt new file mode 100644 index 000000000000..74617b1c0c5c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2024 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.clocks.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.text.Layout +import android.text.TextPaint +import android.util.AttributeSet +import android.util.Log +import android.util.MathUtils +import android.util.TypedValue +import android.view.View.MeasureSpec.AT_MOST +import android.view.View.MeasureSpec.EXACTLY +import android.view.animation.Interpolator +import android.widget.TextView +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.animation.TextAnimator +import com.android.systemui.animation.TypefaceVariantCache +import com.android.systemui.customization.R +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.shared.clocks.AssetLoader +import com.android.systemui.shared.clocks.ClockAnimation +import com.android.systemui.shared.clocks.DigitTranslateAnimator +import com.android.systemui.shared.clocks.DimensionParser +import com.android.systemui.shared.clocks.FontTextStyle +import com.android.systemui.shared.clocks.LogUtil +import com.android.systemui.shared.clocks.RenderType +import com.android.systemui.shared.clocks.TextStyle +import java.lang.Thread +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +private val TAG = SimpleDigitalClockTextView::class.simpleName!! + +@SuppressLint("AppCompatCustomView") +open class SimpleDigitalClockTextView( + ctx: Context, + messageBuffer: MessageBuffer, + attrs: AttributeSet? = null, +) : TextView(ctx, attrs), SimpleDigitalClockView { + val lockScreenPaint = TextPaint() + override lateinit var textStyle: FontTextStyle + lateinit var aodStyle: FontTextStyle + private val parser = DimensionParser(ctx) + var maxSingleDigitHeight = -1 + var maxSingleDigitWidth = -1 + var digitTranslateAnimator: DigitTranslateAnimator? = null + var aodFontSizePx: Float = -1F + var isVertical: Boolean = false + + // Store the font size when there's no height constraint as a reference when adjusting font size + private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE + // Calculated by height of styled text view / text size + // Used as a factor to calculate a smaller font size when text height is constrained + @VisibleForTesting var fontSizeAdjustFactor = 1F + + private val initThread = Thread.currentThread() + + // textBounds is the size of text in LS, which only measures current text in lockscreen style + var textBounds = Rect() + // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD + // especially for the textView which has different bounds during the animation + // prevTextBounds holds the state we are transitioning from + private val prevTextBounds = Rect() + // targetTextBounds holds the state we are interpolating to + private val targetTextBounds = Rect() + protected val logger = Logger(messageBuffer, this::class.simpleName!!) + get() = field ?: LogUtil.FALLBACK_INIT_LOGGER + + private var aodDozingInterpolator: Interpolator? = null + + @VisibleForTesting lateinit var textAnimator: TextAnimator + @VisibleForTesting var outlineAnimator: TextAnimator? = null + // used for hollow style for AOD version + // because stroke style for some fonts have some unwanted inner strokes + // we want to draw this layer on top to oclude them + @VisibleForTesting var innerAnimator: TextAnimator? = null + + lateinit var typefaceCache: TypefaceVariantCache + private set + + private fun setTypefaceCache(value: TypefaceVariantCache) { + typefaceCache = value + if (this::textAnimator.isInitialized) { + textAnimator.typefaceCache = value + } + outlineAnimator?.typefaceCache = value + innerAnimator?.typefaceCache = value + } + + @VisibleForTesting + var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb -> + TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also { + if (this::typefaceCache.isInitialized) { + it.typefaceCache = typefaceCache + } + } + } + + override var verticalAlignment: VerticalAlignment = VerticalAlignment.CENTER + override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT + override var isAnimationEnabled = true + override var dozeFraction: Float = 0F + set(value) { + field = value + invalidate() + } + + // Have to passthrough to unify View with SimpleDigitalClockView + override var text: String + get() = super.getText().toString() + set(value) = super.setText(value) + + var textBorderWidth = 0F + var aodBorderWidth = 0F + var baselineFromMeasure = 0 + + var textFillColor: Int? = null + var textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR + var aodFillColor = AOD_DEFAULT_COLOR + var aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR + + override fun updateColors(assets: AssetLoader, isRegionDark: Boolean) { + val fillColor = if (isRegionDark) textStyle.fillColorLight else textStyle.fillColorDark + textFillColor = + fillColor?.let { assets.readColor(it) } + ?: assets.seedColor + ?: getDefaultColor(assets, isRegionDark) + // for NumberOverlapView to read correct color + lockScreenPaint.color = textFillColor as Int + textStyle.outlineColor?.let { textOutlineColor = assets.readColor(it) } + ?: run { textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR } + (aodStyle.fillColorLight ?: aodStyle.fillColorDark)?.let { + aodFillColor = assets.readColor(it) + } ?: run { aodFillColor = AOD_DEFAULT_COLOR } + aodStyle.outlineColor?.let { aodOutlineColor = assets.readColor(it) } + ?: run { aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR } + if (dozeFraction < 1f) { + textAnimator.setTextStyle(color = textFillColor, animate = false) + outlineAnimator?.setTextStyle(color = textOutlineColor, animate = false) + } + invalidate() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + logger.d("onMeasure()") + if (isVertical) { + // use at_most to avoid apply measuredWidth from last measuring to measuredHeight + // cause we use max to setMeasuredDimension + super.onMeasure( + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST), + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST), + ) + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + val layout = this.layout + if (layout != null) { + if (!this::textAnimator.isInitialized) { + textAnimator = textAnimatorFactory(layout, ::invalidate) + outlineAnimator = textAnimatorFactory(layout) {} + innerAnimator = textAnimatorFactory(layout) {} + setInterpolatorPaint() + } else { + textAnimator.updateLayout(layout) + outlineAnimator?.updateLayout(layout) + innerAnimator?.updateLayout(layout) + } + baselineFromMeasure = layout.getLineBaseline(0) + } else { + val currentThread = Thread.currentThread() + Log.wtf( + TAG, + "TextView.getLayout() is null after measure! " + + "currentThread=$currentThread; initThread=$initThread", + ) + } + + var expectedWidth: Int + var expectedHeight: Int + + if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { + // For view which has fixed height, e.g. small clock, + // we should always return the size required from parent view + expectedHeight = heightMeasureSpec + } else { + expectedHeight = + MeasureSpec.makeMeasureSpec( + if (isSingleDigit()) { + maxSingleDigitHeight + } else { + textBounds.height() + 2 * lockScreenPaint.strokeWidth.toInt() + }, + MeasureSpec.getMode(measuredHeight), + ) + } + if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) { + expectedWidth = widthMeasureSpec + } else { + expectedWidth = + MeasureSpec.makeMeasureSpec( + if (isSingleDigit()) { + maxSingleDigitWidth + } else { + max( + textBounds.width() + 2 * lockScreenPaint.strokeWidth.toInt(), + MeasureSpec.getSize(measuredWidth), + ) + }, + MeasureSpec.getMode(measuredWidth), + ) + } + + if (isVertical) { + expectedWidth = expectedHeight.also { expectedHeight = expectedWidth } + } + setMeasuredDimension(expectedWidth, expectedHeight) + } + + override fun onDraw(canvas: Canvas) { + if (isVertical) { + canvas.save() + canvas.translate(0F, measuredHeight.toFloat()) + canvas.rotate(-90F) + } + logger.d({ "onDraw(); ls: $str1; aod: $str2;" }) { + str1 = textAnimator.textInterpolator.shapedText + str2 = outlineAnimator?.textInterpolator?.shapedText + } + val translation = getLocalTranslation() + canvas.translate(translation.x.toFloat(), translation.y.toFloat()) + digitTranslateAnimator?.let { + canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat()) + } + + if (aodStyle.renderType == RenderType.HOLLOW_TEXT) { + canvas.saveLayer( + -translation.x.toFloat(), + -translation.y.toFloat(), + (-translation.x + measuredWidth).toFloat(), + (-translation.y + measuredHeight).toFloat(), + null, + ) + outlineAnimator?.draw(canvas) + canvas.saveLayer( + -translation.x.toFloat(), + -translation.y.toFloat(), + (-translation.x + measuredWidth).toFloat(), + (-translation.y + measuredHeight).toFloat(), + Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }, + ) + innerAnimator?.draw(canvas) + canvas.restore() + canvas.restore() + } else if (aodStyle.renderType != RenderType.CHANGE_WEIGHT) { + outlineAnimator?.draw(canvas) + } + textAnimator.draw(canvas) + + digitTranslateAnimator?.let { + canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat()) + } + canvas.translate(-translation.x.toFloat(), -translation.y.toFloat()) + if (isVertical) { + canvas.restore() + } + } + + override fun invalidate() { + logger.d("invalidate()") + super.invalidate() + (parent as? DigitalClockFaceView)?.invalidate() + } + + override fun refreshTime() { + logger.d("refreshTime()") + refreshText() + } + + override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { + if (!this::textAnimator.isInitialized) { + return + } + val fvar = if (isDozing) aodStyle.fontVariation else textStyle.fontVariation + textAnimator.setTextStyle( + animate = isAnimated && isAnimationEnabled, + color = if (isDozing) aodFillColor else textFillColor, + textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, + fvar = fvar, + duration = aodStyle.transitionDuration, + interpolator = aodDozingInterpolator, + ) + updateTextBoundsForTextAnimator() + outlineAnimator?.setTextStyle( + animate = isAnimated && isAnimationEnabled, + color = if (isDozing) aodOutlineColor else textOutlineColor, + textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, + fvar = fvar, + strokeWidth = if (isDozing) aodBorderWidth else textBorderWidth, + duration = aodStyle.transitionDuration, + interpolator = aodDozingInterpolator, + ) + innerAnimator?.setTextStyle( + animate = isAnimated && isAnimationEnabled, + color = Color.WHITE, + textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, + fvar = fvar, + duration = aodStyle.transitionDuration, + interpolator = aodDozingInterpolator, + ) + } + + override fun animateCharge() { + if (!this::textAnimator.isInitialized || textAnimator.isRunning()) { + // Skip charge animation if dozing animation is already playing. + return + } + logger.d("animateCharge()") + val middleFvar = if (dozeFraction == 0F) aodStyle.fontVariation else textStyle.fontVariation + val endFvar = if (dozeFraction == 0F) textStyle.fontVariation else aodStyle.fontVariation + val startAnimPhase2 = Runnable { + textAnimator.setTextStyle(fvar = endFvar, animate = isAnimationEnabled) + outlineAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled) + innerAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled) + updateTextBoundsForTextAnimator() + } + textAnimator.setTextStyle( + fvar = middleFvar, + animate = isAnimationEnabled, + onAnimationEnd = startAnimPhase2, + ) + outlineAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled) + innerAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled) + updateTextBoundsForTextAnimator() + } + + fun refreshText() { + lockScreenPaint.getTextBounds(text, 0, text.length, textBounds) + if (this::textAnimator.isInitialized) { + textAnimator.textInterpolator.targetPaint.getTextBounds( + text, + 0, + text.length, + targetTextBounds, + ) + } + if (layout == null) { + requestLayout() + } else { + textAnimator.updateLayout(layout) + outlineAnimator?.updateLayout(layout) + innerAnimator?.updateLayout(layout) + } + } + + private fun isSingleDigit(): Boolean { + return id == R.id.HOUR_FIRST_DIGIT || + id == R.id.HOUR_SECOND_DIGIT || + id == R.id.MINUTE_FIRST_DIGIT || + id == R.id.MINUTE_SECOND_DIGIT + } + + private fun updateInterpolatedTextBounds(): Rect { + val interpolatedTextBounds = Rect() + if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) { + interpolatedTextBounds.left = + MathUtils.lerp( + prevTextBounds.left, + targetTextBounds.left, + textAnimator.animator.animatedValue as Float, + ) + .toInt() + + interpolatedTextBounds.right = + MathUtils.lerp( + prevTextBounds.right, + targetTextBounds.right, + textAnimator.animator.animatedValue as Float, + ) + .toInt() + + interpolatedTextBounds.top = + MathUtils.lerp( + prevTextBounds.top, + targetTextBounds.top, + textAnimator.animator.animatedValue as Float, + ) + .toInt() + + interpolatedTextBounds.bottom = + MathUtils.lerp( + prevTextBounds.bottom, + targetTextBounds.bottom, + textAnimator.animator.animatedValue as Float, + ) + .toInt() + } else { + interpolatedTextBounds.set(targetTextBounds) + } + return interpolatedTextBounds + } + + private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point { + val viewWidth = if (isVertical) measuredHeight else measuredWidth + when (horizontalAlignment) { + HorizontalAlignment.LEFT -> { + inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left + } + HorizontalAlignment.RIGHT -> { + inPoint.x = + viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt() + } + HorizontalAlignment.CENTER -> { + inPoint.x = + (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left + } + } + return inPoint + } + + // translation of reference point of text + // used for translation when calling textInterpolator + fun getLocalTranslation(): Point { + val viewHeight = if (isVertical) measuredWidth else measuredHeight + val interpolatedTextBounds = updateInterpolatedTextBounds() + val localTranslation = Point(0, 0) + val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure + // get the change from current baseline to expected baseline + when (verticalAlignment) { + VerticalAlignment.CENTER -> { + localTranslation.y = + ((viewHeight - interpolatedTextBounds.height()) / 2 - + interpolatedTextBounds.top - + correctedBaseline) + } + VerticalAlignment.TOP -> { + localTranslation.y = + (-interpolatedTextBounds.top + lockScreenPaint.strokeWidth - correctedBaseline) + .toInt() + } + VerticalAlignment.BOTTOM -> { + localTranslation.y = + viewHeight - + interpolatedTextBounds.bottom - + lockScreenPaint.strokeWidth.toInt() - + correctedBaseline + } + VerticalAlignment.BASELINE -> { + localTranslation.y = -lockScreenPaint.strokeWidth.toInt() + } + } + + return updateXtranslation(localTranslation, interpolatedTextBounds) + } + + override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) { + this.textStyle = textStyle as FontTextStyle + val typefaceName = "fonts/" + textStyle.fontFamily + setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName)) + lockScreenPaint.strokeJoin = Paint.Join.ROUND + lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(textStyle.fontVariation) + textStyle.fontFeatureSettings?.let { + lockScreenPaint.fontFeatureSettings = it + fontFeatureSettings = it + } + typeface = lockScreenPaint.typeface + textStyle.lineHeight?.let { lineHeight = it.toInt() } + // borderWidth in textStyle and aodStyle is used to draw, + // strokeWidth in lockScreenPaint is used to measure and get enough space for the text + textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) } + + if (aodStyle != null && aodStyle is FontTextStyle) { + this.aodStyle = aodStyle + } else { + this.aodStyle = textStyle.copy() + } + this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator } + aodBorderWidth = parser.convert(this.aodStyle.borderWidth ?: DEFAULT_AOD_STROKE_WIDTH) + lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth)) + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + setInterpolatorPaint() + recomputeMaxSingleDigitSizes() + invalidate() + } + + // When constrainedByHeight is on, targetFontSizePx is the constrained height of textView + override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) { + val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight) + val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f) + aodFontSizePx = + adjustedFontSizePx * (aodStyle.fontSizeScale ?: textStyle.fontSizeScale ?: 1f) + if (fontSizePx > 0) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) + lockScreenPaint.textSize = textSize + lockScreenPaint.getTextBounds(text, 0, text.length, textBounds) + targetTextBounds.set(textBounds) + } + if (!constrainedByHeight) { + val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2 + fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize + } + textStyle.borderWidthScale?.let { + textBorderWidth = fontSizePx * it + if (dozeFraction < 1.0F) { + outlineAnimator?.setTextStyle(strokeWidth = textBorderWidth, animate = false) + } + } + aodStyle.borderWidthScale?.let { + aodBorderWidth = fontSizePx * it + if (dozeFraction > 0.0F) { + outlineAnimator?.setTextStyle(strokeWidth = aodBorderWidth, animate = false) + } + } + + lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth)) + recomputeMaxSingleDigitSizes() + + if (this::textAnimator.isInitialized) { + textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false) + } + outlineAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false) + innerAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false) + } + + private fun recomputeMaxSingleDigitSizes() { + val rectForCalculate = Rect() + maxSingleDigitHeight = 0 + maxSingleDigitWidth = 0 + + for (i in 0..9) { + lockScreenPaint.getTextBounds(i.toString(), 0, 1, rectForCalculate) + maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height()) + maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width()) + } + maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth.toInt() + maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth.toInt() + } + + // called without animation, can be used to set the initial state of animator + private fun setInterpolatorPaint() { + if (this::textAnimator.isInitialized) { + // set initial style + textAnimator.textInterpolator.targetPaint.set(lockScreenPaint) + textAnimator.textInterpolator.onTargetPaintModified() + textAnimator.setTextStyle( + fvar = textStyle.fontVariation, + textSize = lockScreenPaint.textSize, + color = textFillColor, + animate = false, + ) + } + + if (outlineAnimator != null) { + outlineAnimator!! + .textInterpolator + .targetPaint + .set( + TextPaint(lockScreenPaint).also { + it.style = + if (aodStyle.renderType == RenderType.HOLLOW_TEXT) + Paint.Style.FILL_AND_STROKE + else Paint.Style.STROKE + } + ) + outlineAnimator!!.textInterpolator.onTargetPaintModified() + outlineAnimator!!.setTextStyle( + fvar = aodStyle.fontVariation, + textSize = lockScreenPaint.textSize, + color = Color.TRANSPARENT, + animate = false, + ) + } + + if (innerAnimator != null) { + innerAnimator!! + .textInterpolator + .targetPaint + .set(TextPaint(lockScreenPaint).also { it.style = Paint.Style.FILL }) + innerAnimator!!.textInterpolator.onTargetPaintModified() + innerAnimator!!.setTextStyle( + fvar = aodStyle.fontVariation, + textSize = lockScreenPaint.textSize, + color = Color.WHITE, + animate = false, + ) + } + } + + /* Called after textAnimator.setTextStyle + * textAnimator.setTextStyle will update targetPaint, + * and rebase if previous animator is canceled + * so basePaint will store the state we transition from + * and targetPaint will store the state we transition to + */ + private fun updateTextBoundsForTextAnimator() { + textAnimator.textInterpolator.basePaint.getTextBounds(text, 0, text.length, prevTextBounds) + textAnimator.textInterpolator.targetPaint.getTextBounds( + text, + 0, + text.length, + targetTextBounds, + ) + } + + /* + * Adjust text size to adapt to large display / font size + * where the text view will be constrained by height + */ + private fun adjustFontSize(targetFontSizePx: Float?, constrainedByHeight: Boolean): Float { + return if (constrainedByHeight) { + min((targetFontSizePx ?: 0F) / fontSizeAdjustFactor, lastUnconstrainedTextSize) + } else { + lastUnconstrainedTextSize = targetFontSizePx ?: 1F + lastUnconstrainedTextSize + } + } + + companion object { + val DEFAULT_AOD_STROKE_WIDTH = "2dp" + val TEXT_OUTLINE_DEFAULT_COLOR = Color.TRANSPARENT + val AOD_DEFAULT_COLOR = Color.TRANSPARENT + val AOD_OUTLINE_DEFAULT_COLOR = Color.WHITE + private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0" + private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0" + + fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) = + assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt new file mode 100644 index 000000000000..bbd2d3d1f782 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.clocks.view + +import androidx.annotation.VisibleForTesting +import com.android.systemui.shared.clocks.AssetLoader +import com.android.systemui.shared.clocks.TextStyle + +interface SimpleDigitalClockView { + var text: String + var verticalAlignment: VerticalAlignment + var horizontalAlignment: HorizontalAlignment + var dozeFraction: Float + val textStyle: TextStyle + @VisibleForTesting var isAnimationEnabled: Boolean + + fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) + + fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) + + fun updateColors(assets: AssetLoader, isRegionDark: Boolean) + + fun refreshTime() + + fun animateCharge() + + fun animateDoze(isDozing: Boolean, isAnimated: Boolean) +} + +enum class VerticalAlignment { + TOP, + BOTTOM, + BASELINE, // default + CENTER, +} + +enum class HorizontalAlignment { + LEFT, + RIGHT, + CENTER, // default +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index d4d966ad2ef7..2312bbd2d7f8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.nano.CommunalHubState +import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.coroutines.collectLastValue import com.android.systemui.lifecycle.InstantTaskExecutorRule import com.google.common.truth.Truth.assertThat @@ -102,7 +103,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = widgetId, provider = provider, rank = rank, - userSerialNumber = userSerialNumber + userSerialNumber = userSerialNumber, ) } assertThat(widgets()) @@ -110,7 +111,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { communalItemRankEntry1, communalWidgetItemEntry1, communalItemRankEntry2, - communalWidgetItemEntry2 + communalWidgetItemEntry2, ) } @@ -129,7 +130,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - userSerialNumber = userSerialNumber + userSerialNumber = userSerialNumber, ) } @@ -165,7 +166,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { communalItemRankEntry1, communalWidgetItemEntry1, communalItemRankEntry2, - communalWidgetItemEntry2 + communalWidgetItemEntry2, ) communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId) @@ -251,6 +252,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = "pk_name/cls_name_4", itemId = 4L, userSerialNumber = 0, + spanY = 3, ) assertThat(widgets()) .containsExactly( @@ -267,6 +269,68 @@ class CommunalWidgetDaoTest : SysuiTestCase() { } @Test + fun addWidget_withDifferentSpanY_readsCorrectValuesInDb() = + testScope.runTest { + val widgets = collectLastValue(communalWidgetDao.getWidgets()) + + // Add widgets with different spanY values + communalWidgetDao.addWidget( + widgetId = 1, + provider = ComponentName("pkg_name", "cls_name_1"), + rank = 0, + userSerialNumber = 0, + spanY = CommunalContentSize.FULL.span, + ) + communalWidgetDao.addWidget( + widgetId = 2, + provider = ComponentName("pkg_name", "cls_name_2"), + rank = 1, + userSerialNumber = 0, + spanY = CommunalContentSize.HALF.span, + ) + communalWidgetDao.addWidget( + widgetId = 3, + provider = ComponentName("pkg_name", "cls_name_3"), + rank = 2, + userSerialNumber = 0, + spanY = CommunalContentSize.THIRD.span, + ) + + // Verify that the widgets have the correct spanY values + assertThat(widgets()) + .containsExactly( + CommunalItemRank(uid = 1L, rank = 0), + CommunalWidgetItem( + uid = 1L, + widgetId = 1, + componentName = "pkg_name/cls_name_1", + itemId = 1L, + userSerialNumber = 0, + spanY = CommunalContentSize.FULL.span, + ), + CommunalItemRank(uid = 2L, rank = 1), + CommunalWidgetItem( + uid = 2L, + widgetId = 2, + componentName = "pkg_name/cls_name_2", + itemId = 2L, + userSerialNumber = 0, + spanY = CommunalContentSize.HALF.span, + ), + CommunalItemRank(uid = 3L, rank = 2), + CommunalWidgetItem( + uid = 3L, + widgetId = 3, + componentName = "pkg_name/cls_name_3", + itemId = 3L, + userSerialNumber = 0, + spanY = CommunalContentSize.THIRD.span, + ), + ) + .inOrder() + } + + @Test fun restoreCommunalHubState() = testScope.runTest { // Set up db @@ -288,6 +352,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = fakeWidget.componentName, itemId = rank.uid, userSerialNumber = fakeWidget.userSerialNumber, + spanY = 3, ) expected[rank] = widget } @@ -343,6 +408,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = widgetInfo1.provider.flattenToString(), itemId = communalItemRankEntry1.uid, userSerialNumber = widgetInfo1.userSerialNumber, + spanY = 3, ) val communalWidgetItemEntry2 = CommunalWidgetItem( @@ -351,6 +417,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = widgetInfo2.provider.flattenToString(), itemId = communalItemRankEntry2.uid, userSerialNumber = widgetInfo2.userSerialNumber, + spanY = 3, ) val communalWidgetItemEntry3 = CommunalWidgetItem( @@ -359,6 +426,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = widgetInfo3.provider.flattenToString(), itemId = communalItemRankEntry3.uid, userSerialNumber = widgetInfo3.userSerialNumber, + spanY = 3, ) val fakeState = CommunalHubState().apply { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt index eba395bdb5a3..596db0767867 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt @@ -117,6 +117,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { componentName = defaultWidgets[0], rank = 0, userSerialNumber = 0, + spanY = 3, ) verify(communalWidgetDao) .addWidget( @@ -124,6 +125,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { componentName = defaultWidgets[1], rank = 1, userSerialNumber = 0, + spanY = 3, ) verify(communalWidgetDao) .addWidget( @@ -131,6 +133,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { componentName = defaultWidgets[2], rank = 2, userSerialNumber = 0, + spanY = 3, ) } @@ -152,6 +155,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { componentName = any(), rank = anyInt(), userSerialNumber = anyInt(), + spanY = anyInt(), ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 980a5ec8c494..3d30eccc4572 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -143,7 +143,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fun communalWidgets_queryWidgetsFromDb() = testScope.runTest { val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) - val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0) + val communalWidgetItemEntry = + CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0, 3) fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) fakeProviders.value = mapOf(1 to providerInfoA) @@ -169,19 +170,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3), CommunalItemRank(uid = 3L, rank = 3) to - CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0), + CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0, 3), CommunalItemRank(uid = 4L, rank = 4) to - CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0), - ) - fakeProviders.value = - mapOf( - 1 to providerInfoA, - 2 to providerInfoB, + CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0, 3), ) + fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB) // Expect to see only widget 1 and 2 val communalWidgets by collectLastValue(underTest.communalWidgets) @@ -207,15 +204,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), - ) - fakeProviders.value = - mapOf( - 1 to providerInfoA, - 2 to providerInfoB, + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3), ) + fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB) // Expect two widgets val communalWidgets by collectLastValue(underTest.communalWidgets) @@ -235,11 +228,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) // Provider info updated for widget 1 - fakeProviders.value = - mapOf( - 1 to providerInfoC, - 2 to providerInfoB, - ) + fakeProviders.value = mapOf(1 to providerInfoC, 2 to providerInfoB) runCurrent() assertThat(communalWidgets) @@ -269,7 +258,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever( communalWidgetHost.allocateIdAndBindWidget( any<ComponentName>(), - any<UserHandle>() + any<UserHandle>(), ) ) .thenReturn(id) @@ -294,7 +283,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever( communalWidgetHost.allocateIdAndBindWidget( any<ComponentName>(), - any<UserHandle>() + any<UserHandle>(), ) ) .thenReturn(id) @@ -303,7 +292,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) verify(communalWidgetDao, never()) - .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt()) + .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt()) verify(appWidgetHost).deleteAppWidgetId(id) // Verify backup not requested @@ -321,7 +310,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever( communalWidgetHost.allocateIdAndBindWidget( any<ComponentName>(), - any<UserHandle>() + any<UserHandle>(), ) ) .thenReturn(id) @@ -332,7 +321,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) verify(communalWidgetDao, never()) - .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt()) + .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt()) verify(appWidgetHost).deleteAppWidgetId(id) // Verify backup not requested @@ -350,7 +339,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever( communalWidgetHost.allocateIdAndBindWidget( any<ComponentName>(), - any<UserHandle>() + any<UserHandle>(), ) ) .thenReturn(id) @@ -650,8 +639,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { eq(newWidgetId), componentNameCaptor.capture(), eq(2), - eq(testUserSerialNumber(workProfile)) + eq(testUserSerialNumber(workProfile)), + anyInt(), ) + assertThat(componentNameCaptor.firstValue) .isEqualTo(ComponentName("pk_name", "fake_widget_2")) } @@ -662,9 +653,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3), ) // Widget 1 is installed @@ -707,7 +698,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3) ) // Widget 1 is pending install @@ -732,7 +723,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { componentName = ComponentName("pk_1", "cls_1"), icon = fakeIcon, user = mainUser, - ), + ) ) // Package for widget 1 finished installing @@ -749,10 +740,23 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { appWidgetId = 1, providerInfo = providerInfoA, rank = 1, - ), + ) ) } + @Test + fun updateWidgetSpanY_updatesWidgetInDaoAndRequestsBackup() = + testScope.runTest { + val widgetId = 1 + val newSpanY = 6 + + underTest.updateWidgetSpanY(widgetId, newSpanY) + runCurrent() + + verify(communalWidgetDao).updateWidgetSpanY(widgetId, newSpanY) + verify(backupManager).dataChanged() + } + private fun setAppWidgetIds(ids: List<Int>) { whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt index 8f9e23824809..8b1341114c68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt @@ -145,7 +145,7 @@ class KeyboardRepositoryTest : SysuiTestCase() { fakeInputManager.addPhysicalKeyboard( PHYSICAL_NOT_FULL_KEYBOARD_ID, - isFullKeyboard = false + isFullKeyboard = false, ) assertThat(isKeyboardConnected).isFalse() @@ -223,7 +223,7 @@ class KeyboardRepositoryTest : SysuiTestCase() { backlightListenerCaptor.value.onBacklightChanged( current = 1, max = 5, - triggeredByKeyPress = false + triggeredByKeyPress = false, ) assertThat(backlight).isNull() } @@ -239,7 +239,7 @@ class KeyboardRepositoryTest : SysuiTestCase() { backlightListenerCaptor.value.onBacklightChanged( current = 1, max = 5, - triggeredByKeyPress = true + triggeredByKeyPress = true, ) assertThat(backlight).isNotNull() } @@ -318,15 +318,75 @@ class KeyboardRepositoryTest : SysuiTestCase() { } } + @Test + fun connectedKeyboards_emitsAllKeyboards() { + testScope.runTest { + val firstKeyboard = Keyboard(vendorId = 1, productId = 1) + val secondKeyboard = Keyboard(vendorId = 2, productId = 2) + captureDeviceListener() + val keyboards by collectLastValueImmediately(underTest.connectedKeyboards) + + fakeInputManager.addPhysicalKeyboard( + PHYSICAL_FULL_KEYBOARD_ID, + vendorId = firstKeyboard.vendorId, + productId = firstKeyboard.productId, + ) + assertThat(keyboards) + .containsExactly(Keyboard(firstKeyboard.vendorId, firstKeyboard.productId)) + + fakeInputManager.addPhysicalKeyboard( + ANOTHER_PHYSICAL_FULL_KEYBOARD_ID, + vendorId = secondKeyboard.vendorId, + productId = secondKeyboard.productId, + ) + assertThat(keyboards) + .containsExactly( + Keyboard(firstKeyboard.vendorId, firstKeyboard.productId), + Keyboard(secondKeyboard.vendorId, secondKeyboard.productId), + ) + } + } + + @Test + fun connectedKeyboards_emitsOnlyFullPhysicalKeyboards() { + testScope.runTest { + captureDeviceListener() + val keyboards by collectLastValueImmediately(underTest.connectedKeyboards) + + fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID) + fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD) + fakeInputManager.addPhysicalKeyboard( + PHYSICAL_NOT_FULL_KEYBOARD_ID, + isFullKeyboard = false, + ) + + assertThat(keyboards).hasSize(1) + } + } + + @Test + fun connectedKeyboards_emitsOnlyConnectedKeyboards() { + testScope.runTest { + captureDeviceListener() + val keyboards by collectLastValueImmediately(underTest.connectedKeyboards) + + fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID) + fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID) + fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID) + + assertThat(keyboards).hasSize(1) + } + } + private fun KeyboardBacklightListener.onBacklightChanged( current: Int, max: Int, - triggeredByKeyPress: Boolean = true + triggeredByKeyPress: Boolean = true, ) { onKeyboardBacklightChanged( /* deviceId= */ 0, TestBacklightState(current, max), - triggeredByKeyPress + triggeredByKeyPress, ) } @@ -343,7 +403,7 @@ class KeyboardRepositoryTest : SysuiTestCase() { private class TestBacklightState( private val brightnessLevel: Int, - private val maxBrightnessLevel: Int + private val maxBrightnessLevel: Int, ) : KeyboardBacklightState() { override fun getBrightnessLevel() = brightnessLevel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 523a89ad5740..5b0b59de47c2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -71,7 +71,11 @@ import androidx.test.filters.SmallTest; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; +import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.DisableSceneContainer; +import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.log.LogWtfHandlerRule; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -90,6 +94,10 @@ import com.android.systemui.util.time.FakeSystemClock; import com.google.android.collect.Lists; +import dagger.Lazy; + +import kotlinx.coroutines.flow.StateFlow; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -152,6 +160,12 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private BroadcastDispatcher mBroadcastDispatcher; @Mock private KeyguardStateController mKeyguardStateController; + @Mock + private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; + @Mock + private DeviceUnlockedInteractor mDeviceUnlockedInteractor; + @Mock + private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow; private UserInfo mCurrentUser; private UserInfo mSecondaryUser; @@ -238,6 +252,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext); mLockscreenUserManager.setUpWithPresenter(mPresenter); + when(mDeviceUnlockedInteractor.getDeviceUnlockStatus()) + .thenReturn(mDeviceUnlockStatusStateFlow); + mBackgroundExecutor.runAllReady(); } @@ -493,7 +510,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { } @Test - public void testUpdateIsPublicMode() { + @DisableSceneContainer + public void testUpdateIsPublicMode_sceneContainerDisabled() { when(mKeyguardStateController.isMethodSecure()).thenReturn(true); when(mKeyguardStateController.isShowing()).thenReturn(false); @@ -527,6 +545,57 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mBackgroundExecutor.runAllReady(); assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0)); verify(listener, never()).onNotificationStateChanged(); + + verify(mDeviceUnlockedInteractorLazy, never()).get(); + } + + @Test + @EnableSceneContainer + public void testUpdateIsPublicMode_sceneContainerEnabled() { + when(mDeviceUnlockedInteractorLazy.get()).thenReturn(mDeviceUnlockedInteractor); + + // device is unlocked + when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus( + /* isUnlocked = */ true, + /* deviceUnlockSource = */ null + )); + + NotificationStateChangedListener listener = mock(NotificationStateChangedListener.class); + mLockscreenUserManager.addNotificationStateChangedListener(listener); + mLockscreenUserManager.mCurrentProfiles.append(0, mock(UserInfo.class)); + + // first call explicitly sets user 0 to not public; notifies + mLockscreenUserManager.updatePublicMode(); + mBackgroundExecutor.runAllReady(); + assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0)); + verify(listener).onNotificationStateChanged(); + clearInvocations(listener); + + // calling again has no changes; does not notify + mLockscreenUserManager.updatePublicMode(); + mBackgroundExecutor.runAllReady(); + assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0)); + verify(listener, never()).onNotificationStateChanged(); + + // device is not unlocked + when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus( + /* isUnlocked = */ false, + /* deviceUnlockSource = */ null + )); + + // Calling again with device now not unlocked makes user 0 public; notifies + when(mKeyguardStateController.isShowing()).thenReturn(true); + mLockscreenUserManager.updatePublicMode(); + mBackgroundExecutor.runAllReady(); + assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0)); + verify(listener).onNotificationStateChanged(); + clearInvocations(listener); + + // calling again has no changes; does not notify + mLockscreenUserManager.updatePublicMode(); + mBackgroundExecutor.runAllReady(); + assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0)); + verify(listener, never()).onNotificationStateChanged(); } @Test @@ -972,7 +1041,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mSettings, mock(DumpManager.class), mock(LockPatternUtils.class), - mFakeFeatureFlags); + mFakeFeatureFlags, + mDeviceUnlockedInteractorLazy + ); } public BroadcastReceiver getBaseBroadcastReceiverForTest() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt index 28857a08c2bd..34f46088ad79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt @@ -208,8 +208,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(footerVisible).isTrue() } - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun onClick_whenHistoryDisabled_leadsToSettingsPage() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -222,8 +222,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(onClick?.backStack).isEmpty() } - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun onClick_whenHistoryEnabled_leadsToHistoryPage() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -237,8 +237,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS) } - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -263,8 +263,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { .containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS) } - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json new file mode 100644 index 000000000000..c3fb8d4d5ab6 --- /dev/null +++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json @@ -0,0 +1,88 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "a49f2f7d25cf12d1baf9a3a3e6243b64", + "entities": [ + { + "tableName": "communal_widget_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1, `span_y` INTEGER NOT NULL DEFAULT 3)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "component_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "item_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSerialNumber", + "columnName": "user_serial_number", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "spanY", + "columnName": "span_y", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + }, + { + "tableName": "communal_item_rank_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a49f2f7d25cf12d1baf9a3a3e6243b64')" + ] + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index aabfbd11b70e..65c01ed9eecd 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -710,9 +710,16 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv @Override public void onShareButtonTapped() { if (clipboardSharedTransitions()) { - if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) { - finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED, - IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext)); + switch (mClipboardModel.getType()) { + case TEXT: + case URI: + finish(CLIPBOARD_OVERLAY_SHARE_TAPPED, + IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext)); + break; + case IMAGE: + finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED, + IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext)); + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index 8f1854f93fe4..17f4f0c83d6f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -26,7 +26,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.android.systemui.res.R -@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 3) +@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 4) abstract class CommunalDatabase : RoomDatabase() { abstract fun communalWidgetDao(): CommunalWidgetDao @@ -43,19 +43,16 @@ abstract class CommunalDatabase : RoomDatabase() { * @param callback An optional callback registered to the database. Only effective when a * new instance is created. */ - fun getInstance( - context: Context, - callback: Callback? = null, - ): CommunalDatabase { + fun getInstance(context: Context, callback: Callback? = null): CommunalDatabase { if (instance == null) { instance = Room.databaseBuilder( context, CommunalDatabase::class.java, - context.resources.getString(R.string.config_communalDatabase) + context.resources.getString(R.string.config_communalDatabase), ) .also { builder -> - builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3) + builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) builder.fallbackToDestructiveMigration(dropAllTables = true) callback?.let { callback -> builder.addCallback(callback) } } @@ -103,5 +100,21 @@ abstract class CommunalDatabase : RoomDatabase() { ) } } + + /** + * This migration adds a span_y column to the communal_widget_table and sets its default + * value to 3. + */ + @VisibleForTesting + val MIGRATION_3_4 = + object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Migrating from version 3 to 4") + db.execSQL( + "ALTER TABLE communal_widget_table " + + "ADD COLUMN span_y INTEGER NOT NULL DEFAULT 3" + ) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt index e33aead11842..f9d2a843c213 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt @@ -40,6 +40,12 @@ data class CommunalWidgetItem( */ @ColumnInfo(name = "user_serial_number", defaultValue = "$USER_SERIAL_NUMBER_UNDEFINED") val userSerialNumber: Int, + + /** + * The vertical span of the widget. Span_Y default value corresponds to + * CommunalContentSize.HALF.span + */ + @ColumnInfo(name = "span_y", defaultValue = "3") val spanY: Int, ) { companion object { /** diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 93b86bd10133..5dd4c1cb7f72 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -25,6 +25,7 @@ import androidx.room.RoomDatabase import androidx.room.Transaction import androidx.sqlite.db.SupportSQLiteDatabase import com.android.systemui.communal.nano.CommunalHubState +import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.widgets.CommunalWidgetHost import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS import com.android.systemui.dagger.SysUISingleton @@ -153,14 +154,15 @@ interface CommunalWidgetDao { @Query( "INSERT INTO communal_widget_table" + - "(widget_id, component_name, item_id, user_serial_number) " + - "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber)" + "(widget_id, component_name, item_id, user_serial_number, span_y) " + + "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber, :spanY)" ) fun insertWidget( widgetId: Int, componentName: String, itemId: Long, userSerialNumber: Int, + spanY: Int = 3, ): Long @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)") @@ -169,6 +171,9 @@ interface CommunalWidgetDao { @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid") fun updateItemRank(itemUid: Long, order: Int) + @Query("UPDATE communal_widget_table SET span_y = :spanY WHERE widget_id = :widgetId") + fun updateWidgetSpanY(widgetId: Int, spanY: Int) + @Query("DELETE FROM communal_widget_table") fun clearCommunalWidgetsTable() @Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable() @@ -189,12 +194,14 @@ interface CommunalWidgetDao { provider: ComponentName, rank: Int? = null, userSerialNumber: Int, + spanY: Int = CommunalContentSize.HALF.span, ): Long { return addWidget( widgetId = widgetId, componentName = provider.flattenToString(), rank = rank, userSerialNumber = userSerialNumber, + spanY = spanY, ) } @@ -204,6 +211,7 @@ interface CommunalWidgetDao { componentName: String, rank: Int? = null, userSerialNumber: Int, + spanY: Int = 3, ): Long { val widgets = getWidgetsNow() @@ -224,6 +232,7 @@ interface CommunalWidgetDao { componentName = componentName, itemId = insertItemRank(newRank), userSerialNumber = userSerialNumber, + spanY = spanY, ) } @@ -246,7 +255,8 @@ interface CommunalWidgetDao { clearCommunalItemRankTable() state.widgets.forEach { - addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber) + val spanY = if (it.spanY != 0) it.spanY else CommunalContentSize.HALF.span + addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber, spanY) } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 6cdd9fffe077..3312f3cac64b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -92,6 +92,14 @@ interface CommunalWidgetRepository { /** Aborts the restore process and removes files from disk if necessary. */ fun abortRestoreWidgets() + + /** + * Update the spanY of a widget in the database. + * + * @param widgetId id of the widget to update. + * @param spanY new spanY value for the widget. + */ + fun updateWidgetSpanY(widgetId: Int, spanY: Int) } @SysUISingleton @@ -118,20 +126,30 @@ constructor( /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */ private val widgetEntries: Flow<List<CommunalWidgetEntry>> = - combine( - communalWidgetDao.getWidgets(), - communalWidgetHost.appWidgetProviders, - ) { entries, providers -> + combine(communalWidgetDao.getWidgets(), communalWidgetHost.appWidgetProviders) { + entries, + providers -> entries.mapNotNull { (rank, widget) -> CommunalWidgetEntry( appWidgetId = widget.widgetId, componentName = widget.componentName, rank = rank.rank, - providerInfo = providers[widget.widgetId] + providerInfo = providers[widget.widgetId], ) } } + override fun updateWidgetSpanY(widgetId: Int, spanY: Int) { + bgScope.launch { + communalWidgetDao.updateWidgetSpanY(widgetId, spanY) + logger.i({ "Updated spanY of widget $int1 to $int2." }) { + int1 = widgetId + int2 = spanY + } + backupManager.dataChanged() + } + } + @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = widgetEntries @@ -197,6 +215,7 @@ constructor( provider = provider, rank = rank, userSerialNumber = userManager.getUserSerialNumber(user.identifier), + spanY = 3, ) backupManager.dataChanged() } else { @@ -325,6 +344,7 @@ constructor( componentName = restoredWidget.componentName rank = restoredWidget.rank userSerialNumber = userManager.getUserSerialNumber(newUser.identifier) + spanY = restoredWidget.spanY } } val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() } @@ -383,6 +403,7 @@ constructor( appWidgetId = entry.appWidgetId, providerInfo = entry.providerInfo!!, rank = entry.rank, + spanY = entry.spanY, ) } @@ -400,6 +421,7 @@ constructor( appWidgetId = entry.appWidgetId, providerInfo = entry.providerInfo!!, rank = entry.rank, + spanY = entry.spanY, ) } @@ -412,6 +434,7 @@ constructor( componentName = componentName, icon = session.icon, user = session.user, + spanY = entry.spanY, ) } else { null @@ -423,5 +446,6 @@ constructor( val componentName: String, val rank: Int, var providerInfo: AppWidgetProviderInfo? = null, + var spanY: Int = 3, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto index bc14ae1eaff4..7602a7afce4e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto +++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto @@ -38,5 +38,8 @@ message CommunalHubState { // Serial number of the user associated with the widget. int32 user_serial_number = 4; + + // The vertical span of the widget + int32 span_y = 5; } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt index 63b1a14b3135..bcbc8f65ce36 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -31,6 +31,7 @@ sealed interface CommunalWidgetContentModel { override val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, override val rank: Int, + val spanY: Int = 3, ) : CommunalWidgetContentModel /** Widget is pending installation */ @@ -40,5 +41,6 @@ sealed interface CommunalWidgetContentModel { val componentName: ComponentName, val icon: Bitmap?, val user: UserHandle, + val spanY: Int = 3, ) : CommunalWidgetContentModel } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a94fbd937afc..a5b22775f3d5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -56,6 +56,7 @@ import com.android.systemui.rotationlock.RotationLockNewModule; import com.android.systemui.scene.SceneContainerFrameworkModule; import com.android.systemui.screenshot.ReferenceScreenshotModule; import com.android.systemui.settings.MultiUserUtilsModule; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.ShadeModule; import com.android.systemui.startable.Dependencies; @@ -178,9 +179,9 @@ public abstract class ReferenceSystemUIModule { @Provides @SysUISingleton static IndividualSensorPrivacyController provideIndividualSensorPrivacyController( - SensorPrivacyManager sensorPrivacyManager) { + SensorPrivacyManager sensorPrivacyManager, UserTracker userTracker) { IndividualSensorPrivacyController spC = new IndividualSensorPrivacyControllerImpl( - sensorPrivacyManager); + sensorPrivacyManager, userTracker); spC.init(); return spC; } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt index 5a008bddc748..7711c48924cc 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt @@ -38,7 +38,7 @@ class InputDeviceRepository constructor( @Background private val backgroundHandler: Handler, @Background private val backgroundScope: CoroutineScope, - private val inputManager: InputManager + private val inputManager: InputManager, ) { sealed interface DeviceChange @@ -50,11 +50,11 @@ constructor( data object FreshStart : DeviceChange /** - * Emits collection of all currently connected keyboards and what was the last [DeviceChange]. - * It emits collection so that every new subscriber to this SharedFlow can get latest state of - * all keyboards. Otherwise we might get into situation where subscriber timing on - * initialization matter and later subscriber will only get latest device and will miss all - * previous devices. + * Emits collection of all currently connected input devices and what was the last + * [DeviceChange]. It emits collection so that every new subscriber to this SharedFlow can get + * latest state of all input devices. Otherwise we might get into situation where subscriber + * timing on initialization matter and later subscriber will only get latest device and will + * miss all previous devices. */ // TODO(b/351984587): Replace with StateFlow @SuppressLint("SharedFlowCreation") @@ -79,11 +79,7 @@ constructor( inputManager.registerInputDeviceListener(listener, backgroundHandler) awaitClose { inputManager.unregisterInputDeviceListener(listener) } } - .shareIn( - scope = backgroundScope, - started = SharingStarted.Lazily, - replay = 1, - ) + .shareIn(scope = backgroundScope, started = SharingStarted.Lazily, replay = 1) private fun <T> SendChannel<T>.sendWithLogging(element: T) { trySendWithFailureLogging(element, TAG) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt index f49cfdda8b0a..021c069008c3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt @@ -50,6 +50,8 @@ class CommandLineKeyboardRepository @Inject constructor(commandRegistry: Command private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null) override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull() + override val connectedKeyboards: Flow<Set<Keyboard>> = MutableStateFlow(emptySet()) + init { Log.i(TAG, "initializing shell command $COMMAND") commandRegistry.registerCommand(COMMAND) { KeyboardCommand() } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index a20dfa5a4c3e..3329fe29dade 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -61,6 +61,9 @@ interface KeyboardRepository { */ val newlyConnectedKeyboard: Flow<Keyboard> + /** Emits set of currently connected keyboards */ + val connectedKeyboards: Flow<Set<Keyboard>> + /** * Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only * happen when physical keyboard is connected @@ -74,7 +77,7 @@ class KeyboardRepositoryImpl constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val inputManager: InputManager, - inputDeviceRepository: InputDeviceRepository + inputDeviceRepository: InputDeviceRepository, ) : KeyboardRepository { @FlowPreview @@ -93,6 +96,13 @@ constructor( .mapNotNull { deviceIdToKeyboard(it) } .flowOn(backgroundDispatcher) + override val connectedKeyboards: Flow<Set<Keyboard>> = + inputDeviceRepository.deviceChange + .map { (deviceIds, _) -> deviceIds } + .map { deviceIds -> deviceIds.filter { isPhysicalFullKeyboard(it) } } + .distinctUntilChanged() + .map { deviceIds -> deviceIds.mapNotNull { deviceIdToKeyboard(it) }.toSet() } + override val isAnyKeyboardConnected: Flow<Boolean> = inputDeviceRepository.deviceChange .map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index 38608d0e793a..ab8a9539b7f2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -202,10 +202,10 @@ constructor( // Return the single display to be screenshot based upon the request. private suspend fun getDisplayToScreenshot(screenshotRequest: ScreenshotRequest): Display { return when (screenshotRequest.source) { - // TODO(b/367394043): Overview requests should use a display ID provided in - // ScreenshotRequest. ScreenshotSource.SCREENSHOT_OVERVIEW -> - displayRepository.getDisplay(Display.DEFAULT_DISPLAY) + // Show on the display where overview was shown if available. + displayRepository.getDisplay(screenshotRequest.displayId) + ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display") // Key chord and vendor gesture occur on the device itself, so screenshot the device's diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 4f47536f6b32..f83548ddbf45 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -994,7 +994,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW // be dropped, causing the shade expansion to fail silently. Since the shade doesn't open, // it doesn't become visible, and the bounds will never update. Therefore, we must detect // the incorrect bounds here and force the update so that touches are routed correctly. - if (SceneContainerFlag.isEnabled() && mWindowRootView.getVisibility() == View.INVISIBLE) { + if (SceneContainerFlag.isEnabled() + && mWindowRootView != null + && mWindowRootView.getVisibility() == View.INVISIBLE) { Rect bounds = newConfig.windowConfiguration.getBounds(); if (mWindowRootView.getWidth() != bounds.width()) { mLogger.logConfigChangeWidthAdjust(mWindowRootView.getWidth(), bounds.width()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 7244f8a64c19..e47952fd6c4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -62,11 +62,13 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; @@ -286,6 +288,8 @@ public class NotificationLockscreenUserManagerImpl implements protected ContentObserver mLockscreenSettingsObserver; protected ContentObserver mSettingsObserver; + private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; + @Inject public NotificationLockscreenUserManagerImpl(Context context, BroadcastDispatcher broadcastDispatcher, @@ -305,7 +309,8 @@ public class NotificationLockscreenUserManagerImpl implements SecureSettings secureSettings, DumpManager dumpManager, LockPatternUtils lockPatternUtils, - FeatureFlagsClassic featureFlags) { + FeatureFlagsClassic featureFlags, + Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) { mContext = context; mMainExecutor = mainExecutor; mBackgroundExecutor = backgroundExecutor; @@ -325,6 +330,7 @@ public class NotificationLockscreenUserManagerImpl implements mSecureSettings = secureSettings; mKeyguardStateController = keyguardStateController; mFeatureFlags = featureFlags; + mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy; mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); @@ -748,8 +754,13 @@ public class NotificationLockscreenUserManagerImpl implements // camera on the keyguard has a state of SHADE but the keyguard is still showing. final boolean showingKeyguard = mState != StatusBarState.SHADE || mKeyguardStateController.isShowing(); - final boolean devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure(); - + final boolean devicePublic; + if (SceneContainerFlag.isEnabled()) { + devicePublic = !mDeviceUnlockedInteractorLazy.get() + .getDeviceUnlockStatus().getValue().isUnlocked(); + } else { + devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure(); + } // Look for public mode users. Users are considered public in either case of: // - device keyguard is shown in secure mode; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 5d14be8c974c..73ad0e50793a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -272,7 +272,7 @@ public class StatusBarStateControllerImpl implements * Updates the {@link StatusBarState} and notifies registered listeners, if needed. */ private void updateStateAndNotifyListeners(int state) { - if (state != mUpcomingState) { + if (state != mUpcomingState && !SceneContainerFlag.isEnabled()) { Log.d(TAG, "setState: requested state " + StatusBarState.toString(state) + "!= upcomingState: " + StatusBarState.toString(mUpcomingState) + ". " + "This usually means the status bar state transition was interrupted before " @@ -728,20 +728,23 @@ public class StatusBarStateControllerImpl implements // doesn't work well for clients of this class (like remote input) that expect the device to // be fully and properly unlocked when the state changes to SHADE. // - // Therefore, we calculate the device to be in a locked-ish state (KEYGUARD or SHADE_LOCKED, + // Therefore, we consider the device to be in a keyguardish state (KEYGUARD or SHADE_LOCKED, // but not SHADE) if *any* of these are still true: // 1. deviceUnlockStatus.isUnlocked is false. - // 2. We are on (currentScene equals) a locked-ish scene (Lockscreen, Bouncer, or Communal). - // 3. We are over (backStack contains) a locked-ish scene (Lockscreen or Communal). + // 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal). + // 3. backStack contains a keyguardish scene (Lockscreen or Communal). + + final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal; + final boolean overKeyguardish = overLockscreen || overCommunal; if (isOccluded) { // Occlusion is special; even though the device is still technically on the lockscreen, // the UI behaves as if it is unlocked. newState = StatusBarState.SHADE; - } else if (onLockscreen || onBouncer || onCommunal || overLockscreen || overCommunal) { - // We get here if we are on or over a locked-ish scene, even if isUnlocked is true; we + } else if (onKeyguardish || overKeyguardish) { + // We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we // want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a - // locked-ish scene. + // keyguardish scene. if (onShade || onQuickSettings || overShade || overlaidShade || overlaidQuickSettings) { newState = StatusBarState.SHADE_LOCKED; } else { @@ -751,7 +754,7 @@ public class StatusBarStateControllerImpl implements newState = StatusBarState.SHADE; } else if (onShade || onQuickSettings) { // We get here if deviceUnlockStatus.isUnlocked is false but we are no longer on or over - // a locked-ish scene; we want to return SHADE_LOCKED until isUnlocked is also true. + // a keyguardish scene; we want to return SHADE_LOCKED until isUnlocked is also true. newState = StatusBarState.SHADE_LOCKED; } else { throw new IllegalArgumentException( 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 5f4f72f293a6..0474344ee390 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -594,7 +594,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener = (extractor, which) -> updateTheme(); private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor; - private final GlanceableHubContainerController mGlanceableHubContainerController; + + // Only use before the scene container. Null if scene container is enabled. + @Nullable private final GlanceableHubContainerController mGlanceableHubContainerController; private final EmergencyGestureIntentFactory mEmergencyGestureIntentFactory; @@ -807,7 +809,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mFingerprintManager = fingerprintManager; mActivityStarter = activityStarter; mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor; - mGlanceableHubContainerController = glanceableHubContainerController; + if (!SceneContainerFlag.isEnabled()) { + mGlanceableHubContainerController = glanceableHubContainerController; + } else { + mGlanceableHubContainerController = null; + } mEmergencyGestureIntentFactory = emergencyGestureIntentFactory; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; @@ -2972,7 +2978,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void handleCommunalHubTouch(MotionEvent event) { - mGlanceableHubContainerController.onTouchEvent(event); + if (mGlanceableHubContainerController != null) { + mGlanceableHubContainerController.onTouchEvent(event); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index b1754fd59cee..200f0804e42b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -34,11 +34,15 @@ import android.view.ViewParent; import androidx.annotation.Nullable; +import com.android.compose.animation.scene.ObservableTransitionState; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.scene.domain.interactor.SceneInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.ActionClickLogger; import com.android.systemui.statusbar.CommandQueue; @@ -52,6 +56,9 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.kotlin.JavaAdapter; + +import dagger.Lazy; import java.util.concurrent.Executor; @@ -80,6 +87,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, private final ActionClickLogger mActionClickLogger; private int mDisabled2; protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver(); + private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; + private final Lazy<SceneInteractor> mSceneInteractorLazy; /** */ @@ -95,7 +104,10 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, ShadeController shadeController, CommandQueue commandQueue, ActionClickLogger clickLogger, - @Main Executor executor) { + @Main Executor executor, + Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy, + Lazy<SceneInteractor> sceneInteractorLazy, + JavaAdapter javaAdapter) { mContext = context; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mShadeController = shadeController; @@ -113,20 +125,28 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, mActionClickLogger = clickLogger; mActivityIntentHelper = new ActivityIntentHelper(mContext); mGroupExpansionManager = groupExpansionManager; + mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy; + mSceneInteractorLazy = sceneInteractorLazy; + + if (SceneContainerFlag.isEnabled()) { + javaAdapter.alwaysCollectFlow( + mDeviceUnlockedInteractorLazy.get().getDeviceUnlockStatus(), + deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState())); + javaAdapter.alwaysCollectFlow( + mSceneInteractorLazy.get().getTransitionState(), + deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState())); + } } @Override public void onStateChanged(int state) { - boolean hasPendingRemoteInput = mPendingRemoteInputView != null; - if (state == StatusBarState.SHADE - && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) { - if (!mStatusBarStateController.isKeyguardRequested() - && mKeyguardStateController.isUnlocked()) { - if (hasPendingRemoteInput) { - mExecutor.execute(mPendingRemoteInputView::callOnClick); - } - mPendingRemoteInputView = null; - } + if (mPendingRemoteInputView == null) { + return; + } + + if (state == StatusBarState.SHADE && canRetryPendingRemoteInput()) { + mExecutor.execute(mPendingRemoteInputView::callOnClick); + mPendingRemoteInputView = null; } } @@ -320,6 +340,23 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, } } + /** + * Returns {@code true} if it is safe to retry a pending remote input. The exact criteria for + * this vary depending whether the scene container is enabled. + */ + private boolean canRetryPendingRemoteInput() { + if (SceneContainerFlag.isEnabled()) { + final boolean isUnlocked = mDeviceUnlockedInteractorLazy.get() + .getDeviceUnlockStatus().getValue().isUnlocked(); + final boolean isIdle = mSceneInteractorLazy.get() + .getTransitionState().getValue() instanceof ObservableTransitionState.Idle; + return isUnlocked && isIdle; + } else { + return mKeyguardStateController.isUnlocked() + && !mStatusBarStateController.isKeyguardRequested(); + } + } + protected class ChallengeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java index da928a364984..3cf206643207 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java @@ -32,6 +32,7 @@ import android.util.SparseBooleanArray; import androidx.annotation.NonNull; import com.android.internal.camera.flags.Flags; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.ListenerSet; import java.util.Set; @@ -41,14 +42,17 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr private static final int[] SENSORS = new int[] {CAMERA, MICROPHONE}; private final @NonNull SensorPrivacyManager mSensorPrivacyManager; + private final @NonNull UserTracker mUserTracker; private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray(); private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray(); private Boolean mRequiresAuthentication; private final ListenerSet<Callback> mCallbacks = new ListenerSet<>(); public IndividualSensorPrivacyControllerImpl( - @NonNull SensorPrivacyManager sensorPrivacyManager) { + @NonNull SensorPrivacyManager sensorPrivacyManager, + @NonNull UserTracker userTracker) { mSensorPrivacyManager = sensorPrivacyManager; + mUserTracker = userTracker; } @Override @@ -94,12 +98,14 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr @Override public void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked) { - mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked); + mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked, + mUserTracker.getUserId()); } @Override public void suppressSensorPrivacyReminders(int sensor, boolean suppress) { - mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress); + mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress, + mUserTracker.getUserId()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 7c055c8876ae..7f90242fee2e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -135,3 +135,15 @@ fun <T1, T2, T3, T4, T5, R> combineFlows( ): Flow<R> { return combine(flow, flow2, flow3, flow4, flow5, transform) } + +fun <T1, T2, T3, T4, T5, T6, R> combineFlows( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + transform: (T1, T2, T3, T4, T5, T6) -> R, +): Flow<R> { + return combine(flow, flow2, flow3, flow4, flow5, flow6, transform) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 079c72f049a6..1f92bc1df9c8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -37,11 +37,8 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioService; import android.media.IVolumeController; -import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; -import android.media.RoutingSessionInfo; import android.media.VolumePolicy; -import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession.Token; import android.net.Uri; @@ -88,7 +85,6 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -217,7 +213,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa VolumeDialogControllerImpl.class.getSimpleName()); mWorker = new W(mWorkerLooper); mRouter2Manager = MediaRouter2Manager.getInstance(mContext); - mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext); + mMediaSessionsCallbacksW = new MediaSessionsCallbacks(); mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW); mAudioSharingInteractor = audioSharingInteractor; mJavaAdapter = javaAdapter; @@ -1360,16 +1356,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; - private final boolean mVolumeAdjustmentForRemoteGroupSessions; - - public MediaSessionsCallbacks(Context context) { - mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean( - com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); - } @Override public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { - if (showForSession(token)) { addStream(token, "onRemoteUpdate"); int stream = 0; @@ -1396,12 +1385,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax); mCallbacks.onStateChanged(mState); } - } } @Override public void onRemoteVolumeChanged(Token token, int flags) { - if (showForSession(token)) { addStream(token, "onRemoteVolumeChanged"); int stream = 0; synchronized (mRemoteStreams) { @@ -1420,27 +1407,27 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa if (showUI) { onShowRequestedW(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED); } - } } @Override public void onRemoteRemoved(Token token) { - if (showForSession(token)) { - int stream = 0; - synchronized (mRemoteStreams) { - if (!mRemoteStreams.containsKey(token)) { - Log.d(TAG, "onRemoteRemoved: stream doesn't exist, " - + "aborting remote removed for token:" + token.toString()); - return; - } - stream = mRemoteStreams.get(token); - } - mState.states.remove(stream); - if (mState.activeStream == stream) { - updateActiveStreamW(-1); + int stream; + synchronized (mRemoteStreams) { + if (!mRemoteStreams.containsKey(token)) { + Log.d( + TAG, + "onRemoteRemoved: stream doesn't exist, " + + "aborting remote removed for token:" + + token.toString()); + return; } - mCallbacks.onStateChanged(mState); + stream = mRemoteStreams.get(token); + } + mState.states.remove(stream); + if (mState.activeStream == stream) { + updateActiveStreamW(-1); } + mCallbacks.onStateChanged(mState); } public void setStreamVolume(int stream, int level) { @@ -1449,39 +1436,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); return; } - if (showForSession(token)) { - mMediaSessions.setVolume(token, level); - } - } - - private boolean showForSession(Token token) { - if (mVolumeAdjustmentForRemoteGroupSessions) { - if (DEBUG) { - Log.d(TAG, "Volume adjustment for remote group sessions allowed," - + " showForSession: true"); - } - return true; - } - MediaController ctr = new MediaController(mContext, token); - String packageName = ctr.getPackageName(); - List<RoutingSessionInfo> sessions = - mRouter2Manager.getRoutingSessions(packageName); - if (DEBUG) { - Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name " - + packageName); - } - for (RoutingSessionInfo session : sessions) { - if (DEBUG) { - Log.d(TAG, "Found routingSessionInfo: " + session); - } - if (!session.isSystemSession() - && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { - return true; - } - } - - Log.d(TAG, "No routing session for " + packageName); - return false; + mMediaSessions.setVolume(token, level); } private Token findToken(int stream) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index 5fc19711a26c..8075d117a58a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -104,6 +104,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Mock private Animator mAnimator; + @Mock + private Animator mEndAnimator; private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor = ArgumentCaptor.forClass(Animator.AnimatorListener.class); @@ -123,7 +125,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator); - when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator); + when(mClipboardOverlayView.getExitAnimation()).thenReturn(mEndAnimator); when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator); when(mClipboardOverlayWindow.getWindowInsets()).thenReturn( getImeInsets(new Rect(0, 0, 0, 0))); @@ -318,11 +320,11 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mOverlayController.setClipData(mSampleClipData, ""); mCallbacks.onShareButtonTapped(); - verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); - mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator); + verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture()); + mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator); verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, ""); - verify(mClipboardOverlayView, times(1)).getFadeOutAnimation(); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); } @Test @@ -343,8 +345,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { initController(); mCallbacks.onDismissButtonTapped(); - verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); - mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator); + verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture()); + mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator); // package name is null since we haven't actually set a source for this test verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null); @@ -403,14 +405,18 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mOverlayController.setClipData(mSampleClipData, "first.package"); mCallbacks.onShareButtonTapped(); + verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture()); + mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator); mOverlayController.setClipData(mSampleClipData, "second.package"); mCallbacks.onShareButtonTapped(); + verify(mEndAnimator, times(2)).addListener(mAnimatorListenerCaptor.capture()); + mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator); - verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package"); - verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package"); verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package"); + verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package"); verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package"); + verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package"); verifyNoMoreInteractions(mUiEventLogger); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt index ad2550255e29..7d5a334b45ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt @@ -148,6 +148,31 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { ) } + @Test + fun migrate3To4_addSpanYColumn_defaultValuePopulated() { + val databaseV3 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 3) + + val fakeWidgetsV3 = + listOf( + FakeCommunalWidgetItemV3(1, "test_widget_1", 11, 0), + FakeCommunalWidgetItemV3(2, "test_widget_2", 12, 10), + FakeCommunalWidgetItemV3(3, "test_widget_3", 13, 0), + ) + databaseV3.insertWidgetsV3(fakeWidgetsV3) + + databaseV3.verifyWidgetsV3(fakeWidgetsV3) + + val databaseV4 = + migrationTestHelper.runMigrationsAndValidate( + name = DATABASE_NAME, + version = 4, + validateDroppedTables = false, + CommunalDatabase.MIGRATION_3_4, + ) + + databaseV4.verifyWidgetsV4(fakeWidgetsV3.map { it.getV4() }) + } + private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { widgets.forEach { widget -> execSQL( @@ -157,6 +182,22 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { } } + private fun SupportSQLiteDatabase.insertWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) { + widgets.forEach { widget -> + execSQL( + "INSERT INTO communal_widget_table(" + + "widget_id, " + + "component_name, " + + "item_id, " + + "user_serial_number) " + + "VALUES(${widget.widgetId}, " + + "'${widget.componentName}', " + + "${widget.itemId}, " + + "${widget.userSerialNumber})" + ) + } + } + private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { val cursor = query("SELECT * FROM communal_widget_table") assertThat(cursor.moveToFirst()).isTrue() @@ -193,6 +234,42 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { assertThat(cursor.isAfterLast).isTrue() } + private fun SupportSQLiteDatabase.verifyWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) { + val cursor = query("SELECT * FROM communal_widget_table") + assertThat(cursor.moveToFirst()).isTrue() + + widgets.forEach { widget -> + assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) + assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) + .isEqualTo(widget.componentName) + assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) + assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) + .isEqualTo(widget.userSerialNumber) + + cursor.moveToNext() + } + assertThat(cursor.isAfterLast).isTrue() + } + + private fun SupportSQLiteDatabase.verifyWidgetsV4(widgets: List<FakeCommunalWidgetItemV4>) { + val cursor = query("SELECT * FROM communal_widget_table") + assertThat(cursor.moveToFirst()).isTrue() + + widgets.forEach { widget -> + assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) + assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) + .isEqualTo(widget.componentName) + assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) + assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) + .isEqualTo(widget.userSerialNumber) + assertThat(cursor.getInt(cursor.getColumnIndex("span_y"))).isEqualTo(widget.spanY) + + cursor.moveToNext() + } + + assertThat(cursor.isAfterLast).isTrue() + } + private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) { ranks.forEach { rank -> execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})") @@ -238,10 +315,27 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { val userSerialNumber: Int, ) - private data class FakeCommunalItemRank( - val rank: Int, + private fun FakeCommunalWidgetItemV3.getV4(): FakeCommunalWidgetItemV4 { + return FakeCommunalWidgetItemV4(widgetId, componentName, itemId, userSerialNumber, 3) + } + + private data class FakeCommunalWidgetItemV3( + val widgetId: Int, + val componentName: String, + val itemId: Int, + val userSerialNumber: Int, + ) + + private data class FakeCommunalWidgetItemV4( + val widgetId: Int, + val componentName: String, + val itemId: Int, + val userSerialNumber: Int, + val spanY: Int, ) + private data class FakeCommunalItemRank(val rank: Int) + companion object { private const val DATABASE_NAME = "communal_db" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt index 15705fbfec33..0bea56007a01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt @@ -257,6 +257,58 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) + fun executeScreenshots_fromOverview_honorsDisplay() = + testScope.runTest { + val displayId = 1 + setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId)) + val onSaved = { _: Uri? -> } + screenshotExecutor.executeScreenshots( + createScreenshotRequest( + displayId = displayId, + source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW, + ), + onSaved, + callback, + ) + + val dataCaptor = ArgumentCaptor<ScreenshotData>() + + verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) + + assertThat(dataCaptor.value.displayId).isEqualTo(displayId) + + screenshotExecutor.onDestroy() + } + + @Test + @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE) + fun executeScreenshots_fromOverviewInvalidDisplay_usesDefault() = + testScope.runTest { + setDisplays( + display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY), + display(TYPE_EXTERNAL, id = 1), + ) + val onSaved = { _: Uri? -> } + screenshotExecutor.executeScreenshots( + createScreenshotRequest( + displayId = 5, + source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW, + ), + onSaved, + callback, + ) + + val dataCaptor = ArgumentCaptor<ScreenshotData>() + + verify(controller).handleScreenshot(dataCaptor.capture(), any(), any()) + + assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + screenshotExecutor.onDestroy() + } + + @Test fun onDestroy_propagatedToControllers() = testScope.runTest { setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1)) @@ -527,9 +579,14 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { runCurrent() } - private fun createScreenshotRequest(type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN) = - ScreenshotRequest.Builder(type, WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER) + private fun createScreenshotRequest( + type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN, + source: Int = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER, + displayId: Int = Display.DEFAULT_DISPLAY, + ) = + ScreenshotRequest.Builder(type, source) .setTopComponent(topComponent) + .setDisplayId(displayId) .also { if (type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { it.setBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt new file mode 100644 index 000000000000..040a9e959094 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 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. + */ + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.clocks.FontTextStyle +import com.android.systemui.shared.clocks.LogUtil +import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SimpleDigitalClockTextViewTest : SysuiTestCase() { + private val messageBuffer = LogUtil.DEBUG_MESSAGE_BUFFER + private lateinit var underTest: SimpleDigitalClockTextView + private val defaultLargeClockTextSize = 500F + private val smallerTextSize = 300F + private val largerTextSize = 800F + private val firstMeasureTextSize = 100F + + @Before + fun setup() { + underTest = SimpleDigitalClockTextView(context, messageBuffer) + underTest.textStyle = FontTextStyle() + underTest.aodStyle = FontTextStyle() + underTest.text = "0" + underTest.applyTextSize(defaultLargeClockTextSize) + } + + @Test + fun applySmallerConstrainedTextSize_applyConstrainedTextSize() { + underTest.applyTextSize(smallerTextSize, constrainedByHeight = true) + assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor) + } + + @Test + fun applyLargerConstrainedTextSize_applyUnconstrainedTextSize() { + underTest.applyTextSize(largerTextSize, constrainedByHeight = true) + assertEquals(defaultLargeClockTextSize, underTest.textSize) + } + + @Test + fun applyFirstMeasureConstrainedTextSize_getConstrainedTextSize() { + underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true) + underTest.applyTextSize(smallerTextSize, constrainedByHeight = true) + assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor) + } + + @Test + fun applySmallFirstMeasureConstrainedSizeAndLargerConstrainedTextSize_applyDefaultSize() { + underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true) + underTest.applyTextSize(largerTextSize, constrainedByHeight = true) + assertEquals(defaultLargeClockTextSize, underTest.textSize) + } + + @Test + fun applyFirstMeasureConstrainedTextSize_applyUnconstrainedTextSize() { + underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true) + underTest.applyTextSize(defaultLargeClockTextSize) + assertEquals(defaultLargeClockTextSize, underTest.textSize) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index c5238194d0a4..81c40dc1778e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -36,7 +36,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.ActionClickLogger; @@ -50,8 +52,11 @@ import com.android.systemui.statusbar.notification.row.NotificationContentView; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,8 +76,14 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock private ActivityStarter mActivityStarter; + @Mock private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy; + @Mock private Lazy<SceneInteractor> mSceneInteractorLazy; + @Mock private JavaAdapter mJavaAdapter; private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + @Mock private DeviceUnlockedInteractor mDeviceUnlockedInteractor; + @Mock private SceneInteractor mSceneInteractor; + private int mCurrentUserId = 0; private StatusBarRemoteInputCallback mRemoteInputCallback; @@ -90,7 +101,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager, mActivityStarter, mShadeController, new CommandQueue(mContext, new FakeDisplayTracker(mContext)), - mock(ActionClickLogger.class), mFakeExecutor)); + mock(ActionClickLogger.class), mFakeExecutor, mDeviceUnlockedInteractorLazy, + mSceneInteractorLazy, mJavaAdapter)); mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index f62beeb16ae5..beba0f0c6286 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -133,10 +133,6 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { when(mRingerModeInternalLiveData.getValue()).thenReturn(-1); when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); when(mUserTracker.getUserContext()).thenReturn(mContext); - // Enable group volume adjustments - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions, - true); mCallback = mock(VolumeDialogControllerImpl.C.class); mThreadFactory.setLooper(TestableLooper.get(this).getLooper()); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 5d7e7c726c6c..1302faaf82ca 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -31,7 +31,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : provider: ComponentName, user: UserHandle, rank: Int?, - configurator: WidgetConfigurator? + configurator: WidgetConfigurator?, ) { coroutineScope.launch { val id = nextWidgetId++ @@ -93,6 +93,22 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : _communalWidgets.value = fakeDatabase.values.toList() } + override fun updateWidgetSpanY(widgetId: Int, spanY: Int) { + coroutineScope.launch { + fakeDatabase[widgetId]?.let { widget -> + when (widget) { + is CommunalWidgetContentModel.Available -> { + fakeDatabase[widgetId] = widget.copy(spanY = spanY) + } + is CommunalWidgetContentModel.Pending -> { + fakeDatabase[widgetId] = widget.copy(spanY = spanY) + } + } + _communalWidgets.value = fakeDatabase.values.toList() + } + } + } + override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {} override fun abortRestoreWidgets() {} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt index b37cac1d36fd..ba316835f2b3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt @@ -19,8 +19,10 @@ package com.android.systemui.keyboard.data.repository import com.android.systemui.keyboard.data.model.Keyboard import com.android.systemui.keyboard.shared.model.BacklightModel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.filterNotNull class FakeKeyboardRepository : KeyboardRepository { @@ -32,8 +34,14 @@ class FakeKeyboardRepository : KeyboardRepository { // filtering to make sure backlight doesn't have default initial value override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull() - private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null) - override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull() + // implemented as channel because original implementation is modeling events: it doesn't hold + // state so it won't always emit once connected. And it's bad if some tests depend on that + // incorrect behaviour. + private val _newlyConnectedKeyboard: Channel<Keyboard> = Channel() + override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.consumeAsFlow() + + private val _connectedKeyboards: MutableStateFlow<Set<Keyboard>> = MutableStateFlow(setOf()) + override val connectedKeyboards: Flow<Set<Keyboard>> = _connectedKeyboards fun setBacklight(state: BacklightModel) { _backlightState.value = state @@ -43,7 +51,14 @@ class FakeKeyboardRepository : KeyboardRepository { _isAnyKeyboardConnected.value = connected } + fun setConnectedKeyboards(keyboards: Set<Keyboard>) { + _connectedKeyboards.value = keyboards + _isAnyKeyboardConnected.value = keyboards.isNotEmpty() + } + fun setNewlyConnectedKeyboard(keyboard: Keyboard) { - _newlyConnectedKeyboard.value = keyboard + _newlyConnectedKeyboard.trySend(keyboard) + _connectedKeyboards.value += keyboard + _isAnyKeyboardConnected.value = true } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index c87d516d2ab4..ab9cc20763a8 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -205,6 +205,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { .verifyCallerCanExecuteAppFunction( callingUid, callingPid, + targetUser, requestInternal.getCallingPackage(), targetPackageName, requestInternal.getClientRequest().getFunctionIdentifier()) diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java index 3592ed587ab0..5393b939b5ed 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java +++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java @@ -64,6 +64,9 @@ public interface CallerValidator { * {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions can * still opt-out of caller having {@link Manifest.permission#EXECUTE_APP_FUNCTIONS}. * + * @param callingUid The calling uid. + * @param callingPid The calling pid. + * @param targetUser The user which the caller is requesting to execute as. * @param callerPackageName The calling package (as previously validated). * @param targetPackageName The package that owns the app function to execute. * @param functionId The id of the app function to execute. @@ -72,6 +75,7 @@ public interface CallerValidator { AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction( int callingUid, int callingPid, + @NonNull UserHandle targetUser, @NonNull String callerPackageName, @NonNull String targetPackageName, @NonNull String functionId); diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java index 8b6251a59e3a..e85a70d5845a 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java @@ -93,6 +93,7 @@ class CallerValidatorImpl implements CallerValidator { public AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction( int callingUid, int callingPid, + @NonNull UserHandle targetUser, @NonNull String callerPackageName, @NonNull String targetPackageName, @NonNull String functionId) { @@ -122,7 +123,10 @@ class CallerValidatorImpl implements CallerValidator { FutureAppSearchSession futureAppSearchSession = new FutureAppSearchSessionImpl( - mContext.getSystemService(AppSearchManager.class), + Objects.requireNonNull( + mContext + .createContextAsUser(targetUser, 0) + .getSystemService(AppSearchManager.class)), THREAD_POOL_EXECUTOR, new SearchContext.Builder(APP_FUNCTION_STATIC_METADATA_DB).build()); diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 0a9109b3e98c..d752429e64f7 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -16,7 +16,6 @@ package com.android.server.media; -import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED; import static android.media.VolumeProvider.VOLUME_CONTROL_ABSOLUTE; import static android.media.VolumeProvider.VOLUME_CONTROL_FIXED; import static android.media.VolumeProvider.VOLUME_CONTROL_RELATIVE; @@ -48,9 +47,7 @@ import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioSystem; import android.media.MediaMetadata; -import android.media.MediaRouter2Manager; import android.media.Rating; -import android.media.RoutingSessionInfo; import android.media.VolumeProvider; import android.media.session.ISession; import android.media.session.ISessionCallback; @@ -186,7 +183,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private final MediaSessionService mService; private final UriGrantsManagerInternal mUgmInternal; private final Context mContext; - private final boolean mVolumeAdjustmentForRemoteGroupSessions; private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions; @@ -311,8 +307,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde mAudioAttrs = DEFAULT_ATTRIBUTES; mPolicies = policies; mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); - mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions(); @@ -659,49 +653,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } return false; } - if (mVolumeAdjustmentForRemoteGroupSessions) { - if (DEBUG) { - Slog.d( - TAG, - "Volume adjustment for remote group sessions allowed so MediaSessionRecord" - + " can handle volume key"); - } - return true; - } - // See b/228021646 for details. - MediaRouter2Manager mRouter2Manager = MediaRouter2Manager.getInstance(mContext); - List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName); - boolean foundNonSystemSession = false; - boolean remoteSessionAllowVolumeAdjustment = true; - if (DEBUG) { - Slog.d( - TAG, - "Found " - + sessions.size() - + " routing sessions for package name " - + mPackageName); - } - for (RoutingSessionInfo session : sessions) { - if (DEBUG) { - Slog.d(TAG, "Found routingSessionInfo: " + session); - } - if (!session.isSystemSession()) { - foundNonSystemSession = true; - if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) { - remoteSessionAllowVolumeAdjustment = false; - } - } - } - if (!foundNonSystemSession) { - if (DEBUG) { - Slog.d( - TAG, - "Package " + mPackageName - + " has a remote media session but no associated routing session"); - } - } - - return foundNonSystemSession && remoteSessionAllowVolumeAdjustment; + return true; } @Override diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 197f0070b553..e47b4c2ee147 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -373,6 +373,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { //The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS static final int DOUBLE_PRESS_PRIMARY_NOTHING = 0; static final int DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP = 1; + static final int DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP = 2; // Must match: config_triplePressOnStemPrimaryBehavior in config.xml // The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS @@ -1596,6 +1597,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { performStemPrimaryDoublePressSwitchToRecentTask(); } break; + case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP: + final int stemPrimaryKeyDeviceId = INVALID_INPUT_DEVICE_ID; + handleKeyGestureInKeyGestureController( + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS, + stemPrimaryKeyDeviceId, KEYCODE_STEM_PRIMARY, /* metaState= */ 0); + break; } } @@ -7244,6 +7251,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { return "DOUBLE_PRESS_PRIMARY_NOTHING"; case DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP: return "DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP"; + case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP: + return "DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP"; default: return Integer.toString(behavior); } diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 1346a294b7d8..dc482422f9d9 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -1282,11 +1282,9 @@ public final class HintManagerService extends SystemService { boolean updateHintAllowedByProcState(boolean allowed) { synchronized (this) { if (allowed && !mUpdateAllowedByProcState && !mShouldForcePause) { - Slogf.e(TAG, "ADPF IS GETTING RESUMED? UID: " + mUid + " TAG: " + mTag); resume(); } if (!allowed && mUpdateAllowedByProcState) { - Slogf.e(TAG, "ADPF IS GETTING PAUSED? UID: " + mUid + " TAG: " + mTag); pause(); } mUpdateAllowedByProcState = allowed; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index cbe6700f4d41..6ede334aec80 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -230,6 +230,20 @@ java_library { } java_library { + name: "servicestests-utils-ravenwood", + srcs: [ + "utils/**/*.java", + "utils/**/*.kt", + "utils-mockito/**/*.kt", + ], + libs: [ + "android.test.runner.stubs.system", + "junit", + "mockito-ravenwood-prebuilt", + ], +} + +java_library { name: "mockito-test-utils", srcs: [ "utils-mockito/**/*.kt", diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index cf58d9b49832..c6cc941ba1cd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -70,6 +70,7 @@ import java.util.Set; import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper @@ -147,6 +148,147 @@ public class ZenModeDiffTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testRuleDiff_toStringNoChangeAddRemove() throws Exception { + // Start with two identical rules + ZenModeConfig.ZenRule r1 = makeRule(); + ZenModeConfig.ZenRule r2 = makeRule(); + + ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); + assertThat(d.toString()).isEqualTo("ZenRuleDiff{no changes}"); + + d = new ZenModeDiff.RuleDiff(r1, null); + assertThat(d.toString()).isEqualTo("ZenRuleDiff{removed}"); + + d = new ZenModeDiff.RuleDiff(null, r2); + assertThat(d.toString()).isEqualTo("ZenRuleDiff{added}"); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testRuleDiff_toString() throws Exception { + // Start with two identical rules + ZenModeConfig.ZenRule r1 = makeRule(); + ZenModeConfig.ZenRule r2 = makeRule(); + + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + List<Field> fieldsForDiff = getFieldsForDiffCheck( + ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false); + generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo); + + ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); + assertThat(d.toString()).isEqualTo("ZenRuleDiff{" + + "enabled:true->false, " + + "conditionOverride:2->1, " + + "name:string1->string2, " + + "zenMode:2->1, " + + "conditionId:null->, " + + "condition:null->Condition[" + + "state=STATE_TRUE," + + "id=hello:," + + "summary=," + + "line1=," + + "line2=," + + "icon=-1," + + "source=SOURCE_UNKNOWN," + + "flags=2], " + + "component:null->ComponentInfo{b/b}, " + + "configurationActivity:null->ComponentInfo{a/a}, " + + "id:string1->string2, " + + "creationTime:200->100, " + + "enabler:string1->string2, " + + "zenPolicy:ZenPolicyDiff{" + + "mPriorityCategories_Reminders:1->2, " + + "mPriorityCategories_Events:1->2, " + + "mPriorityCategories_Messages:1->2, " + + "mPriorityCategories_Calls:1->2, " + + "mPriorityCategories_RepeatCallers:1->2, " + + "mPriorityCategories_Alarms:1->2, " + + "mPriorityCategories_Media:1->2, " + + "mPriorityCategories_System:1->2, " + + "mPriorityCategories_Conversations:1->2, " + + "mVisualEffects_FullScreenIntent:1->2, " + + "mVisualEffects_Lights:1->2, " + + "mVisualEffects_Peek:1->2, " + + "mVisualEffects_StatusBar:1->2, " + + "mVisualEffects_Badge:1->2, " + + "mVisualEffects_Ambient:1->2, " + + "mVisualEffects_NotificationList:1->2, " + + "mPriorityMessages:2->1, " + + "mPriorityCalls:2->1, " + + "mConversationSenders:2->1, " + + "mAllowChannels:2->1}, " + + "modified:true->false, " + + "pkg:string1->string2, " + + "zenDeviceEffects:ZenDeviceEffectsDiff{" + + "mGrayscale:true->false, " + + "mSuppressAmbientDisplay:true->false, " + + "mDimWallpaper:true->false, " + + "mNightMode:true->false, " + + "mDisableAutoBrightness:true->false, " + + "mDisableTapToWake:true->false, " + + "mDisableTiltToWake:true->false, " + + "mDisableTouch:true->false, " + + "mMinimizeRadioUsage:true->false, " + + "mMaximizeDoze:true->false, " + + "mExtraEffects:[effect1]->[effect2]}, " + + "triggerDescription:string1->string2, " + + "type:2->1, " + + "allowManualInvocation:true->false, " + + "iconResName:string1->string2, " + + "legacySuppressedEffects:2->1}"); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testRuleDiff_toStringNullStartPolicy() throws Exception { + // Start with two identical rules + ZenModeConfig.ZenRule r1 = makeRule(); + ZenModeConfig.ZenRule r2 = makeRule(); + + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + List<Field> fieldsForDiff = getFieldsForDiffCheck( + ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false); + generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo); + + // Create a ZenRule with ZenDeviceEffects and ZenPolicy as null. + r1.zenPolicy = null; + r1.zenDeviceEffects = null; + ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); + assertThat(d.toString()).isEqualTo("ZenRuleDiff{" + + "enabled:true->false, " + + "conditionOverride:2->1, " + + "name:string1->string2, " + + "zenMode:2->1, " + + "conditionId:null->, " + + "condition:null->Condition[" + + "state=STATE_TRUE," + + "id=hello:," + + "summary=," + + "line1=," + + "line2=," + + "icon=-1," + + "source=SOURCE_UNKNOWN," + + "flags=2], " + + "component:null->ComponentInfo{b/b}, " + + "configurationActivity:null->ComponentInfo{a/a}, " + + "id:string1->string2, " + + "creationTime:200->100, " + + "enabler:string1->string2, " + + "zenPolicy:ZenPolicyDiff{added}, " + + "modified:true->false, " + + "pkg:string1->string2, " + + "zenDeviceEffects:ZenDeviceEffectsDiff{added}, " + + "triggerDescription:string1->string2, " + + "type:2->1, " + + "allowManualInvocation:true->false, " + + "iconResName:string1->string2, " + + "legacySuppressedEffects:2->1}"); + } + + @Test public void testDeviceEffectsDiff_addRemoveSame() { // Test add, remove, and both sides same ZenDeviceEffects effects = new ZenDeviceEffects.Builder().build(); @@ -216,12 +358,18 @@ public class ZenModeDiffTest extends UiServiceTestCase { generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo); d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2); - assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{mNightMode:true->false, " - + "mDisableTapToWake:true->false, mDisableAutoBrightness:true->false, " - + "mSuppressAmbientDisplay:true->false, mDisableTiltToWake:true->false, " - + "mGrayscale:true->false, mDisableTouch:true->false, mMaximizeDoze:true->false, " - + "mMinimizeRadioUsage:true->false, mExtraEffects:null->[], " - + "mDimWallpaper:true->false}"); + assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{" + + "mGrayscale:true->false, " + + "mSuppressAmbientDisplay:true->false, " + + "mDimWallpaper:true->false, " + + "mNightMode:true->false, " + + "mDisableAutoBrightness:true->false, " + + "mDisableTapToWake:true->false, " + + "mDisableTiltToWake:true->false, " + + "mDisableTouch:true->false, " + + "mMinimizeRadioUsage:true->false, " + + "mMaximizeDoze:true->false, " + + "mExtraEffects:[effect1]->[effect2]}"); } @@ -307,17 +455,27 @@ public class ZenModeDiffTest extends UiServiceTestCase { generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, expectedTo); d = new ZenModeDiff.PolicyDiff(policy1, policy2); - assertThat(d.toString()).isEqualTo("ZenPolicyDiff{mPriorityCalls:2->1, " - + "mVisualEffects_StatusBar:1->2, mPriorityCategories_RepeatCallers:1->2, " - + "mPriorityCategories_Calls:1->2, mPriorityCategories_Media:1->2, " - + "mConversationSenders:2->1, mPriorityCategories_Reminders:1->2, " - + "mVisualEffects_Badge:1->2, mPriorityCategories_Messages:1->2, " - + "mAllowChannels:2->1, mPriorityMessages:2->1, " - + "mVisualEffects_NotificationList:1->2, mVisualEffects_FullScreenIntent:1->2, " - + "mPriorityCategories_Alarms:1->2, mVisualEffects_Lights:1->2, " - + "mPriorityCategories_Events:1->2, mVisualEffects_Ambient:1->2, " - + "mPriorityCategories_System:1->2, mPriorityCategories_Conversations:1->2, " - + "mVisualEffects_Peek:1->2}"); + assertThat(d.toString()).isEqualTo("ZenPolicyDiff{" + + "mPriorityCategories_Reminders:1->2, " + + "mPriorityCategories_Events:1->2, " + + "mPriorityCategories_Messages:1->2, " + + "mPriorityCategories_Calls:1->2, " + + "mPriorityCategories_RepeatCallers:1->2, " + + "mPriorityCategories_Alarms:1->2, " + + "mPriorityCategories_Media:1->2, " + + "mPriorityCategories_System:1->2, " + + "mPriorityCategories_Conversations:1->2, " + + "mVisualEffects_FullScreenIntent:1->2, " + + "mVisualEffects_Lights:1->2, " + + "mVisualEffects_Peek:1->2, " + + "mVisualEffects_StatusBar:1->2, " + + "mVisualEffects_Badge:1->2, " + + "mVisualEffects_Ambient:1->2, " + + "mVisualEffects_NotificationList:1->2, " + + "mPriorityMessages:2->1, " + + "mPriorityCalls:2->1, " + + "mConversationSenders:2->1, " + + "mAllowChannels:2->1}"); } private static Set<String> getZenRuleExemptFields() { @@ -701,6 +859,44 @@ public class ZenModeDiffTest extends UiServiceTestCase { expectedA.put(f.getName(), "string1"); f.set(b, "string2"); expectedB.put(f.getName(), "string2"); + } else if (Set.class.equals(t)) { + Set<String> aSet = Set.of("effect1"); + Set<String> bSet = Set.of("effect2"); + f.set(a, aSet); + expectedA.put(f.getName(), aSet); + f.set(b, bSet); + expectedB.put(f.getName(), bSet); + } else if (ZenDeviceEffects.class.equals(t)) { + // Recurse into generating field diffs for ZenDeviceEffects. + ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build(); + ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build(); + // maps mapping field name -> expected output value as we set diffs + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + + List<Field> fieldsForDiff = getFieldsForDiffCheck( + ZenDeviceEffects.class, Collections.emptySet() /*no exempt fields*/, true); + generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo); + f.set(a, effects1); + expectedA.put(f.getName(), effects1); + f.set(b, effects2); + expectedB.put(f.getName(), effects2); + } else if (ZenPolicy.class.equals(t)) { + // Recurse into generating field diffs for ZenPolicy. + ZenPolicy policy1 = new ZenPolicy.Builder().build(); + ZenPolicy policy2 = new ZenPolicy.Builder().build(); + // maps mapping field name -> expected output value as we set diffs + ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); + ArrayMap<String, Object> expectedTo = new ArrayMap<>(); + + List<Field> fieldsForDiff = getFieldsForDiffCheck(ZenPolicy.class, + Collections.emptySet(), false); + generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, + expectedTo); + f.set(a, policy1); + expectedA.put(f.getName(), policy1); + f.set(b, policy2); + expectedB.put(f.getName(), policy2); } else { // catch-all for other types: have the field be "added" f.set(a, null); diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java index 9b92ff45952b..3ea3235df0f4 100644 --- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java @@ -23,6 +23,7 @@ import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS; import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP; import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS; @@ -32,6 +33,7 @@ import static com.android.server.policy.PhoneWindowManager.TRIPLE_PRESS_PRIMARY_ import android.app.ActivityManager.RecentTaskInfo; import android.app.ActivityTaskManager.RootTaskInfo; import android.content.ComponentName; +import android.hardware.input.KeyGestureEvent; import android.os.RemoteException; import android.provider.Settings; import android.view.Display; @@ -236,6 +238,19 @@ public class StemKeyGestureTests extends ShortcutKeyTestBase { } @Test + public void stemDoubleKey_behaviorIsLaunchFitness_gestureEventFired() { + overrideBehavior( + STEM_PRIMARY_BUTTON_DOUBLE_PRESS, DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP); + setUpPhoneWindowManager(/* supportSettingsUpdate= */ true); + + sendKey(KEYCODE_STEM_PRIMARY); + sendKey(KEYCODE_STEM_PRIMARY); + + mPhoneWindowManager.assertKeyGestureEventSentToKeyGestureController( + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS); + } + + @Test public void stemTripleKey_EarlyShortPress_AllAppsThenBackToOriginalThenToggleA11y() throws RemoteException { overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 1aa908792c0e..a85f8666d2e1 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -919,4 +919,9 @@ class TestPhoneWindowManager { mTestLooper.dispatchAll(); Assert.assertEquals(expectEnabled, mIsTalkBackEnabled); } + + void assertKeyGestureEventSentToKeyGestureController(int gestureType) { + verify(mInputManagerInternal) + .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); + } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index ff302f6b1a65..ff966ae66e0a 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -2298,13 +2298,9 @@ public class TelephonyManager { * * See {@link #getImei(int)} for details on the required permissions and behavior * when the caller does not hold sufficient permissions. - * - * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) public String getImei() { return getImei(getSlotIndex()); } @@ -2343,13 +2339,9 @@ public class TelephonyManager { * </ul> * * @param slotIndex of which IMEI is returned - * - * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) public String getImei(int slotIndex) { ITelephony telephony = getITelephony(); if (telephony == null) return null; @@ -2366,11 +2358,7 @@ public class TelephonyManager { /** * Returns the Type Allocation Code from the IMEI. Return null if Type Allocation Code is not * available. - * - * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) @Nullable public String getTypeAllocationCode() { return getTypeAllocationCode(getSlotIndex()); @@ -2381,11 +2369,7 @@ public class TelephonyManager { * available. * * @param slotIndex of which Type Allocation Code is returned - * - * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_GSM}. */ - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) @Nullable public String getTypeAllocationCode(int slotIndex) { ITelephony telephony = getITelephony(); @@ -19378,12 +19362,9 @@ public class TelephonyManager { * </ul> * * @return Primary IMEI of type string - * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_GSM}. * @throws SecurityException if the caller does not have the required permission/privileges */ @NonNull - @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM) public String getPrimaryImei() { try { ITelephony telephony = getITelephony(); |