Merge "[kairos] Rename kt-frp to Kairos" into main
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 238c028..9eac108 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 @@
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
import androidx.test.filters.LargeTest;
@@ -47,6 +48,8 @@
@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 @@
}
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/cmds/uinput/README.md b/cmds/uinput/README.md
index 5d3f12e..6138388 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -83,6 +83,11 @@
Due to the sequential nature in which this is parsed, the `type` field must be specified before
the `data` field in this JSON Object.
+Every `register` command will need a `"UI_SET_EVBIT"` configuration entry that lists what types of
+axes it declares. This entry should be the first in the list. For example, if the uinput device has
+`"UI_SET_KEYBIT"` and `"UI_SET_RELBIT"` configuration entries, it will also need a `"UI_SET_EVBIT"`
+entry with data of `["EV_KEY", "EV_REL"]` or the other configuration entries will be ignored.
+
`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`.
`abs_info` fields are provided to set the device axes information. It is an array of below objects:
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 8447a7f..287e787 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -604,6 +604,7 @@
public class TelephonyManager {
method @NonNull public static int[] getAllNetworkTypes();
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Set<java.lang.String> getPackagesWithCarrierPrivileges();
}
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 20bcf5f..4b6c62e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -91,6 +91,7 @@
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 @@
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 @@
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.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 {
@@ -13046,6 +13104,7 @@
method public void onPanelHidden();
method public void onPanelRevealed(int);
method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
+ method @FlaggedApi("android.service.notification.notification_classification") public final void setAdjustmentTypeSupportedState(@NonNull String, boolean);
method public final void unsnoozeNotification(@NonNull String);
field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f32d805..9bcdf95 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -396,6 +396,7 @@
method public void cleanUpCallersAfter(long);
method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
method public android.content.ComponentName getEffectsSuppressor();
+ method @FlaggedApi("android.service.notification.notification_classification") @NonNull public java.util.Set<java.lang.String> getUnsupportedAdjustmentTypes();
method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index edcdb6c..f34341f 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -34,6 +34,7 @@
import android.util.proto.ProtoOutputStream;
import android.util.proto.WireTypeMismatchException;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -777,7 +778,9 @@
mStartComponent = other.mStartComponent;
}
- private ApplicationStartInfo(@NonNull Parcel in) {
+ /** @hide */
+ @VisibleForTesting
+ public ApplicationStartInfo(@NonNull Parcel in) {
mStartupState = in.readInt();
mPid = in.readInt();
mRealUid = in.readInt();
@@ -1061,12 +1064,21 @@
if (other == null || !(other instanceof ApplicationStartInfo)) {
return false;
}
+
final ApplicationStartInfo o = (ApplicationStartInfo) other;
- return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid
- && mDefiningUid == o.mDefiningUid && mReason == o.mReason
- && mStartupState == o.mStartupState && mStartType == o.mStartType
- && mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName)
- && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped
+
+ return mPid == o.mPid
+ && mRealUid == o.mRealUid
+ && mPackageUid == o.mPackageUid
+ && mDefiningUid == o.mDefiningUid
+ && mReason == o.mReason
+ && mStartupState == o.mStartupState
+ && mStartType == o.mStartType
+ && mLaunchMode == o.mLaunchMode
+ && TextUtils.equals(mPackageName, o.mPackageName)
+ && TextUtils.equals(mProcessName, o.mProcessName)
+ && timestampsEquals(o)
+ && mWasForceStopped == o.mWasForceStopped
&& mMonoticCreationTimeMs == o.mMonoticCreationTimeMs
&& mStartComponent == o.mStartComponent;
}
@@ -1074,7 +1086,7 @@
@Override
public int hashCode() {
return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState,
- mStartType, mLaunchMode, mProcessName, mStartupTimestampsNs,
+ mStartType, mLaunchMode, mPackageName, mProcessName, mStartupTimestampsNs,
mMonoticCreationTimeMs, mStartComponent);
}
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 8a54b5d..3b2aab4 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -160,8 +160,8 @@
void requestBindProvider(in ComponentName component);
void requestUnbindProvider(in IConditionProvider token);
- void setNotificationsShownFromListener(in INotificationListener token, in String[] keys);
+ void setNotificationsShownFromListener(in INotificationListener token, in String[] keys);
ParceledListSlice getActiveNotificationsFromListener(in INotificationListener token, in String[] keys, int trim);
ParceledListSlice getSnoozedNotificationsFromListener(in INotificationListener token, int trim);
void clearRequestedListenerHints(in INotificationListener token);
@@ -261,4 +261,7 @@
void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser);
boolean appCanBePromoted(String pkg, int uid);
boolean canBePromoted(String pkg);
+
+ void setAdjustmentTypeSupportedState(in INotificationListener token, String key, boolean supported);
+ List<String> getUnsupportedAdjustmentTypes();
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index c7b84ae..dfed1f7 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -68,9 +68,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -3094,4 +3096,19 @@
}
}
+ /**
+ * Returns the list of {@link Adjustment} keys that the current approved
+ * {@link android.service.notification.NotificationAssistantService} does not support.
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public @NonNull Set<String> getUnsupportedAdjustmentTypes() {
+ INotificationManager service = getService();
+ try {
+ return new HashSet<>(service.getUnsupportedAdjustmentTypes());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fb2655c..e985f88 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5039,6 +5039,25 @@
"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/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index 2cdae21..44f2a4c 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -114,6 +114,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -125,6 +136,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 160cbdf..300740e 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -309,3 +309,11 @@
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 0000000..38a7956
--- /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 0000000..7a9484a
--- /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 0000000..d3071fd
--- /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 0000000..ac85585
--- /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 0000000..70b4a02
--- /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 0000000..6a1cb4f
--- /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 0000000..4d0379d7
--- /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 0000000..ccf2119
--- /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/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 1b21bdf..9e3a9b3 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -69,6 +69,7 @@
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -80,6 +81,7 @@
import com.android.internal.util.ArrayUtils;
import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -2157,6 +2159,12 @@
private final Set<Set<DeviceCameraInfo>> mConcurrentCameraIdCombinations = new ArraySet<>();
+ // Diagnostic messages for ArrayIndexOutOfBoundsException in extractCameraIdListLocked
+ // b/367649718
+ private static final int DEVICE_STATUS_ARRAY_SIZE = 10;
+ private final ArrayDeque<String> mDeviceStatusHistory =
+ new ArrayDeque<>(DEVICE_STATUS_ARRAY_SIZE);
+
// Registered availability callbacks and their executors
private final ArrayMap<AvailabilityCallback, Executor> mCallbackMap = new ArrayMap<>();
@@ -2274,6 +2282,10 @@
}
try {
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "connectCameraServiceLocked(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
CameraStatus[] cameraStatuses = cameraService.addListener(this);
for (CameraStatus cameraStatus : cameraStatuses) {
DeviceCameraInfo info = new DeviceCameraInfo(cameraStatus.cameraId,
@@ -2296,6 +2308,10 @@
}
}
mCameraService = cameraService;
+
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "connectCameraServiceLocked(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
} catch (ServiceSpecificException e) {
// Unexpected failure
throw new IllegalStateException("Failed to register a camera service listener", e);
@@ -2349,18 +2365,28 @@
}
private String[] extractCameraIdListLocked(int deviceId, int devicePolicy) {
- List<String> cameraIds = new ArrayList<>();
- for (int i = 0; i < mDeviceStatus.size(); i++) {
- int status = mDeviceStatus.valueAt(i);
- DeviceCameraInfo info = mDeviceStatus.keyAt(i);
- if (status == ICameraServiceListener.STATUS_NOT_PRESENT
- || status == ICameraServiceListener.STATUS_ENUMERATING
- || shouldHideCamera(deviceId, devicePolicy, info)) {
- continue;
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "extractCameraIdListLocked(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+ try {
+ List<String> cameraIds = new ArrayList<>();
+ for (int i = 0; i < mDeviceStatus.size(); i++) {
+ int status = mDeviceStatus.valueAt(i);
+ DeviceCameraInfo info = mDeviceStatus.keyAt(i);
+ if (status == ICameraServiceListener.STATUS_NOT_PRESENT
+ || status == ICameraServiceListener.STATUS_ENUMERATING
+ || shouldHideCamera(deviceId, devicePolicy, info)) {
+ continue;
+ }
+ cameraIds.add(info.mCameraId);
}
- cameraIds.add(info.mCameraId);
+ return cameraIds.toArray(new String[0]);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ String message = e.getMessage();
+ String messageWithHistory = message + ": {"
+ + String.join(" -> ", mDeviceStatusHistory) + "}";
+ throw new ArrayIndexOutOfBoundsException(messageWithHistory);
}
- return cameraIds.toArray(new String[0]);
}
private Set<Set<String>> extractConcurrentCameraIdListLocked(int deviceId,
@@ -2488,6 +2514,10 @@
synchronized (mLock) {
connectCameraServiceLocked();
try {
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "getCameraIdListNoLazy(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
// The purpose of the addListener, removeListener pair here is to get a fresh
// list of camera ids from cameraserver. We do this since for in test processes,
// changes can happen w.r.t non-changeable permissions (eg: SYSTEM_CAMERA
@@ -2521,6 +2551,9 @@
onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info);
mTorchStatus.remove(info);
}
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "getCameraIdListNoLazy(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
} catch (ServiceSpecificException e) {
// Unexpected failure
throw new IllegalStateException("Failed to register a camera service listener",
@@ -3209,7 +3242,13 @@
public void onStatusChanged(int status, String cameraId, int deviceId)
throws RemoteException {
synchronized(mLock) {
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("onStatusChanged(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
onStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId));
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("onStatusChanged(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
}
}
@@ -3352,6 +3391,10 @@
*/
public void binderDied() {
synchronized(mLock) {
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("binderDied(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
// Only do this once per service death
if (mCameraService == null) return;
@@ -3380,6 +3423,10 @@
mConcurrentCameraIdCombinations.clear();
scheduleCameraServiceReconnectionLocked();
+
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("binderDied(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
}
}
@@ -3409,5 +3456,13 @@
return Objects.hash(mCameraId, mDeviceId);
}
}
+
+ private void addDeviceStatusHistoryLocked(String log) {
+ if (mDeviceStatusHistory.size() == DEVICE_STATUS_ARRAY_SIZE) {
+ mDeviceStatusHistory.removeFirst();
+ }
+ mDeviceStatusHistory.addLast(log);
+ }
+
} // CameraManagerGlobal
} // CameraManager
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 73b5d94..e598097 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -431,6 +431,17 @@
*/
public abstract IntArray getDisplayGroupIds();
+
+ /**
+ * Get all display ids belonging to the display group with given id.
+ */
+ public abstract int[] getDisplayIdsForGroup(int groupId);
+
+ /**
+ * Get the mapping of display group ids to the display ids that belong to them.
+ */
+ public abstract SparseArray<int[]> getDisplayIdsByGroupsIds();
+
/**
* Get all available display ids.
*/
diff --git a/core/java/android/net/vcn/VcnTransportInfo.java b/core/java/android/net/vcn/VcnTransportInfo.java
index f546910..1fc91ee 100644
--- a/core/java/android/net/vcn/VcnTransportInfo.java
+++ b/core/java/android/net/vcn/VcnTransportInfo.java
@@ -17,9 +17,11 @@
package android.net.vcn;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS;
import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkCapabilities;
@@ -29,6 +31,8 @@
import android.os.Parcelable;
import android.telephony.SubscriptionManager;
+import com.android.internal.util.Preconditions;
+
import java.util.Objects;
/**
@@ -47,6 +51,7 @@
*
* @hide
*/
+// TODO: Do not store WifiInfo and subscription ID in VcnTransportInfo anymore
public class VcnTransportInfo implements TransportInfo, Parcelable {
@Nullable private final WifiInfo mWifiInfo;
private final int mSubId;
@@ -195,4 +200,42 @@
return new VcnTransportInfo[size];
}
};
+
+ /** This class can be used to construct a {@link VcnTransportInfo}. */
+ public static final class Builder {
+ private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
+
+ /** Construct Builder */
+ public Builder() {}
+
+ /**
+ * Sets the maximum supported IKEv2/IPsec NATT keepalive timeout.
+ *
+ * <p>This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs,
+ * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data.
+ *
+ * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN
+ * Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN
+ * Gateway.
+ * @return this {@link Builder} instance, for chaining
+ */
+ @NonNull
+ public Builder setMinUdpPort4500NatTimeoutSeconds(
+ @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS)
+ int minUdpPort4500NatTimeoutSeconds) {
+ Preconditions.checkArgument(
+ minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
+ "Timeout must be at least 120s");
+
+ mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds;
+ return Builder.this;
+ }
+
+ /** Build a VcnTransportInfo instance */
+ @NonNull
+ public VcnTransportInfo build() {
+ return new VcnTransportInfo(
+ null /* wifiInfo */, INVALID_SUBSCRIPTION_ID, mMinUdpPort4500NatTimeoutSeconds);
+ }
+ }
}
diff --git a/core/java/android/net/vcn/VcnUtils.java b/core/java/android/net/vcn/VcnUtils.java
new file mode 100644
index 0000000..6dc5180
--- /dev/null
+++ b/core/java/android/net/vcn/VcnUtils.java
@@ -0,0 +1,97 @@
+/*
+ * 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.net.vcn;
+
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
+
+import java.util.List;
+
+/**
+ * Utility class for VCN callers get information from VCN network
+ *
+ * @hide
+ */
+public class VcnUtils {
+ /** Get the WifiInfo of the VCN's underlying WiFi network */
+ @Nullable
+ public static WifiInfo getWifiInfoFromVcnCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ final NetworkCapabilities underlyingCaps =
+ getVcnUnderlyingCaps(connectivityMgr, networkCapabilities);
+
+ if (underlyingCaps == null) {
+ return null;
+ }
+
+ final TransportInfo underlyingTransportInfo = underlyingCaps.getTransportInfo();
+ if (!(underlyingTransportInfo instanceof WifiInfo)) {
+ return null;
+ }
+
+ return (WifiInfo) underlyingTransportInfo;
+ }
+
+ /** Get the subscription ID of the VCN's underlying Cell network */
+ public static int getSubIdFromVcnCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ final NetworkCapabilities underlyingCaps =
+ getVcnUnderlyingCaps(connectivityMgr, networkCapabilities);
+
+ if (underlyingCaps == null) {
+ return INVALID_SUBSCRIPTION_ID;
+ }
+
+ final NetworkSpecifier underlyingNetworkSpecifier = underlyingCaps.getNetworkSpecifier();
+ if (!(underlyingNetworkSpecifier instanceof TelephonyNetworkSpecifier)) {
+ return INVALID_SUBSCRIPTION_ID;
+ }
+
+ return ((TelephonyNetworkSpecifier) underlyingNetworkSpecifier).getSubscriptionId();
+ }
+
+ @Nullable
+ private static NetworkCapabilities getVcnUnderlyingCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ // Return null if it is not a VCN network
+ if (networkCapabilities.getTransportInfo() == null
+ || !(networkCapabilities.getTransportInfo() instanceof VcnTransportInfo)) {
+ return null;
+ }
+
+ // As of Android 16, VCN has one underlying network, and only one. If there are more
+ // than one networks due to future changes in the VCN mainline code, just take the first
+ // network
+ final List<Network> underlyingNws = networkCapabilities.getUnderlyingNetworks();
+ if (underlyingNws == null) {
+ return null;
+ }
+
+ return connectivityMgr.getNetworkCapabilities(underlyingNws.get(0));
+ }
+}
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 48d7cf7..7b7058e 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -393,6 +393,23 @@
}
}
+ /**
+ * Informs the notification manager about what {@link Adjustment Adjustments} are supported by
+ * this NAS.
+ *
+ * For backwards compatibility, we assume all Adjustment types are supported by the NAS.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public final void setAdjustmentTypeSupportedState(@NonNull @Adjustment.Keys String key,
+ boolean supported) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().setAdjustmentTypeSupportedState(mWrapper, key, supported);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
@Override
public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder,
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 60a7d6b..c9f4647 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -16,6 +16,7 @@
package android.service.notification;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.Flags;
@@ -24,6 +25,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
@@ -62,6 +64,7 @@
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.
@@ -71,6 +74,19 @@
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;
}
/**
@@ -92,6 +108,9 @@
*/
@Override
public String toString() {
+ if (mDetailedDiff != null) {
+ return mDetailedDiff.toString();
+ }
return mFrom + "->" + mTo;
}
@@ -99,6 +118,9 @@
* 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);
}
@@ -114,7 +136,8 @@
@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.
@@ -549,8 +572,16 @@
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));
@@ -559,9 +590,12 @@
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,
@@ -629,7 +663,7 @@
sb.append(key);
sb.append(":");
- sb.append(diff);
+ sb.append(diff.toString());
}
if (becameActive()) {
@@ -663,4 +697,338 @@
return mActiveDiff != null && !mActiveDiff.to();
}
}
+
+ /**
+ * Diff class representing a change between two
+ * {@link android.service.notification.ZenDeviceEffects}.
+ */
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public static class DeviceEffectsDiff extends BaseDiff {
+ public static final String FIELD_GRAYSCALE = "mGrayscale";
+ public static final String FIELD_SUPPRESS_AMBIENT_DISPLAY = "mSuppressAmbientDisplay";
+ public static final String FIELD_DIM_WALLPAPER = "mDimWallpaper";
+ public static final String FIELD_NIGHT_MODE = "mNightMode";
+ public static final String FIELD_DISABLE_AUTO_BRIGHTNESS = "mDisableAutoBrightness";
+ public static final String FIELD_DISABLE_TAP_TO_WAKE = "mDisableTapToWake";
+ public static final String FIELD_DISABLE_TILT_TO_WAKE = "mDisableTiltToWake";
+ public static final String FIELD_DISABLE_TOUCH = "mDisableTouch";
+ public static final String FIELD_MINIMIZE_RADIO_USAGE = "mMinimizeRadioUsage";
+ public static final String FIELD_MAXIMIZE_DOZE = "mMaximizeDoze";
+ public static final String FIELD_EXTRA_EFFECTS = "mExtraEffects";
+ // NOTE: new field strings must match the variable names in ZenDeviceEffects
+
+ /**
+ * Create a DeviceEffectsDiff representing the difference between two ZenDeviceEffects
+ * objects.
+ * @param from previous ZenDeviceEffects
+ * @param to new ZenDeviceEffects
+ * @return The diff between the two given ZenDeviceEffects
+ */
+ public DeviceEffectsDiff(ZenDeviceEffects from, ZenDeviceEffects to) {
+ super(from, to);
+ // Short-circuit the both-null case
+ if (from == null && to == null) {
+ return;
+ }
+ if (hasExistenceChange()) {
+ // either added or removed; return here. otherwise (they're not both null) there's
+ // field diffs.
+ return;
+ }
+
+ // Compare all fields, knowing there's some diff and that neither is null.
+ if (from.shouldDisplayGrayscale() != to.shouldDisplayGrayscale()) {
+ addField(FIELD_GRAYSCALE, new FieldDiff<>(from.shouldDisplayGrayscale(),
+ to.shouldDisplayGrayscale()));
+ }
+ if (from.shouldSuppressAmbientDisplay() != to.shouldSuppressAmbientDisplay()) {
+ addField(FIELD_SUPPRESS_AMBIENT_DISPLAY,
+ new FieldDiff<>(from.shouldSuppressAmbientDisplay(),
+ to.shouldSuppressAmbientDisplay()));
+ }
+ if (from.shouldDimWallpaper() != to.shouldDimWallpaper()) {
+ addField(FIELD_DIM_WALLPAPER, new FieldDiff<>(from.shouldDimWallpaper(),
+ to.shouldDimWallpaper()));
+ }
+ if (from.shouldUseNightMode() != to.shouldUseNightMode()) {
+ addField(FIELD_NIGHT_MODE, new FieldDiff<>(from.shouldUseNightMode(),
+ to.shouldUseNightMode()));
+ }
+ if (from.shouldDisableAutoBrightness() != to.shouldDisableAutoBrightness()) {
+ addField(FIELD_DISABLE_AUTO_BRIGHTNESS,
+ new FieldDiff<>(from.shouldDisableAutoBrightness(),
+ to.shouldDisableAutoBrightness()));
+ }
+ if (from.shouldDisableTapToWake() != to.shouldDisableTapToWake()) {
+ addField(FIELD_DISABLE_TAP_TO_WAKE, new FieldDiff<>(from.shouldDisableTapToWake(),
+ to.shouldDisableTapToWake()));
+ }
+ if (from.shouldDisableTiltToWake() != to.shouldDisableTiltToWake()) {
+ addField(FIELD_DISABLE_TILT_TO_WAKE,
+ new FieldDiff<>(from.shouldDisableTiltToWake(),
+ to.shouldDisableTiltToWake()));
+ }
+ if (from.shouldDisableTouch() != to.shouldDisableTouch()) {
+ addField(FIELD_DISABLE_TOUCH, new FieldDiff<>(from.shouldDisableTouch(),
+ to.shouldDisableTouch()));
+ }
+ if (from.shouldMinimizeRadioUsage() != to.shouldMinimizeRadioUsage()) {
+ addField(FIELD_MINIMIZE_RADIO_USAGE,
+ new FieldDiff<>(from.shouldMinimizeRadioUsage(),
+ to.shouldMinimizeRadioUsage()));
+ }
+ if (from.shouldMaximizeDoze() != to.shouldMaximizeDoze()) {
+ addField(FIELD_MAXIMIZE_DOZE, new FieldDiff<>(from.shouldMaximizeDoze(),
+ to.shouldMaximizeDoze()));
+ }
+ if (!Objects.equals(from.getExtraEffects(), to.getExtraEffects())) {
+ addField(FIELD_EXTRA_EFFECTS, new FieldDiff<>(from.getExtraEffects(),
+ to.getExtraEffects()));
+ }
+ }
+
+ /**
+ * Returns whether this object represents an actual diff.
+ */
+ @Override
+ public boolean hasDiff() {
+ return hasExistenceChange() || hasFieldDiffs();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ZenDeviceEffectsDiff{");
+ if (!hasDiff()) {
+ sb.append("no changes");
+ }
+
+ // If added or deleted, we just append that.
+ if (hasExistenceChange()) {
+ if (wasAdded()) {
+ sb.append("added");
+ } else if (wasRemoved()) {
+ sb.append("removed");
+ }
+ }
+
+ // Append all of the individual field diffs
+ boolean first = true;
+ for (String key : fieldNamesWithDiff()) {
+ FieldDiff diff = getDiffForField(key);
+ if (diff == null) {
+ // The diff should not have null diffs added, but we add this to be defensive.
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+
+ sb.append(key);
+ sb.append(":");
+ sb.append(diff);
+ }
+
+ return sb.append("}").toString();
+ }
+ }
+
+ /**
+ * Diff class representing a change between two {@link android.service.notification.ZenPolicy}.
+ */
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public static class PolicyDiff extends BaseDiff {
+ public static final String FIELD_PRIORITY_CATEGORY_REMINDERS =
+ "mPriorityCategories_Reminders";
+ public static final String FIELD_PRIORITY_CATEGORY_EVENTS = "mPriorityCategories_Events";
+ public static final String FIELD_PRIORITY_CATEGORY_MESSAGES =
+ "mPriorityCategories_Messages";
+ public static final String FIELD_PRIORITY_CATEGORY_CALLS = "mPriorityCategories_Calls";
+ public static final String FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS =
+ "mPriorityCategories_RepeatCallers";
+ public static final String FIELD_PRIORITY_CATEGORY_ALARMS = "mPriorityCategories_Alarms";
+ public static final String FIELD_PRIORITY_CATEGORY_MEDIA = "mPriorityCategories_Media";
+ public static final String FIELD_PRIORITY_CATEGORY_SYSTEM = "mPriorityCategories_System";
+ public static final String FIELD_PRIORITY_CATEGORY_CONVERSATIONS =
+ "mPriorityCategories_Conversations";
+
+ public static final String FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT =
+ "mVisualEffects_FullScreenIntent";
+ public static final String FIELD_VISUAL_EFFECT_LIGHTS = "mVisualEffects_Lights";
+ public static final String FIELD_VISUAL_EFFECT_PEEK = "mVisualEffects_Peek";
+ public static final String FIELD_VISUAL_EFFECT_STATUS_BAR = "mVisualEffects_StatusBar";
+ public static final String FIELD_VISUAL_EFFECT_BADGE = "mVisualEffects_Badge";
+ public static final String FIELD_VISUAL_EFFECT_AMBIENT = "mVisualEffects_Ambient";
+ public static final String FIELD_VISUAL_EFFECT_NOTIFICATION_LIST =
+ "mVisualEffects_NotificationList";
+
+ public static final String FIELD_PRIORITY_MESSAGES = "mPriorityMessages";
+ public static final String FIELD_PRIORITY_CALLS = "mPriorityCalls";
+ public static final String FIELD_CONVERSATION_SENDERS = "mConversationSenders";
+ public static final String FIELD_ALLOW_CHANNELS = "mAllowChannels";
+
+ /**
+ * Create a PolicyDiff representing the difference between two ZenPolicy objects.
+ *
+ * @param from previous ZenPolicy
+ * @param to new ZenPolicy
+ * @return The diff between the two given ZenPolicy
+ */
+ public PolicyDiff(ZenPolicy from, ZenPolicy to) {
+ super(from, to);
+ // Short-circuit the both-null case
+ if (from == null && to == null) {
+ return;
+ }
+ if (hasExistenceChange()) {
+ // either added or removed; return here. otherwise (they're not both null) there's
+ // field diffs.
+ return;
+ }
+
+ // Compare all fields, knowing there's some diff and that neither is null.
+ if (from.getPriorityCategoryReminders() != to.getPriorityCategoryReminders()) {
+ addField(FIELD_PRIORITY_CATEGORY_REMINDERS,
+ new FieldDiff<>(from.getPriorityCategoryReminders(),
+ to.getPriorityCategoryReminders()));
+ }
+ if (from.getPriorityCategoryEvents() != to.getPriorityCategoryEvents()) {
+ addField(FIELD_PRIORITY_CATEGORY_EVENTS,
+ new FieldDiff<>(from.getPriorityCategoryEvents(),
+ to.getPriorityCategoryEvents()));
+ }
+ if (from.getPriorityCategoryMessages() != to.getPriorityCategoryMessages()) {
+ addField(FIELD_PRIORITY_CATEGORY_MESSAGES,
+ new FieldDiff<>(from.getPriorityCategoryMessages(),
+ to.getPriorityCategoryMessages()));
+ }
+ if (from.getPriorityCategoryCalls() != to.getPriorityCategoryCalls()) {
+ addField(FIELD_PRIORITY_CATEGORY_CALLS,
+ new FieldDiff<>(from.getPriorityCategoryCalls(),
+ to.getPriorityCategoryCalls()));
+ }
+ if (from.getPriorityCategoryRepeatCallers() != to.getPriorityCategoryRepeatCallers()) {
+ addField(FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS,
+ new FieldDiff<>(from.getPriorityCategoryRepeatCallers(),
+ to.getPriorityCategoryRepeatCallers()));
+ }
+ if (from.getPriorityCategoryAlarms() != to.getPriorityCategoryAlarms()) {
+ addField(FIELD_PRIORITY_CATEGORY_ALARMS,
+ new FieldDiff<>(from.getPriorityCategoryAlarms(),
+ to.getPriorityCategoryAlarms()));
+ }
+ if (from.getPriorityCategoryMedia() != to.getPriorityCategoryMedia()) {
+ addField(FIELD_PRIORITY_CATEGORY_MEDIA,
+ new FieldDiff<>(from.getPriorityCategoryMedia(),
+ to.getPriorityCategoryMedia()));
+ }
+ if (from.getPriorityCategorySystem() != to.getPriorityCategorySystem()) {
+ addField(FIELD_PRIORITY_CATEGORY_SYSTEM,
+ new FieldDiff<>(from.getPriorityCategorySystem(),
+ to.getPriorityCategorySystem()));
+ }
+ if (from.getPriorityCategoryConversations() != to.getPriorityCategoryConversations()) {
+ addField(FIELD_PRIORITY_CATEGORY_CONVERSATIONS,
+ new FieldDiff<>(from.getPriorityCategoryConversations(),
+ to.getPriorityCategoryConversations()));
+ }
+ if (from.getVisualEffectFullScreenIntent() != to.getVisualEffectFullScreenIntent()) {
+ addField(FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT,
+ new FieldDiff<>(from.getVisualEffectFullScreenIntent(),
+ to.getVisualEffectFullScreenIntent()));
+ }
+ if (from.getVisualEffectLights() != to.getVisualEffectLights()) {
+ addField(FIELD_VISUAL_EFFECT_LIGHTS,
+ new FieldDiff<>(from.getVisualEffectLights(), to.getVisualEffectLights()));
+ }
+ if (from.getVisualEffectPeek() != to.getVisualEffectPeek()) {
+ addField(FIELD_VISUAL_EFFECT_PEEK, new FieldDiff<>(from.getVisualEffectPeek(),
+ to.getVisualEffectPeek()));
+ }
+ if (from.getVisualEffectStatusBar() != to.getVisualEffectStatusBar()) {
+ addField(FIELD_VISUAL_EFFECT_STATUS_BAR,
+ new FieldDiff<>(from.getVisualEffectStatusBar(),
+ to.getVisualEffectStatusBar()));
+ }
+ if (from.getVisualEffectBadge() != to.getVisualEffectBadge()) {
+ addField(FIELD_VISUAL_EFFECT_BADGE, new FieldDiff<>(from.getVisualEffectBadge(),
+ to.getVisualEffectBadge()));
+ }
+ if (from.getVisualEffectAmbient() != to.getVisualEffectAmbient()) {
+ addField(FIELD_VISUAL_EFFECT_AMBIENT, new FieldDiff<>(from.getVisualEffectAmbient(),
+ to.getVisualEffectAmbient()));
+ }
+ if (from.getVisualEffectNotificationList() != to.getVisualEffectNotificationList()) {
+ addField(FIELD_VISUAL_EFFECT_NOTIFICATION_LIST,
+ new FieldDiff<>(from.getVisualEffectNotificationList(),
+ to.getVisualEffectNotificationList()));
+ }
+ if (from.getPriorityMessageSenders() != to.getPriorityMessageSenders()) {
+ addField(FIELD_PRIORITY_MESSAGES, new FieldDiff<>(from.getPriorityMessageSenders(),
+ to.getPriorityMessageSenders()));
+ }
+ if (from.getPriorityCallSenders() != to.getPriorityCallSenders()) {
+ addField(FIELD_PRIORITY_CALLS, new FieldDiff<>(from.getPriorityCallSenders(),
+ to.getPriorityCallSenders()));
+ }
+ if (from.getPriorityConversationSenders() != to.getPriorityConversationSenders()) {
+ addField(FIELD_CONVERSATION_SENDERS,
+ new FieldDiff<>(from.getPriorityConversationSenders(),
+ to.getPriorityConversationSenders()));
+ }
+ if (from.getPriorityChannelsAllowed() != to.getPriorityChannelsAllowed()) {
+ addField(FIELD_ALLOW_CHANNELS, new FieldDiff<>(from.getPriorityChannelsAllowed(),
+ to.getPriorityChannelsAllowed()));
+ }
+ }
+
+ /**
+ * Returns whether this object represents an actual diff.
+ */
+ @Override
+ public boolean hasDiff() {
+ return hasExistenceChange() || hasFieldDiffs();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ZenPolicyDiff{");
+ // The diff should not have null diffs added, but we add this to be defensive.
+ if (!hasDiff()) {
+ sb.append("no changes");
+ }
+
+ // If added or deleted, we just append that.
+ if (hasExistenceChange()) {
+ if (wasAdded()) {
+ sb.append("added");
+ } else if (wasRemoved()) {
+ sb.append("removed");
+ }
+ }
+
+ // Go through all of the individual fields
+ boolean first = true;
+ for (String key : fieldNamesWithDiff()) {
+ FieldDiff diff = getDiffForField(key);
+ if (diff == null) {
+ // this shouldn't happen...
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+
+ sb.append(key);
+ sb.append(":");
+ sb.append(diff);
+ }
+
+ return sb.append("}").toString();
+ }
+ }
+
}
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index be0d7b3..4cff67e 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -29,6 +29,8 @@
import android.os.Parcelable;
import android.util.proto.ProtoOutputStream;
+import androidx.annotation.VisibleForTesting;
+
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -207,8 +209,10 @@
/**
* Total number of priority categories. Keep updated with any updates to PriorityCategory enum.
+ * If this changes, you must update {@link ZenModeDiff.PolicyDiff} to include new categories.
* @hide
*/
+ @VisibleForTesting
public static final int NUM_PRIORITY_CATEGORIES = 9;
/** @hide */
@@ -241,8 +245,10 @@
/**
* Total number of visual effects. Keep updated with any updates to VisualEffect enum.
+ * If this changes, you must update {@link ZenModeDiff.PolicyDiff} to include new categories.
* @hide
*/
+ @VisibleForTesting
public static final int NUM_VISUAL_EFFECTS = 7;
/** @hide */
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3599332..ec5b488 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -178,3 +178,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "handwriting_unsupported_show_soft_input_fix"
+ namespace: "text"
+ description: "Don't show soft keyboard on stylus input if text field doesn't support handwriting and getShowSoftInputOnFocus() returns false."
+ bug: "363180475"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index f132963..c217999 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -19,6 +19,7 @@
import static com.android.text.flags.Flags.handwritingCursorPosition;
import static com.android.text.flags.Flags.handwritingTrackDisabled;
import static com.android.text.flags.Flags.handwritingUnsupportedMessage;
+import static com.android.text.flags.Flags.handwritingUnsupportedShowSoftInputFix;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
@@ -241,7 +242,11 @@
if (!candidateView.hasFocus()) {
requestFocusWithoutReveal(candidateView);
}
- mImm.showSoftInput(candidateView, 0);
+ if (!handwritingUnsupportedShowSoftInputFix()
+ || (candidateView instanceof TextView tv
+ && tv.getShowSoftInputOnFocus())) {
+ mImm.showSoftInput(candidateView, 0);
+ }
mState.mHandled = true;
mState.mShouldInitHandwriting = false;
motionEvent.setAction((motionEvent.getAction()
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 513587e..b9e9750 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -4,6 +4,13 @@
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
flag {
+ name: "a11y_expansion_state_api"
+ namespace: "accessibility"
+ description: "Enables new APIs for an app to convey if a node is expanded or collapsed."
+ bug: "362782536"
+}
+
+flag {
name: "a11y_overlay_callbacks"
is_exported: true
namespace: "accessibility"
diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java
index 2db4285..7f3858e 100644
--- a/core/java/android/view/autofill/AutofillStateFingerprint.java
+++ b/core/java/android/view/autofill/AutofillStateFingerprint.java
@@ -97,7 +97,6 @@
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 @@
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/DesktopModeFlags.java b/core/java/android/window/flags/DesktopModeFlags.java
index 944a106..47af50da 100644
--- a/core/java/android/window/flags/DesktopModeFlags.java
+++ b/core/java/android/window/flags/DesktopModeFlags.java
@@ -64,7 +64,8 @@
ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
Flags::enableDesktopWindowingTaskbarRunningApps, true),
- ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false);
+ ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
+ ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false);
private static final String TAG = "DesktopModeFlagsUtil";
// Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index fbc30ed..70ac12f 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 @@
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/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 4d0cd27..f3dc896 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -56,6 +56,7 @@
import android.tracing.perfetto.Producer;
import android.tracing.perfetto.TracingContext;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.LongArray;
import android.util.Slog;
@@ -351,6 +352,10 @@
}
private void registerGroupsLocally(@NonNull IProtoLogGroup[] protoLogGroups) {
+ // Verify we don't have id collisions, if we do we want to know as soon as possible and
+ // we might want to manually specify an id for the group with a collision
+ verifyNoCollisionsOrDuplicates(protoLogGroups);
+
final var groupsLoggingToLogcat = new ArrayList<String>();
for (IProtoLogGroup protoLogGroup : protoLogGroups) {
mLogGroups.put(protoLogGroup.name(), protoLogGroup);
@@ -369,6 +374,19 @@
}
}
+ private void verifyNoCollisionsOrDuplicates(@NonNull IProtoLogGroup[] protoLogGroups) {
+ final var groupId = new ArraySet<Integer>();
+
+ for (IProtoLogGroup protoLogGroup : protoLogGroups) {
+ if (groupId.contains(protoLogGroup.getId())) {
+ throw new RuntimeException(
+ "Group with same id (" + protoLogGroup.getId() + ") registered twice. "
+ + "Potential duplicate or hash id collision.");
+ }
+ groupId.add(protoLogGroup.getId());
+ }
+ }
+
/**
* Responds to a shell command.
*/
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index adf03fe..60213b1 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -22,8 +22,8 @@
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogLevel;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
/**
* ProtoLog API - exposes static logging methods. Usage of this API is similar
@@ -73,7 +73,7 @@
if (sProtoLogInstance != null) {
// The ProtoLog instance has already been initialized in this process
final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups();
- final var allGroups = new ArrayList<>(alreadyRegisteredGroups);
+ final var allGroups = new HashSet<>(alreadyRegisteredGroups);
allGroups.addAll(Arrays.stream(groups).toList());
groups = allGroups.toArray(new IProtoLogGroup[0]);
}
diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java
new file mode 100644
index 0000000..6521870
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java
@@ -0,0 +1,93 @@
+/*
+ * 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.internal.protolog;
+
+import android.annotation.NonNull;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+public class ProtoLogGroup implements IProtoLogGroup {
+
+ /** The name should be unique across the codebase. */
+ @NonNull
+ private final String mName;
+ @NonNull
+ private final String mTag;
+ private final boolean mEnabled;
+ private boolean mLogToProto;
+ private boolean mLogToLogcat;
+
+ public ProtoLogGroup(@NonNull String name) {
+ this(name, name);
+ }
+
+ public ProtoLogGroup(@NonNull String name, @NonNull String tag) {
+ this(name, tag, true);
+ }
+
+ public ProtoLogGroup(@NonNull String name, @NonNull String tag, boolean enabled) {
+ mName = name;
+ mTag = tag;
+ mEnabled = enabled;
+ mLogToProto = enabled;
+ mLogToLogcat = enabled;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Deprecated
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ @NonNull
+ public String getTag() {
+ return mTag;
+ }
+
+ @Deprecated
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ mLogToLogcat = logToLogcat;
+ }
+
+ @Override
+ @NonNull
+ public String name() {
+ return mName;
+ }
+
+ @Override
+ public int getId() {
+ return mName.hashCode();
+ }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d35c66e..ed33ede 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 5c0dca2..1a3a30d 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 807df1b..0ccef91 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 9821d43..56e18e6 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -250,7 +250,7 @@
"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 0000000..987f68d
--- /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 0000000..67d407a
--- /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 0000000..7f73a1e
--- /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 cdc8a9e..7cf49ab 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -61,7 +61,7 @@
"androidx.annotation_annotation",
"androidx.test.rules",
"frameworks-base-testutils",
- "servicestests-utils",
+ "servicestests-utils-ravenwood",
],
srcs: [
"src/android/util/IRemoteMemoryIntArray.aidl",
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index b866382..68d8ebb 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -41,6 +41,7 @@
import android.text.TextUtils;
import com.android.internal.annotations.GuardedBy;
+import com.android.text.flags.Flags;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -2000,6 +2001,14 @@
}
/**
+ * A change ID for new font variation settings management.
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = 36)
+ public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L;
+
+ /**
* Sets TrueType or OpenType font variation settings. The settings string is constructed from
* multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
* and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
@@ -2028,12 +2037,16 @@
* </li>
* </ul>
*
+ * <p>Note: If the application that targets API 35 or before, this function mutates the
+ * underlying typeface instance.
+ *
* @param fontVariationSettings font variation settings. You can pass null or empty string as
* no variation settings.
*
- * @return true if the given settings is effective to at least one font file underlying this
- * typeface. This function also returns true for empty settings string. Otherwise
- * returns false
+ * @return If the application that targets API 36 or later and is running on devices API 36 or
+ * later, this function always returns true. Otherwise, this function returns true if
+ * the given settings is effective to at least one font file underlying this typeface.
+ * This function also returns true for empty settings string. Otherwise returns false.
*
* @throws IllegalArgumentException If given string is not a valid font variation settings
* format
@@ -2042,6 +2055,26 @@
* @see FontVariationAxis
*/
public boolean setFontVariationSettings(String fontVariationSettings) {
+ final boolean useFontVariationStore = Flags.typefaceRedesign()
+ && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
+ if (useFontVariationStore) {
+ FontVariationAxis[] axes =
+ FontVariationAxis.fromFontVariationSettings(fontVariationSettings);
+ if (axes == null) {
+ nSetFontVariationOverride(mNativePaint, 0);
+ mFontVariationSettings = null;
+ return true;
+ }
+
+ long builderPtr = nCreateFontVariationBuilder(axes.length);
+ for (int i = 0; i < axes.length; ++i) {
+ nAddFontVariationToBuilder(builderPtr, axes[i].getOpenTypeTagValue(),
+ axes[i].getStyleValue());
+ }
+ nSetFontVariationOverride(mNativePaint, builderPtr);
+ mFontVariationSettings = fontVariationSettings;
+ return true;
+ }
final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
if (settings == mFontVariationSettings
|| (settings != null && settings.equals(mFontVariationSettings))) {
@@ -3829,7 +3862,12 @@
private static native void nSetTextSize(long paintPtr, float textSize);
@CriticalNative
private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr);
-
+ @CriticalNative
+ private static native long nCreateFontVariationBuilder(int size);
+ @CriticalNative
+ private static native void nAddFontVariationToBuilder(long builderPtr, int tag, float value);
+ @CriticalNative
+ private static native void nSetFontVariationOverride(long paintPtr, long builderPtr);
// Following Native methods are kept for old Robolectric JNI signature used by
// SystemUIGoogleRoboRNGTests
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index f857429..a796ecc 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -220,6 +220,7 @@
"//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
"//frameworks/libs/systemui:iconloader_base",
"com_android_wm_shell_flags_lib",
+ "PlatformAnimationLib",
"WindowManager-Shell-proto",
"WindowManager-Shell-lite-proto",
"WindowManager-Shell-shared",
diff --git a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml
new file mode 100644
index 0000000..5260450
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M240,840L240,720L120,720L120,640L320,640L320,840L240,840ZM640,840L640,640L840,640L840,720L720,720L720,840L640,840ZM120,320L120,240L240,240L240,120L320,120L320,320L120,320ZM640,320L640,120L720,120L720,240L840,240L840,320L640,320Z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 766852d..d5b9703 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -97,6 +97,12 @@
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
+ <!-- no translation found for windowing_app_handle_education_tooltip (6398482412956375783) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (6285279585554484957) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_exit_education_tooltip (6685429075790085337) -->
+ <skip />
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
<string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
@@ -129,7 +135,8 @@
<string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open menu"</string>
<string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string>
<string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string>
- <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string>
+ <!-- no translation found for desktop_mode_non_resizable_snap_text (3771776422751387878) -->
+ <skip />
<string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string>
<string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string>
<string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index 75c445c..18048ff 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -97,6 +97,12 @@
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmera?\nToque para ajustar o enquadramento"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string>
+ <!-- no translation found for windowing_app_handle_education_tooltip (6398482412956375783) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (6285279585554484957) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_exit_education_tooltip (6685429075790085337) -->
+ <skip />
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string>
<string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string>
@@ -126,15 +132,12 @@
<string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string>
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
- <!-- no translation found for desktop_mode_app_header_chip_text (6366422614991687237) -->
- <skip />
+ <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir o menu"</string>
<string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string>
<string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string>
- <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Não é possível redimensionar o app"</string>
- <!-- no translation found for desktop_mode_maximize_menu_maximize_button_text (3090199175564175845) -->
+ <!-- no translation found for desktop_mode_non_resizable_snap_text (3771776422751387878) -->
<skip />
- <!-- no translation found for desktop_mode_maximize_menu_snap_left_button_text (8077452201179893424) -->
- <skip />
- <!-- no translation found for desktop_mode_maximize_menu_snap_right_button_text (7117751068945657304) -->
- <skip />
+ <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string>
+ <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string>
+ <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 05ce361..71bcb59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -20,10 +20,11 @@
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.net.Uri
-private val browserIntent = Intent()
+private val GenericBrowserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse("http:"))
@@ -32,9 +33,9 @@
* Returns a boolean indicating whether a given package is a browser app.
*/
fun isBrowserApp(context: Context, packageName: String, userId: Int): Boolean {
- browserIntent.setPackage(packageName)
+ GenericBrowserIntent.setPackage(packageName)
val list = context.packageManager.queryIntentActivitiesAsUser(
- browserIntent, PackageManager.MATCH_ALL, userId
+ GenericBrowserIntent, PackageManager.MATCH_ALL, userId
)
list.forEach {
@@ -44,3 +45,17 @@
}
return false
}
+
+/**
+ * Returns intent if there is a browser application available to handle the uri. Otherwise, returns
+ * null.
+ */
+fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? {
+ val intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER)
+ .setData(uri)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK)
+ // If there is no browser application available to handle intent, return null
+ val component = intent.resolveActivity(packageManager) ?: return null
+ intent.setComponent(component)
+ return intent
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 3e5adf3..5836085 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -1501,10 +1501,6 @@
int rootIdx = -1;
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change c = info.getChanges().get(i);
- if (c.hasFlags(FLAG_IS_WALLPAPER)) {
- st.setAlpha(c.getLeash(), 1.0f);
- continue;
- }
if (TransitionUtil.isOpeningMode(c.getMode())) {
final Point offset = c.getEndRelOffset();
st.setPosition(c.getLeash(), offset.x, offset.y);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2c03059..8ce3884 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.dagger;
+import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import android.annotation.Nullable;
@@ -61,8 +62,10 @@
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
+import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
@@ -87,6 +90,8 @@
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarterInitializer;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.PipTransitionController;
@@ -240,6 +245,7 @@
IWindowManager windowManager,
ShellCommandHandler shellCommandHandler,
ShellTaskOrganizer taskOrganizer,
+ @DynamicOverride DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -266,6 +272,7 @@
shellCommandHandler,
windowManager,
taskOrganizer,
+ desktopRepository,
displayController,
shellController,
displayInsetsController,
@@ -330,9 +337,13 @@
static FreeformComponents provideFreeformComponents(
FreeformTaskListener taskListener,
FreeformTaskTransitionHandler transitionHandler,
- FreeformTaskTransitionObserver transitionObserver) {
+ FreeformTaskTransitionObserver transitionObserver,
+ FreeformTaskTransitionStarterInitializer transitionStarterInitializer) {
return new FreeformComponents(
- taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver));
+ taskListener,
+ Optional.of(transitionHandler),
+ Optional.of(transitionObserver),
+ Optional.of(transitionStarterInitializer));
}
@WMSingleton
@@ -356,27 +367,15 @@
@WMSingleton
@Provides
static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
- ShellInit shellInit,
Transitions transitions,
- Context context,
- WindowDecorViewModel windowDecorViewModel,
DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor,
- @ShellAnimationThread ShellExecutor animExecutor,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
- InteractionJankMonitor interactionJankMonitor,
- @ShellMainThread Handler handler) {
+ @ShellAnimationThread ShellExecutor animExecutor) {
return new FreeformTaskTransitionHandler(
- shellInit,
transitions,
- context,
- windowDecorViewModel,
displayController,
mainExecutor,
- animExecutor,
- desktopModeTaskRepository,
- interactionJankMonitor,
- handler);
+ animExecutor);
}
@WMSingleton
@@ -390,6 +389,23 @@
context, shellInit, transitions, windowDecorViewModel);
}
+ @WMSingleton
+ @Provides
+ static FreeformTaskTransitionStarterInitializer provideFreeformTaskTransitionStarterInitializer(
+ ShellInit shellInit,
+ WindowDecorViewModel windowDecorViewModel,
+ FreeformTaskTransitionHandler freeformTaskTransitionHandler,
+ Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler) {
+ FreeformTaskTransitionStarter transitionStarter;
+ if (desktopMixedTransitionHandler.isPresent()) {
+ transitionStarter = desktopMixedTransitionHandler.get();
+ } else {
+ transitionStarter = freeformTaskTransitionHandler;
+ }
+ return new FreeformTaskTransitionStarterInitializer(shellInit, windowDecorViewModel,
+ transitionStarter);
+ }
+
//
// One handed mode
//
@@ -699,7 +715,17 @@
InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
return new ExitDesktopTaskTransitionHandler(
- transitions, context, interactionJankMonitor, handler);
+ transitions, context, interactionJankMonitor, handler);
+ }
+
+ @WMSingleton
+ @Provides
+ static CloseDesktopTaskTransitionHandler provideCloseDesktopTaskTransitionHandler(
+ Context context,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellAnimationThread ShellExecutor animExecutor
+ ) {
+ return new CloseDesktopTaskTransitionHandler(context, mainExecutor, animExecutor);
}
@WMSingleton
@@ -758,6 +784,32 @@
@WMSingleton
@Provides
+ static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler(
+ Context context,
+ Transitions transitions,
+ @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ FreeformTaskTransitionHandler freeformTaskTransitionHandler,
+ CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
+ InteractionJankMonitor interactionJankMonitor,
+ @ShellMainThread Handler handler
+ ) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ || !ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new DesktopMixedTransitionHandler(
+ context,
+ transitions,
+ desktopModeTaskRepository,
+ freeformTaskTransitionHandler,
+ closeDesktopTaskTransitionHandler,
+ interactionJankMonitor,
+ handler));
+ }
+
+ @WMSingleton
+ @Provides
static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
Context context,
ShellInit shellInit,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt
new file mode 100644
index 0000000..a16c15df
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.TypedValue
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.app.animation.Interpolators
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.transition.Transitions
+import java.util.function.Supplier
+
+/** The [Transitions.TransitionHandler] that handles transitions for closing desktop mode tasks. */
+class CloseDesktopTaskTransitionHandler
+@JvmOverloads
+constructor(
+ private val context: Context,
+ private val mainExecutor: ShellExecutor,
+ private val animExecutor: ShellExecutor,
+ private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
+) : Transitions.TransitionHandler {
+
+ private val runningAnimations = mutableMapOf<IBinder, List<Animator>>()
+
+ /** Returns null, as it only handles transitions started from Shell. */
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? = null
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ if (info.type != WindowManager.TRANSIT_CLOSE) return false
+ val animations = mutableListOf<Animator>()
+ val onAnimFinish: (Animator) -> Unit = { animator ->
+ mainExecutor.execute {
+ // Animation completed
+ animations.remove(animator)
+ if (animations.isEmpty()) {
+ // All animations completed, finish the transition
+ runningAnimations.remove(transition)
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ }
+ }
+ }
+ animations +=
+ info.changes
+ .filter {
+ it.mode == WindowManager.TRANSIT_CLOSE &&
+ it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+ .map { createCloseAnimation(it, finishTransaction, onAnimFinish) }
+ if (animations.isEmpty()) return false
+ runningAnimations[transition] = animations
+ animExecutor.execute { animations.forEach(Animator::start) }
+ return true
+ }
+
+ private fun createCloseAnimation(
+ change: TransitionInfo.Change,
+ finishTransaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ ): Animator {
+ finishTransaction.hide(change.leash)
+ return AnimatorSet().apply {
+ playTogether(createBoundsCloseAnimation(change), createAlphaCloseAnimation(change))
+ addListener(onEnd = onAnimFinish)
+ }
+ }
+
+ private fun createBoundsCloseAnimation(change: TransitionInfo.Change): Animator {
+ val startBounds = change.startAbsBounds
+ val endBounds =
+ Rect(startBounds).apply {
+ // Scale the end bounds of the window down with an anchor in the center
+ inset(
+ (startBounds.width().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt(),
+ (startBounds.height().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt()
+ )
+ val offsetY =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ CLOSE_ANIM_OFFSET_Y,
+ context.resources.displayMetrics
+ )
+ .toInt()
+ offset(/* dx= */ 0, offsetY)
+ }
+ return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
+ duration = CLOSE_ANIM_DURATION_BOUNDS
+ interpolator = Interpolators.STANDARD_ACCELERATE
+ addUpdateListener { animation ->
+ val animBounds = animation.animatedValue as Rect
+ val animScale = 1 - (1 - CLOSE_ANIM_SCALE) * animation.animatedFraction
+ transactionSupplier
+ .get()
+ .setPosition(change.leash, animBounds.left.toFloat(), animBounds.top.toFloat())
+ .setScale(change.leash, animScale, animScale)
+ .apply()
+ }
+ }
+ }
+
+ private fun createAlphaCloseAnimation(change: TransitionInfo.Change): Animator =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = CLOSE_ANIM_DURATION_ALPHA
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transactionSupplier
+ .get()
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .apply()
+ }
+ }
+
+ private companion object {
+ const val CLOSE_ANIM_DURATION_BOUNDS = 200L
+ const val CLOSE_ANIM_DURATION_ALPHA = 100L
+ const val CLOSE_ANIM_SCALE = 0.95f
+ const val CLOSE_ANIM_OFFSET_Y = 36.0f
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
new file mode 100644
index 0000000..ec3f8c5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.os.Handler
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.transition.MixedTransitionHandler
+import com.android.wm.shell.transition.Transitions
+
+/** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */
+class DesktopMixedTransitionHandler(
+ private val context: Context,
+ private val transitions: Transitions,
+ private val desktopTaskRepository: DesktopModeTaskRepository,
+ private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
+ private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
+ private val interactionJankMonitor: InteractionJankMonitor,
+ @ShellMainThread private val handler: Handler,
+) : MixedTransitionHandler, FreeformTaskTransitionStarter {
+
+ /** Delegates starting transition to [FreeformTaskTransitionHandler]. */
+ override fun startWindowingModeTransition(
+ targetWindowingMode: Int,
+ wct: WindowContainerTransaction?,
+ ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct)
+
+ /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */
+ override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder =
+ freeformTaskTransitionHandler.startMinimizedModeTransition(wct)
+
+ /** Starts close transition and handles or delegates desktop task close animation. */
+ override fun startRemoveTransition(wct: WindowContainerTransaction?) {
+ requireNotNull(wct)
+ transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this)
+ }
+
+ /** Returns null, as it only handles transitions started from Shell. */
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? = null
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ val closeChange = findCloseDesktopTaskChange(info)
+ if (closeChange == null) {
+ ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: Should have closing desktop task", TAG)
+ return false
+ }
+ if (isLastDesktopTask(closeChange)) {
+ // Dispatch close desktop task animation to the default transition handlers.
+ return dispatchCloseLastDesktopTaskAnimation(
+ transition,
+ info,
+ closeChange,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ )
+ }
+ // Animate close desktop task transition with [CloseDesktopTaskTransitionHandler].
+ return closeDesktopTaskTransitionHandler.startAnimation(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ )
+ }
+
+ /**
+ * Dispatch close desktop task animation to the default transition handlers. Allows delegating
+ * it to Launcher to animate in sync with show Home transition.
+ */
+ private fun dispatchCloseLastDesktopTaskAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ change: TransitionInfo.Change,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ // Starting the jank trace if closing the last window in desktop mode.
+ interactionJankMonitor.begin(
+ change.leash,
+ context,
+ handler,
+ CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
+ )
+ // Dispatch the last desktop task closing animation.
+ return transitions.dispatchTransition(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ { wct ->
+ // Finish the jank trace when closing the last window in desktop mode.
+ interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE)
+ finishCallback.onTransitionFinished(wct)
+ },
+ /* skip= */ this
+ ) != null
+ }
+
+ private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean =
+ change.taskInfo?.let {
+ desktopTaskRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1
+ } ?: false
+
+ private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? {
+ if (info.type != WindowManager.TRANSIT_CLOSE) return null
+ return info.changes.firstOrNull { change ->
+ change.mode == WindowManager.TRANSIT_CLOSE &&
+ !change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER) &&
+ change.taskInfo?.taskId != INVALID_TASK_ID &&
+ change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+ }
+
+ companion object {
+ private const val TAG = "DesktopMixedTransitionHandler"
+ }
+}
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 0e8c4e7..985224e 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
@@ -60,6 +60,7 @@
* @property minimizedTasks task ids for active freeform tasks that are currently minimized.
* @property closingTasks task ids for tasks that are going to close, but are currently visible.
* @property freeformTasksInZOrder list of current freeform task ids ordered from top to bottom
+ * @property fullImmersiveTaskId the task id of the desktop task that is in full-immersive mode.
* (top is at index 0).
*/
private data class DesktopTaskData(
@@ -69,13 +70,15 @@
// TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
val closingTasks: ArraySet<Int> = ArraySet(),
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
+ var fullImmersiveTaskId: Int? = null,
) {
fun deepCopy(): DesktopTaskData = DesktopTaskData(
activeTasks = ArraySet(activeTasks),
visibleTasks = ArraySet(visibleTasks),
minimizedTasks = ArraySet(minimizedTasks),
closingTasks = ArraySet(closingTasks),
- freeformTasksInZOrder = ArrayList(freeformTasksInZOrder)
+ freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
+ fullImmersiveTaskId = fullImmersiveTaskId
)
}
@@ -300,6 +303,23 @@
}
}
+ /** Set whether the given task is the full-immersive task in this display. */
+ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
+ val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
+ if (immersive) {
+ desktopData.fullImmersiveTaskId = taskId
+ } else {
+ if (desktopData.fullImmersiveTaskId == taskId) {
+ desktopData.fullImmersiveTaskId = null
+ }
+ }
+ }
+
+ /* Whether the task is in full-immersive state. */
+ fun isTaskInFullImmersiveState(taskId: Int): Boolean {
+ return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
+ }
+
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
@@ -330,8 +350,17 @@
/** 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 125805c..b8bb73b 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.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 @@
/** 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 @@
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 @@
}
// 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 @@
// 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 @@
// 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 @@
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 d84349b..7e0741f 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 @@
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/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index a1dfb68..68a250d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -45,6 +45,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -95,7 +96,25 @@
}
}
.flowOn(backgroundDispatcher)
- .collectLatest { captionState -> showEducation(captionState) }
+ .collectLatest { captionState ->
+ showEducation(captionState)
+ // After showing first tooltip, mark education as viewed
+ appHandleEducationDatastoreRepository.updateEducationViewedTimestampMillis(true)
+ }
+ }
+
+ applicationCoroutineScope.launch {
+ if (isFeatureUsed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .filter { captionState ->
+ captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
+ }
+ .take(1)
+ .flowOn(backgroundDispatcher)
+ .collect {
+ // If user expands app handle, mark user has used the feature
+ appHandleEducationDatastoreRepository.updateFeatureUsedTimestampMillis(true)
+ }
}
}
}
@@ -272,6 +291,13 @@
.map { preferences -> preferences.hasEducationViewedTimestampMillis() }
.distinctUntilChanged()
+ /**
+ * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in
+ * datastore proto object.
+ */
+ private suspend fun isFeatureUsed(): Boolean =
+ appHandleEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis()
+
private fun getSize(@DimenRes resourceId: Int): Int {
if (resourceId == Resources.ID_NULL) return 0
return context.resources.getDimensionPixelSize(resourceId)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
index f420c5b..d21b208 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -71,6 +71,37 @@
suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first()
/**
+ * Updates [WindowingEducationProto.educationViewedTimestampMillis_] field in datastore with
+ * current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateEducationViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setEducationViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearEducationViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
+ * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current
+ * timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences.toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build()
+ } else {
+ preferences.toBuilder().clearFeatureUsedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
* Updates [AppHandleEducation.appUsageStats] and
* [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with
* [appUsageStats] and [appUsageStatsLastUpdateTimestamp].
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
index eee5aae..3379ff2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
@@ -35,6 +35,7 @@
public final ShellTaskOrganizer.TaskListener mTaskListener;
public final Optional<Transitions.TransitionHandler> mTransitionHandler;
public final Optional<Transitions.TransitionObserver> mTransitionObserver;
+ public final Optional<FreeformTaskTransitionStarterInitializer> mTransitionStarterInitializer;
/**
* Creates an instance with the given components.
@@ -42,10 +43,12 @@
public FreeformComponents(
ShellTaskOrganizer.TaskListener taskListener,
Optional<Transitions.TransitionHandler> transitionHandler,
- Optional<Transitions.TransitionObserver> transitionObserver) {
+ Optional<Transitions.TransitionObserver> transitionObserver,
+ Optional<FreeformTaskTransitionStarterInitializer> transitionStarterInitializer) {
mTaskListener = taskListener;
mTransitionHandler = transitionHandler;
mTransitionObserver = transitionObserver;
+ mTransitionStarterInitializer = transitionStarterInitializer;
}
/**
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 83cc18b..7f7f105 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.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 @@
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/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 517e209..6aaf001 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -19,16 +19,12 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
-import android.content.Context;
import android.graphics.Rect;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArrayMap;
import android.view.SurfaceControl;
@@ -40,14 +36,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.shared.annotations.ShellMainThread;
-import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -59,48 +50,24 @@
public class FreeformTaskTransitionHandler
implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
private static final int CLOSE_ANIM_DURATION = 400;
- private final Context mContext;
private final Transitions mTransitions;
- private final WindowDecorViewModel mWindowDecorViewModel;
- private final DesktopModeTaskRepository mDesktopModeTaskRepository;
private final DisplayController mDisplayController;
- private final InteractionJankMonitor mInteractionJankMonitor;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
- @ShellMainThread
- private final Handler mHandler;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
public FreeformTaskTransitionHandler(
- ShellInit shellInit,
Transitions transitions,
- Context context,
- WindowDecorViewModel windowDecorViewModel,
DisplayController displayController,
ShellExecutor mainExecutor,
- ShellExecutor animExecutor,
- DesktopModeTaskRepository desktopModeTaskRepository,
- InteractionJankMonitor interactionJankMonitor,
- @ShellMainThread Handler handler) {
+ ShellExecutor animExecutor) {
mTransitions = transitions;
- mContext = context;
- mWindowDecorViewModel = windowDecorViewModel;
- mDesktopModeTaskRepository = desktopModeTaskRepository;
mDisplayController = displayController;
- mInteractionJankMonitor = interactionJankMonitor;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
- mHandler = handler;
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- shellInit.addInitCallback(this::onInit, this);
- }
- }
-
- private void onInit() {
- mWindowDecorViewModel.setFreeformTaskTransitionStarter(this);
}
@Override
@@ -269,20 +236,12 @@
startBounds.top + (animation.getAnimatedFraction() * screenHeight));
t.apply();
});
- if (mDesktopModeTaskRepository.getActiveNonMinimizedTaskCount(
- change.getTaskInfo().displayId) == 1) {
- // Starting the jank trace if closing the last window in desktop mode.
- mInteractionJankMonitor.begin(
- sc, mContext, mHandler, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
- }
animator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animations.remove(animator);
onAnimFinish.run();
- mInteractionJankMonitor.end(
- CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
}
});
animations.add(animator);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
new file mode 100644
index 0000000..98bdf05
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.wm.shell.freeform
+
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.windowdecor.WindowDecorViewModel
+
+/**
+ * Sets up [FreeformTaskTransitionStarter] for [WindowDecorViewModel] when shell finishes
+ * initializing.
+ *
+ * Used to extract the setup logic from the starter implementation.
+ */
+class FreeformTaskTransitionStarterInitializer(
+ shellInit: ShellInit,
+ private val windowDecorViewModel: WindowDecorViewModel,
+ private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter
+) {
+ init {
+ shellInit.addInitCallback(::onShellInit, this)
+ }
+
+ /** Sets up [WindowDecorViewModel] transition starter with [FreeformTaskTransitionStarter] */
+ private fun onShellInit() {
+ windowDecorViewModel.setFreeformTaskTransitionStarter(freeformTaskTransitionStarter)
+ }
+}
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 1b9bf2a..dc0bc78 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 @@
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.merge(finishTransaction);
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);
+ 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 8077aee..f7ed1dd 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.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 @@
"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/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 6e084d6..2747249 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -146,6 +146,11 @@
Slog.w(TAG, "Failed to relayout snapshot starting window");
return null;
}
+ if (!surfaceControl.isValid()) {
+ snapshotSurface.clearWindowSynced();
+ Slog.w(TAG, "Unable to draw snapshot, no valid surface");
+ return null;
+ }
SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot,
info.taskBounds, topWindowInsetsState, true /* releaseAfterDraw */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
index 2f5059f..399e39a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -17,13 +17,13 @@
package com.android.wm.shell.transition;
import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
import static com.android.wm.shell.transition.Transitions.TransitionObserver;
import android.annotation.NonNull;
-import android.app.ActivityManager.RunningTaskInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
@@ -62,10 +62,9 @@
final List<TransitionInfo.Change> changes = info.getChanges();
for (int i = changes.size() - 1; i >= 0; i--) {
final TransitionInfo.Change change = changes.get(i);
- final RunningTaskInfo task = change.getTaskInfo();
- if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
- if (mFocusedDisplayId != task.displayId) {
- mFocusedDisplayId = task.displayId;
+ if (change.hasFlags(FLAG_IS_DISPLAY) && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ if (mFocusedDisplayId != change.getEndDisplayId()) {
+ mFocusedDisplayId = change.getEndDisplayId();
notifyFocusedDisplayChanged();
}
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d280dcd..d5e92e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1036,9 +1036,14 @@
* Gives every handler (in order) a chance to animate until one consumes the transition.
* @return the handler which consumed the transition.
*/
- TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT,
- @NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) {
+ public TransitionHandler dispatchTransition(
+ @NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull TransitionFinishCallback finishCB,
+ @Nullable TransitionHandler skip
+ ) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
if (mHandlers.get(i) == skip) continue;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 05065be..839973f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -194,6 +194,8 @@
ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw,
boolean setTaskCropAndPosition,
+ boolean isStatusBarVisible,
+ boolean isKeyguardVisibleAndOccluded,
InsetsState displayInsetsState) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
@@ -204,6 +206,8 @@
: R.dimen.freeform_decor_shadow_unfocused_thickness;
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
+ relayoutParams.mIsCaptionVisible = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -240,7 +244,8 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
- setTaskCropAndPosition, mDisplayController.getInsetsState(taskInfo.displayId));
+ setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
+ mDisplayController.getInsetsState(taskInfo.displayId));
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index c34a0bc..3330f96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -22,9 +22,6 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.content.Intent.ACTION_MAIN;
-import static android.content.Intent.CATEGORY_APP_BROWSER;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -58,7 +55,6 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
-import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -107,6 +103,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
@@ -158,6 +155,7 @@
private final ActivityTaskManager mActivityTaskManager;
private final ShellCommandHandler mShellCommandHandler;
private final ShellTaskOrganizer mTaskOrganizer;
+ private final DesktopModeTaskRepository mDesktopRepository;
private final ShellController mShellController;
private final Context mContext;
private final @ShellMainThread Handler mMainHandler;
@@ -229,6 +227,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
+ DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -254,6 +253,7 @@
shellCommandHandler,
windowManager,
taskOrganizer,
+ desktopRepository,
displayController,
shellController,
displayInsetsController,
@@ -288,6 +288,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
+ DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -316,6 +317,7 @@
mBgExecutor = bgExecutor;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
+ mDesktopRepository = desktopRepository;
mShellController = shellController;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
@@ -560,20 +562,17 @@
decoration.closeMaximizeMenu();
}
- private void onOpenInBrowser(int taskId, @NonNull Uri uri) {
+ private void onOpenInBrowser(int taskId, @NonNull Intent intent) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
if (decoration == null) {
return;
}
- openInBrowser(uri, decoration.getUser());
+ openInBrowser(intent, decoration.getUser());
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
}
- private void openInBrowser(Uri uri, @NonNull UserHandle userHandle) {
- final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER)
- .setData(uri)
- .addFlags(FLAG_ACTIVITY_NEW_TASK);
+ private void openInBrowser(@NonNull Intent intent, @NonNull UserHandle userHandle) {
mContext.startActivityAsUser(intent, userHandle);
}
@@ -1421,6 +1420,7 @@
mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */),
mDisplayController,
mSplitScreenController,
+ mDesktopRepository,
mTaskOrganizer,
taskInfo,
taskSurface,
@@ -1472,8 +1472,8 @@
onToSplitScreen(taskInfo.taskId);
return Unit.INSTANCE;
});
- windowDecoration.setOpenInBrowserClickListener((uri) -> {
- onOpenInBrowser(taskInfo.taskId, uri);
+ windowDecoration.setOpenInBrowserClickListener((intent) -> {
+ onOpenInBrowser(taskInfo.taskId, intent);
});
windowDecoration.setOnNewWindowClickListener(() -> {
onNewWindow(taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 99457d8..5daa3ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -44,12 +44,14 @@
import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -62,10 +64,12 @@
import android.util.Size;
import android.util.Slog;
import android.view.Choreographer;
+import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.window.TaskSnapshot;
@@ -89,6 +93,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -166,7 +171,7 @@
private CapturedLink mCapturedLink;
private Uri mGenericLink;
private Uri mWebUri;
- private Consumer<Uri> mOpenInBrowserClickListener;
+ private Consumer<Intent> mOpenInBrowserClickListener;
private ExclusionRegionListener mExclusionRegionListener;
@@ -188,12 +193,14 @@
private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
private final MultiInstanceHelper mMultiInstanceHelper;
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
+ private final DesktopModeTaskRepository mDesktopRepository;
DesktopModeWindowDecoration(
Context context,
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -207,8 +214,8 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
- this (context, userContext, displayController, splitScreenController, taskOrganizer,
- taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
+ this (context, userContext, displayController, splitScreenController, desktopRepository,
+ taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
assistContentRequester,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
@@ -225,6 +232,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -264,6 +272,7 @@
mMultiInstanceHelper = multiInstanceHelper;
mWindowManagerWrapper = windowManagerWrapper;
mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
+ mDesktopRepository = desktopRepository;
}
/**
@@ -335,7 +344,7 @@
mDragPositioningCallback = dragPositioningCallback;
}
- void setOpenInBrowserClickListener(Consumer<Uri> listener) {
+ void setOpenInBrowserClickListener(Consumer<Intent> listener) {
mOpenInBrowserClickListener = listener;
}
@@ -439,8 +448,11 @@
mHandleMenu.relayout(startT, mResult.mCaptionX);
}
+ final boolean inFullImmersive = mDesktopRepository
+ .isTaskInFullImmersiveState(taskInfo.taskId);
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
+ inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId));
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -480,11 +492,17 @@
if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
notifyCaptionStateChanged();
}
- mWindowDecorViewHolder.bindData(mTaskInfo,
- position,
- mResult.mCaptionWidth,
- mResult.mCaptionHeight,
- isCaptionVisible());
+
+ if (isAppHandle(mWindowDecorViewHolder)) {
+ mWindowDecorViewHolder.bindData(new AppHandleViewHolder.HandleData(
+ mTaskInfo, position, mResult.mCaptionWidth, mResult.mCaptionHeight,
+ isCaptionVisible()
+ ));
+ } else {
+ mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData(
+ mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive
+ ));
+ }
Trace.endSection();
if (!mTaskInfo.isFocused) {
@@ -518,21 +536,28 @@
}
@Nullable
- private Uri getBrowserLink() {
+ private Intent getBrowserLink() {
// Do not show browser link in browser applications
final ComponentName baseActivity = mTaskInfo.baseActivity;
if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
baseActivity.getPackageName(), mUserContext.getUserId())) {
return null;
}
+
+ final Uri browserLink;
// If the captured link is available and has not expired, return the captured link.
// Otherwise, return the generic link which is set to null if a generic link is unavailable.
if (mCapturedLink != null && !mCapturedLink.mExpired) {
- return mCapturedLink.mUri;
+ browserLink = mCapturedLink.mUri;
} else if (mWebUri != null) {
- return mWebUri;
+ browserLink = mWebUri;
+ } else {
+ browserLink = mGenericLink;
}
- return mGenericLink;
+
+ if (browserLink == null) return null;
+ return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager());
+
}
UserHandle getUser() {
@@ -738,7 +763,11 @@
Context context,
ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw,
- boolean shouldSetTaskPositionAndCrop) {
+ boolean shouldSetTaskPositionAndCrop,
+ boolean isStatusBarVisible,
+ boolean isKeyguardVisibleAndOccluded,
+ boolean inFullImmersiveMode,
+ @NonNull InsetsState displayInsetsState) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
captionLayoutId == R.layout.desktop_mode_app_header;
@@ -749,6 +778,28 @@
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
+ final boolean showCaption;
+ if (Flags.enableFullyImmersiveInDesktop()) {
+ if (inFullImmersiveMode) {
+ showCaption = isStatusBarVisible && !isKeyguardVisibleAndOccluded;
+ } else {
+ showCaption = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+ }
+ } else {
+ // Caption should always be visible in freeform mode. When not in freeform,
+ // align with the status bar except when showing over keyguard (where it should not
+ // shown).
+ // TODO(b/356405803): Investigate how it's possible for the status bar visibility to
+ // be false while a freeform window is open if the status bar is always
+ // forcibly-shown. It may be that the InsetsState (from which |mIsStatusBarVisible|
+ // is set) still contains an invisible insets source in immersive cases even if the
+ // status bar is shown?
+ showCaption = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+ }
+ relayoutParams.mIsCaptionVisible = showCaption;
+
if (isAppHeader) {
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -767,6 +818,13 @@
// including non-immersive apps that just don't handle caption insets properly.
relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
}
+ if (Flags.enableFullyImmersiveInDesktop() && inFullImmersiveMode) {
+ final Insets systemBarInsets = displayInsetsState.calculateInsets(
+ taskInfo.getConfiguration().windowConfiguration.getBounds(),
+ WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(),
+ false /* ignoreVisibility */);
+ relayoutParams.mCaptionTopPadding = systemBarInsets.top;
+ }
// Report occluding elements as bounding rects to the insets system so that apps can
// draw in the empty space in the center:
// First, the "app chip" section of the caption bar (+ some extra margins).
@@ -1049,7 +1107,7 @@
}
/**
- * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.
+ * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.fmdra
*/
private int determineMaxY(int requiredEmptySpace, Rect stableBounds) {
return stableBounds.bottom - requiredEmptySpace;
@@ -1172,8 +1230,8 @@
/* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener,
/* onNewWindowClickListener= */ mOnNewWindowClickListener,
/* onManageWindowsClickListener= */ mOnManageWindowsClickListener,
- /* openInBrowserClickListener= */ (uri) -> {
- mOpenInBrowserClickListener.accept(uri);
+ /* openInBrowserClickListener= */ (intent) -> {
+ mOpenInBrowserClickListener.accept(intent);
onCapturedLinkExpired();
return Unit.INSTANCE;
},
@@ -1532,6 +1590,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -1550,6 +1609,7 @@
userContext,
displayController,
splitScreenController,
+ desktopRepository,
taskOrganizer,
taskInfo,
taskSurface,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 9a5b4f5..98fef47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -20,13 +20,13 @@
import android.annotation.SuppressLint
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
+import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
-import android.net.Uri
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_OUTSIDE
@@ -70,7 +70,7 @@
private val shouldShowWindowingPill: Boolean,
private val shouldShowNewWindowButton: Boolean,
private val shouldShowManageWindowsButton: Boolean,
- private val openInBrowserLink: Uri?,
+ private val openInBrowserIntent: Intent?,
private val captionWidth: Int,
private val captionHeight: Int,
captionX: Int
@@ -107,7 +107,7 @@
private val globalMenuPosition: Point = Point()
private val shouldShowBrowserPill: Boolean
- get() = openInBrowserLink != null
+ get() = openInBrowserIntent != null
init {
updateHandleMenuPillPositions(captionX)
@@ -119,7 +119,7 @@
onToSplitScreenClickListener: () -> Unit,
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
- openInBrowserClickListener: (Uri) -> Unit,
+ openInBrowserClickListener: (Intent) -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
) {
@@ -152,7 +152,7 @@
onToSplitScreenClickListener: () -> Unit,
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
- openInBrowserClickListener: (Uri) -> Unit,
+ openInBrowserClickListener: (Intent) -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit
) {
@@ -172,7 +172,7 @@
this.onNewWindowClickListener = onNewWindowClickListener
this.onManageWindowsClickListener = onManageWindowsClickListener
this.onOpenInBrowserClickListener = {
- openInBrowserClickListener.invoke(openInBrowserLink!!)
+ openInBrowserClickListener.invoke(openInBrowserIntent!!)
}
this.onCloseMenuClickListener = onCloseMenuClickListener
this.onOutsideTouchListener = onOutsideTouchListener
@@ -661,7 +661,7 @@
shouldShowWindowingPill: Boolean,
shouldShowNewWindowButton: Boolean,
shouldShowManageWindowsButton: Boolean,
- openInBrowserLink: Uri?,
+ openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
captionX: Int
@@ -680,7 +680,7 @@
shouldShowWindowingPill: Boolean,
shouldShowNewWindowButton: Boolean,
shouldShowManageWindowsButton: Boolean,
- openInBrowserLink: Uri?,
+ openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
captionX: Int
@@ -695,7 +695,7 @@
shouldShowWindowingPill,
shouldShowNewWindowButton,
shouldShowManageWindowsButton,
- openInBrowserLink,
+ openInBrowserIntent,
captionWidth,
captionHeight,
captionX
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index c1a55b4..000beba1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -144,8 +144,8 @@
TaskDragResizer mTaskDragResizer;
boolean mIsCaptionVisible;
- private boolean mIsStatusBarVisible;
- private boolean mIsKeyguardVisibleAndOccluded;
+ boolean mIsStatusBarVisible;
+ boolean mIsKeyguardVisibleAndOccluded;
/** The most recent set of insets applied to this window decoration. */
private WindowDecorationInsets mWindowDecorationInsets;
@@ -241,7 +241,7 @@
}
rootView = null; // Clear it just in case we use it accidentally
- updateCaptionVisibility(outResult.mRootView);
+ updateCaptionVisibility(outResult.mRootView, params);
final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
outResult.mWidth = taskBounds.width();
@@ -527,17 +527,10 @@
}
/**
- * Checks if task has entered/exited immersive mode and requires a change in caption visibility.
+ * Update caption visibility state and views.
*/
- private void updateCaptionVisibility(View rootView) {
- // Caption should always be visible in freeform mode. When not in freeform, align with the
- // status bar except when showing over keyguard (where it should not shown).
- // TODO(b/356405803): Investigate how it's possible for the status bar visibility to be
- // false while a freeform window is open if the status bar is always forcibly-shown. It
- // may be that the InsetsState (from which |mIsStatusBarVisible| is set) still contains
- // an invisible insets source in immersive cases even if the status bar is shown?
- mIsCaptionVisible = mTaskInfo.isFreeform()
- || (mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded);
+ private void updateCaptionVisibility(View rootView, @NonNull RelayoutParams params) {
+ mIsCaptionVisible = params.mIsCaptionVisible;
setCaptionVisibility(rootView, mIsCaptionVisible);
}
@@ -737,6 +730,7 @@
int mCornerRadius;
int mCaptionTopPadding;
+ boolean mIsCaptionVisible;
Configuration mWindowDecorConfig;
@@ -755,6 +749,7 @@
mCornerRadius = 0;
mCaptionTopPadding = 0;
+ mIsCaptionVisible = false;
mApplyStartTransactionOnDraw = false;
mSetTaskPositionAndCrop = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 8c102eb..b5700ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -42,6 +42,7 @@
import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
/**
* A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
@@ -53,11 +54,20 @@
onCaptionButtonClickListener: OnClickListener,
private val windowManagerWrapper: WindowManagerWrapper,
private val handler: Handler
-) : WindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) {
companion object {
private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
}
+
+ data class HandleData(
+ val taskInfo: RunningTaskInfo,
+ val position: Point,
+ val width: Int,
+ val height: Int,
+ val isCaptionVisible: Boolean
+ ) : Data()
+
private lateinit var taskInfo: RunningTaskInfo
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
@@ -89,7 +99,11 @@
}
}
- override fun bindData(
+ override fun bindData(data: HandleData) {
+ bindData(data.taskInfo, data.position, data.width, data.height, data.isCaptionVisible)
+ }
+
+ private fun bindData(
taskInfo: RunningTaskInfo,
position: Point,
width: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 306103c..52bf400 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -16,12 +16,12 @@
package com.android.wm.shell.windowdecor.viewholder
import android.annotation.ColorInt
+import android.annotation.DrawableRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
-import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
@@ -60,7 +60,6 @@
import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
-import com.android.wm.shell.windowdecor.extension.requestingImmersive
/**
* A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
@@ -76,7 +75,13 @@
appName: CharSequence,
appIconBitmap: Bitmap,
onMaximizeHoverAnimationFinishedListener: () -> Unit
-) : WindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) {
+
+ data class HeaderData(
+ val taskInfo: RunningTaskInfo,
+ val isRequestingImmersive: Boolean,
+ val inFullImmersiveState: Boolean,
+ ) : Data()
private val decorThemeUtil = DecorThemeUtil(context)
private val lightColors = dynamicLightColorScheme(context)
@@ -153,15 +158,17 @@
onMaximizeHoverAnimationFinishedListener
}
- override fun bindData(
+ override fun bindData(data: HeaderData) {
+ bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState)
+ }
+
+ private fun bindData(
taskInfo: RunningTaskInfo,
- position: Point,
- width: Int,
- height: Int,
- isCaptionVisible: Boolean
+ isRequestingImmersive: Boolean,
+ inFullImmersiveState: Boolean,
) {
if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) {
- bindDataWithThemedHeaders(taskInfo)
+ bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState)
} else {
bindDataLegacy(taskInfo)
}
@@ -200,7 +207,11 @@
minimizeWindowButton.isGone = !enableMinimizeButton()
}
- private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) {
+ private fun bindDataWithThemedHeaders(
+ taskInfo: RunningTaskInfo,
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ) {
val header = fillHeaderInfo(taskInfo)
val headerStyle = getHeaderStyle(header)
@@ -254,13 +265,7 @@
drawableInsets = maximizeDrawableInsets
)
)
- setIcon(
- if (taskInfo.requestingImmersive && Flags.enableFullyImmersiveInDesktop()) {
- R.drawable.decor_desktop_mode_immersive_button_dark
- } else {
- R.drawable.decor_desktop_mode_maximize_button_dark
- }
- )
+ setIcon(getMaximizeButtonIcon(requestingImmersive, inFullImmersiveState))
}
// Close button.
closeWindowButton.apply {
@@ -331,6 +336,32 @@
}
}
+ @DrawableRes
+ private fun getMaximizeButtonIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Int = when {
+ shouldShowEnterFullImmersiveIcon(requestingImmersive, inFullImmersiveState) -> {
+ R.drawable.decor_desktop_mode_immersive_button_dark
+ }
+ shouldShowExitFullImmersiveIcon(requestingImmersive, inFullImmersiveState) -> {
+ R.drawable.decor_desktop_mode_immersive_exit_button_dark
+ }
+ else -> R.drawable.decor_desktop_mode_maximize_button_dark
+ }
+
+ private fun shouldShowEnterFullImmersiveIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Boolean = Flags.enableFullyImmersiveInDesktop()
+ && requestingImmersive && !inFullImmersiveState
+
+ private fun shouldShowExitFullImmersiveIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Boolean = Flags.enableFullyImmersiveInDesktop()
+ && requestingImmersive && inFullImmersiveState
+
private fun getHeaderStyle(header: Header): HeaderStyle {
return HeaderStyle(
background = getHeaderBackground(header),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 5ea55b3..1fe743d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -17,31 +17,28 @@
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
-import android.graphics.Point
import android.view.View
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
/**
* Encapsulates the root [View] of a window decoration and its children to facilitate looking up
* children (via findViewById) and updating to the latest data from [RunningTaskInfo].
*/
-abstract class WindowDecorationViewHolder(rootView: View) {
+abstract class WindowDecorationViewHolder<T : Data>(rootView: View) {
val context: Context = rootView.context
/**
* A signal to the view holder that new data is available and that the views should be updated to
* reflect it.
*/
- abstract fun bindData(
- taskInfo: RunningTaskInfo,
- position: Point,
- width: Int,
- height: Int,
- isCaptionVisible: Boolean
- )
+ abstract fun bindData(data: T)
/** Callback when the handle menu is opened. */
abstract fun onHandleMenuOpened()
/** Callback when the handle menu is closed. */
abstract fun onHandleMenuClosed()
+
+ /** Data clas that contains the information needed to update the view holder. */
+ abstract class Data
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
new file mode 100644
index 0000000..9b4cc17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.ShellExecutor
+import java.util.function.Supplier
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.mock
+
+/**
+ * Test class for [CloseDesktopTaskTransitionHandler]
+ *
+ * Usage: atest WMShellUnitTests:CloseDesktopTaskTransitionHandlerTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() {
+
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock lateinit var closingTaskLeash: SurfaceControl
+
+ private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
+
+ private lateinit var handler: CloseDesktopTaskTransitionHandler
+
+ @Before
+ fun setUp() {
+ handler =
+ CloseDesktopTaskTransitionHandler(
+ context,
+ testExecutor,
+ testExecutor,
+ transactionSupplier
+ )
+ }
+
+ @Test
+ fun handleRequest_returnsNull() {
+ assertNull(handler.handleRequest(mock(), mock()))
+ }
+
+ @Test
+ fun startAnimation_openTransition_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ createTransitionInfo(
+ type = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate open transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionFullscreenTask_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate fullscreen task close transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionOpeningFreeformTask_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ createTransitionInfo(
+ changeMode = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate opening freeform task close transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertTrue("Should animate closing freeform task close transition", animates)
+ }
+
+ private fun createTransitionInfo(
+ type: Int = WindowManager.TRANSIT_CLOSE,
+ changeMode: Int = WindowManager.TRANSIT_CLOSE,
+ task: RunningTaskInfo
+ ): TransitionInfo =
+ TransitionInfo(type, 0 /* flags */).apply {
+ addChange(
+ TransitionInfo.Change(mock(), closingTaskLeash).apply {
+ mode = changeMode
+ parent = null
+ taskInfo = task
+ }
+ )
+ }
+
+ private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(windowingMode)
+ .build()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
new file mode 100644
index 0000000..2b60200
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.os.Handler
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for [DesktopMixedTransitionHandler]
+ *
+ * Usage: atest WMShellUnitTests:DesktopMixedTransitionHandlerTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopMixedTransitionHandlerTest : ShellTestCase() {
+
+ @Mock lateinit var transitions: Transitions
+ @Mock lateinit var desktopTaskRepository: DesktopModeTaskRepository
+ @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
+ @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+ @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
+ @Mock lateinit var mockHandler: Handler
+ @Mock lateinit var closingTaskLeash: SurfaceControl
+
+ private lateinit var mixedHandler: DesktopMixedTransitionHandler
+
+ @Before
+ fun setUp() {
+ mixedHandler =
+ DesktopMixedTransitionHandler(
+ context,
+ transitions,
+ desktopTaskRepository,
+ freeformTaskTransitionHandler,
+ closeDesktopTaskTransitionHandler,
+ interactionJankMonitor,
+ mockHandler
+ )
+ }
+
+ @Test
+ fun startWindowingModeTransition_callsFreeformTaskTransitionHandler() {
+ val windowingMode = WINDOWING_MODE_FULLSCREEN
+ val wct = WindowContainerTransaction()
+
+ mixedHandler.startWindowingModeTransition(windowingMode, wct)
+
+ verify(freeformTaskTransitionHandler).startWindowingModeTransition(windowingMode, wct)
+ }
+
+ @Test
+ fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() {
+ val wct = WindowContainerTransaction()
+ whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any()))
+ .thenReturn(mock())
+
+ mixedHandler.startMinimizedModeTransition(wct)
+
+ verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct)
+ }
+
+ @Test
+ fun startRemoveTransition_startsCloseTransition() {
+ val wct = WindowContainerTransaction()
+
+ mixedHandler.startRemoveTransition(wct)
+
+ verify(transitions).startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)
+ }
+
+ @Test
+ fun handleRequest_returnsNull() {
+ assertNull(mixedHandler.handleRequest(mock(), mock()))
+ }
+
+ @Test
+ fun startAnimation_withoutClosingDesktopTask_returnsFalse() {
+ val transition = mock<IBinder>()
+ val transitionInfo =
+ createTransitionInfo(
+ changeMode = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ )
+ whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()))
+ .thenReturn(true)
+
+ val started = mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not start animation without closing desktop task", started)
+ }
+
+ @Test
+ fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
+ val transition = mock<IBinder>()
+ val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
+ whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2)
+ whenever(
+ closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+ )
+ .thenReturn(true)
+
+ val started = mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertTrue("Should delegate animation to close transition handler", started)
+ verify(closeDesktopTaskTransitionHandler)
+ .startAnimation(eq(transition), eq(transitionInfo), any(), any(), any())
+ }
+
+ @Test
+ fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
+ val transition = mock<IBinder>()
+ val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
+ whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1)
+ whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
+ .thenReturn(mock())
+
+ mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ verify(transitions)
+ .dispatchTransition(
+ eq(transition),
+ eq(transitionInfo),
+ any(),
+ any(),
+ any(),
+ eq(mixedHandler)
+ )
+ verify(interactionJankMonitor)
+ .begin(
+ closingTaskLeash,
+ context,
+ mockHandler,
+ CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+ )
+ }
+
+ private fun createTransitionInfo(
+ type: Int = WindowManager.TRANSIT_CLOSE,
+ changeMode: Int = WindowManager.TRANSIT_CLOSE,
+ task: RunningTaskInfo
+ ): TransitionInfo =
+ TransitionInfo(type, 0 /* flags */).apply {
+ addChange(
+ TransitionInfo.Change(mock(), closingTaskLeash).apply {
+ mode = changeMode
+ parent = null
+ taskInfo = task
+ }
+ )
+ }
+
+ private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(windowingMode)
+ .build()
+}
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 bc40d89..97ceecc 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 @@
}
@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)
@@ -882,6 +894,51 @@
assertThat(tasks).containsExactly(1, 3).inOrder()
}
+ @Test
+ fun setTaskInFullImmersiveState_savedAsInImmersiveState() {
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ }
+
+ @Test
+ fun removeTaskInFullImmersiveState_removedAsInImmersiveState() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = false)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+ }
+
+ @Test
+ fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = false)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ }
+
+ @Test
+ fun setTaskInFullImmersiveState_sameDisplay_overridesExistingFullImmersiveTask() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
+ }
+
+ @Test
+ fun setTaskInFullImmersiveState_differentDisplay_bothAreImmersive() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
+ }
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 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 8870846..2ddb1ac 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 @@
}
@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 @@
}
@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 @@
@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 @@
}
@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 @@
}
@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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 045e077..bc5ae97 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 @@
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 @@
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 @@
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/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index aad31a6..5596ad7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -54,6 +54,7 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
@@ -190,6 +191,35 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHandleExpanded_shouldMarkFeatureViewed() =
+ testScope.runTest {
+ setShouldShowAppHandleEducation(false)
+
+ // Simulate app handle visible and expanded.
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+ // Wait for some time before verifying
+ waitForBufferDelay()
+
+ verify(mockDataStoreRepository, times(1)).updateFeatureUsedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_showFirstTooltip_shouldMarkEducationViewed() =
+ testScope.runTest {
+ // App handle is visible. Should show education tooltip.
+ setShouldShowAppHandleEducation(true)
+
+ // Simulate app handle visible.
+ testCaptionStateFlow.value = createAppHandleState()
+ // Wait for first tooltip to showup.
+ waitForBufferDelay()
+
+ verify(mockDataStoreRepository, times(1)).updateEducationViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded. Should show second education
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 1c1c650..c286544 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -109,6 +109,24 @@
assertThat(result).isEqualTo(windowingEducationProto)
}
+ @Test
+ fun updateEducationViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateEducationViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasEducationViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
+ fun updateFeatureUsedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateFeatureUsedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasFeatureUsedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
companion object {
private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
}
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 763d015..3b2c7e6 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 @@
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 @@
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/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 5f75423..ce482cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -30,7 +30,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
@@ -56,12 +55,11 @@
import android.os.Looper;
import android.os.UserHandle;
import android.testing.TestableContext;
-import android.view.IWindowSession;
import android.view.InsetsState;
import android.view.Surface;
import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
import android.view.WindowMetrics;
+import android.window.SnapshotDrawerUtils;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskSnapshot;
@@ -220,18 +218,10 @@
createWindowInfo(taskId, android.R.style.Theme, mBinder);
TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100),
new Rect(0, 0, 0, 50), true /* hasImeSurface */);
- final IWindowSession session = WindowManagerGlobal.getWindowSession();
- spyOn(session);
- doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay(
- any() /* window */, any() /* attrs */,
- anyInt() /* viewVisibility */, anyInt() /* displayId */,
- anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
- any() /* outInsetsState */, any() /* outActiveControls */,
- any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
- TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
- mBinder,
- snapshot, mTestExecutor, () -> {
- });
+ final TaskSnapshotWindow mockSnapshotWindow = new TaskSnapshotWindow(
+ snapshot, SnapshotDrawerUtils.getOrCreateTaskDescription(windowInfo.taskInfo),
+ snapshot.getOrientation(),
+ () -> {}, mTestExecutor);
spyOn(mockSnapshotWindow);
try (AutoCloseable mockTaskSnapshotSession = new AutoCloseable() {
MockitoSession mockSession = mockitoSession()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
index d37b4cf..d63158c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -18,7 +18,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -97,50 +97,38 @@
}
@Test
- public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+ public void testOnlyDisplayChangeAffectsDisplayFocus() throws RemoteException {
final IBinder binder = mock(IBinder.class);
final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
- // Open a task on the default display, which doesn't change display focus because the
- // default display already has it.
+ // Open a task on the secondary display, but it doesn't change display focus because it only
+ // has a task change.
TransitionInfo info = mock(TransitionInfo.class);
final List<TransitionInfo.Change> changes = new ArrayList<>();
- setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ setupTaskChange(changes, 123 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
true /* focused */);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
clearInvocations(mListener);
- // Open a new task on the secondary display and verify display focus changes to the display.
+ // Moving the secondary display to front must change display focus to it.
changes.clear();
- setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
- true /* focused */);
+ setupDisplayToTopChange(changes, SECONDARY_DISPLAY_ID);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
- clearInvocations(mListener);
+ verify(mListener, times(1))
+ .onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
- // Open the first task to front and verify display focus goes back to the default display.
+ // Moving the secondary display to front must change display focus back to it.
changes.clear();
- setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
- true /* focused */);
+ setupDisplayToTopChange(changes, DEFAULT_DISPLAY);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
- clearInvocations(mListener);
-
- // Open another task on the default display and verify no display focus switch as it's
- // already on the default display.
- changes.clear();
- setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
- true /* focused */);
- when(info.getChanges()).thenReturn(changes);
- mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
}
- private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+ private void setupTaskChange(List<TransitionInfo.Change> changes, int taskId,
@TransitionMode int mode, int displayId, boolean focused) {
TransitionInfo.Change change = mock(TransitionInfo.Change.class);
RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
@@ -152,4 +140,12 @@
when(change.getMode()).thenReturn(mode);
changes.add(change);
}
+
+ private void setupDisplayToTopChange(List<TransitionInfo.Change> changes, int displayId) {
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(true);
+ when(change.hasFlags(FLAG_IS_DISPLAY)).thenReturn(true);
+ when(change.getEndDisplayId()).thenReturn(displayId);
+ changes.add(change);
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index d141c2d..0f16b9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -47,6 +47,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
@@ -66,6 +68,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
@@ -81,6 +85,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 9aa6a52..5ae4ca8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -27,6 +27,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.ACTION_MAIN
import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.hardware.display.DisplayManager
@@ -86,6 +87,7 @@
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
@@ -159,6 +161,7 @@
@Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
@Mock private lateinit var mockDisplayController: DisplayController
@Mock private lateinit var mockSplitScreenController: SplitScreenController
+ @Mock private lateinit var mockDesktopRepository: DesktopModeTaskRepository
@Mock private lateinit var mockDisplayLayout: DisplayLayout
@Mock private lateinit var displayInsetsController: DisplayInsetsController
@Mock private lateinit var mockSyncQueue: SyncTransactionQueue
@@ -230,6 +233,7 @@
mockShellCommandHandler,
mockWindowManager,
mockTaskOrganizer,
+ mockDesktopRepository,
mockDisplayController,
mockShellController,
displayInsetsController,
@@ -930,13 +934,13 @@
@Test
fun testDecor_onClickToOpenBrowser_closeMenus() {
val openInBrowserListenerCaptor = forClass(Consumer::class.java)
- as ArgumentCaptor<Consumer<Uri>>
+ as ArgumentCaptor<Consumer<Intent>>
val decor = createOpenTaskDecoration(
windowingMode = WINDOWING_MODE_FULLSCREEN,
onOpenInBrowserClickListener = openInBrowserListenerCaptor
)
- openInBrowserListenerCaptor.value.accept(Uri.EMPTY)
+ openInBrowserListenerCaptor.value.accept(Intent())
verify(decor).closeHandleMenu()
verify(decor).closeMaximizeMenu()
@@ -946,20 +950,19 @@
fun testDecor_onClickToOpenBrowser_opensBrowser() {
doNothing().whenever(spyContext).startActivity(any())
val uri = Uri.parse("https://www.google.com")
+ val intent = Intent(ACTION_MAIN, uri)
val openInBrowserListenerCaptor = forClass(Consumer::class.java)
- as ArgumentCaptor<Consumer<Uri>>
+ as ArgumentCaptor<Consumer<Intent>>
createOpenTaskDecoration(
windowingMode = WINDOWING_MODE_FULLSCREEN,
onOpenInBrowserClickListener = openInBrowserListenerCaptor
)
- openInBrowserListenerCaptor.value.accept(uri)
+ openInBrowserListenerCaptor.value.accept(intent)
verify(spyContext).startActivityAsUser(argThat { intent ->
- intent.data == uri
- && ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0)
- && intent.categories.contains(Intent.CATEGORY_LAUNCHER)
- && intent.action == Intent.ACTION_MAIN
+ uri.equals(intent.data)
+ && intent.action == ACTION_MAIN
}, eq(mockUserHandle))
}
@@ -1233,8 +1236,8 @@
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
onToSplitScreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
- onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Uri>> =
- forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Uri>>,
+ onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Intent>> =
+ forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Intent>>,
onCaptionButtonClickListener: ArgumentCaptor<View.OnClickListener> =
forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener>,
onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
@@ -1296,8 +1299,8 @@
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any(), any(), any(), any(), any(), any())
+ any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index f007115..3e7f3bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -23,6 +23,7 @@
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
@@ -37,6 +38,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
@@ -53,9 +55,11 @@
import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
@@ -102,6 +106,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -124,6 +129,7 @@
import org.mockito.Mock;
import org.mockito.quality.Strictness;
+import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -157,6 +163,8 @@
@Mock
private ShellTaskOrganizer mMockShellTaskOrganizer;
@Mock
+ private DesktopModeTaskRepository mMockDesktopRepository;
+ @Mock
private Choreographer mMockChoreographer;
@Mock
private SyncTransactionQueue mMockSyncQueue;
@@ -187,7 +195,7 @@
@Mock
private Handler mMockHandler;
@Mock
- private Consumer<Uri> mMockOpenInBrowserClickListener;
+ private Consumer<Intent> mMockOpenInBrowserClickListener;
@Mock
private AppToWebGenericLinksParser mMockGenericLinksParser;
@Mock
@@ -242,9 +250,11 @@
when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any()))
.thenReturn(false);
when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel");
- final ActivityInfo activityInfo = new ActivityInfo();
- activityInfo.applicationInfo = new ApplicationInfo();
+ final ActivityInfo activityInfo = createActivityInfo();
when(mMockPackageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo);
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = activityInfo;
+ when(mMockPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo);
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
@@ -284,7 +294,11 @@
DesktopModeWindowDecoration.updateRelayoutParams(
relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
}
@@ -300,7 +314,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
}
@@ -321,7 +339,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
}
@@ -343,7 +365,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
}
@@ -361,7 +387,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
}
@@ -378,7 +408,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -394,7 +428,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -410,7 +448,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
}
@@ -427,7 +469,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -444,7 +490,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -462,7 +512,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
}
@@ -481,7 +535,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
}
@@ -498,7 +556,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -517,7 +579,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -525,6 +591,171 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_addsPaddingInFullImmersive() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000));
+ final InsetsState insetsState = createInsetsState(List.of(
+ createInsetsSource(
+ 0 /* id */, statusBars(), true /* visible */, new Rect(0, 0, 1000, 50)),
+ createInsetsSource(
+ 1 /* id */, captionBar(), true /* visible */, new Rect(0, 0, 1000, 100))));
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ insetsState);
+
+ // Takes status bar inset as padding, ignores caption bar inset.
+ assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_statusBarInvisible_captionVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ // Header is always shown because it's assumed the status bar is always visible.
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_statusBarVisibleAndNotOverKeyguard_captionVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_statusBarInvisible_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_overKeyguard_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ true,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_fullyImmersive_captionVisFollowsStatusBar() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_fullyImmersive_overKeyguard_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ true,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
public void relayout_fullscreenTask_appliesTransactionImmediately() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -771,7 +1002,6 @@
// Verify handle menu's browser link is set to captured link since menu was opened before
// captured link expired
- createHandleMenu(decor);
verifyHandleMenuCreated(TEST_URI1);
}
@@ -782,7 +1012,7 @@
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, null /* web uri */,
null /* generic link */);
- final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+ final ArgumentCaptor<Function1<Intent, Unit>> openInBrowserCaptor =
ArgumentCaptor.forClass(Function1.class);
// Simulate menu opening and clicking open in browser button
@@ -797,7 +1027,7 @@
any(),
any()
);
- openInBrowserCaptor.getValue().invoke(TEST_URI1);
+ openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
// Verify handle menu's browser link not set to captured link since link not valid after
// open in browser clicked
@@ -812,7 +1042,7 @@
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, null /* web uri */,
null /* generic link */);
- final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+ final ArgumentCaptor<Function1<Intent, Unit>> openInBrowserCaptor =
ArgumentCaptor.forClass(Function1.class);
createHandleMenu(decor);
verify(mMockHandleMenu).show(
@@ -826,9 +1056,10 @@
any()
);
- openInBrowserCaptor.getValue().invoke(TEST_URI1);
+ openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
- verify(mMockOpenInBrowserClickListener).accept(TEST_URI1);
+ verify(mMockOpenInBrowserClickListener).accept(
+ argThat(intent -> intent.getData() == TEST_URI1));
}
@Test
@@ -1021,8 +1252,9 @@
private void verifyHandleMenuCreated(@Nullable Uri uri) {
verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
- any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(),
- anyInt(), anyInt());
+ any(), anyBoolean(), anyBoolean(), anyBoolean(),
+ argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
+ anyInt(), anyInt(), anyInt());
}
private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
@@ -1086,9 +1318,9 @@
boolean relayout) {
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mContext, mMockDisplayController, mMockSplitScreenController,
- mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
- mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory,
- mMockRootTaskDisplayAreaOrganizer,
+ mMockDesktopRepository, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
+ mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue,
+ mMockAppHeaderViewHolderFactory, mMockRootTaskDisplayAreaOrganizer,
mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new,
mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
@@ -1128,19 +1360,39 @@
decor.onAssistContentReceived(mAssistContent);
}
+ private static ActivityInfo createActivityInfo() {
+ final ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.packageName = "DesktopModeWindowDecorationTestPackage";
+ final ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.applicationInfo = applicationInfo;
+ activityInfo.name = "DesktopModeWindowDecorationTest";
+ return activityInfo;
+ }
+
private static boolean hasNoInputChannelFeature(RelayoutParams params) {
return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL)
!= 0;
}
- private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
- final InsetsState state = new InsetsState();
- final InsetsSource source = new InsetsSource(/* id= */0, type);
+ private InsetsSource createInsetsSource(int id, @WindowInsets.Type.InsetsType int type,
+ boolean visible, @NonNull Rect frame) {
+ final InsetsSource source = new InsetsSource(id, type);
source.setVisible(visible);
- state.addSource(source);
+ source.setFrame(frame);
+ return source;
+ }
+
+ private InsetsState createInsetsState(@NonNull List<InsetsSource> sources) {
+ final InsetsState state = new InsetsState();
+ sources.forEach(state::addSource);
return state;
}
+ private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
+ final InsetsSource source = createInsetsSource(0 /* id */, type, visible, new Rect());
+ return createInsetsState(List.of(source));
+ }
+
private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
View.OnGenericMotionListener, DragDetector.MotionEventHandler {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 2e117ac..94cabc4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -47,6 +47,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -246,6 +247,7 @@
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
@@ -319,6 +321,7 @@
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
@@ -571,11 +574,7 @@
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- assertTrue(mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, statusBars())
- .isVisible());
- assertTrue(mInsetsState.sourceSize() == 1);
- assertTrue(mInsetsState.sourceAt(0).getType() == statusBars());
-
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
@@ -623,33 +622,6 @@
}
@Test
- public void testRelayout_captionHidden_insetsRemoved() {
- final Display defaultDisplay = mock(Display.class);
- doReturn(defaultDisplay).when(mMockDisplayController)
- .getDisplay(Display.DEFAULT_DISPLAY);
-
- final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
- .setDisplayId(Display.DEFAULT_DISPLAY)
- .setVisible(true)
- .setBounds(new Rect(0, 0, 1000, 1000))
- .build();
- taskInfo.isFocused = true;
- // Caption visible at first.
- when(mMockDisplayController.getInsetsState(taskInfo.displayId))
- .thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
-
- // Hide caption so insets are removed.
- windowDecor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
-
- verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(captionBar()));
- verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(mandatorySystemGestures()));
- }
-
- @Test
public void testRelayout_captionHidden_neverWasVisible_insetsNotRemoved() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
@@ -661,9 +633,8 @@
.setBounds(new Rect(0, 0, 1000, 1000))
.build();
// Hidden from the beginning, so no insets were ever added.
- when(mMockDisplayController.getInsetsState(taskInfo.displayId))
- .thenReturn(createInsetsState(statusBars(), false /* visible */));
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = false;
windowDecor.relayout(taskInfo);
// Never added.
@@ -692,7 +663,7 @@
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
// Relayout will add insets.
- mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
@@ -740,6 +711,7 @@
final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
.setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
// Relayout twice with different bounds.
final ActivityManager.RunningTaskInfo firstTaskInfo =
@@ -767,6 +739,7 @@
final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
.setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
// Relayout twice with the same bounds.
final ActivityManager.RunningTaskInfo firstTaskInfo =
@@ -797,6 +770,7 @@
final ActivityManager.RunningTaskInfo taskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mInsetSourceFlags =
FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
windowDecor.relayout(taskInfo);
@@ -901,76 +875,61 @@
}
@Test
- public void onStatusBarVisibilityChange_fullscreen_shownToHidden_hidesCaption() {
+ public void onStatusBarVisibilityChange() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
+ assertTrue(decor.mIsStatusBarVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
- assertFalse(decor.mIsCaptionVisible);
+ verify(decor, times(2)).relayout(task);
}
@Test
- public void onStatusBarVisibilityChange_fullscreen_hiddenToShown_showsCaption() {
+ public void onStatusBarVisibilityChange_noChange_doesNotRelayout() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
when(mMockDisplayController.getInsetsState(task.displayId))
- .thenReturn(createInsetsState(statusBars(), false /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ .thenReturn(createInsetsState(statusBars(), true /* visible */));
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertFalse(decor.mIsCaptionVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
- assertTrue(decor.mIsCaptionVisible);
+ verify(decor, times(1)).relayout(task);
}
@Test
- public void onStatusBarVisibilityChange_freeform_shownToHidden_keepsCaption() {
- final ActivityManager.RunningTaskInfo task = createTaskInfo();
- task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
- when(mMockDisplayController.getInsetsState(task.displayId))
- .thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
- decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
-
- decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
-
- assertTrue(decor.mIsCaptionVisible);
- }
-
- @Test
- public void onKeyguardStateChange_hiddenToShownAndOccluding_hidesCaption() {
+ public void onKeyguardStateChange() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
+ assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
- assertFalse(decor.mIsCaptionVisible);
+ assertTrue(decor.mIsKeyguardVisibleAndOccluded);
+ verify(decor, times(2)).relayout(task);
}
@Test
- public void onKeyguardStateChange_showingAndOccludingToHidden_showsCaption() {
+ public void onKeyguardStateChange_noChange_doesNotRelayout() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
- decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
- assertFalse(decor.mIsCaptionVisible);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
+ decor.relayout(task);
+ assertFalse(decor.mIsKeyguardVisibleAndOccluded);
- decor.onKeyguardStateChanged(false /* visible */, false /* occluding */);
+ decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
- assertTrue(decor.mIsCaptionVisible);
+ verify(decor, times(1)).relayout(task);
}
private ActivityManager.RunningTaskInfo createTaskInfo() {
diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h
index 708f96e..7eb849f 100644
--- a/libs/hwui/hwui/Paint.h
+++ b/libs/hwui/hwui/Paint.h
@@ -159,6 +159,14 @@
return SkSamplingOptions(this->filterMode());
}
+ void setVariationOverride(minikin::VariationSettings&& varSettings) {
+ mFontVariationOverride = std::move(varSettings);
+ }
+
+ const minikin::VariationSettings& getFontVariationOverride() const {
+ return mFontVariationOverride;
+ }
+
// The Java flags (Paint.java) no longer fit into the native apis directly.
// These methods handle converting to and from them and the native representations
// in android::Paint.
@@ -179,6 +187,7 @@
float mLetterSpacing = 0;
float mWordSpacing = 0;
std::vector<minikin::FontFeature> mFontFeatureSettings;
+ minikin::VariationSettings mFontVariationOverride;
uint32_t mMinikinLocaleListId;
std::optional<minikin::FamilyVariant> mFamilyVariant;
uint32_t mHyphenEdit = 0;
diff --git a/libs/hwui/hwui/PaintImpl.cpp b/libs/hwui/hwui/PaintImpl.cpp
index c32ea01..6dfcedc 100644
--- a/libs/hwui/hwui/PaintImpl.cpp
+++ b/libs/hwui/hwui/PaintImpl.cpp
@@ -39,6 +39,7 @@
, mLetterSpacing(paint.mLetterSpacing)
, mWordSpacing(paint.mWordSpacing)
, mFontFeatureSettings(paint.mFontFeatureSettings)
+ , mFontVariationOverride(paint.mFontVariationOverride)
, mMinikinLocaleListId(paint.mMinikinLocaleListId)
, mFamilyVariant(paint.mFamilyVariant)
, mHyphenEdit(paint.mHyphenEdit)
@@ -59,6 +60,7 @@
mLetterSpacing = other.mLetterSpacing;
mWordSpacing = other.mWordSpacing;
mFontFeatureSettings = other.mFontFeatureSettings;
+ mFontVariationOverride = other.mFontVariationOverride;
mMinikinLocaleListId = other.mMinikinLocaleListId;
mFamilyVariant = other.mFamilyVariant;
mHyphenEdit = other.mHyphenEdit;
@@ -76,6 +78,7 @@
return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && a.mFont == b.mFont &&
a.mLooper == b.mLooper && a.mLetterSpacing == b.mLetterSpacing &&
a.mWordSpacing == b.mWordSpacing && a.mFontFeatureSettings == b.mFontFeatureSettings &&
+ a.mFontVariationOverride == b.mFontVariationOverride &&
a.mMinikinLocaleListId == b.mMinikinLocaleListId &&
a.mFamilyVariant == b.mFamilyVariant && a.mHyphenEdit == b.mHyphenEdit &&
a.mTypeface == b.mTypeface && a.mAlign == b.mAlign &&
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 286f06a..da23792 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -1127,6 +1127,36 @@
return leftMinikinPaint == rightMinikinPaint;
}
+ struct VariationBuilder {
+ std::vector<minikin::FontVariation> varSettings;
+ };
+
+ static jlong createFontVariationBuilder(CRITICAL_JNI_PARAMS_COMMA jint size) {
+ VariationBuilder* builder = new VariationBuilder();
+ builder->varSettings.reserve(size);
+ return reinterpret_cast<jlong>(builder);
+ }
+
+ static void addFontVariationToBuilder(CRITICAL_JNI_PARAMS_COMMA jlong builderPtr, jint tag,
+ jfloat value) {
+ VariationBuilder* builder = reinterpret_cast<VariationBuilder*>(builderPtr);
+ builder->varSettings.emplace_back(static_cast<minikin::AxisTag>(tag), value);
+ }
+
+ static void setFontVariationOverride(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle,
+ jlong builderPtr) {
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+ if (builderPtr == 0) {
+ paint->setVariationOverride(minikin::VariationSettings());
+ return;
+ }
+
+ VariationBuilder* builder = reinterpret_cast<VariationBuilder*>(builderPtr);
+ paint->setVariationOverride(
+ minikin::VariationSettings(builder->varSettings, false /* sorted */));
+ delete builder;
+ }
+
}; // namespace PaintGlue
static const JNINativeMethod methods[] = {
@@ -1235,6 +1265,9 @@
{"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
{"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
{"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
+ {"nCreateFontVariationBuilder", "(I)J", (void*)PaintGlue::createFontVariationBuilder},
+ {"nAddFontVariationToBuilder", "(JIF)V", (void*)PaintGlue::addFontVariationToBuilder},
+ {"nSetFontVariationOverride", "(JJ)V", (void*)PaintGlue::setFontVariationOverride},
};
int register_android_graphics_Paint(JNIEnv* env) {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index f0ab6ec..0f24654 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -1089,7 +1089,24 @@
defaultRingtoneUri = ContentProvider.getUriWithoutUserId(defaultRingtoneUri);
if (defaultRingtoneUri == null) {
return -1;
- } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
+ }
+
+ if (Flags.enableRingtoneHapticsCustomization()
+ && Utils.hasVibration(defaultRingtoneUri)) {
+ // skip to check TYPE_ALARM because the customized haptic hasn't enabled in alarm
+ if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_RINGTONE_URI.toString())) {
+ return TYPE_RINGTONE;
+ } else if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_NOTIFICATION_URI.toString())) {
+ return TYPE_NOTIFICATION;
+ } else if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_ALARM_ALERT_URI.toString())) {
+ return TYPE_ALARM;
+ }
+ }
+
+ if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
return TYPE_RINGTONE;
} else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
return TYPE_NOTIFICATION;
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 9899e4e..83a4dd5 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -22,7 +22,6 @@
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 @@
}
};
- 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 @@
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 @@
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 3955ff0..5f5058d 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 @@
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 @@
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/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 94231b0..2db90fe 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -90,7 +90,11 @@
method public void onEnable(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onEnableFinished(int);
method public void onEnableStarted();
+ method public void onGetOemAppSearchIntent(@NonNull java.util.List<java.lang.String>, @NonNull java.util.function.Consumer<android.content.Intent>);
method public void onHceEventReceived(int);
+ method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String);
+ method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String);
+ method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onReaderOptionChanged(boolean);
method public void onRfDiscoveryStarted(boolean);
diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
index e49ef7e..48c7ee6 100644
--- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
+++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
@@ -15,9 +15,14 @@
*/
package android.nfc;
+import android.content.ComponentName;
+import android.nfc.cardemulation.ApduServiceInfo;
+import android.nfc.NdefMessage;
import android.nfc.Tag;
import android.os.ResultReceiver;
+import java.util.List;
+
/**
* @hide
*/
@@ -41,4 +46,8 @@
void onCardEmulationActivated(boolean isActivated);
void onRfFieldActivated(boolean isActivated);
void onRfDiscoveryStarted(boolean isDiscoveryStarted);
+ void onGetOemAppSearchIntent(in List<String> firstPackage, in ResultReceiver intentConsumer);
+ void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent);
+ void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category);
+ void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category);
}
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 6d5c069..8484dca 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -23,8 +23,12 @@
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.nfc.cardemulation.ApduServiceInfo;
import android.os.Binder;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.Log;
@@ -306,6 +310,60 @@
* @param isDiscoveryStarted true, if RF discovery started, else RF state is Idle.
*/
void onRfDiscoveryStarted(boolean isDiscoveryStarted);
+
+ /**
+ * Gets the intent to find the OEM package in the OEM App market. If the consumer returns
+ * {@code null} or a timeout occurs, the intent from the first available package will be
+ * used instead.
+ *
+ * @param packages the OEM packages name stored in the tag
+ * @param intentConsumer The {@link Consumer} to be completed.
+ * The {@link Consumer#accept(Object)} should be called with
+ * the Intent required.
+ *
+ */
+ void onGetOemAppSearchIntent(@NonNull List<String> packages,
+ @NonNull Consumer<Intent> intentConsumer);
+
+ /**
+ * Checks if the NDEF message contains any specific OEM package executable content
+ *
+ * @param tag the {@link android.nfc.Tag Tag}
+ * @param message NDEF Message to read from tag
+ * @param hasOemExecutableContent The {@link Consumer} to be completed. If there is
+ * OEM package executable content, the
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}, otherwise call with
+ * {@link Boolean#FALSE}.
+ */
+ void onNdefMessage(@NonNull Tag tag, @NonNull NdefMessage message,
+ @NonNull Consumer<Boolean> hasOemExecutableContent);
+
+ /**
+ * Callback to indicate the app chooser activity should be launched for handling CE
+ * transaction. This is invoked for example when there are more than 1 app installed that
+ * can handle the HCE transaction. OEMs can launch the Activity based
+ * on their requirement.
+ *
+ * @param selectedAid the selected AID from APDU
+ * @param services {@link ApduServiceInfo} of the service triggering the activity
+ * @param failedComponent the component failed to be resolved
+ * @param category the category of the service
+ */
+ void onLaunchHceAppChooserActivity(@NonNull String selectedAid,
+ @NonNull List<ApduServiceInfo> services,
+ @NonNull ComponentName failedComponent,
+ @NonNull String category);
+
+ /**
+ * Callback to indicate tap again dialog should be launched for handling HCE transaction.
+ * This is invoked for example when a CE service needs the device to unlocked before
+ * handling the transaction. OEMs can launch the Activity based on their requirement.
+ *
+ * @param service {@link ApduServiceInfo} of the service triggering the dialog
+ * @param category the category of the service
+ */
+ void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);
}
@@ -562,25 +620,25 @@
public void onApplyRouting(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onApplyRouting, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onApplyRouting, ex));
}
@Override
public void onNdefRead(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onNdefRead, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onNdefRead, ex));
}
@Override
public void onEnable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isAllowed), cb::onEnable, ex));
+ new ReceiverWrapper<>(isAllowed), cb::onEnable, ex));
}
@Override
public void onDisable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isAllowed), cb::onDisable, ex));
+ new ReceiverWrapper<>(isAllowed), cb::onDisable, ex));
}
@Override
public void onBootStarted() throws RemoteException {
@@ -616,7 +674,7 @@
public void onTagDispatch(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onTagDispatch, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onTagDispatch, ex));
}
@Override
public void onRoutingChanged() throws RemoteException {
@@ -635,6 +693,59 @@
handleVoidCallback(enabled, cb::onReaderOptionChanged, ex));
}
+ @Override
+ public void onGetOemAppSearchIntent(List<String> packages, ResultReceiver intentConsumer)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) ->
+ handleVoid2ArgCallback(packages, new ReceiverWrapper<>(intentConsumer),
+ cb::onGetOemAppSearchIntent, ex));
+ }
+
+ @Override
+ public void onNdefMessage(Tag tag, NdefMessage message,
+ ResultReceiver hasOemExecutableContent) throws RemoteException {
+ mCallbackMap.forEach((cb, ex) -> {
+ synchronized (mLock) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ ex.execute(() -> cb.onNdefMessage(
+ tag, message, new ReceiverWrapper<>(hasOemExecutableContent)));
+ } catch (RuntimeException exception) {
+ throw exception;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLaunchHceAppChooserActivity(String selectedAid,
+ List<ApduServiceInfo> services,
+ ComponentName failedComponent, String category)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) -> {
+ synchronized (mLock) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ ex.execute(() -> cb.onLaunchHceAppChooserActivity(
+ selectedAid, services, failedComponent, category));
+ } catch (RuntimeException exception) {
+ throw exception;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLaunchHceTapAgainActivity(ApduServiceInfo service, String category)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) ->
+ handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
+ }
+
private <T> void handleVoidCallback(
T input, Consumer<T> callbackMethod, Executor executor) {
synchronized (mLock) {
@@ -718,7 +829,7 @@
}
}
- private class ReceiverWrapper implements Consumer<Boolean> {
+ private class ReceiverWrapper<T> implements Consumer<T> {
private final ResultReceiver mResultReceiver;
ReceiverWrapper(ResultReceiver resultReceiver) {
@@ -726,12 +837,19 @@
}
@Override
- public void accept(Boolean result) {
- mResultReceiver.send(result ? 1 : 0, null);
+ public void accept(T result) {
+ if (result instanceof Boolean) {
+ mResultReceiver.send((Boolean) result ? 1 : 0, null);
+ } else if (result instanceof Intent) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("intent", (Intent) result);
+ mResultReceiver.send(0, bundle);
+ }
+
}
@Override
- public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
+ public Consumer<T> andThen(Consumer<? super T> after) {
return Consumer.super.andThen(after);
}
}
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 91882fd..50db501 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -1,6 +1,17 @@
{
"presubmit": [
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -12,6 +23,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
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 a1761e5..0000000
--- 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 9c1e503f..970eeb2 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 0041eb2..979ff96 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 @@
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/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
index 34de5c4..e6726dc 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
@@ -68,6 +68,7 @@
private View mExtraWidgetContainer;
private ImageView mExtraWidget;
+ @Nullable private String mExtraWidgetContentDescription;
private boolean mIsCheckBox = false; // whether to display this button as a checkbox
private View.OnClickListener mExtraWidgetOnClickListener;
@@ -173,6 +174,12 @@
setExtraWidgetOnClickListener(mExtraWidgetOnClickListener);
+ if (mExtraWidget != null) {
+ mExtraWidget.setContentDescription(mExtraWidgetContentDescription != null
+ ? mExtraWidgetContentDescription
+ : getContext().getString(R.string.settings_label));
+ }
+
if (Flags.allowSetTitleMaxLines()) {
TextView title = (TextView) holder.findViewById(android.R.id.title);
title.setMaxLines(mTitleMaxLines);
@@ -210,6 +217,17 @@
}
/**
+ * Sets the content description of the extra widget. If {@code null}, a default content
+ * description will be used ("Settings").
+ */
+ public void setExtraWidgetContentDescription(@Nullable String contentDescription) {
+ if (!TextUtils.equals(mExtraWidgetContentDescription, contentDescription)) {
+ mExtraWidgetContentDescription = contentDescription;
+ notifyChanged();
+ }
+ }
+
+ /**
* Returns whether this preference is a checkbox.
*/
public boolean isCheckBox() {
diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml
index d4e01de..2fd84f3 100644
--- a/packages/SettingsLib/res/values-en-rGB/strings.xml
+++ b/packages/SettingsLib/res/values-en-rGB/strings.xml
@@ -585,7 +585,6 @@
<string name="media_transfer_this_device_name_desktop" msgid="7912386128141470452">"This computer (internal)"</string>
<!-- no translation found for media_transfer_this_device_name_tv (5285685336836896535) -->
<skip />
- <string name="media_transfer_internal_mic" msgid="797333824290228595">"Microphone (internal)"</string>
<string name="media_transfer_dock_speaker_device_name" msgid="2856219597113881950">"Dock speaker"</string>
<string name="media_transfer_external_device_name" msgid="2588672258721846418">"External device"</string>
<string name="media_transfer_default_device_name" msgid="4315604017399871828">"Connected device"</string>
@@ -688,9 +687,11 @@
<string name="cached_apps_freezer_reboot_dialog_text" msgid="695330563489230096">"Your device must be rebooted for this change to apply. Reboot now or cancel."</string>
<string name="media_transfer_wired_headphone_name" msgid="8698668536022665254">"Wired headphones"</string>
<string name="media_transfer_headphone_name" msgid="1131962659136578852">"Headphone"</string>
- <string name="media_transfer_usb_speaker_name" msgid="4736537022543593896">"USB speaker"</string>
+ <!-- no translation found for media_transfer_usb_audio_name (1789292056757821355) -->
+ <skip />
<string name="media_transfer_wired_device_mic_name" msgid="7417067197803840965">"Mic jack"</string>
- <string name="media_transfer_usb_device_mic_name" msgid="9189914846215516322">"USB mic"</string>
+ <!-- no translation found for media_transfer_usb_device_mic_name (7171789543226269822) -->
+ <skip />
<string name="wifi_hotspot_switch_on_text" msgid="9212273118217786155">"On"</string>
<string name="wifi_hotspot_switch_off_text" msgid="7245567251496959764">"Off"</string>
<string name="carrier_network_change_mode" msgid="4257621815706644026">"Operator network changing"</string>
diff --git a/packages/SettingsLib/res/values-pt-rBR/strings.xml b/packages/SettingsLib/res/values-pt-rBR/strings.xml
index 7f5bb0f..e286643 100644
--- a/packages/SettingsLib/res/values-pt-rBR/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rBR/strings.xml
@@ -585,7 +585,6 @@
<string name="media_transfer_this_device_name_desktop" msgid="7912386128141470452">"Este computador (interno)"</string>
<!-- no translation found for media_transfer_this_device_name_tv (5285685336836896535) -->
<skip />
- <string name="media_transfer_internal_mic" msgid="797333824290228595">"Microfone (interno)"</string>
<string name="media_transfer_dock_speaker_device_name" msgid="2856219597113881950">"Alto-falante da base"</string>
<string name="media_transfer_external_device_name" msgid="2588672258721846418">"Dispositivo externo"</string>
<string name="media_transfer_default_device_name" msgid="4315604017399871828">"Dispositivo conectado"</string>
@@ -688,9 +687,11 @@
<string name="cached_apps_freezer_reboot_dialog_text" msgid="695330563489230096">"É necessário reinicializar o dispositivo para que a mudança seja aplicada. Faça isso agora ou cancele."</string>
<string name="media_transfer_wired_headphone_name" msgid="8698668536022665254">"Fones de ouvido com fio"</string>
<string name="media_transfer_headphone_name" msgid="1131962659136578852">"Fone de ouvido"</string>
- <string name="media_transfer_usb_speaker_name" msgid="4736537022543593896">"Alto-falante USB"</string>
+ <!-- no translation found for media_transfer_usb_audio_name (1789292056757821355) -->
+ <skip />
<string name="media_transfer_wired_device_mic_name" msgid="7417067197803840965">"Entrada para microfone"</string>
- <string name="media_transfer_usb_device_mic_name" msgid="9189914846215516322">"Microfone USB"</string>
+ <!-- no translation found for media_transfer_usb_device_mic_name (7171789543226269822) -->
+ <skip />
<string name="wifi_hotspot_switch_on_text" msgid="9212273118217786155">"Ativado"</string>
<string name="wifi_hotspot_switch_off_text" msgid="7245567251496959764">"Desativado"</string>
<string name="carrier_network_change_mode" msgid="4257621815706644026">"Alteração de rede da operadora"</string>
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
index 243ce85..2b8b3b7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
@@ -19,8 +19,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import android.app.Application;
import android.platform.test.annotations.DisableFlags;
@@ -68,7 +66,7 @@
mPreference = new SelectorWithWidgetPreference(mContext);
View view = LayoutInflater.from(mContext)
- .inflate(R.layout.preference_selector_with_widget, null /* root */);
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
@@ -104,28 +102,28 @@
@Test
public void onBindViewHolder_withSummary_containerShouldBeVisible() {
mPreference.setSummary("some summary");
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.VISIBLE, summaryContainer.getVisibility());
}
@Test
public void onBindViewHolder_emptySummary_containerShouldBeGone() {
mPreference.setSummary("");
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.GONE, summaryContainer.getVisibility());
}
@@ -184,25 +182,49 @@
}
@Test
- public void nullSummary_containerShouldBeGone() {
- mPreference.setSummary(null);
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ public void onBindViewHolder_appliesWidgetContentDescription() {
+ mPreference = new SelectorWithWidgetPreference(mContext);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), /* root= */ null);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
+
+ mPreference.setExtraWidgetContentDescription("this is clearer");
mPreference.onBindViewHolder(preferenceViewHolder);
+
+ View widget = preferenceViewHolder.findViewById(R.id.selector_extra_widget);
+ assertThat(widget.getContentDescription().toString()).isEqualTo("this is clearer");
+
+ mPreference.setExtraWidgetContentDescription(null);
+ mPreference.onBindViewHolder(preferenceViewHolder);
+
+ assertThat(widget.getContentDescription().toString()).isEqualTo("Settings");
+ }
+
+ @Test
+ public void nullSummary_containerShouldBeGone() {
+ mPreference.setSummary(null);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
+ PreferenceViewHolder preferenceViewHolder =
+ PreferenceViewHolder.createInstanceForTests(view);
+
+ mPreference.onBindViewHolder(preferenceViewHolder);
+
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.GONE, summaryContainer.getVisibility());
}
@Test
public void setAppendixVisibility_setGone_shouldBeGone() {
mPreference.setAppendixVisibility(View.GONE);
-
View view = LayoutInflater.from(mContext)
- .inflate(R.layout.preference_selector_with_widget, null /* root */);
- PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view);
+ .inflate(mPreference.getLayoutResource(), null /* root */);
+ PreferenceViewHolder holder =
+ PreferenceViewHolder.createInstanceForTests(view);
+
mPreference.onBindViewHolder(holder);
+
assertThat(holder.findViewById(R.id.appendix).getVisibility()).isEqualTo(View.GONE);
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index f6e1057..0773bd7 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 @@
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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index b57629f..d98b2da5 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -630,6 +630,13 @@
}
flag {
+ name: "screenshot_multidisplay_focus_change"
+ namespace: "systemui"
+ description: "Only capture a single display when screenshotting"
+ bug: "362720389"
+}
+
+flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
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 dc9e267..56de096 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 @@
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 5e6f88e..5fa5db8 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 @@
overSlop: Float,
pointersDown: Int,
): DragController {
- if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
- return NoOpDragController
- }
-
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
@@ -189,7 +185,7 @@
return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
}
- private fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
+ internal fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
if (startedPosition == null) return null
return layoutImpl.swipeSourceDetector.source(
layoutSize = layoutImpl.lastSize,
@@ -199,7 +195,7 @@
)
}
- private fun resolveSwipe(
+ internal fun resolveSwipe(
pointersDown: Int,
fromSource: SwipeSource.Resolved?,
isUpOrLeft: Boolean,
@@ -559,6 +555,14 @@
val connection: PriorityNestedScrollConnection = nestedScrollConnection()
+ private fun PointersInfo.resolveSwipe(isUpOrLeft: Boolean): Swipe.Resolved {
+ return draggableHandler.resolveSwipe(
+ pointersDown = pointersDown,
+ fromSource = draggableHandler.resolveSwipeSource(startedPosition),
+ isUpOrLeft = isUpOrLeft,
+ )
+ }
+
private fun nestedScrollConnection(): PriorityNestedScrollConnection {
// If we performed a long gesture before entering priority mode, we would have to avoid
// moving on to the next scene.
@@ -575,36 +579,19 @@
val transitionState = layoutState.transitionState
val scene = transitionState.currentScene
val fromScene = layoutImpl.scene(scene)
- val nextScene =
+ val resolvedSwipe =
when {
- amount < 0f -> {
- val actionUpOrLeft =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Left
- Orientation.Vertical -> SwipeDirection.Resolved.Up
- },
- pointerCount = pointersInfo().pointersDown,
- fromSource = null,
- )
- fromScene.userActions[actionUpOrLeft]
- }
- amount > 0f -> {
- val actionDownOrRight =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Right
- Orientation.Vertical -> SwipeDirection.Resolved.Down
- },
- pointerCount = pointersInfo().pointersDown,
- fromSource = null,
- )
- fromScene.userActions[actionDownOrRight]
- }
+ amount < 0f -> pointersInfo().resolveSwipe(isUpOrLeft = true)
+ amount > 0f -> pointersInfo().resolveSwipe(isUpOrLeft = false)
else -> null
}
+ val nextScene =
+ resolvedSwipe?.let {
+ fromScene.userActions[it]
+ ?: if (it.fromSource != null) {
+ fromScene.userActions[it.copy(fromSource = null)]
+ } else null
+ }
if (nextScene != null) return true
if (transitionState !is TransitionState.Idle) return false
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 4ae9718..5ddc284 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.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 @@
@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 @@
this.then(
MultiPointerDraggableElement(
orientation,
- enabled,
startDragImmediately,
onDragStarted,
onFirstPointerDown,
@@ -100,7 +96,6 @@
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 @@
override fun create(): MultiPointerDraggableNode =
MultiPointerDraggableNode(
orientation = orientation,
- enabled = enabled,
startDragImmediately = startDragImmediately,
onDragStarted = onDragStarted,
onFirstPointerDown = onFirstPointerDown,
@@ -121,7 +115,6 @@
override fun update(node: MultiPointerDraggableNode) {
node.orientation = orientation
- node.enabled = enabled
node.startDragImmediately = startDragImmediately
node.onDragStarted = onDragStarted
node.onFirstPointerDown = onFirstPointerDown
@@ -131,7 +124,6 @@
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 @@
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 @@
}
}
- 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()
@@ -213,8 +179,9 @@
internal fun pointersInfo(): PointersInfo {
return PointersInfo(
+ // This may be null, i.e. when the user uses TalkBack
startedPosition = startedPosition,
- // Note: We could have 0 pointers during fling or for other reasons.
+ // We could have 0 pointers during fling or for other reasons.
pointersDown = pointersDown.coerceAtLeast(1),
)
}
@@ -253,9 +220,7 @@
velocityTracker.resetTracking()
velocityTracker.addPointerInputChange(firstPointerDown)
startedPosition = firstPointerDown.position
- if (enabled()) {
- onFirstPointerDown()
- }
+ onFirstPointerDown()
}
// Changes with at least one pointer
@@ -294,10 +259,6 @@
}
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 6e89814..cec8883 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 @@
* @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 @@
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 @@
modifier,
swipeSourceDetector,
swipeDetector,
- gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -621,7 +616,6 @@
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 @@
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 9e7be37..65c4043 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.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 @@
* 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 f758102..54ee783 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 @@
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 98d4aaa..d201be9 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 @@
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 @@
delegate(
MultiPointerDraggableNode(
orientation = draggableHandler.orientation,
- enabled = ::enabled,
startDragImmediately = ::startDragImmediately,
onDragStarted = draggableHandler::onDragStarted,
onFirstPointerDown = ::onFirstPointerDown,
@@ -124,22 +144,6 @@
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 @@
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 2c41b35..dd4f99f 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 @@
val transitionInterceptionThreshold = 0.05f
- var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
-
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -123,13 +121,16 @@
// 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) }
val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
+ var pointerInfoOwner: () -> PointersInfo = {
+ PointersInfo(startedPosition = Offset.Zero, pointersDown = 1)
+ }
+
fun nestedScrollConnection(
nestedScrollBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: Boolean = false,
@@ -140,9 +141,7 @@
topOrLeftBehavior = nestedScrollBehavior,
bottomOrRightBehavior = nestedScrollBehavior,
isExternalOverscrollGesture = { isExternalOverscrollGesture },
- pointersInfoOwner = {
- PointersInfo(startedPosition = Offset.Zero, pointersDown = 1)
- },
+ pointersInfoOwner = { pointerInfoOwner() },
)
.connection
@@ -156,11 +155,18 @@
fun downOffset(fractionOfScreen: Float) =
if (fractionOfScreen < 0f) {
- error("upOffset() is required, not implemented yet")
+ error("use upOffset()")
} else {
Offset(x = 0f, y = down(fractionOfScreen))
}
+ fun upOffset(fractionOfScreen: Float) =
+ if (fractionOfScreen < 0f) {
+ error("use downOffset()")
+ } else {
+ Offset(x = 0f, y = up(fractionOfScreen))
+ }
+
val transitionState: TransitionState
get() = layoutState.transitionState
@@ -343,13 +349,6 @@
}
@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)
@@ -1135,6 +1134,45 @@
}
@Test
+ fun nestedScrollUseFromSourceInfo() = runGestureTest {
+ // Start at scene C.
+ navigateToSceneC()
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
+
+ // Drag from the **top** of the screen
+ pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1) }
+ assertIdle(currentScene = SceneC)
+
+ nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ // userAction: Swipe.Up to SceneB
+ toScene = SceneB,
+ progress = 0.1f,
+ )
+
+ // Reset to SceneC
+ nestedScroll.preFling(Velocity.Zero)
+ advanceUntilIdle()
+
+ // Drag from the **bottom** of the screen
+ pointerInfoOwner = {
+ PointersInfo(startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1)
+ }
+ assertIdle(currentScene = SceneC)
+
+ nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ // userAction: Swipe(SwipeDirection.Up, fromSource = Edge.Bottom) to SceneA
+ toScene = SceneA,
+ progress = 0.1f,
+ )
+ }
+
+ @Test
fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
// Swipe up from the middle to transition to scene B.
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
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 493f3a1..c8f6e6d 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.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 @@
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 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
// We want to start a drag gesture immediately
startDragImmediately = { true },
onDragStarted = { _, _, _ ->
@@ -238,7 +239,6 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
@@ -358,7 +358,6 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
@@ -464,7 +463,6 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
verticalStarted = true
@@ -477,7 +475,6 @@
)
.multiPointerDraggable(
orientation = Orientation.Horizontal,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
horizontalStarted = true
@@ -570,7 +567,6 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
swipeDetector =
object : SwipeDetector {
@@ -672,7 +668,6 @@
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
SimpleDragController(
@@ -744,7 +739,6 @@
.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 25e8713..ce64628 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.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.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 @@
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 c399abc..81d92fa 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -36,6 +36,7 @@
"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 5eafbfc..ec466f0 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 1863cd8..9877406 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 @@
// 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 @@
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 @@
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 @@
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 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -314,7 +315,7 @@
interpolator = null,
duration = COLOR_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -327,7 +328,7 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -336,7 +337,7 @@
duration = APPEAR_ANIM_DURATION,
interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -353,7 +354,7 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = dozingWeightInternal,
@@ -362,7 +363,7 @@
interpolator = Interpolators.EMPHASIZED_DECELERATE,
duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -381,7 +382,7 @@
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_1,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
setTextStyle(
@@ -391,7 +392,7 @@
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_0,
delay = chargeAnimationDelay.toLong(),
- onAnimationEnd = startAnimPhase2
+ onAnimationEnd = startAnimPhase2,
)
}
@@ -404,7 +405,7 @@
interpolator = null,
duration = DOZE_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -444,7 +445,7 @@
interpolator: TimeInterpolator?,
duration: Long,
delay: Long,
- onAnimationEnd: Runnable?
+ onAnimationEnd: Runnable?,
) {
textAnimator?.let {
it.setTextStyle(
@@ -454,7 +455,7 @@
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
it.glyphFilter = glyphFilter
}
@@ -468,7 +469,7 @@
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
textAnimator.glyphFilter = glyphFilter
}
@@ -476,6 +477,7 @@
}
fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context)
@@ -560,18 +562,11 @@
* @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 0000000..d001ef96
--- /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 0000000..5a04169
--- /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 0000000..f5e8432
--- /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 954155d..9da3022 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 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 @@
selfChange: Boolean,
uris: Collection<Uri>,
flags: Int,
- userId: Int
+ userId: Int,
) {
scope.launch(bgDispatcher) { querySettings() }
}
@@ -180,7 +180,7 @@
override fun onPluginLoaded(
plugin: ClockProviderPlugin,
pluginContext: Context,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
plugin.initialize(clockBuffers)
@@ -218,7 +218,7 @@
override fun onPluginUnloaded(
plugin: ClockProviderPlugin,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
for (clock in plugin.getClocks()) {
val id = clock.clockId
@@ -290,12 +290,12 @@
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 @@
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 @@
pluginManager.addPluginListener(
pluginListener,
ClockProviderPlugin::class.java,
- /*allowMultiple=*/ true
+ /*allowMultiple=*/ true,
)
scope.launch(bgDispatcher) { querySettings() }
@@ -427,7 +427,7 @@
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 @@
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
- settingObserver
+ settingObserver,
)
}
}
@@ -504,7 +504,7 @@
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 @@
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 @@
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 @@
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 4802e34..07191c6 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 @@
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 0000000..3869706
--- /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 0000000..2be6c65
--- /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 0000000..34cb4ef
--- /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 0000000..f5a9375
--- /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 0000000..eb72346
--- /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 0000000..c29c8da
--- /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 0000000..74617b1
--- /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 0000000..bbd2d3d
--- /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/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 75a77cf..194b41f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -27,12 +27,23 @@
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.SysuiTestableContext
+import com.android.systemui.biometrics.data.repository.biometricStatusRepository
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
/** Create [FingerprintSensorPropertiesInternal] for a test. */
internal fun fingerprintSensorPropertiesInternal(
ids: List<Int> = listOf(0),
strong: Boolean = true,
- sensorType: Int = FingerprintSensorProperties.TYPE_REAR
+ sensorType: Int = FingerprintSensorProperties.TYPE_REAR,
): List<FingerprintSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -41,15 +52,15 @@
"vendor/model/revision" /* hardwareVersion */,
"1.01" /* firmwareVersion */,
"00000001" /* serialNumber */,
- "" /* softwareVersion */
+ "", /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */,
"" /* firmwareVersion */,
"" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
- )
+ "vendor/version/revision", /* softwareVersion */
+ ),
)
return ids.map { id ->
FingerprintSensorPropertiesInternal(
@@ -58,7 +69,7 @@
5 /* maxEnrollmentsPerUser */,
componentInfo,
sensorType,
- false /* resetLockoutRequiresHardwareAuthToken */
+ false, /* resetLockoutRequiresHardwareAuthToken */
)
}
}
@@ -75,15 +86,15 @@
"vendor/model/revision" /* hardwareVersion */,
"1.01" /* firmwareVersion */,
"00000001" /* serialNumber */,
- "" /* softwareVersion */
+ "", /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */,
"" /* firmwareVersion */,
"" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
- )
+ "vendor/version/revision", /* softwareVersion */
+ ),
)
return ids.map { id ->
FaceSensorPropertiesInternal(
@@ -94,7 +105,7 @@
FaceSensorProperties.TYPE_RGB,
true /* supportsFaceDetection */,
true /* supportsSelfIllumination */,
- false /* resetLockoutRequiresHardwareAuthToken */
+ false, /* resetLockoutRequiresHardwareAuthToken */
)
}
}
@@ -145,3 +156,67 @@
info.negativeButtonText = negativeButton
return info
}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal fun TestScope.updateSfpsIndicatorRequests(
+ kosmos: Kosmos,
+ mContext: SysuiTestableContext,
+ primaryBouncerRequest: Boolean? = null,
+ alternateBouncerRequest: Boolean? = null,
+ biometricPromptRequest: Boolean? = null,
+ // TODO(b/365182034): update when rest to unlock feature is implemented
+ // progressBarShowing: Boolean? = null
+) {
+ biometricPromptRequest?.let { hasBiometricPromptRequest ->
+ if (hasBiometricPromptRequest) {
+ kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+ AuthenticationReason.BiometricPromptAuthentication
+ )
+ } else {
+ kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+ AuthenticationReason.NotRunning
+ )
+ }
+ }
+
+ primaryBouncerRequest?.let { hasPrimaryBouncerRequest ->
+ updatePrimaryBouncer(
+ kosmos,
+ mContext,
+ isShowing = hasPrimaryBouncerRequest,
+ isAnimatingAway = false,
+ fpsDetectionRunning = true,
+ isUnlockingWithFpAllowed = true,
+ )
+ }
+
+ alternateBouncerRequest?.let { hasAlternateBouncerRequest ->
+ kosmos.keyguardBouncerRepository.setAlternateVisible(hasAlternateBouncerRequest)
+ }
+
+ // TODO(b/365182034): set progress bar visibility when rest to unlock feature is implemented
+
+ runCurrent()
+}
+
+internal fun updatePrimaryBouncer(
+ kosmos: Kosmos,
+ mContext: SysuiTestableContext,
+ isShowing: Boolean,
+ isAnimatingAway: Boolean,
+ fpsDetectionRunning: Boolean,
+ isUnlockingWithFpAllowed: Boolean,
+) {
+ kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
+ kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
+ val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+ kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
+ primaryStartDisappearAnimation
+ )
+
+ whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
+ .thenReturn(fpsDetectionRunning)
+ whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+ .thenReturn(isUnlockingWithFpAllowed)
+ mContext.orCreateTestableResources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, true)
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 7fa165c..57df662 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -16,64 +16,48 @@
package com.android.systemui.biometrics.ui.binder
-import android.animation.Animator
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManager
-import android.hardware.display.DisplayManagerGlobal
import android.testing.TestableLooper
-import android.view.Display
-import android.view.DisplayInfo
import android.view.LayoutInflater
import android.view.View
-import android.view.ViewPropertyAnimator
-import android.view.WindowInsets
import android.view.WindowManager
-import android.view.WindowMetrics
import android.view.layoutInflater
import android.view.windowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.airbnb.lottie.LottieAnimationView
-import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.biometrics.updateSfpsIndicatorRequests
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.displayStateRepository
-import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.firstValue
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -83,84 +67,25 @@
private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var displayManager: DisplayManager
- @Mock
- private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
@Mock private lateinit var layoutInflater: LayoutInflater
@Mock private lateinit var sideFpsView: View
-
- private val contextDisplayInfo = DisplayInfo()
-
- private var displayWidth: Int = 0
- private var displayHeight: Int = 0
- private var boundsWidth: Int = 0
- private var boundsHeight: Int = 0
-
- private lateinit var deviceConfig: DeviceConfig
- private lateinit var sensorLocation: SensorLocationInternal
-
- enum class DeviceConfig {
- X_ALIGNED,
- Y_ALIGNED,
- }
+ @Captor private lateinit var viewCaptor: ArgumentCaptor<View>
@Before
fun setup() {
allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
-
- mContext = spy(mContext)
-
- val resources = mContext.resources
- whenever(mContext.display)
- .thenReturn(
- Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
- )
-
kosmos.layoutInflater = layoutInflater
-
- whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser)
- .thenReturn(MutableStateFlow(false))
-
- context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, kosmos.windowManager)
-
`when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
`when`(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
.thenReturn(mock(LottieAnimationView::class.java))
- with(mock(ViewPropertyAnimator::class.java)) {
- `when`(sideFpsView.animate()).thenReturn(this)
- `when`(alpha(Mockito.anyFloat())).thenReturn(this)
- `when`(setStartDelay(Mockito.anyLong())).thenReturn(this)
- `when`(setDuration(Mockito.anyLong())).thenReturn(this)
- `when`(setListener(any())).thenAnswer {
- (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
- mock(Animator::class.java)
- )
- this
- }
- }
}
@Test
fun verifyIndicatorNotAdded_whenInRearDisplayMode() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = true
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
- runCurrent()
-
+ setupTestConfiguration(isInRearDisplayMode = true)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
verify(kosmos.windowManager, never()).addView(any(), any())
}
}
@@ -168,33 +93,14 @@
@Test
fun verifyIndicatorShowAndHide_onPrimaryBouncerShowAndHide() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- // Show primary bouncer
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
// Hide primary bouncer
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -204,30 +110,19 @@
@Test
fun verifyIndicatorShowAndHide_onAlternateBouncerShowAndHide() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- // Show alternate bouncer
- kosmos.keyguardBouncerRepository.setAlternateVisible(true)
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
- var viewCaptor = argumentCaptor<View>()
verify(kosmos.windowManager).addView(viewCaptor.capture(), any())
verify(viewCaptor.firstValue)
.announceForAccessibility(
mContext.getText(R.string.accessibility_side_fingerprint_indicator_label)
)
- // Hide alternate bouncer
- kosmos.keyguardBouncerRepository.setAlternateVisible(false)
+ updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -237,30 +132,14 @@
@Test
fun verifyIndicatorShownAndHidden_onSystemServerAuthenticationStartedAndStopped() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
- // System server authentication started
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
// System server authentication stopped
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -269,45 +148,37 @@
// On progress bar shown - hide indicator
// On progress bar hidden - show indicator
+ // TODO(b/365182034): update + enable when rest to unlock feature is implemented
+ @Ignore("b/365182034")
@Test
fun verifyIndicatorProgressBarInteraction() {
kosmos.testScope.runTest {
// Pre-auth conditions
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- // Show primary bouncer
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
val inOrder = inOrder(kosmos.windowManager)
-
// Verify indicator shown
inOrder.verify(kosmos.windowManager).addView(any(), any())
// Set progress bar visible
- kosmos.sideFpsProgressBarViewModel.setVisible(true)
-
+ updateSfpsIndicatorRequests(
+ kosmos,
+ mContext,
+ primaryBouncerRequest = true,
+ ) // , progressBarShowing = true)
runCurrent()
// Verify indicator hidden
inOrder.verify(kosmos.windowManager).removeView(any())
// Set progress bar invisible
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
+ updateSfpsIndicatorRequests(
+ kosmos,
+ mContext,
+ primaryBouncerRequest = true,
+ ) // , progressBarShowing = false)
runCurrent()
// Verify indicator shown
@@ -315,78 +186,18 @@
}
}
- private fun updatePrimaryBouncer(
- isShowing: Boolean,
- isAnimatingAway: Boolean,
- fpsDetectionRunning: Boolean,
- isUnlockingWithFpAllowed: Boolean,
- ) {
- kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
- kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
- val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
- kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
- primaryStartDisappearAnimation
- )
-
- whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
- .thenReturn(fpsDetectionRunning)
- whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
- .thenReturn(isUnlockingWithFpAllowed)
- mContext.orCreateTestableResources.addOverride(
- R.bool.config_show_sidefps_hint_on_bouncer,
- true
- )
- }
-
- private suspend fun TestScope.setupTestConfiguration(
- deviceConfig: DeviceConfig,
- rotation: DisplayRotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode: Boolean,
- ) {
- this@SideFpsOverlayViewBinderTest.deviceConfig = deviceConfig
-
- when (deviceConfig) {
- DeviceConfig.X_ALIGNED -> {
- displayWidth = 3000
- displayHeight = 1500
- boundsWidth = 200
- boundsHeight = 100
- sensorLocation = SensorLocationInternal("", 2500, 0, boundsWidth / 2)
- }
- DeviceConfig.Y_ALIGNED -> {
- displayWidth = 2500
- displayHeight = 2000
- boundsWidth = 100
- boundsHeight = 200
- sensorLocation = SensorLocationInternal("", displayWidth, 300, boundsHeight / 2)
- }
- }
-
- whenever(kosmos.windowManager.maximumWindowMetrics)
- .thenReturn(
- WindowMetrics(
- Rect(0, 0, displayWidth, displayHeight),
- mock(WindowInsets::class.java),
- )
- )
-
- contextDisplayInfo.uniqueId = DISPLAY_ID
-
+ private suspend fun TestScope.setupTestConfiguration(isInRearDisplayMode: Boolean) {
kosmos.fingerprintPropertyRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.POWER_BUTTON,
- sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+ sensorLocations = emptyMap(),
)
kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
- kosmos.displayStateRepository.setCurrentRotation(rotation)
+ kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
kosmos.displayRepository.emitDisplayChangeEvent(0)
kosmos.sideFpsOverlayViewBinder.start()
runCurrent()
}
-
- companion object {
- private const val DISPLAY_ID = "displayId"
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 0db7b62..84d062a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -30,23 +30,19 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.airbnb.lottie.model.KeyPath
-import com.android.keyguard.keyguardUpdateMonitor
import com.android.settingslib.Utils
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.LottieCallback
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.biometrics.updateSfpsIndicatorRequests
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.displayStateRepository
-import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
@@ -84,17 +80,17 @@
private val indicatorColor =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorPrimaryFixed
+ com.android.internal.R.attr.materialColorPrimaryFixed,
)
private val outerRimColor =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorPrimaryFixedDim
+ com.android.internal.R.attr.materialColorPrimaryFixedDim,
)
private val chevronFill =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorOnPrimaryFixed
+ com.android.internal.R.attr.materialColorOnPrimaryFixed,
)
private val color_blue400 =
context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400)
@@ -133,7 +129,7 @@
setupTestConfiguration(
DeviceConfig.X_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewProperties by
@@ -167,7 +163,7 @@
setupTestConfiguration(
DeviceConfig.Y_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewProperties by
@@ -201,7 +197,7 @@
setupTestConfiguration(
DeviceConfig.X_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewParams by
@@ -243,7 +239,7 @@
setupTestConfiguration(
DeviceConfig.Y_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewParams by
@@ -284,17 +280,7 @@
kosmos.testScope.runTest {
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -312,17 +298,7 @@
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
setDarkMode(true)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -338,17 +314,7 @@
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
setDarkMode(false)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -371,29 +337,6 @@
mContext.resources.configuration.uiMode = uiMode
}
- private fun updatePrimaryBouncer(
- isShowing: Boolean,
- isAnimatingAway: Boolean,
- fpsDetectionRunning: Boolean,
- isUnlockingWithFpAllowed: Boolean,
- ) {
- kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
- kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
- val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
- kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
- primaryStartDisappearAnimation
- )
-
- whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
- .thenReturn(fpsDetectionRunning)
- whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
- .thenReturn(isUnlockingWithFpAllowed)
- mContext.orCreateTestableResources.addOverride(
- R.bool.config_show_sidefps_hint_on_bouncer,
- true
- )
- }
-
private suspend fun TestScope.setupTestConfiguration(
deviceConfig: DeviceConfig,
rotation: DisplayRotation = DisplayRotation.ROTATION_0,
@@ -432,7 +375,7 @@
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.POWER_BUTTON,
- sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+ sensorLocations = mapOf(DISPLAY_ID to sensorLocation),
)
kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
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 d4d966a..2312bbd 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.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 @@
widgetId = widgetId,
provider = provider,
rank = rank,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
assertThat(widgets())
@@ -110,7 +111,7 @@
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
}
@@ -129,7 +130,7 @@
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
@@ -165,7 +166,7 @@
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId)
@@ -251,6 +252,7 @@
componentName = "pk_name/cls_name_4",
itemId = 4L,
userSerialNumber = 0,
+ spanY = 3,
)
assertThat(widgets())
.containsExactly(
@@ -267,6 +269,68 @@
}
@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 @@
componentName = fakeWidget.componentName,
itemId = rank.uid,
userSerialNumber = fakeWidget.userSerialNumber,
+ spanY = 3,
)
expected[rank] = widget
}
@@ -343,6 +408,7 @@
componentName = widgetInfo1.provider.flattenToString(),
itemId = communalItemRankEntry1.uid,
userSerialNumber = widgetInfo1.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry2 =
CommunalWidgetItem(
@@ -351,6 +417,7 @@
componentName = widgetInfo2.provider.flattenToString(),
itemId = communalItemRankEntry2.uid,
userSerialNumber = widgetInfo2.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry3 =
CommunalWidgetItem(
@@ -359,6 +426,7 @@
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 eba395b..596db07 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 @@
componentName = defaultWidgets[0],
rank = 0,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -124,6 +125,7 @@
componentName = defaultWidgets[1],
rank = 1,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -131,6 +133,7 @@
componentName = defaultWidgets[2],
rank = 2,
userSerialNumber = 0,
+ spanY = 3,
)
}
@@ -152,6 +155,7 @@
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 980a5ec..3d30ecc 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 @@
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 @@
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),
+ CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0, 3),
)
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
- )
+ 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 @@
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),
)
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
- )
+ fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB)
// Expect two widgets
val communalWidgets by collectLastValue(underTest.communalWidgets)
@@ -235,11 +228,7 @@
)
// 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 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -294,7 +283,7 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -303,7 +292,7 @@
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 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -332,7 +321,7 @@
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 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -650,8 +639,10 @@
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 @@
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 @@
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 @@
componentName = ComponentName("pk_1", "cls_1"),
icon = fakeIcon,
user = mainUser,
- ),
+ )
)
// Package for widget 1 finished installing
@@ -749,10 +740,23 @@
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 8f9e238..8b13411 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 @@
fakeInputManager.addPhysicalKeyboard(
PHYSICAL_NOT_FULL_KEYBOARD_ID,
- isFullKeyboard = false
+ isFullKeyboard = false,
)
assertThat(isKeyboardConnected).isFalse()
@@ -223,7 +223,7 @@
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = false
+ triggeredByKeyPress = false,
)
assertThat(backlight).isNull()
}
@@ -239,7 +239,7 @@
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = true
+ triggeredByKeyPress = true,
)
assertThat(backlight).isNotNull()
}
@@ -318,15 +318,75 @@
}
}
+ @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 @@
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/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
index 7da2e9a..fc9e595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
@@ -408,6 +408,40 @@
verify(mockImageLoader, times(1)).loadBitmap(any(), anyInt(), anyInt(), anyInt())
}
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testLoadMediaDataInBg_fromResumeToActive_doesNotCancelResumeToActiveTask() =
+ testScope.runTest {
+ val mockImageLoader = mock<ImageLoader>()
+ val mediaDataLoader =
+ MediaDataLoader(
+ context,
+ testDispatcher,
+ testScope,
+ mediaControllerFactory,
+ mediaFlags,
+ mockImageLoader,
+ statusBarManager,
+ )
+ metadataBuilder.putString(
+ MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+ "content://album_art_uri",
+ )
+
+ testScope.launch {
+ mediaDataLoader.loadMediaData(
+ KEY,
+ createMediaNotification(),
+ isConvertingToActive = true,
+ )
+ }
+ testScope.launch { mediaDataLoader.loadMediaData(KEY, createMediaNotification()) }
+ testScope.launch { mediaDataLoader.loadMediaData(KEY, createMediaNotification()) }
+ testScope.advanceUntilIdle()
+
+ verify(mockImageLoader, times(2)).loadBitmap(any(), anyInt(), anyInt(), anyInt())
+ }
+
private fun createMediaNotification(
mediaSession: MediaSession? = session,
applicationInfo: ApplicationInfo? = null,
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 523a89a..5b0b59d 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 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.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 @@
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 @@
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
+ when(mDeviceUnlockedInteractor.getDeviceUnlockStatus())
+ .thenReturn(mDeviceUnlockStatusStateFlow);
+
mBackgroundExecutor.runAllReady();
}
@@ -493,7 +510,8 @@
}
@Test
- public void testUpdateIsPublicMode() {
+ @DisableSceneContainer
+ public void testUpdateIsPublicMode_sceneContainerDisabled() {
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
when(mKeyguardStateController.isShowing()).thenReturn(false);
@@ -527,6 +545,57 @@
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 @@
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 28857a0..34f4608 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 @@
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 @@
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 @@
.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 @@
.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/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 3b3ed39..91cd019 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -215,17 +215,4 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
tools:srcCompat="@tools:sample/avatars" />
-
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon_overlay"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY"
- android:importantForAccessibility="no"
- app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
- app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
- app:layout_constraintStart_toStartOf="@+id/biometric_icon"
- app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
index 2a00495..51117a7 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
@@ -40,19 +40,6 @@
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon_overlay"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY"
- android:importantForAccessibility="no"
- app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
- app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
- app:layout_constraintStart_toStartOf="@+id/biometric_icon"
- app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
-
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml
index 9b7ae5d..cea405b 100644
--- a/packages/SystemUI/res/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res/values-en-rGB/strings.xml
@@ -573,8 +573,7 @@
<string name="notification_section_header_conversations" msgid="821834744538345661">"Conversations"</string>
<string name="accessibility_notification_section_header_gentle_clear_all" msgid="6490207897764933919">"Clear all silent notifications"</string>
<string name="dnd_suppressing_shade_text" msgid="5588252250634464042">"Notifications paused by Do Not Disturb"</string>
- <!-- no translation found for modes_suppressing_shade_text (6037581130837903239) -->
- <skip />
+ <string name="modes_suppressing_shade_text" msgid="6037581130837903239">"{count,plural,offset:1 =0{No notifications}=1{Notifications paused by {mode}}=2{Notifications paused by {mode} and one other mode}other{Notifications paused by {mode} and # other modes}}"</string>
<string name="media_projection_action_text" msgid="3634906766918186440">"Start now"</string>
<string name="empty_shade_text" msgid="8935967157319717412">"No notifications"</string>
<string name="no_unseen_notif_text" msgid="395512586119868682">"No new notifications"</string>
@@ -1436,6 +1435,8 @@
<string name="all_apps_edu_toast_content" msgid="8807496014667211562">"To view all your apps, press the action key on your keyboard"</string>
<string name="redacted_notification_single_line_title" msgid="212019960919261670">"Redacted"</string>
<string name="redacted_notification_single_line_text" msgid="8684166405005242945">"Unlock to view"</string>
+ <!-- no translation found for contextual_education_dialog_title (4630392552837487324) -->
+ <skip />
<string name="back_edu_notification_title" msgid="5624780717751357278">"Use your touchpad to go back"</string>
<string name="back_edu_notification_content" msgid="2497557451540954068">"Swipe left or right using three fingers. Tap to learn more gestures."</string>
<string name="home_edu_notification_title" msgid="6097902076909654045">"Use your touchpad to go home"</string>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index e9dd039f3..7bd4ca8 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -64,4 +64,9 @@
<item name="android:windowLightNavigationBar">false</item>
</style>
+ <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+ <!-- To make the dialog wrap to content when the education text is short -->
+ <item name="windowMinWidthMajor">0%</item>
+ <item name="windowMinWidthMinor">0%</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml
index 5c9679d..399523e 100644
--- a/packages/SystemUI/res/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res/values-pt-rBR/strings.xml
@@ -1436,6 +1436,8 @@
<string name="all_apps_edu_toast_content" msgid="8807496014667211562">"Para ver todos os apps, pressione a tecla de ação no teclado"</string>
<string name="redacted_notification_single_line_title" msgid="212019960919261670">"Encoberto"</string>
<string name="redacted_notification_single_line_text" msgid="8684166405005242945">"Desbloquear para visualizar"</string>
+ <!-- no translation found for contextual_education_dialog_title (4630392552837487324) -->
+ <skip />
<string name="back_edu_notification_title" msgid="5624780717751357278">"Use o touchpad para voltar"</string>
<string name="back_edu_notification_content" msgid="2497557451540954068">"Deslize para a esquerda ou direita usando três dedos. Toque para aprender outros gestos."</string>
<string name="home_edu_notification_title" msgid="6097902076909654045">"Use o touchpad para acessar a tela inicial"</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b34d6e4..1c09f84 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1721,7 +1721,7 @@
<item name="android:windowLightNavigationBar">true</item>
</style>
- <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+ <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar">
<!-- To make the dialog wrap to content when the education text is short -->
<item name="windowMinWidthMajor">0%</item>
<item name="windowMinWidthMinor">0%</item>
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 0000000..c3fb8d4
--- /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/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 22130f8..8e01e04 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -114,6 +114,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
+import com.android.keyguard.logging.SimLogger;
import com.android.settingslib.Utils;
import com.android.settingslib.WirelessUtils;
import com.android.settingslib.fuelgauge.BatteryStatus;
@@ -285,6 +286,7 @@
private final Context mContext;
private final UserTracker mUserTracker;
private final KeyguardUpdateMonitorLogger mLogger;
+ private final SimLogger mSimLogger;
private final boolean mIsSystemUser;
private final Provider<JavaAdapter> mJavaAdapter;
private final Provider<SceneInteractor> mSceneInteractor;
@@ -582,14 +584,14 @@
private void handleSimSubscriptionInfoChanged() {
Assert.isMainThread();
- mLogger.v("onSubscriptionInfoChanged()");
+ mSimLogger.v("onSubscriptionInfoChanged()");
List<SubscriptionInfo> subscriptionInfos = getSubscriptionInfo(true /* forceReload */);
if (!subscriptionInfos.isEmpty()) {
for (SubscriptionInfo subInfo : subscriptionInfos) {
- mLogger.logSubInfo(subInfo);
+ mSimLogger.logSubInfo(subInfo);
}
} else {
- mLogger.v("onSubscriptionInfoChanged: list is null");
+ mSimLogger.v("onSubscriptionInfoChanged: list is null");
}
// Hack level over 9000: Because the subscription id is not yet valid when we see the
@@ -612,7 +614,7 @@
while (iter.hasNext()) {
Map.Entry<Integer, SimData> simData = iter.next();
if (!activeSubIds.contains(simData.getKey())) {
- mLogger.logInvalidSubId(simData.getKey());
+ mSimLogger.logInvalidSubId(simData.getKey());
iter.remove();
SimData data = simData.getValue();
@@ -1700,7 +1702,7 @@
}
return;
}
- mLogger.logSimStateFromIntent(action,
+ mSimLogger.logSimStateFromIntent(action,
intent.getStringExtra(Intent.EXTRA_SIM_STATE),
args.slotId,
args.subId);
@@ -1720,7 +1722,7 @@
ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
- mLogger.logServiceStateIntent(action, serviceState, subId);
+ mSimLogger.logServiceStateIntent(action, serviceState, subId);
mHandler.sendMessage(
mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState));
} else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) {
@@ -2154,6 +2156,7 @@
LatencyTracker latencyTracker,
ActiveUnlockConfig activeUnlockConfiguration,
KeyguardUpdateMonitorLogger logger,
+ SimLogger simLogger,
UiEventLogger uiEventLogger,
// This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
Provider<SessionTracker> sessionTrackerProvider,
@@ -2196,6 +2199,7 @@
mSensorPrivacyManager = sensorPrivacyManager;
mActiveUnlockConfig = activeUnlockConfiguration;
mLogger = logger;
+ mSimLogger = simLogger;
mUiEventLogger = uiEventLogger;
mSessionTrackerProvider = sessionTrackerProvider;
mTrustManager = trustManager;
@@ -3369,36 +3373,39 @@
}
/**
+ * Removes all valid subscription info from the map for the given slotId.
+ */
+ private void invalidateSlot(int slotId) {
+ Iterator<Map.Entry<Integer, SimData>> iter = mSimDatas.entrySet().iterator();
+ while (iter.hasNext()) {
+ SimData data = iter.next().getValue();
+ if (data.slotId == slotId && SubscriptionManager.isValidSubscriptionId(data.subId)) {
+ mSimLogger.logInvalidSubId(data.subId);
+ iter.remove();
+ }
+ }
+ }
+
+ /**
* Handle {@link #MSG_SIM_STATE_CHANGE}
*/
@VisibleForTesting
void handleSimStateChange(int subId, int slotId, int state) {
Assert.isMainThread();
- mLogger.logSimState(subId, slotId, state);
+ mSimLogger.logSimState(subId, slotId, state);
- boolean becameAbsent = false;
+ boolean becameAbsent = ABSENT_SIM_STATE_LIST.contains(state);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
- mLogger.w("invalid subId in handleSimStateChange()");
+ mSimLogger.w("invalid subId in handleSimStateChange()");
/* Only handle No SIM(ABSENT) and Card Error(CARD_IO_ERROR) due to
* handleServiceStateChange() handle other case */
- if (state == TelephonyManager.SIM_STATE_ABSENT) {
- updateTelephonyCapable(true);
- // Even though the subscription is not valid anymore, we need to notify that the
- // SIM card was removed so we can update the UI.
- becameAbsent = true;
- for (SimData data : mSimDatas.values()) {
- // Set the SIM state of all SimData associated with that slot to ABSENT se we
- // do not move back into PIN/PUK locked and not detect the change below.
- if (data.slotId == slotId) {
- data.simState = TelephonyManager.SIM_STATE_ABSENT;
- }
- }
- } else if (state == TelephonyManager.SIM_STATE_CARD_IO_ERROR) {
+ if (state == TelephonyManager.SIM_STATE_ABSENT
+ || state == TelephonyManager.SIM_STATE_CARD_IO_ERROR) {
updateTelephonyCapable(true);
}
- }
- becameAbsent |= ABSENT_SIM_STATE_LIST.contains(state);
+ invalidateSlot(slotId);
+ }
// TODO(b/327476182): Preserve SIM_STATE_CARD_IO_ERROR sims in a separate data source.
SimData data = mSimDatas.get(subId);
@@ -3428,10 +3435,10 @@
*/
@VisibleForTesting
void handleServiceStateChange(int subId, ServiceState serviceState) {
- mLogger.logServiceStateChange(subId, serviceState);
+ mSimLogger.logServiceStateChange(subId, serviceState);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
- mLogger.w("invalid subId in handleServiceStateChange()");
+ mSimLogger.w("invalid subId in handleServiceStateChange()");
return;
} else {
updateTelephonyCapable(true);
@@ -3711,7 +3718,7 @@
*/
@MainThread
public void reportSimUnlocked(int subId) {
- mLogger.logSimUnlocked(subId);
+ mSimLogger.logSimUnlocked(subId);
handleSimStateChange(subId, getSlotId(subId), TelephonyManager.SIM_STATE_READY);
}
@@ -3870,6 +3877,11 @@
private boolean refreshSimState(int subId, int slotId) {
int state = mTelephonyManager.getSimState(slotId);
SimData data = mSimDatas.get(subId);
+
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ invalidateSlot(slotId);
+ }
+
final boolean changed;
if (data == null) {
data = new SimData(state, slotId, subId);
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 0b58f06..12fc3c2 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -20,8 +20,6 @@
import android.hardware.biometrics.BiometricConstants.LockoutMode
import android.hardware.biometrics.BiometricSourceType
import android.os.PowerManager
-import android.telephony.ServiceState
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyManager
@@ -34,7 +32,6 @@
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.ERROR
-import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.log.core.LogLevel.VERBOSE
import com.android.systemui.log.core.LogLevel.WARNING
import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
@@ -63,7 +60,7 @@
"ActiveUnlock",
DEBUG,
{ str1 = reason },
- { "initiate active unlock triggerReason=$str1" }
+ { "initiate active unlock triggerReason=$str1" },
)
}
@@ -75,7 +72,7 @@
{
"Skip requesting active unlock from wake reason that doesn't trigger face auth" +
" reason=${PowerManager.wakeReasonToString(int1)}"
- }
+ },
)
}
@@ -92,7 +89,7 @@
TAG,
DEBUG,
{ bool1 = deviceProvisioned },
- { "DEVICE_PROVISIONED state = $bool1" }
+ { "DEVICE_PROVISIONED state = $bool1" },
)
}
@@ -108,7 +105,7 @@
str1 = originalErrMsg
int1 = msgId
},
- { "Face error received: $str1 msgId= $int1" }
+ { "Face error received: $str1 msgId= $int1" },
)
}
@@ -117,7 +114,7 @@
TAG,
DEBUG,
{ int1 = authUserId },
- { "Face authenticated for wrong user: $int1" }
+ { "Face authenticated for wrong user: $int1" },
)
}
@@ -130,7 +127,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = authUserId },
- { "Fingerprint authenticated for wrong user: $int1" }
+ { "Fingerprint authenticated for wrong user: $int1" },
)
}
@@ -139,7 +136,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = userId },
- { "Fingerprint disabled by DPM for userId: $int1" }
+ { "Fingerprint disabled by DPM for userId: $int1" },
)
}
@@ -148,7 +145,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = mode },
- { "handleFingerprintLockoutReset: $int1" }
+ { "handleFingerprintLockoutReset: $int1" },
)
}
@@ -157,7 +154,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = fingerprintRunningState },
- { "fingerprintRunningState: $int1" }
+ { "fingerprintRunningState: $int1" },
)
}
@@ -169,7 +166,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1" }
+ { "Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -181,7 +178,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Face detected: userId: $int1, isStrongBiometric: $bool1" }
+ { "Face detected: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -193,7 +190,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Fingerprint detected: userId: $int1, isStrongBiometric: $bool1" }
+ { "Fingerprint detected: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -205,22 +202,13 @@
str1 = originalErrMsg
int1 = msgId
},
- { "Fingerprint error received: $str1 msgId= $int1" }
- )
- }
-
- fun logInvalidSubId(subId: Int) {
- logBuffer.log(
- TAG,
- INFO,
- { int1 = subId },
- { "Previously active sub id $int1 is now invalid, will remove" }
+ { "Fingerprint error received: $str1 msgId= $int1" },
)
}
fun logPrimaryKeyguardBouncerChanged(
primaryBouncerIsOrWillBeShowing: Boolean,
- primaryBouncerFullyShown: Boolean
+ primaryBouncerFullyShown: Boolean,
) {
logBuffer.log(
TAG,
@@ -232,7 +220,7 @@
{
"handlePrimaryBouncerChanged " +
"primaryBouncerIsOrWillBeShowing=$bool1 primaryBouncerFullyShown=$bool2"
- }
+ },
)
}
@@ -249,7 +237,7 @@
bool2 = occluded
bool3 = visible
},
- { "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" }
+ { "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" },
)
}
@@ -258,7 +246,7 @@
TAG,
ERROR,
{ int1 = userId },
- { "No Profile Owner or Device Owner supervision app found for User $int1" }
+ { "No Profile Owner or Device Owner supervision app found for User $int1" },
)
}
@@ -279,7 +267,7 @@
int2 = delay
str1 = "$errString"
},
- { "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" }
+ { "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" },
)
}
@@ -288,7 +276,7 @@
TAG,
WARNING,
{ int1 = retryCount },
- { "Retrying fingerprint attempt: $int1" }
+ { "Retrying fingerprint attempt: $int1" },
)
}
@@ -306,32 +294,7 @@
{
"sendPrimaryBouncerChanged primaryBouncerIsOrWillBeShowing=$bool1 " +
"primaryBouncerFullyShown=$bool2"
- }
- )
- }
-
- fun logServiceStateChange(subId: Int, serviceState: ServiceState?) {
- logBuffer.log(
- TAG,
- DEBUG,
- {
- int1 = subId
- str1 = "$serviceState"
},
- { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }
- )
- }
-
- fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
- logBuffer.log(
- TAG,
- VERBOSE,
- {
- str1 = action
- str2 = "$serviceState"
- int1 = subId
- },
- { "action $str1 serviceState=$str2 subId=$int1" }
)
}
@@ -344,51 +307,16 @@
str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
},
- { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" }
+ { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" },
)
}
- fun logSimState(subId: Int, slotId: Int, state: Int) {
- logBuffer.log(
- TAG,
- DEBUG,
- {
- int1 = subId
- int2 = slotId
- long1 = state.toLong()
- },
- { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }
- )
- }
-
- fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
- logBuffer.log(
- TAG,
- VERBOSE,
- {
- str1 = action
- str2 = extraSimState
- int1 = slotId
- int2 = subId
- },
- { "action $str1 state: $str2 slotId: $int1 subid: $int2" }
- )
- }
-
- fun logSimUnlocked(subId: Int) {
- logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
- }
-
- fun logSubInfo(subInfo: SubscriptionInfo?) {
- logBuffer.log(TAG, DEBUG, { str1 = "$subInfo" }, { "SubInfo:$str1" })
- }
-
fun logTimeFormatChanged(newTimeFormat: String?) {
logBuffer.log(
TAG,
DEBUG,
{ str1 = newTimeFormat },
- { "handleTimeFormatUpdate timeFormat=$str1" }
+ { "handleTimeFormatUpdate timeFormat=$str1" },
)
}
@@ -402,7 +330,7 @@
fun logUnexpectedFpCancellationSignalState(
fingerprintRunningState: Int,
- unlockPossible: Boolean
+ unlockPossible: Boolean,
) {
logBuffer.log(
TAG,
@@ -414,7 +342,7 @@
{
"Cancellation signal is not null, high chance of bug in " +
"fp auth lifecycle management. FP state: $int1, unlockPossible: $bool1"
- }
+ },
)
}
@@ -425,7 +353,7 @@
fun logUserRequestedUnlock(
requestOrigin: ActiveUnlockConfig.ActiveUnlockRequestOrigin,
reason: String?,
- dismissKeyguard: Boolean
+ dismissKeyguard: Boolean,
) {
logBuffer.log(
"ActiveUnlock",
@@ -435,7 +363,7 @@
str2 = reason
bool1 = dismissKeyguard
},
- { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }
+ { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" },
)
}
@@ -443,7 +371,7 @@
flags: Int,
newlyUnlocked: Boolean,
userId: Int,
- message: String?
+ message: String?,
) {
logBuffer.log(
TAG,
@@ -457,7 +385,7 @@
{
"trustGrantedWithFlags[user=$int2] newlyUnlocked=$bool1 " +
"flags=${TrustGrantFlags(int1)} message=$str1"
- }
+ },
)
}
@@ -470,7 +398,7 @@
bool2 = isNowTrusted
int1 = userId
},
- { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" }
+ { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" },
)
}
@@ -478,7 +406,7 @@
secure: Boolean,
canDismissLockScreen: Boolean,
trusted: Boolean,
- trustManaged: Boolean
+ trustManaged: Boolean,
) {
logBuffer.log(
"KeyguardState",
@@ -492,7 +420,7 @@
{
"#update secure=$bool1 canDismissKeyguard=$bool2" +
" trusted=$bool3 trustManaged=$bool4"
- }
+ },
)
}
@@ -501,7 +429,7 @@
TAG,
VERBOSE,
{ bool1 = assistantVisible },
- { "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" }
+ { "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" },
)
}
@@ -510,7 +438,7 @@
TAG,
VERBOSE,
{ bool1 = allow },
- { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" }
+ { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" },
)
}
@@ -519,7 +447,7 @@
TAG,
VERBOSE,
{ bool1 = assistantVisible },
- { "Updating mAssistantVisible to new value: $bool1" }
+ { "Updating mAssistantVisible to new value: $bool1" },
)
}
@@ -531,7 +459,7 @@
bool1 = isStrongBiometric
int1 = userId
},
- { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" }
+ { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" },
)
}
@@ -543,7 +471,7 @@
{
"MSG_BIOMETRIC_AUTHENTICATION_CONTINUE already queued up, " +
"ignoring updating FP listening state to $int1"
- }
+ },
)
}
@@ -551,7 +479,7 @@
userId: Int,
oldValue: Boolean,
newValue: Boolean,
- context: String
+ context: String,
) {
logBuffer.log(
TAG,
@@ -568,7 +496,7 @@
"old: $bool1, " +
"new: $bool2 " +
"context: $str1"
- }
+ },
)
}
@@ -591,7 +519,7 @@
"plugged=$str1, " +
"chargingStatus=$int2, " +
"maxChargingWattage= $long2}"
- }
+ },
)
}
@@ -604,7 +532,7 @@
TAG,
DEBUG,
{ str1 = "$biometricSourceType" },
- { "notifying about enrollments changed: $str1" }
+ { "notifying about enrollments changed: $str1" },
)
}
@@ -616,7 +544,7 @@
int1 = userId
str1 = context
},
- { "userCurrentlySwitching: $str1, userId: $int1" }
+ { "userCurrentlySwitching: $str1, userId: $int1" },
)
}
@@ -628,7 +556,7 @@
int1 = userId
str1 = context
},
- { "userSwitchComplete: $str1, userId: $int1" }
+ { "userSwitchComplete: $str1, userId: $int1" },
)
}
@@ -637,7 +565,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = acquireInfo },
- { "fingerprint acquire message: $int1" }
+ { "fingerprint acquire message: $int1" },
)
}
@@ -646,7 +574,7 @@
TAG,
DEBUG,
{ bool1 = keepUnlocked },
- { "keepUnlockedOnFold changed to: $bool1" }
+ { "keepUnlockedOnFold changed to: $bool1" },
)
}
@@ -662,7 +590,7 @@
int1 = userId
bool1 = isUnlocked
},
- { "userStopped userId: $int1 isUnlocked: $bool1" }
+ { "userStopped userId: $int1 isUnlocked: $bool1" },
)
}
@@ -678,7 +606,7 @@
int1 = userId
bool1 = isUnlocked
},
- { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" }
+ { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" },
)
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt
new file mode 100644
index 0000000..a81698b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.keyguard.logging
+
+import android.content.Intent
+import android.telephony.ServiceState
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.WARNING
+import com.android.systemui.log.dagger.SimLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "SimLog"
+
+/** Helper class for logging for SIM events */
+class SimLogger @Inject constructor(@SimLog private val logBuffer: LogBuffer) {
+ fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+ fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+ fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+ fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
+
+ fun logInvalidSubId(subId: Int) {
+ logBuffer.log(
+ TAG,
+ INFO,
+ { int1 = subId },
+ { "Previously active sub id $int1 is now invalid, will remove" },
+ )
+ }
+
+ fun logServiceStateChange(subId: Int, serviceState: ServiceState?) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = subId
+ str1 = "$serviceState"
+ },
+ { "handleServiceStateChange(subId=$int1, serviceState=$str1)" },
+ )
+ }
+
+ fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = action
+ str2 = "$serviceState"
+ int1 = subId
+ },
+ { "action $str1 serviceState=$str2 subId=$int1" },
+ )
+ }
+
+ fun logServiceProvidersUpdated(intent: Intent) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ int1 = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID)
+ str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
+ str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
+ },
+ { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" },
+ )
+ }
+
+ fun logSimState(subId: Int, slotId: Int, state: Int) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = subId
+ int2 = slotId
+ long1 = state.toLong()
+ },
+ { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" },
+ )
+ }
+
+ fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = action
+ str2 = extraSimState
+ int1 = slotId
+ int2 = subId
+ },
+ { "action $str1 state: $str2 slotId: $int1 subid: $int2" },
+ )
+ }
+
+ fun logSimUnlocked(subId: Int) {
+ logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
+ }
+
+ fun logSubInfo(subInfo: SubscriptionInfo?) {
+ logBuffer.log(TAG, DEBUG, { str1 = "$subInfo" }, { "SubInfo:$str1" })
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 73f75a4..18446f02 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -18,13 +18,11 @@
import android.animation.Animator
import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.graphics.Outline
import android.graphics.Rect
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.util.TypedValue
-import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
@@ -52,7 +50,6 @@
import com.android.systemui.res.R
import kotlin.math.abs
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/** Helper for [BiometricViewBinder] to handle resize transitions. */
@@ -98,7 +95,7 @@
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1f,
- view.resources.displayMetrics
+ view.resources.displayMetrics,
)
val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
@@ -114,7 +111,7 @@
0,
view.width + cornerRadiusPx,
view.height,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
PromptPosition.Left -> {
@@ -123,7 +120,7 @@
0,
view.width,
view.height,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
PromptPosition.Bottom,
@@ -133,7 +130,7 @@
0,
view.width,
view.height + cornerRadiusPx,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
}
@@ -160,16 +157,13 @@
fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
if (hideSensorIcon) {
smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
}
}
@@ -189,24 +183,24 @@
R.id.biometric_icon,
ConstraintSet.LEFT,
ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.LEFT,
- position.left
+ position.left,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
smallConstraintSet.connect(
R.id.biometric_icon,
ConstraintSet.LEFT,
ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.LEFT,
- position.left
+ position.left,
)
}
if (position.top != 0) {
@@ -216,13 +210,13 @@
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.TOP,
- position.top
+ position.top,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.TOP,
- position.top
+ position.top,
)
}
if (position.right != 0) {
@@ -233,24 +227,24 @@
R.id.biometric_icon,
ConstraintSet.RIGHT,
ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.RIGHT,
- position.right
+ position.right,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
smallConstraintSet.connect(
R.id.biometric_icon,
ConstraintSet.RIGHT,
ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.RIGHT,
- position.right
+ position.right,
)
}
if (position.bottom != 0) {
@@ -260,13 +254,13 @@
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.BOTTOM,
- position.bottom
+ position.bottom,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.BOTTOM,
- position.bottom
+ position.bottom,
)
}
iconHolderView.layoutParams = iconParams
@@ -305,11 +299,11 @@
} else if (bounds.right < 0) {
mediumConstraintSet.setGuidelineBegin(
rightGuideline.id,
- abs(bounds.right)
+ abs(bounds.right),
)
smallConstraintSet.setGuidelineBegin(
rightGuideline.id,
- abs(bounds.right)
+ abs(bounds.right),
)
}
@@ -362,13 +356,13 @@
R.id.scrollView,
ConstraintSet.LEFT,
R.id.midGuideline,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
flipConstraintSet.connect(
R.id.scrollView,
ConstraintSet.RIGHT,
R.id.rightGuideline,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
} else if (position.isTop) {
// Top position is only used for 180 rotation Udfps
@@ -377,24 +371,24 @@
R.id.scrollView,
ConstraintSet.TOP,
R.id.indicator,
- ConstraintSet.BOTTOM
+ ConstraintSet.BOTTOM,
)
mediumConstraintSet.connect(
R.id.scrollView,
ConstraintSet.BOTTOM,
R.id.button_bar,
- ConstraintSet.TOP
+ ConstraintSet.TOP,
)
mediumConstraintSet.connect(
R.id.panel,
ConstraintSet.TOP,
R.id.biometric_icon,
- ConstraintSet.TOP
+ ConstraintSet.TOP,
)
mediumConstraintSet.setMargin(
R.id.panel,
ConstraintSet.TOP,
- (-24 * pxToDp).toInt()
+ (-24 * pxToDp).toInt(),
)
mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt b/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt
new file mode 100644
index 0000000..08a79c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.bouncer.util
+
+import android.app.ActivityManager
+import android.content.res.Resources
+import com.android.systemui.res.R
+import java.io.File
+
+private const val ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key"
+
+/**
+ * In general, we enable unlocking the insecure keyguard with the menu key. However, there are some
+ * cases where we wish to disable it, notably when the menu button placement or technology is prone
+ * to false positives.
+ *
+ * @return true if the menu key should be enabled
+ */
+fun Resources.shouldEnableMenuKey(): Boolean {
+ val configDisabled = getBoolean(R.bool.config_disableMenuKeyInLockScreen)
+ val isTestHarness = ActivityManager.isRunningInTestHarness()
+ val fileOverride = File(ENABLE_MENU_KEY_FILE).exists()
+ return !configDisabled || isTestHarness || fileOverride
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index aabfbd1..65c01ed 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 @@
@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 8f1854f..17f4f0c 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.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 @@
* @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 @@
)
}
}
+
+ /**
+ * 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 e33aead..f9d2a84 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 @@
*/
@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 93b86bd..5dd4c1c 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.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 @@
@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 @@
@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 @@
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 @@
componentName: String,
rank: Int? = null,
userSerialNumber: Int,
+ spanY: Int = 3,
): Long {
val widgets = getWidgetsNow()
@@ -224,6 +232,7 @@
componentName = componentName,
itemId = insertItemRank(newRank),
userSerialNumber = userSerialNumber,
+ spanY = spanY,
)
}
@@ -246,7 +255,8 @@
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 6cdd9ff..3312f3c 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 @@
/** 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 @@
/** 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 @@
provider = provider,
rank = rank,
userSerialNumber = userManager.getUserSerialNumber(user.identifier),
+ spanY = 3,
)
backupManager.dataChanged()
} else {
@@ -325,6 +344,7 @@
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 @@
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -400,6 +421,7 @@
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -412,6 +434,7 @@
componentName = componentName,
icon = session.icon,
user = session.user,
+ spanY = entry.spanY,
)
} else {
null
@@ -423,5 +446,6 @@
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 bc14ae1..7602a7a 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 @@
// 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 63b1a14..bcbc8f6 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 @@
override val appWidgetId: Int,
val providerInfo: AppWidgetProviderInfo,
override val rank: Int,
+ val spanY: Int = 3,
) : CommunalWidgetContentModel
/** Widget is pending installation */
@@ -40,5 +41,6 @@
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 a94fbd9..a5b2277 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.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 @@
@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/display/data/repository/FocusedDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
new file mode 100644
index 0000000..dc07cca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.display.data.repository
+
+import android.annotation.MainThread
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.FocusedDisplayRepoLog
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import com.android.wm.shell.shared.FocusTransitionListener
+import com.android.wm.shell.shared.ShellTransitions
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository tracking display focus. */
+@SysUISingleton
+@MainThread
+class FocusedDisplayRepository
+@Inject
+constructor(
+ @Application val scope: CoroutineScope,
+ @Main private val mainExecutor: Executor,
+ transitions: ShellTransitions,
+ @FocusedDisplayRepoLog logBuffer: LogBuffer,
+) {
+ val focusedTask: Flow<Int> =
+ conflatedCallbackFlow {
+ val listener = FocusTransitionListener { displayId -> trySend(displayId) }
+ transitions.setFocusTransitionListener(listener, mainExecutor)
+ awaitClose { transitions.unsetFocusTransitionListener(listener) }
+ }
+ .onEach {
+ logBuffer.log(
+ "FocusedDisplayRepository",
+ LogLevel.INFO,
+ { str1 = it.toString() },
+ { "Newly focused display: $str1" },
+ )
+ }
+
+ /** Provides the currently focused display. */
+ val focusedDisplayId: StateFlow<Int>
+ get() = focusedTask.stateIn(scope, SharingStarted.Eagerly, DEFAULT_DISPLAY)
+}
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 5a008bd..7711c48 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 @@
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 @@
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 @@
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 f49cfdd..021c069 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 @@
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 a20dfa5..3329fe2 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 @@
*/
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 @@
constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val inputManager: InputManager,
- inputDeviceRepository: InputDeviceRepository
+ inputDeviceRepository: InputDeviceRepository,
) : KeyboardRepository {
@FlowPreview
@@ -93,6 +96,13 @@
.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/log/dagger/FocusedDisplayRepoLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/FocusedDisplayRepoLog.kt
new file mode 100644
index 0000000..302f962
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/FocusedDisplayRepoLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for display metrics related logging. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FocusedDisplayRepoLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 2053b53..4e975ff 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -480,6 +480,16 @@
}
/**
+ * Provides a {@link LogBuffer} for use by SIM events.
+ */
+ @Provides
+ @SysUISingleton
+ @SimLog
+ public static LogBuffer provideSimLogBuffer(LogBufferFactory factory) {
+ return factory.create("SimLog", 500);
+ }
+
+ /**
* Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
*/
@Provides
@@ -655,6 +665,14 @@
return factory.create("DisplayMetricsRepo", 50);
}
+ /** Provides a {@link LogBuffer} for focus related logs. */
+ @Provides
+ @SysUISingleton
+ @FocusedDisplayRepoLog
+ public static LogBuffer provideFocusedDisplayRepoLogBuffer(LogBufferFactory factory) {
+ return factory.create("FocusedDisplayRepo", 50);
+ }
+
/** Provides a {@link LogBuffer} for the scene framework. */
@Provides
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt
new file mode 100644
index 0000000..64fc6e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for SIM events. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class SimLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 222d783..4528b04 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -417,6 +417,7 @@
override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
if (useQsMediaPlayer && isMediaNotification(sbn)) {
var isNewlyActiveEntry = false
+ var isConvertingToActive = false
Assert.isMainThread()
val oldKey = findExistingEntry(key, sbn.packageName)
if (oldKey == null) {
@@ -433,9 +434,10 @@
// Resume -> active conversion; move to new key
val oldData = mediaEntries.remove(oldKey)!!
isNewlyActiveEntry = true
+ isConvertingToActive = true
mediaEntries.put(key, oldData)
}
- loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
} else {
onNotificationRemoved(key)
}
@@ -535,10 +537,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) {
if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
applicationScope.launch {
- loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
}
} else {
backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
@@ -550,10 +553,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) =
withContext(backgroundDispatcher) {
val lastActive = systemClock.elapsedRealtime()
- val result = mediaDataLoader.get().loadMediaData(key, sbn)
+ val result = mediaDataLoader.get().loadMediaData(key, sbn, isConvertingToActive)
if (result == null) {
Log.d(TAG, "No result from loadMediaData")
return@withContext
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index 7b55dac8..7b8703d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -111,16 +111,26 @@
* If a new [loadMediaData] is issued while existing load is in progress, the existing (old)
* load will be cancelled.
*/
- suspend fun loadMediaData(key: String, sbn: StatusBarNotification): MediaDataLoaderResult? {
- val loadMediaJob = backgroundScope.async { loadMediaDataInBackground(key, sbn) }
+ suspend fun loadMediaData(
+ key: String,
+ sbn: StatusBarNotification,
+ isConvertingToActive: Boolean = false,
+ ): MediaDataLoaderResult? {
+ val loadMediaJob =
+ backgroundScope.async { loadMediaDataInBackground(key, sbn, isConvertingToActive) }
loadMediaJob.invokeOnCompletion {
// We need to make sure we're removing THIS job after cancellation, not
// a job that we created later.
mediaProcessingJobs.remove(key, loadMediaJob)
}
- val existingJob = mediaProcessingJobs.put(key, loadMediaJob)
+ var existingJob: Job? = null
+ // Do not cancel loading jobs that convert resume players to active.
+ if (!isConvertingToActive) {
+ existingJob = mediaProcessingJobs.put(key, loadMediaJob)
+ existingJob?.cancel("New processing job incoming.")
+ }
logD(TAG) { "Loading media data for $key... / existing job: $existingJob" }
- existingJob?.cancel("New processing job incoming.")
+
return loadMediaJob.await()
}
@@ -129,12 +139,16 @@
private suspend fun loadMediaDataInBackground(
key: String,
sbn: StatusBarNotification,
+ isConvertingToActive: Boolean = false,
): MediaDataLoaderResult? =
traceCoroutine("MediaDataLoader#loadMediaData") {
// We have apps spamming us with quick notification updates which can cause
// us to spend significant CPU time loading duplicate data. This debounces
// those requests at the cost of a bit of latency.
- delay(DEBOUNCE_DELAY_MS)
+ // No delay needed to load jobs converting resume players to active.
+ if (!isConvertingToActive) {
+ delay(DEBOUNCE_DELAY_MS)
+ }
val token =
sbn.notification.extras.getParcelable(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index fd7b6dc..affc7b7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -330,6 +330,7 @@
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
if (useQsMediaPlayer && isMediaNotification(sbn)) {
var isNewlyActiveEntry = false
+ var isConvertingToActive = false
Assert.isMainThread()
val oldKey = findExistingEntry(key, sbn.packageName)
if (oldKey == null) {
@@ -347,9 +348,10 @@
// Resume -> active conversion; move to new key
val oldData = mediaDataRepository.removeMediaEntry(oldKey)!!
isNewlyActiveEntry = true
+ isConvertingToActive = true
mediaDataRepository.addMediaEntry(key, oldData)
}
- loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
} else {
onNotificationRemoved(key)
}
@@ -488,10 +490,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) {
if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
applicationScope.launch {
- loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
}
} else {
backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
@@ -835,10 +838,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) =
withContext(backgroundDispatcher) {
val lastActive = systemClock.elapsedRealtime()
- val result = mediaDataLoader.get().loadMediaData(key, sbn)
+ val result = mediaDataLoader.get().loadMediaData(key, sbn, isConvertingToActive)
if (result == null) {
Log.d(TAG, "No result from loadMediaData")
return@withContext
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
index 078d534..f563f87 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
@@ -26,18 +26,11 @@
/** A logger for all events related to the media tap-to-transfer receiver experience. */
@SysUISingleton
-class MediaTttReceiverLogger
-@Inject
-constructor(
- @MediaTttReceiverLogBuffer buffer: LogBuffer,
-) : TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
+class MediaTttReceiverLogger @Inject constructor(@MediaTttReceiverLogBuffer buffer: LogBuffer) :
+ TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
/** Logs a change in the chip state for the given [mediaRouteId]. */
- fun logStateChange(
- stateName: String,
- mediaRouteId: String,
- packageName: String?,
- ) {
+ fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
}
@@ -51,12 +44,27 @@
MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
}
- fun logRippleAnimationEnd(id: Int) {
+ fun logRippleAnimationEnd(id: Int, type: String) {
buffer.log(
tag,
LogLevel.DEBUG,
- { int1 = id },
- { "ripple animation for view with id: $int1 is ended" }
+ {
+ int1 = id
+ str1 = type
+ },
+ { "ripple animation for view with id=$int1 is ended, animation type=$str1" },
+ )
+ }
+
+ fun logRippleAnimationStart(id: Int, type: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ int1 = id
+ str1 = type
+ },
+ { "ripple animation for view with id=$int1 is started, animation type=$str1" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
index a232971..9d00435 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
@@ -69,7 +69,9 @@
)
rippleView.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
- override fun onViewDetachedFromWindow(view: View) {}
+ override fun onViewDetachedFromWindow(view: View) {
+ view.visibility = View.GONE
+ }
override fun onViewAttachedToWindow(view: View) {
if (view == null) {
@@ -81,7 +83,7 @@
} else {
layoutRipple(attachedRippleView)
}
- attachedRippleView.expandRipple()
+ attachedRippleView.expandRipple(mediaTttReceiverLogger)
attachedRippleView.removeOnAttachStateChangeListener(this)
}
}
@@ -126,7 +128,7 @@
iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f)
iconRippleView.setCenter(
width * 0.5f,
- height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin()
+ height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin(),
)
iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 81059e3..cd733ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -37,10 +37,14 @@
isStarted = false
}
- fun expandRipple(onAnimationEnd: Runnable? = null) {
+ fun expandRipple(logger: MediaTttReceiverLogger, onAnimationEnd: Runnable? = null) {
duration = DEFAULT_DURATION
isStarted = true
- super.startRipple(onAnimationEnd)
+ super.startRipple {
+ logger.logRippleAnimationEnd(id, EXPAND)
+ onAnimationEnd?.run()
+ }
+ logger.logRippleAnimationStart(id, EXPAND)
}
/** Used to animate out the ripple. No-op if the ripple was never started via [startRipple]. */
@@ -53,10 +57,14 @@
animator.removeAllListeners()
animator.addListener(
object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ onAnimationEnd(animation)
+ }
+
override fun onAnimationEnd(animation: Animator) {
animation?.let {
visibility = GONE
- logger.logRippleAnimationEnd(id)
+ logger.logRippleAnimationEnd(id, COLLAPSE)
}
onAnimationEnd?.run()
isStarted = false
@@ -64,13 +72,14 @@
}
)
animator.reverse()
+ logger.logRippleAnimationStart(id, COLLAPSE)
}
// Expands the ripple to cover full screen.
fun expandToFull(
newHeight: Float,
logger: MediaTttReceiverLogger,
- onAnimationEnd: Runnable? = null
+ onAnimationEnd: Runnable? = null,
) {
if (!isStarted) {
return
@@ -95,10 +104,14 @@
}
animator.addListener(
object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ onAnimationEnd(animation)
+ }
+
override fun onAnimationEnd(animation: Animator) {
animation?.let {
visibility = GONE
- logger.logRippleAnimationEnd(id)
+ logger.logRippleAnimationEnd(id, EXPAND_TO_FULL)
}
onAnimationEnd?.run()
isStarted = false
@@ -106,6 +119,7 @@
}
)
animator.start()
+ logger.logRippleAnimationStart(id, EXPAND_TO_FULL)
}
// Calculates the actual starting percentage according to ripple shader progress set method.
@@ -151,5 +165,8 @@
companion object {
const val DEFAULT_DURATION = 333L
const val EXPAND_TO_FULL_DURATION = 1000L
+ private const val COLLAPSE = "collapse"
+ private const val EXPAND_TO_FULL = "expand to full"
+ private const val EXPAND = "expand"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index f7a505a..5048a5d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -326,6 +326,11 @@
logGesture(mInRejectedExclusion
? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED
: SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED);
+ if (!mInRejectedExclusion) {
+ // Log successful back gesture to contextual edu stats
+ mOverviewProxyService.updateContextualEduStats(mIsTrackpadThreeFingerSwipe,
+ GestureType.BACK);
+ }
}
@Override
@@ -1153,8 +1158,6 @@
if (mAllowGesture) {
if (mBackAnimation != null) {
mBackAnimation.onThresholdCrossed();
- mOverviewProxyService.updateContextualEduStats(
- mIsTrackpadThreeFingerSwipe, GestureType.BACK);
} else {
pilferPointers();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 66ac01a..51d2329 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -52,7 +52,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
@@ -62,7 +61,6 @@
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -191,58 +189,22 @@
val context = inflater.context
val composeView =
ComposeView(context).apply {
- setBackPressedDispatcher()
- setContent {
- PlatformTheme {
- val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
-
- AnimatedVisibility(
- visible = visible,
- modifier =
- Modifier.windowInsetsPadding(WindowInsets.navigationBars)
- .thenIf(notificationScrimClippingParams.isEnabled) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
+ repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ setViewTreeOnBackPressedDispatcherOwner(
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher =
+ OnBackPressedDispatcher().apply {
+ setOnBackInvokedDispatcher(
+ it.viewRootImpl.onBackInvokedDispatcher
)
}
- .graphicsLayer { elevation = 4.dp.toPx() },
- ) {
- val isEditing by
- viewModel.containerViewModel.editModeViewModel.isEditing
- .collectAsStateWithLifecycle()
- val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
- AnimatedContent(
- targetState = isEditing,
- transitionSpec = {
- fadeIn(animationSpecEditMode) togetherWith
- fadeOut(animationSpecEditMode)
- },
- label = "EditModeAnimatedContent",
- ) { editing ->
- if (editing) {
- val qqsPadding by
- viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
- EditMode(
- viewModel = viewModel.containerViewModel.editModeViewModel,
- modifier =
- Modifier.fillMaxWidth()
- .padding(top = { qqsPadding })
- .padding(
- horizontal = {
- QuickSettingsShade.Dimensions.Padding
- .roundToPx()
- }
- ),
- )
- } else {
- CollapsableQuickSettingsSTL()
- }
+
+ override val lifecycle: Lifecycle =
+ this@repeatWhenAttached.lifecycle
}
- }
+ )
+ setContent { this@QSFragmentCompose.Content() }
}
}
}
@@ -261,6 +223,58 @@
return frame
}
+ @Composable
+ private fun Content() {
+ PlatformTheme {
+ val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
+
+ AnimatedVisibility(
+ visible = visible,
+ modifier =
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
+ notificationScrimClippingParams.isEnabled
+ ) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ },
+ ) {
+ val isEditing by
+ viewModel.containerViewModel.editModeViewModel.isEditing
+ .collectAsStateWithLifecycle()
+ val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
+ AnimatedContent(
+ targetState = isEditing,
+ transitionSpec = {
+ fadeIn(animationSpecEditMode) togetherWith fadeOut(animationSpecEditMode)
+ },
+ label = "EditModeAnimatedContent",
+ ) { editing ->
+ if (editing) {
+ val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+ EditMode(
+ viewModel = viewModel.containerViewModel.editModeViewModel,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = { qqsPadding })
+ .padding(
+ horizontal = {
+ QuickSettingsShade.Dimensions.Padding.roundToPx()
+ }
+ ),
+ )
+ } else {
+ CollapsableQuickSettingsSTL()
+ }
+ }
+ }
+ }
+ }
+
/**
* STL that contains both QQS (tiles) and QS (brightness, tiles, footer actions), but no Edit
* mode. It tracks [QSFragmentComposeViewModel.expansionState] to drive the transition between
@@ -649,23 +663,6 @@
}
}
-private fun View.setBackPressedDispatcher() {
- repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- setViewTreeOnBackPressedDispatcherOwner(
- object : OnBackPressedDispatcherOwner {
- override val onBackPressedDispatcher =
- OnBackPressedDispatcher().apply {
- setOnBackInvokedDispatcher(it.viewRootImpl.onBackInvokedDispatcher)
- }
-
- override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
- }
- )
- }
- }
-}
-
private suspend inline fun <Listener : Any, Data> setListenerJob(
listenerFlow: MutableStateFlow<Listener?>,
dataFlow: Flow<Data>,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index 4fdd90b..b5d45a4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -4,10 +4,10 @@
import android.graphics.Bitmap
import android.graphics.Insets
import android.graphics.Rect
-import android.net.Uri
import android.os.Process
import android.os.UserHandle
import android.view.Display
+import android.view.WindowManager
import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.ScreenshotType
import androidx.annotation.VisibleForTesting
@@ -26,11 +26,9 @@
var insets: Insets,
var bitmap: Bitmap?,
var displayId: Int,
- /** App-provided URL representing the content the user was looking at in the screenshot. */
- var contextUrl: Uri? = null,
) {
- val packageNameString: String
- get() = if (topComponent == null) "" else topComponent!!.packageName
+ val packageNameString
+ get() = topComponent?.packageName ?: ""
fun getUserOrDefault(): UserHandle {
return userHandle ?: Process.myUserHandle()
@@ -54,8 +52,8 @@
@VisibleForTesting
fun forTesting() =
ScreenshotData(
- type = 0,
- source = 0,
+ type = WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ source = ScreenshotSource.SCREENSHOT_KEY_CHORD,
userHandle = null,
topComponent = null,
screenBounds = null,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 448f7c4..ab8a953 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -20,9 +20,11 @@
import android.os.Trace
import android.util.Log
import android.view.Display
+import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.internal.logging.UiEventLogger
import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags.screenshotMultidisplayFocusChange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
@@ -40,7 +42,7 @@
suspend fun executeScreenshots(
screenshotRequest: ScreenshotRequest,
onSaved: (Uri?) -> Unit,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
fun onCloseSystemDialogsReceived()
@@ -52,7 +54,7 @@
fun executeScreenshotsAsync(
screenshotRequest: ScreenshotRequest,
onSaved: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
}
@@ -60,7 +62,7 @@
fun handleScreenshot(
screenshot: ScreenshotData,
finisher: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
}
@@ -75,7 +77,7 @@
@Inject
constructor(
private val interactiveScreenshotHandlerFactory: InteractiveScreenshotHandler.Factory,
- displayRepository: DisplayRepository,
+ private val displayRepository: DisplayRepository,
@Application private val mainScope: CoroutineScope,
private val screenshotRequestProcessor: ScreenshotRequestProcessor,
private val uiEventLogger: UiEventLogger,
@@ -95,31 +97,44 @@
override suspend fun executeScreenshots(
screenshotRequest: ScreenshotRequest,
onSaved: (Uri?) -> Unit,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
) {
- val displays = getDisplaysToScreenshot(screenshotRequest.type)
- val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
- if (displays.isEmpty()) {
- Log.wtf(TAG, "No displays found for screenshot.")
- }
- displays.forEach { display ->
- val displayId = display.displayId
- var screenshotHandler: ScreenshotHandler =
- if (displayId == Display.DEFAULT_DISPLAY) {
- getScreenshotController(display)
- } else {
- headlessScreenshotHandler
- }
- Log.d(TAG, "Executing screenshot for display $displayId")
+ if (screenshotMultidisplayFocusChange()) {
+ val display = getDisplayToScreenshot(screenshotRequest)
+ val screenshotHandler = getScreenshotController(display)
dispatchToController(
screenshotHandler,
- rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
- onSaved =
- if (displayId == Display.DEFAULT_DISPLAY) {
- onSaved
- } else { _ -> },
- callback = resultCallbackWrapper.createCallbackForId(displayId)
+ ScreenshotData.fromRequest(screenshotRequest, display.displayId),
+ onSaved,
+ requestCallback,
)
+ } else {
+ val displays = getDisplaysToScreenshot(screenshotRequest.type)
+ val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
+ if (displays.isEmpty()) {
+ Log.e(TAG, "No displays found for screenshot.")
+ }
+
+ displays.forEach { display ->
+ val displayId = display.displayId
+ var screenshotHandler: ScreenshotHandler =
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ getScreenshotController(display)
+ } else {
+ headlessScreenshotHandler
+ }
+
+ Log.d(TAG, "Executing screenshot for display $displayId")
+ dispatchToController(
+ screenshotHandler,
+ rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
+ onSaved =
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ onSaved
+ } else { _ -> },
+ callback = resultCallbackWrapper.createCallbackForId(displayId),
+ )
+ }
}
}
@@ -128,7 +143,7 @@
screenshotHandler: ScreenshotHandler,
rawScreenshotData: ScreenshotData,
onSaved: (Uri?) -> Unit,
- callback: RequestCallback
+ callback: RequestCallback,
) {
// Let's wait before logging "screenshot requested", as we should log the processed
// ScreenshotData.
@@ -160,13 +175,13 @@
uiEventLogger.log(
ScreenshotEvent.getScreenshotSource(screenshotData.source),
0,
- screenshotData.packageNameString
+ screenshotData.packageNameString,
)
}
private fun onFailedScreenshotRequest(
screenshotData: ScreenshotData,
- callback: RequestCallback
+ callback: RequestCallback,
) {
uiEventLogger.log(SCREENSHOT_CAPTURE_FAILED, 0, screenshotData.packageNameString)
getNotificationController(screenshotData.displayId)
@@ -184,6 +199,31 @@
}
}
+ // Return the single display to be screenshot based upon the request.
+ private suspend fun getDisplayToScreenshot(screenshotRequest: ScreenshotRequest): Display {
+ return when (screenshotRequest.source) {
+ ScreenshotSource.SCREENSHOT_OVERVIEW ->
+ // 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
+ // display
+ ScreenshotSource.SCREENSHOT_KEY_CHORD,
+ ScreenshotSource.SCREENSHOT_VENDOR_GESTURE ->
+ displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
+ ?: error("Can't find default display")
+
+ // All other invocations use the focused display
+ else -> focusedDisplay()
+ }
+ }
+
+ // TODO(b/367394043): Determine the focused display here.
+ private suspend fun focusedDisplay() =
+ displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display")
+
/** Propagates the close system dialog signal to the ScreenshotController. */
override fun onCloseSystemDialogsReceived() {
if (screenshotController?.isPendingSharedTransition() == false) {
@@ -214,7 +254,7 @@
override fun executeScreenshotsAsync(
screenshotRequest: ScreenshotRequest,
onSaved: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
) {
mainScope.launch {
executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
@@ -235,9 +275,7 @@
* - If any finished with an error, [reportError] of [originalCallback] is called
* - Otherwise, [onFinish] is called.
*/
- private class MultiResultCallbackWrapper(
- private val originalCallback: RequestCallback,
- ) {
+ private class MultiResultCallbackWrapper(private val originalCallback: RequestCallback) {
private val idsPending = mutableSetOf<Int>()
private val idsWithErrors = mutableSetOf<Int>()
@@ -290,7 +328,7 @@
Display.TYPE_EXTERNAL,
Display.TYPE_INTERNAL,
Display.TYPE_OVERLAY,
- Display.TYPE_WIFI
+ Display.TYPE_WIFI,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4f47536..f83548d 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 @@
// 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/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index bbcf10b..5b82772 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -13,10 +13,9 @@
per-file *Repository* = set noparent
per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com
-per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
-per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
+per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com
-per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
-per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
+per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
+per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 7244f8a..e47952f 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.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 @@
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
+ private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+
@Inject
public NotificationLockscreenUserManagerImpl(Context context,
BroadcastDispatcher broadcastDispatcher,
@@ -305,7 +309,8 @@
SecureSettings secureSettings,
DumpManager dumpManager,
LockPatternUtils lockPatternUtils,
- FeatureFlagsClassic featureFlags) {
+ FeatureFlagsClassic featureFlags,
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
mContext = context;
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
@@ -325,6 +330,7 @@
mSecureSettings = secureSettings;
mKeyguardStateController = keyguardStateController;
mFeatureFlags = featureFlags;
+ mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
mLockScreenUris.add(SHOW_LOCKSCREEN);
mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN);
@@ -748,8 +754,13 @@
// 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 5d14be8..73ad0e5 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 @@
* 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 @@
// 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 @@
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/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 97add30..a24f267 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -91,7 +91,6 @@
constructor(
private val context: Context,
private val featureFlags: FeatureFlags,
- private val smartspaceManager: SmartspaceManager?,
private val activityStarter: ActivityStarter,
private val falsingManager: FalsingManager,
private val systemClock: SystemClock,
@@ -124,6 +123,7 @@
private const val MAX_RECENT_SMARTSPACE_DATA_FOR_DUMP = 5
}
+ private var userSmartspaceManager: SmartspaceManager? = null
private var session: SmartspaceSession? = null
private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
@@ -461,7 +461,11 @@
}
private fun connectSession() {
- if (smartspaceManager == null) return
+ if (userSmartspaceManager == null) {
+ userSmartspaceManager =
+ userTracker.userContext.getSystemService(SmartspaceManager::class.java)
+ }
+ if (userSmartspaceManager == null) return
if (datePlugin == null && weatherPlugin == null && plugin == null) return
if (session != null || smartspaceViews.isEmpty()) {
return
@@ -474,12 +478,14 @@
return
}
- val newSession = smartspaceManager.createSmartspaceSession(
- SmartspaceConfig.Builder(
- context, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD).build())
+ val newSession = userSmartspaceManager?.createSmartspaceSession(
+ SmartspaceConfig.Builder(
+ userTracker.userContext, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD
+ ).build()
+ )
Log.d(TAG, "Starting smartspace session for " +
BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
- newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
+ newSession?.addOnTargetsAvailableListener(uiExecutor, sessionListener)
this.session = newSession
deviceProvisionedController.removeCallback(deviceProvisionedListener)
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 5f4f72f..0474344 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 @@
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 @@
mFingerprintManager = fingerprintManager;
mActivityStarter = activityStarter;
mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor;
- mGlanceableHubContainerController = glanceableHubContainerController;
+ if (!SceneContainerFlag.isEnabled()) {
+ mGlanceableHubContainerController = glanceableHubContainerController;
+ } else {
+ mGlanceableHubContainerController = null;
+ }
mEmergencyGestureIntentFactory = emergencyGestureIntentFactory;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
@@ -2972,7 +2978,9 @@
@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/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 1ea26e5..5ae24f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -63,6 +63,7 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
import com.android.systemui.bouncer.ui.BouncerView;
+import com.android.systemui.bouncer.util.BouncerTestUtilsKt;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
@@ -1552,8 +1553,10 @@
}
public boolean shouldDismissOnMenuPressed() {
- return mPrimaryBouncerView.getDelegate() != null
- && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
+ return (mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed()) || (
+ ComposeBouncerFlags.INSTANCE.isEnabled() && BouncerTestUtilsKt.shouldEnableMenuKey(
+ mContext.getResources()));
}
public boolean interceptMediaKey(KeyEvent event) {
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 b1754fd..200f080 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 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.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 @@
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 @@
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 @@
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 @@
}
}
+ /**
+ * 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 da928a3..3cf2066 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 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 @@
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 @@
@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 7c055c8..7f90242 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 @@
): 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 079c72f..1f92bc1 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.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 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 @@
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 @@
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 @@
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 @@
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);
+ int stream;
+ synchronized (mRemoteStreams) {
+ if (!mRemoteStreams.containsKey(token)) {
+ Log.d(
+ TAG,
+ "onRemoteRemoved: stream doesn't exist, "
+ + "aborting remote removed for token:"
+ + token.toString());
+ return;
}
- mState.states.remove(stream);
- if (mState.activeStream == stream) {
- updateActiveStreamW(-1);
- }
- 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 @@
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/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index ebb9ce9..ed8de69 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -23,6 +23,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogManager;
import com.android.systemui.plugins.VolumeDialog;
@@ -40,6 +41,8 @@
import com.android.systemui.volume.VolumeDialogImpl;
import com.android.systemui.volume.VolumePanelDialogReceiver;
import com.android.systemui.volume.VolumeUI;
+import com.android.systemui.volume.dialog.VolumeDialogPlugin;
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent;
import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.dagger.VolumePanelComponent;
@@ -66,7 +69,8 @@
SpatializerModule.class,
},
subcomponents = {
- VolumePanelComponent.class
+ VolumePanelComponent.class,
+ VolumeDialogPluginComponent.class,
}
)
public interface VolumeModule {
@@ -101,6 +105,7 @@
/** */
@Provides
static VolumeDialog provideVolumeDialog(
+ Lazy<VolumeDialogPlugin> volumeDialogProvider,
Context context,
VolumeDialogController volumeDialogController,
AccessibilityManagerWrapper accessibilityManagerWrapper,
@@ -118,29 +123,33 @@
VibratorHelper vibratorHelper,
SystemClock systemClock,
VolumeDialogInteractor interactor) {
- VolumeDialogImpl impl = new VolumeDialogImpl(
- context,
- volumeDialogController,
- accessibilityManagerWrapper,
- deviceProvisionedController,
- configurationController,
- mediaOutputDialogManager,
- interactionJankMonitor,
- volumePanelNavigationInteractor,
- volumeNavigator,
- true, /* should listen for jank */
- csdFactory,
- devicePostureController,
- Looper.getMainLooper(),
- volumePanelFlag,
- dumpManager,
- secureSettings,
- vibratorHelper,
- systemClock,
- interactor);
- impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
- impl.setAutomute(true);
- impl.setSilentMode(false);
- return impl;
+ if (Flags.volumeRedesign()) {
+ return volumeDialogProvider.get();
+ } else {
+ VolumeDialogImpl impl = new VolumeDialogImpl(
+ context,
+ volumeDialogController,
+ accessibilityManagerWrapper,
+ deviceProvisionedController,
+ configurationController,
+ mediaOutputDialogManager,
+ interactionJankMonitor,
+ volumePanelNavigationInteractor,
+ volumeNavigator,
+ true, /* should listen for jank */
+ csdFactory,
+ devicePostureController,
+ Looper.getMainLooper(),
+ volumePanelFlag,
+ dumpManager,
+ secureSettings,
+ vibratorHelper,
+ systemClock,
+ interactor);
+ impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
+ impl.setAutomute(true);
+ impl.setSilentMode(false);
+ return impl;
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
new file mode 100644
index 0000000..869b3c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
@@ -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 com.android.systemui.volume.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class NewVolumeDialog @Inject constructor(@Application context: Context) :
+ Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.volume_dialog)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
new file mode 100644
index 0000000..b93714a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+class NewVolumeDialogPlugin
+@Inject
+constructor(
+ @Application private val applicationCoroutineScope: CoroutineScope,
+ private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
+) : VolumeDialog {
+
+ private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
+ private var job: Job? = null
+
+ override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
+ job =
+ applicationCoroutineScope.launch {
+ coroutineScope {
+ volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
+ }
+ }
+ }
+
+ private fun showDialog() {
+ val volumeDialogPluginComponent =
+ volumeDialogPluginComponent ?: error("Creating dialog before init was called")
+ volumeDialogPluginComponent.coroutineScope().launch {
+ coroutineScope {
+ val volumeDialogComponent: VolumeDialogComponent =
+ volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
+ with(volumeDialogComponent.volumeDialog()) {
+ setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
+ show()
+ }
+ }
+ }
+ }
+
+ override fun destroy() {
+ job?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
new file mode 100644
index 0000000..74e823e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -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 com.android.systemui.volume.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class VolumeDialog @Inject constructor(@Application context: Context) :
+ Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.volume_dialog)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
new file mode 100644
index 0000000..a2e81d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+class VolumeDialogPlugin
+@Inject
+constructor(
+ @Application private val applicationCoroutineScope: CoroutineScope,
+ private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
+) : VolumeDialog {
+
+ private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
+ private var job: Job? = null
+
+ override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
+ job =
+ applicationCoroutineScope.launch {
+ coroutineScope {
+ volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
+ }
+ }
+ }
+
+ private fun showDialog() {
+ val volumeDialogPluginComponent =
+ volumeDialogPluginComponent ?: error("Creating dialog before init was called")
+ volumeDialogPluginComponent.coroutineScope().launch {
+ coroutineScope {
+ val volumeDialogComponent: VolumeDialogComponent =
+ volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
+ with(volumeDialogComponent.volumeDialog()) {
+ setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
+ show()
+ }
+ }
+ }
+ }
+
+ override fun destroy() {
+ job?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
new file mode 100644
index 0000000..f7ad320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.volume.dialog.dagger
+
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import dagger.BindsInstance
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Core Volume Dialog dagger component. It's managed by
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it.
+ */
+@VolumeDialogScope
+@Subcomponent(modules = [])
+interface VolumeDialogComponent {
+
+ /**
+ * Provides a coroutine scope to use inside [VolumeDialogScope].
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
+ * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
+ * dialog is not shown.
+ */
+ @VolumeDialog fun coroutineScope(): CoroutineScope
+
+ @VolumeDialogScope fun volumeDialog(): com.android.systemui.volume.dialog.VolumeDialog
+
+ @Subcomponent.Factory
+ interface Factory {
+
+ fun create(@BindsInstance @VolumeDialog scope: CoroutineScope): VolumeDialogComponent
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
new file mode 100644
index 0000000..82612a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.volume.dialog.dagger
+
+import com.android.systemui.volume.dialog.dagger.module.VolumeDialogPluginModule
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import dagger.BindsInstance
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Volume Dialog plugin dagger component. It's managed by
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it.
+ */
+@VolumeDialogPluginScope
+@Subcomponent(modules = [VolumeDialogPluginModule::class])
+interface VolumeDialogPluginComponent {
+
+ /**
+ * Provides a coroutine scope to use inside [VolumeDialogPluginScope].
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
+ * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
+ * dialog is not shown.
+ */
+ @VolumeDialogPlugin fun coroutineScope(): CoroutineScope
+
+ fun volumeDialogComponentFactory(): VolumeDialogComponent.Factory
+
+ @Subcomponent.Factory
+ interface Factory {
+
+ fun create(
+ @BindsInstance @VolumeDialogPlugin scope: CoroutineScope
+ ): VolumeDialogPluginComponent
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
new file mode 100644
index 0000000..3fdf86a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.volume.dialog.dagger.module
+
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import dagger.Module
+
+@Module(subcomponents = [VolumeDialogComponent::class]) interface VolumeDialogPluginModule
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt
new file mode 100644
index 0000000..34bddb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Qualifier
+
+/**
+ * Volume Dialog qualifier.
+ *
+ * @see VolumeDialogScope
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class VolumeDialog
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt
new file mode 100644
index 0000000..1038c30
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Qualifier
+
+/**
+ * Volume Dialog plugin qualifier.
+ *
+ * @see VolumeDialogPluginScope
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class VolumeDialogPlugin
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt
new file mode 100644
index 0000000..6c5f672
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Scope
+
+/**
+ * Volume Dialog plugin dependency injection scope. This scope is created alongside Volume Dialog
+ * plugin is initialized and destroyed alongside it. This is effectively almost similar
+ * to @Application now.
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Scope
+annotation class VolumeDialogPluginScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt
new file mode 100644
index 0000000..52caa6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Scope
+
+/**
+ * Volume Panel dependency injection scope. This scope is created alongside Volume Panel and
+ * destroyed when it's lo longer present.
+ */
+@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class VolumeDialogScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
new file mode 100644
index 0000000..ec7c6ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.annotation.SuppressLint
+import android.os.Handler
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.shareIn
+
+private const val BUFFER_CAPACITY = 16
+
+/**
+ * Exposes [VolumeDialogController] callback events in the [event].
+ *
+ * @see VolumeDialogController.Callbacks
+ */
+@VolumeDialog
+class VolumeDialogCallbacksInteractor
+@Inject
+constructor(
+ private val volumeDialogController: VolumeDialogController,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @Background private val bgHandler: Handler,
+) {
+
+ @SuppressLint("SharedFlowCreation") // event-but needed
+ val event: Flow<VolumeDialogEventModel> =
+ callbackFlow {
+ val producer = VolumeDialogEventModelProducer(this)
+ volumeDialogController.addCallback(producer, bgHandler)
+ awaitClose { volumeDialogController.removeCallback(producer) }
+ }
+ .buffer(BUFFER_CAPACITY)
+ .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed())
+
+ private class VolumeDialogEventModelProducer(
+ private val scope: ProducerScope<VolumeDialogEventModel>
+ ) : VolumeDialogController.Callbacks {
+ override fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
+ scope.trySend(
+ VolumeDialogEventModel.ShowRequested(
+ reason = reason,
+ keyguardLocked = keyguardLocked,
+ lockTaskModeState = lockTaskModeState,
+ )
+ )
+ }
+
+ override fun onDismissRequested(reason: Int) {
+ scope.trySend(VolumeDialogEventModel.DismissRequested(reason))
+ }
+
+ override fun onStateChanged(state: VolumeDialogController.State?) {
+ if (state != null) {
+ scope.trySend(VolumeDialogEventModel.StateChanged(VolumeDialogStateModel(state)))
+ }
+ }
+
+ override fun onLayoutDirectionChanged(layoutDirection: Int) {
+ scope.trySend(VolumeDialogEventModel.LayoutDirectionChanged(layoutDirection))
+ }
+
+ // Configuration change is never emitted by the VolumeDialogControllerImpl now.
+ override fun onConfigurationChanged() = Unit
+
+ override fun onShowVibrateHint() {
+ scope.trySend(VolumeDialogEventModel.ShowVibrateHint)
+ }
+
+ override fun onShowSilentHint() {
+ scope.trySend(VolumeDialogEventModel.ShowSilentHint)
+ }
+
+ override fun onScreenOff() {
+ scope.trySend(VolumeDialogEventModel.ScreenOff)
+ }
+
+ override fun onShowSafetyWarning(flags: Int) {
+ scope.trySend(VolumeDialogEventModel.ShowSafetyWarning(flags))
+ }
+
+ override fun onAccessibilityModeChanged(showA11yStream: Boolean) {
+ scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream))
+ }
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionComponentStateChanged(
+ isComponentEnabled: Boolean,
+ fromTooltip: Boolean,
+ ) = Unit
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) =
+ Unit
+
+ override fun onShowCsdWarning(csdWarning: Int, durationMs: Int) {
+ scope.trySend(
+ VolumeDialogEventModel.ShowCsdWarning(
+ csdWarning = csdWarning,
+ durationMs = durationMs,
+ )
+ )
+ }
+
+ override fun onVolumeChangedFromKey() {
+ scope.trySend(VolumeDialogEventModel.VolumeChangedFromKey)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
new file mode 100644
index 0000000..dd51108
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Exposes [VolumeDialogController.getState] in the [volumeDialogState].
+ *
+ * @see [VolumeDialogController]
+ */
+@VolumeDialog
+class VolumeDialogStateInteractor
+@Inject
+constructor(
+ volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
+ private val volumeDialogController: VolumeDialogController,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+) {
+
+ val volumeDialogState: Flow<VolumeDialogStateModel> =
+ volumeDialogCallbacksInteractor.event
+ .onStart { volumeDialogController.getState() }
+ .filterIsInstance(VolumeDialogEventModel.StateChanged::class)
+ .map { it.state }
+ .stateIn(scope = coroutineScope, started = SharingStarted.Eagerly, initialValue = null)
+ .filterNotNull()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
new file mode 100644
index 0000000..ca0310e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.volume.dialog.domain.model
+
+import android.media.AudioManager
+
+/**
+ * Models VolumeDialogController callback events.
+ *
+ * @see VolumeDialogController.Callbacks
+ */
+sealed interface VolumeDialogEventModel {
+
+ data class ShowRequested(
+ val reason: Int,
+ val keyguardLocked: Boolean,
+ val lockTaskModeState: Int,
+ ) : VolumeDialogEventModel
+
+ data class DismissRequested(val reason: Int) : VolumeDialogEventModel
+
+ data class StateChanged(val state: VolumeDialogStateModel) : VolumeDialogEventModel
+
+ data class LayoutDirectionChanged(val layoutDirection: Int) : VolumeDialogEventModel
+
+ data object ShowVibrateHint : VolumeDialogEventModel
+
+ data object ShowSilentHint : VolumeDialogEventModel
+
+ data object ScreenOff : VolumeDialogEventModel
+
+ data class ShowSafetyWarning(val flags: Int) : VolumeDialogEventModel
+
+ data class AccessibilityModeChanged(val showA11yStream: Boolean) : VolumeDialogEventModel
+
+ data class ShowCsdWarning(@AudioManager.CsdWarning val csdWarning: Int, val durationMs: Int) :
+ VolumeDialogEventModel
+
+ data object VolumeChangedFromKey : VolumeDialogEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
new file mode 100644
index 0000000..f1443e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog.domain.model
+
+import android.content.ComponentName
+import android.util.SparseArray
+import androidx.core.util.keyIterator
+import com.android.systemui.plugins.VolumeDialogController
+
+/** Models a state of the Volume Dialog. */
+data class VolumeDialogStateModel(
+ val states: Map<Int, VolumeDialogStreamStateModel>,
+ val ringerModeInternal: Int = 0,
+ val ringerModeExternal: Int = 0,
+ val zenMode: Int = 0,
+ val effectsSuppressor: ComponentName? = null,
+ val effectsSuppressorName: String? = null,
+ val activeStream: Int = NO_ACTIVE_STREAM,
+ val disallowAlarms: Boolean = false,
+ val disallowMedia: Boolean = false,
+ val disallowSystem: Boolean = false,
+ val disallowRinger: Boolean = false,
+) {
+
+ constructor(
+ legacyState: VolumeDialogController.State
+ ) : this(
+ states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) },
+ ringerModeInternal = legacyState.ringerModeInternal,
+ ringerModeExternal = legacyState.ringerModeExternal,
+ zenMode = legacyState.zenMode,
+ effectsSuppressor = legacyState.effectsSuppressor,
+ effectsSuppressorName = legacyState.effectsSuppressorName,
+ activeStream = legacyState.activeStream,
+ disallowAlarms = legacyState.disallowAlarms,
+ disallowMedia = legacyState.disallowMedia,
+ disallowSystem = legacyState.disallowSystem,
+ disallowRinger = legacyState.disallowRinger,
+ )
+
+ companion object {
+ const val NO_ACTIVE_STREAM: Int = -1
+ }
+}
+
+private fun <INPUT, OUTPUT> SparseArray<INPUT>.mapToMap(map: (INPUT) -> OUTPUT): Map<Int, OUTPUT> {
+ val resultMap = mutableMapOf<Int, OUTPUT>()
+ for (key in keyIterator()) {
+ val mappedValue: OUTPUT = map(get(key)!!)
+ resultMap[key] = mappedValue
+ }
+ return resultMap
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
new file mode 100644
index 0000000..a9d367d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.volume.dialog.domain.model
+
+import android.annotation.IntegerRes
+import com.android.systemui.plugins.VolumeDialogController
+
+/** Models a state of an audio stream of the Volume Dialog. */
+data class VolumeDialogStreamStateModel(
+ val isDynamic: Boolean = false,
+ val level: Int = 0,
+ val levelMin: Int = 0,
+ val levelMax: Int = 0,
+ val muted: Boolean = false,
+ val muteSupported: Boolean = false,
+ @IntegerRes val name: Int = 0,
+ val remoteLabel: String? = null,
+ val routedToBluetooth: Boolean = false,
+) {
+ constructor(
+ legacyState: VolumeDialogController.StreamState
+ ) : this(
+ isDynamic = legacyState.dynamic,
+ level = legacyState.level,
+ levelMin = legacyState.levelMin,
+ levelMax = legacyState.levelMax,
+ muted = legacyState.muted,
+ muteSupported = legacyState.muteSupported,
+ name = legacyState.name,
+ remoteLabel = legacyState.remoteLabel,
+ routedToBluetooth = legacyState.routedToBluetooth,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
new file mode 100644
index 0000000..700225d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.volume.dialog.ui.binder
+
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+
+class VolumeDialogViewBinder
+@Inject
+constructor(private val volumeDialogViewModelFactory: VolumeDialogViewModel.Factory) {
+
+ suspend fun bind(view: View) {
+ view.repeatWhenAttached {
+ view.viewModel(
+ traceName = "VolumeDialogViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { volumeDialogViewModelFactory.create() },
+ ) { viewModel ->
+ view.setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
new file mode 100644
index 0000000..f9e91ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
+
+ override suspend fun onActivated(): Nothing {
+ TODO("Not yet implemented")
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): VolumeDialogViewModel
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index e609d5f..34ebba8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -127,6 +127,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated;
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
+import com.android.keyguard.logging.SimLogger;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
@@ -267,6 +268,8 @@
@Mock
private KeyguardUpdateMonitorLogger mKeyguardUpdateMonitorLogger;
@Mock
+ private SimLogger mSimLogger;
+ @Mock
private SessionTracker mSessionTracker;
@Mock
private UiEventLogger mUiEventLogger;
@@ -2234,6 +2237,36 @@
}
@Test
+ public void testOnSimStateChanged_LockedToNotReadyToLocked() {
+ int validSubId = 10;
+ int slotId = 0;
+
+ KeyguardUpdateMonitorCallback keyguardUpdateMonitorCallback = spy(
+ KeyguardUpdateMonitorCallback.class);
+ mKeyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback);
+ // Initially locked
+ mKeyguardUpdateMonitor.handleSimStateChange(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+
+ reset(keyguardUpdateMonitorCallback);
+ // Not ready, with invalid sub id
+ mKeyguardUpdateMonitor.handleSimStateChange(SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ slotId, TelephonyManager.SIM_STATE_NOT_READY);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID, slotId,
+ TelephonyManager.SIM_STATE_NOT_READY);
+
+ reset(keyguardUpdateMonitorCallback);
+ // Back to PIN required, which notifies listeners
+ mKeyguardUpdateMonitor.handleSimStateChange(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ }
+
+ @Test
public void onAuthEnrollmentChangesCallbacksAreNotified() {
KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
ArgumentCaptor<AuthController.Callback> authCallback = ArgumentCaptor.forClass(
@@ -2487,7 +2520,7 @@
mStatusBarStateController, mLockPatternUtils,
mAuthController, mTelephonyListenerManager,
mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
- mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
+ mKeyguardUpdateMonitorLogger, mSimLogger, mUiEventLogger, () -> mSessionTracker,
mTrustManager, mSubscriptionManager, mUserManager,
mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
mPackageManager, mFingerprintManager, mBiometricManager,
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 5fc1971..8075d11 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 @@
@Mock
private Animator mAnimator;
+ @Mock
+ private Animator mEndAnimator;
private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor =
ArgumentCaptor.forClass(Animator.AnimatorListener.class);
@@ -123,7 +125,7 @@
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 @@
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 @@
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 @@
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 ad25502..7d5a334 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 @@
)
}
+ @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 @@
}
}
+ 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 @@
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 @@
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 a295981..0bea560 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,6 +3,8 @@
import android.content.ComponentName
import android.graphics.Bitmap
import android.net.Uri
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.Display.TYPE_INTERNAL
@@ -15,6 +17,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.display.data.repository.display
@@ -75,6 +78,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -106,6 +110,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -115,7 +120,7 @@
screenshotExecutor.executeScreenshots(
createScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE),
onSaved,
- callback
+ callback,
)
verify(controllerFactory).create(eq(internalDisplay))
@@ -137,6 +142,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
testScope.runTest {
setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -149,6 +155,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_allowedTypes_allCaptured() =
testScope.runTest {
whenever(controllerFactory.create(any())).thenReturn(controller)
@@ -157,7 +164,7 @@
display(TYPE_INTERNAL, id = 0),
display(TYPE_EXTERNAL, id = 1),
display(TYPE_OVERLAY, id = 2),
- display(TYPE_WIFI, id = 3)
+ display(TYPE_WIFI, id = 3),
)
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -168,6 +175,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -193,6 +201,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -220,6 +229,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_allDisplaysFail_reportsFail() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -247,6 +257,58 @@
}
@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))
@@ -319,6 +381,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -336,6 +399,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_errorFromProcessor_logsUiError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -379,7 +443,8 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_reportsRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -399,7 +464,27 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_reportsError() =
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ val screenshotRequested =
+ eventLogger.logs.filter {
+ it.eventId == ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id
+ }
+ assertThat(screenshotRequested).hasSize(1)
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_reportsError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -419,7 +504,27 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_reportsError() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ val screenshotRequested =
+ eventLogger.logs.filter {
+ it.eventId == ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED.id
+ }
+ assertThat(screenshotRequested).hasSize(1)
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_showsErrorNotification() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -436,6 +541,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ verify(notificationsController0).notifyScreenshotError(any())
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
fun executeScreenshots_finisherCalledWithNullUri_succeeds() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -459,9 +579,14 @@
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 0000000..040a9e9
--- /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/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 7a8533e..fe287ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -23,6 +23,7 @@
import android.app.smartspace.SmartspaceTarget
import android.content.ComponentName
import android.content.ContentResolver
+import android.content.Context
import android.content.pm.UserInfo
import android.database.ContentObserver
import android.graphics.drawable.Drawable
@@ -207,6 +208,9 @@
private val userHandleManaged: UserHandle = UserHandle(2)
private val userHandleSecondary: UserHandle = UserHandle(3)
+ @Mock private lateinit var userContextPrimary: Context
+ @Mock private lateinit var userContextSecondary: Context
+
private val userList = listOf(
mockUserInfo(userHandlePrimary, isManagedProfile = false),
mockUserInfo(userHandleManaged, isManagedProfile = true),
@@ -234,7 +238,11 @@
`when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
`when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
- setActiveUser(userHandlePrimary)
+ `when`(userContextPrimary.getSystemService(SmartspaceManager::class.java)).thenReturn(
+ smartspaceManager
+ )
+
+ setActiveUser(userHandlePrimary, userContextPrimary)
setAllowPrivateNotifications(userHandlePrimary, true)
setAllowPrivateNotifications(userHandleManaged, true)
setAllowPrivateNotifications(userHandleSecondary, true)
@@ -252,7 +260,6 @@
controller = LockscreenSmartspaceController(
context,
featureFlags,
- smartspaceManager,
activityStarter,
falsingManager,
clock,
@@ -709,7 +716,8 @@
connectSession()
// WHEN the secondary user becomes the active user
- setActiveUser(userHandleSecondary)
+ // Note: it doesn't switch to the SmartspaceManager for userContextSecondary
+ setActiveUser(userHandleSecondary, userContextSecondary)
userListener.onUserChanged(userHandleSecondary.identifier, context)
// WHEN we receive a new list of targets
@@ -912,9 +920,10 @@
clearInvocations(smartspaceView)
}
- private fun setActiveUser(userHandle: UserHandle) {
+ private fun setActiveUser(userHandle: UserHandle, userContext: Context) {
`when`(userTracker.userId).thenReturn(userHandle.identifier)
`when`(userTracker.userHandle).thenReturn(userHandle)
+ `when`(userTracker.userContext).thenReturn(userContext)
}
private fun mockUserInfo(userHandle: UserHandle, isManagedProfile: Boolean): UserInfo {
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 c523819..81c40dc 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.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.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 @@
@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 @@
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 f62beeb..beba0f0 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 @@
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 5d7e7c7..1302faa 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 @@
provider: ComponentName,
user: UserHandle,
rank: Int?,
- configurator: WidgetConfigurator?
+ configurator: WidgetConfigurator?,
) {
coroutineScope.launch {
val id = nextWidgetId++
@@ -93,6 +93,22 @@
_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 b37cac1..ba31683 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 @@
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 @@
// 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 @@
_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/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt
new file mode 100644
index 0000000..e4c793d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.util
+
+import android.testing.UiThreadTest
+import org.junit.Assert.fail
+import org.junit.rules.MethodRule
+import org.junit.runners.model.FrameworkMethod
+import org.junit.runners.model.Statement
+
+/**
+ * A Test rule which prevents us from using the UiThreadTest annotation. See
+ * go/android_junit4_uithreadtest (b/352170965)
+ */
+public class NoUiThreadTestRule : MethodRule {
+ override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement? {
+ if (hasUiThreadAnnotation(method, target)) {
+ fail("UiThreadTest doesn't actually run on the UiThread")
+ }
+ return base
+ }
+
+ private fun hasUiThreadAnnotation(method: FrameworkMethod, target: Any): Boolean {
+ if (method.getAnnotation(UiThreadTest::class.java) != null) {
+ return true
+ } else {
+ return target.javaClass.getAnnotation(UiThreadTest::class.java) != null
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt
new file mode 100644
index 0000000..70dd103
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.util
+
+import android.testing.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.lang.AssertionError
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.model.FrameworkMethod
+import org.junit.runners.model.Statement
+
+/**
+ * Test that NoUiThreadTestRule asserts when it finds a framework method with a UiThreadTest
+ * annotation.
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public class NoUiThreadTestRuleTest : SysuiTestCase() {
+
+ class TestStatement : Statement() {
+ override fun evaluate() {}
+ }
+
+ inner class TestInner {
+ @Test @UiThreadTest fun simpleUiTest() {}
+
+ @Test fun simpleTest() {}
+ }
+
+ /**
+ * Test that NoUiThreadTestRule throws an asserts false if a test is annotated
+ * with @UiThreadTest
+ */
+ @Test(expected = AssertionError::class)
+ fun testNoUiThreadFail() {
+ val method = TestInner::class.java.getDeclaredMethod("simpleUiTest")
+ val frameworkMethod = FrameworkMethod(method)
+ val noUiThreadTestRule = NoUiThreadTestRule()
+ val testStatement = TestStatement()
+ // target needs to be non-null
+ val obj = Object()
+ noUiThreadTestRule.apply(testStatement, frameworkMethod, obj)
+ }
+
+ /**
+ * Test that NoUiThreadTestRule throws an asserts false if a test is annotated
+ * with @UiThreadTest
+ */
+ fun testNoUiThreadOK() {
+ val method = TestInner::class.java.getDeclaredMethod("simpleUiTest")
+ val frameworkMethod = FrameworkMethod(method)
+ val noUiThreadTestRule = NoUiThreadTestRule()
+ val testStatement = TestStatement()
+
+ // because target needs to be non-null
+ val obj = Object()
+ val newStatement = noUiThreadTestRule.apply(testStatement, frameworkMethod, obj)
+ Assert.assertEquals(newStatement, testStatement)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java
new file mode 100644
index 0000000..f81b7de
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * A class to launch runnables on the UI thread explicitly.
+ */
+public class UiThread {
+ private static final String TAG = "UiThread";
+
+ /**
+ * Run a runnable on the UI thread using instrumentation.runOnMainSync.
+ *
+ * @param runnable code to run on the UI thread.
+ * @throws Throwable if the code threw an exception, so it can be reported
+ * to the test.
+ */
+ public static void runOnUiThread(final Runnable runnable) throws Throwable {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ Log.w(
+ TAG,
+ "UiThread.runOnUiThread() should not be called from the "
+ + "main application thread");
+ runnable.run();
+ } else {
+ FutureTask<Void> task = new FutureTask<>(runnable, null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(task);
+ try {
+ task.get();
+ } catch (ExecutionException e) {
+ // Expose the original exception
+ throw e.getCause();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java
new file mode 100644
index 0000000..abf2e8d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import android.os.Looper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Test that UiThread.runOnUiThread() actually runs on the UI Thread.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UiThreadRunTest extends SysuiTestCase {
+
+ @Test
+ public void testUiThread() throws Throwable {
+ UiThread.runOnUiThread(() -> {
+ Assert.assertEquals(Looper.getMainLooper().getThread(), Thread.currentThread());
+ });
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c87d516..ab9cc20 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 @@
.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 3592ed5..5393b93 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 @@
* {@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 @@
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 8b6251a..e85a70d 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 @@
public AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction(
int callingUid,
int callingPid,
+ @NonNull UserHandle targetUser,
@NonNull String callerPackageName,
@NonNull String targetPackageName,
@NonNull String functionId) {
@@ -122,7 +123,10 @@
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/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 3499a3a..0ca3b56 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -5062,6 +5062,8 @@
Log.e(TAG, String.format(tmpl, activityName, pkgName, mAccountType));
return false;
}
+ intent.setComponent(targetActivityInfo.getComponentName());
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return true;
} finally {
Binder.restoreCallingIdentity(bid);
@@ -5083,14 +5085,15 @@
Bundle simulateBundle = p.readBundle();
p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
- if (intent != null && intent.getClass() != Intent.class) {
- return false;
- }
Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
Intent.class);
if (intent == null) {
return (simulateIntent == null);
}
+ if (intent.getClass() != Intent.class || simulateIntent.getClass() != Intent.class) {
+ return false;
+ }
+
if (!intent.filterEquals(simulateIntent)) {
return false;
}
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 71b6456..aca6d0b 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -1005,7 +1005,8 @@
case (int) AppsStartInfoProto.Package.USERS:
AppStartInfoContainer container =
new AppStartInfoContainer(mAppStartInfoHistoryListSize);
- int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS);
+ int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS,
+ pkgName);
synchronized (mLock) {
mData.put(pkgName, uid, container);
}
@@ -1403,7 +1404,7 @@
proto.end(token);
}
- int readFromProto(ProtoInputStream proto, long fieldId)
+ int readFromProto(ProtoInputStream proto, long fieldId, String packageName)
throws IOException, WireTypeMismatchException, ClassNotFoundException {
long token = proto.start(fieldId);
for (int next = proto.nextField();
@@ -1418,6 +1419,7 @@
// have a create time.
ApplicationStartInfo info = new ApplicationStartInfo(0);
info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+ info.setPackageName(packageName);
mInfos.add(info);
break;
case (int) AppsStartInfoProto.Package.User.MONITORING_ENABLED:
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 561030e..f9e8392 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1583,8 +1583,11 @@
synchronized (mCachedAbsVolDrivingStreamsLock) {
mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
- mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
- stream);
+ boolean enabled = true;
+ if (dev == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
+ enabled = mAvrcpAbsVolSupported;
+ }
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", enabled, stream);
});
}
}
@@ -4881,7 +4884,7 @@
if (absDev == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
enabled = mAvrcpAbsVolSupported;
}
- if (stream != streamType) {
+ if (stream != streamType || !enabled) {
mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/"",
enabled, streamType);
}
@@ -10383,10 +10386,10 @@
}
/*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) {
- mAvrcpAbsVolSupported = support;
- if (absVolumeIndexFix()) {
- int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
- synchronized (mCachedAbsVolDrivingStreamsLock) {
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mAvrcpAbsVolSupported = support;
+ if (absVolumeIndexFix()) {
+ int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> {
if (!mAvrcpAbsVolSupported) {
mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
@@ -12499,6 +12502,12 @@
pw.println("\nLoudness alignment:");
mLoudnessCodecHelper.dump(pw);
+ pw.println("\nAbsolute voume devices:");
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.forEach((dev, stream) -> pw.println(
+ "Device type: 0x" + Integer.toHexString(dev) + ", driving stream " + stream));
+ }
+
mAudioSystem.dump(pw);
}
diff --git a/services/core/java/com/android/server/display/DisplayGroup.java b/services/core/java/com/android/server/display/DisplayGroup.java
index 2dcd5cc..f73b66c 100644
--- a/services/core/java/com/android/server/display/DisplayGroup.java
+++ b/services/core/java/com/android/server/display/DisplayGroup.java
@@ -87,4 +87,14 @@
int getIdLocked(int index) {
return mDisplays.get(index).getDisplayIdLocked();
}
+
+ /** Returns the IDs of the {@link LogicalDisplay}s belonging to the DisplayGroup. */
+ int[] getIdsLocked() {
+ final int numDisplays = mDisplays.size();
+ final int[] displayIds = new int[numDisplays];
+ for (int i = 0; i < numDisplays; i++) {
+ displayIds[i] = mDisplays.get(i).getDisplayIdLocked();
+ }
+ return displayIds;
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ec1ec3a..4152ec9 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -5589,6 +5589,20 @@
}
@Override
+ public int[] getDisplayIdsForGroup(int groupId) {
+ synchronized (mSyncRoot) {
+ return mLogicalDisplayMapper.getDisplayIdsForGroupLocked(groupId);
+ }
+ }
+
+ @Override
+ public SparseArray<int[]> getDisplayIdsByGroupsIds() {
+ synchronized (mSyncRoot) {
+ return mLogicalDisplayMapper.getDisplayIdsByGroupIdLocked();
+ }
+ }
+
+ @Override
public IntArray getDisplayIds() {
IntArray displayIds = new IntArray();
synchronized (mSyncRoot) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index c3f6a52..bcb600d 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -344,6 +344,23 @@
return displayIds;
}
+ public int[] getDisplayIdsForGroupLocked(int groupId) {
+ DisplayGroup displayGroup = mDisplayGroups.get(groupId);
+ if (displayGroup == null) {
+ return new int[]{};
+ }
+ return displayGroup.getIdsLocked();
+ }
+
+ public SparseArray<int[]> getDisplayIdsByGroupIdLocked() {
+ SparseArray<int[]> displayIdsByGroupIds = new SparseArray<>();
+ for (int i = 0; i < mDisplayGroups.size(); i++) {
+ int groupId = mDisplayGroups.get(i).getGroupId();
+ displayIdsByGroupIds.put(groupId, getDisplayIdsForGroupLocked(groupId));
+ }
+ return displayIdsByGroupIds;
+ }
+
public void forEachLocked(Consumer<LogicalDisplay> consumer) {
forEachLocked(consumer, /* includeDisabled= */ true);
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 91a4d6f..598901e 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -764,6 +764,7 @@
boolean hasPermissions(List<String> permissions) {
for (String permission : permissions) {
if (mContext.checkPermission(permission, mPid, mUid) != PERMISSION_GRANTED) {
+ Log.e(TAG, "no permission for " + permission);
return false;
}
}
@@ -919,6 +920,14 @@
}
}
if (curAuthState != newAuthState) {
+ if (newAuthState == AUTHORIZATION_DENIED
+ || newAuthState == AUTHORIZATION_DENIED_GRACE_PERIOD) {
+ Log.e(TAG, "updateNanoAppAuthState auth error: "
+ + Long.toHexString(nanoAppId) + ", "
+ + nanoappPermissions + ", "
+ + gracePeriodExpired + ", "
+ + forceDenied);
+ }
// Don't send the callback in the synchronized block or it could end up in a deadlock.
sendAuthStateCallback(nanoAppId, newAuthState);
}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index e1b8e9f..8b06dad 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1653,9 +1653,11 @@
manager));
}
+ List<MediaRoute2Info> routes =
+ userRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters.values().stream()
+ .toList();
userRecord.mHandler.sendMessage(
- obtainMessage(
- UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager));
+ obtainMessage(ManagerRecord::notifyRoutesUpdated, managerRecord, routes));
}
@GuardedBy("mLock")
@@ -2433,6 +2435,51 @@
}
}
+ /**
+ * Notifies the corresponding manager of the availability of the given routes.
+ *
+ * @param routes The routes available to the manager that corresponds to this record.
+ */
+ public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
+ try {
+ mManager.notifyRoutesUpdated(routes);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify routes. Manager probably died.", ex);
+ }
+ }
+
+ /**
+ * Notifies the corresponding manager of an update in the given session.
+ *
+ * @param sessionInfo The updated session info.
+ */
+ public void notifySessionUpdated(RoutingSessionInfo sessionInfo) {
+ try {
+ mManager.notifySessionUpdated(sessionInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "notifySessionUpdatedToManagers: Failed to notify. Manager probably died.",
+ ex);
+ }
+ }
+
+ /**
+ * Notifies the corresponding manager that the given session has been released.
+ *
+ * @param sessionInfo The released session info.
+ */
+ public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
+ try {
+ mManager.notifySessionReleased(sessionInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "notifySessionReleasedToManagers: Failed to notify. Manager probably died.",
+ ex);
+ }
+ }
+
private void updateScanningState(@ScanningState int scanningState) {
if (mScanningState == scanningState) {
return;
@@ -2761,18 +2808,20 @@
getRouterRecords(/* hasSystemRoutingPermission= */ true);
List<RouterRecord> routerRecordsWithoutSystemRoutingPermission =
getRouterRecords(/* hasSystemRoutingPermission= */ false);
- List<IMediaRouter2Manager> managers = getManagers();
+ List<ManagerRecord> managers = getManagerRecords();
// Managers receive all provider updates with all routes.
- notifyRoutesUpdatedToManagers(
- managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
+ List<MediaRoute2Info> routesForPrivilegedRouters =
+ mLastNotifiedRoutesToPrivilegedRouters.values().stream().toList();
+ for (ManagerRecord manager : managers) {
+ manager.notifyRoutesUpdated(routesForPrivilegedRouters);
+ }
// Routers with system routing access (either via {@link MODIFY_AUDIO_ROUTING} or
// {@link BLUETOOTH_CONNECT} + {@link BLUETOOTH_SCAN}) receive all provider updates
// with all routes.
notifyRoutesUpdatedToRouterRecords(
- routerRecordsWithSystemRoutingPermission,
- new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
+ routerRecordsWithSystemRoutingPermission, routesForPrivilegedRouters);
if (!isSystemProvider) {
// Regular routers receive updates from all non-system providers with all non-system
@@ -3068,8 +3117,10 @@
private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo) {
- List<IMediaRouter2Manager> managers = getManagers();
- notifySessionUpdatedToManagers(managers, sessionInfo);
+ List<ManagerRecord> managers = getManagerRecords();
+ for (ManagerRecord manager : managers) {
+ manager.notifySessionUpdated(sessionInfo);
+ }
// For system provider, notify all routers.
if (provider == mSystemProvider) {
@@ -3093,8 +3144,10 @@
private void onSessionReleasedOnHandler(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo) {
- List<IMediaRouter2Manager> managers = getManagers();
- notifySessionReleasedToManagers(managers, sessionInfo);
+ List<ManagerRecord> managers = getManagerRecords();
+ for (ManagerRecord manager : managers) {
+ manager.notifySessionReleased(sessionInfo);
+ }
RouterRecord routerRecord = mSessionToRouterMap.get(sessionInfo.getId());
if (routerRecord == null) {
@@ -3169,20 +3222,6 @@
return true;
}
- private List<IMediaRouter2Manager> getManagers() {
- final List<IMediaRouter2Manager> managers = new ArrayList<>();
- MediaRouter2ServiceImpl service = mServiceRef.get();
- if (service == null) {
- return managers;
- }
- synchronized (service.mLock) {
- for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) {
- managers.add(managerRecord.mManager);
- }
- }
- return managers;
- }
-
private List<RouterRecord> getRouterRecords() {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
@@ -3269,37 +3308,6 @@
}
}
- /**
- * Notifies {@code manager} with all known routes. This only happens once after {@code
- * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String)
- * registerManager()}.
- *
- * @param manager {@link IMediaRouter2Manager} to be notified.
- */
- private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) {
- if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) {
- return;
- }
- try {
- manager.notifyRoutesUpdated(
- new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex);
- }
- }
-
- private void notifyRoutesUpdatedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull List<MediaRoute2Info> routes) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifyRoutesUpdated(routes);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex);
- }
- }
- }
-
private void notifySessionCreatedToManagers(long managerRequestId,
@NonNull RoutingSessionInfo session) {
int requesterId = toRequesterId(managerRequestId);
@@ -3317,32 +3325,6 @@
}
}
- private void notifySessionUpdatedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull RoutingSessionInfo sessionInfo) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifySessionUpdated(sessionInfo);
- } catch (RemoteException ex) {
- Slog.w(TAG, "notifySessionUpdatedToManagers: "
- + "Failed to notify. Manager probably died.", ex);
- }
- }
- }
-
- private void notifySessionReleasedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull RoutingSessionInfo sessionInfo) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifySessionReleased(sessionInfo);
- } catch (RemoteException ex) {
- Slog.w(TAG, "notifySessionReleasedToManagers: "
- + "Failed to notify. Manager probably died.", ex);
- }
- }
- }
-
private void notifyDiscoveryPreferenceChangedToManager(@NonNull RouterRecord routerRecord,
@NonNull IMediaRouter2Manager manager) {
try {
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 0a9109b..d752429 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.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 @@
private final MediaSessionService mService;
private final UriGrantsManagerInternal mUgmInternal;
private final Context mContext;
- private final boolean mVolumeAdjustmentForRemoteGroupSessions;
private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
@@ -311,8 +307,6 @@
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 @@
}
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/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 9d30c56..e8d14cb 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -1435,6 +1435,10 @@
return false;
}
+ if (record.getSbn().getNotification().isMediaNotification()) {
+ return false;
+ }
+
return true;
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 655f2e4..56e0a89 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4117,6 +4117,34 @@
}
@Override
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void setAdjustmentTypeSupportedState(INotificationListener token,
+ @Adjustment.Keys String key, boolean supported) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mNotificationLock) {
+ final ManagedServiceInfo info = mAssistants.checkServiceTokenLocked(token);
+ if (key == null) {
+ return;
+ }
+ mAssistants.setAdjustmentTypeSupportedState(info, key, supported);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public @NonNull List<String> getUnsupportedAdjustmentTypes() {
+ checkCallerIsSystemOrSystemUiOrShell();
+ synchronized (mNotificationLock) {
+ return new ArrayList(mAssistants.mNasUnsupported.getOrDefault(
+ UserHandle.getUserId(Binder.getCallingUid()), new HashSet<>()));
+ }
+ }
+
+ @Override
@FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
public boolean appCanBePromoted(String pkg, int uid) {
checkCallerIsSystemOrSystemUiOrShell();
@@ -7435,7 +7463,7 @@
NotificationRecord r = findNotificationLocked(pkg, null, notificationId, userId);
if (r != null) {
if (DBG) {
- final String type = (flag == FLAG_FOREGROUND_SERVICE) ? "FGS" : "UIJ";
+ final String type = (flag == FLAG_FOREGROUND_SERVICE) ? "FGS" : "UIJ";
Slog.d(TAG, "Remove " + type + " flag not allow. "
+ "Cancel " + type + " notification");
}
@@ -7452,7 +7480,11 @@
// strip flag from all enqueued notifications. listeners will be informed
// in post runnable.
StatusBarNotification sbn = r.getSbn();
- sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ if (notificationForceGrouping()) {
+ sbn.getNotification().flags = (r.getFlags() & ~flag);
+ } else {
+ sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ }
}
}
@@ -7461,7 +7493,11 @@
if (r != null) {
// if posted notification exists, strip its flag and tell listeners
StatusBarNotification sbn = r.getSbn();
- sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ if (notificationForceGrouping()) {
+ sbn.getNotification().flags = (r.getFlags() & ~flag);
+ } else {
+ sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ }
mRankingHelper.sort(mNotificationList);
mListeners.notifyPostedLocked(r, r);
}
@@ -9459,6 +9495,28 @@
}
/**
+ * Check if the notification was a summary that has been auto-grouped
+ * @param r the current notification record
+ * @param old the previous notification record
+ * @return true if the notification record was a summary that was auto-grouped
+ */
+ @GuardedBy("mNotificationLock")
+ private boolean wasSummaryAutogrouped(NotificationRecord r, NotificationRecord old) {
+ boolean wasAutogrouped = false;
+ if (old != null) {
+ boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0;
+ boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0
+ && old.getSbn().getOverrideGroupKey() != null;
+ boolean isNotAutogroupSummary = (r.getFlags() & FLAG_AUTOGROUP_SUMMARY) == 0
+ && (r.getFlags() & FLAG_GROUP_SUMMARY) != 0;
+ if ((wasSummary && wasForcedGrouped) || (wasForcedGrouped && isNotAutogroupSummary)) {
+ wasAutogrouped = true;
+ }
+ }
+ return wasAutogrouped;
+ }
+
+ /**
* Ensures that grouped notification receive their special treatment.
*
* <p>Cancels group children if the new notification causes a group to lose
@@ -9478,14 +9536,9 @@
}
if (notificationForceGrouping()) {
- if (old != null) {
- // If this is an update to a summary that was forced grouped => remove summary flag
- boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0;
- boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0
- && old.getSbn().getOverrideGroupKey() != null;
- if (n.isGroupSummary() && wasSummary && wasForcedGrouped) {
- n.flags &= ~FLAG_GROUP_SUMMARY;
- }
+ // If this is an update to a summary that was forced grouped => remove summary flag
+ if (wasSummaryAutogrouped(r, old)) {
+ n.flags &= ~FLAG_GROUP_SUMMARY;
}
}
@@ -11333,12 +11386,16 @@
static final String TAG_ENABLED_NOTIFICATION_ASSISTANTS = "enabled_assistants";
private static final String ATT_TYPES = "types";
+ private static final String ATT_NAS_UNSUPPORTED = "unsupported_adjustments";
private final Object mLock = new Object();
@GuardedBy("mLock")
private Set<String> mAllowedAdjustments = new ArraySet<>();
+ @GuardedBy("mLock")
+ private Map<Integer, HashSet<String>> mNasUnsupported = new ArrayMap<>();
+
protected ComponentName mDefaultFromConfig = null;
@Override
@@ -11831,6 +11888,10 @@
setNotificationAssistantAccessGrantedForUserInternal(
currentComponent, userId, false, userSet);
}
+ } else {
+ if (android.service.notification.Flags.notificationClassification()) {
+ mNasUnsupported.put(userId, new HashSet<>());
+ }
}
super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
}
@@ -11838,6 +11899,63 @@
private boolean isVerboseLogEnabled() {
return Log.isLoggable("notification_assistant", Log.VERBOSE);
}
+
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @GuardedBy("mNotificationLock")
+ public void setAdjustmentTypeSupportedState(ManagedServiceInfo info,
+ @Adjustment.Keys String key, boolean supported) {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ HashSet<String> disabledAdjustments =
+ mNasUnsupported.getOrDefault(info.userid, new HashSet<>());
+ if (supported) {
+ disabledAdjustments.remove(key);
+ } else {
+ disabledAdjustments.add(key);
+ }
+ mNasUnsupported.put(info.userid, disabledAdjustments);
+ handleSavePolicyFile();
+ }
+
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @GuardedBy("mNotificationLock")
+ public @NonNull Set<String> getUnsupportedAdjustments(@UserIdInt int userId) {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return new HashSet<>();
+ }
+ return mNasUnsupported.getOrDefault(userId, new HashSet<>());
+ }
+
+ @Override
+ protected void writeExtraAttributes(TypedXmlSerializer out, @UserIdInt int approvedUserId)
+ throws IOException {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mNasUnsupported.containsKey(approvedUserId)) {
+ out.attribute(null, ATT_NAS_UNSUPPORTED,
+ TextUtils.join(",", mNasUnsupported.get(approvedUserId)));
+ }
+ }
+ }
+
+ @Override
+ protected void readExtraAttributes(String tag, TypedXmlPullParser parser,
+ @UserIdInt int approvedUserId) throws IOException {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ if (ManagedServices.TAG_MANAGED_SERVICES.equals(tag)) {
+ final String types = XmlUtils.readStringAttribute(parser, ATT_NAS_UNSUPPORTED);
+ synchronized (mLock) {
+ if (!TextUtils.isEmpty(types)) {
+ mNasUnsupported.put(approvedUserId, new HashSet(List.of(types.split(","))));
+ }
+ }
+ }
+ }
}
/**
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index 21a6df2..94b49e5 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -101,6 +101,22 @@
]
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
@@ -117,6 +133,22 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 6e7a009..bc6a40a 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -884,10 +884,13 @@
}
// Allow voice search on wear
- grantPermissionsToSystemPackage(pm,
- getDefaultSystemHandlerActivityPackage(pm,
- SearchManager.INTENT_ACTION_GLOBAL_SEARCH, userId),
- userId, PHONE_PERMISSIONS, CALENDAR_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS);
+ String voiceSearchPackage = getDefaultSystemHandlerActivityPackage(pm,
+ SearchManager.INTENT_ACTION_GLOBAL_SEARCH, userId);
+ grantPermissionsToSystemPackage(pm, voiceSearchPackage,
+ userId, PHONE_PERMISSIONS, CALENDAR_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS,
+ COARSE_BACKGROUND_LOCATION_PERMISSIONS);
+ revokeRuntimePermissions(pm, voiceSearchPackage,
+ FINE_LOCATION_PERMISSIONS, false, userId);
}
// Print Spooler
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 2284050..e47b4c2 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 @@
//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 @@
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;
}
}
@@ -3204,8 +3211,9 @@
return ADD_OKAY;
}
- // Allow virtual device owners to add overlays on the displays they own.
+ // Allow virtual device owners to add overlays on the trusted displays they own.
if (mWindowManagerFuncs.isCallerVirtualDeviceOwner(displayId, callingUid)
+ && mWindowManagerFuncs.isDisplayTrusted(displayId)
&& mContext.checkCallingOrSelfPermission(CREATE_VIRTUAL_DEVICE)
== PERMISSION_GRANTED) {
return ADD_OKAY;
@@ -7243,6 +7251,8 @@
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/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index ad11657..cc31bb1 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -368,6 +368,11 @@
* belongs to.
*/
boolean isCallerVirtualDeviceOwner(int displayId, int callingUid);
+
+ /**
+ * Returns whether the display with the given ID is trusted.
+ */
+ boolean isDisplayTrusted(int displayId);
}
/**
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 0cdf537..4fae798 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -164,6 +164,7 @@
}
private final SparseArray<Interactivity> mInteractivityByGroupId = new SparseArray<>();
+ private SparseBooleanArray mDisplayInteractivities = new SparseBooleanArray();
// The current global interactive state. This is set as soon as an interactive state
// transition begins so as to capture the reason that it happened. At some point
@@ -690,6 +691,42 @@
}
/**
+ * Update the interactivities of the displays in given DisplayGroup.
+ *
+ * @param groupId The group id of the DisplayGroup to update display interactivities for.
+ */
+ private void updateDisplayInteractivities(int groupId, boolean interactive) {
+ final int[] displayIds = mDisplayManagerInternal.getDisplayIdsForGroup(groupId);
+ for (int displayId : displayIds) {
+ mDisplayInteractivities.put(displayId, interactive);
+ }
+
+ }
+
+ private void resetDisplayInteractivities() {
+ final SparseArray<int[]> displaysByGroupId =
+ mDisplayManagerInternal.getDisplayIdsByGroupsIds();
+ SparseBooleanArray newDisplayInteractivities = new SparseBooleanArray();
+ for (int i = 0; i < displaysByGroupId.size(); i++) {
+ final int groupId = displaysByGroupId.keyAt(i);
+ for (int displayId : displaysByGroupId.get(i)) {
+ // If we already know display interactivity, use that
+ if (mDisplayInteractivities.indexOfKey(displayId) > 0) {
+ newDisplayInteractivities.put(
+ displayId, mDisplayInteractivities.get(displayId));
+ } else { // If display is new to Notifier, use the power group's interactive value
+ final Interactivity groupInteractivity = mInteractivityByGroupId.get(groupId);
+ // If group Interactivity hasn't been initialized, assume group is interactive
+ final boolean groupInteractive =
+ groupInteractivity == null || groupInteractivity.isInteractive;
+ newDisplayInteractivities.put(displayId, groupInteractive);
+ }
+ }
+ }
+ mDisplayInteractivities = newDisplayInteractivities;
+ }
+
+ /**
* Called when an individual PowerGroup changes wakefulness.
*/
public void onGroupWakefulnessChangeStarted(int groupId, int wakefulness, int changeReason,
@@ -717,6 +754,12 @@
handleEarlyInteractiveChange(groupId);
mWakefulnessSessionObserver.onWakefulnessChangeStarted(groupId, wakefulness,
changeReason, eventTime);
+
+ // Update input on which displays are interactive
+ if (mFlags.isPerDisplayWakeByTouchEnabled()) {
+ updateDisplayInteractivities(groupId, isInteractive);
+ mInputManagerInternal.setDisplayInteractivities(mDisplayInteractivities);
+ }
}
}
@@ -731,6 +774,16 @@
}
/**
+ * Called when a PowerGroup has been changed.
+ */
+ public void onGroupChanged() {
+ if (mFlags.isPerDisplayWakeByTouchEnabled()) {
+ resetDisplayInteractivities();
+ mInputManagerInternal.setDisplayInteractivities(mDisplayInteractivities);
+ }
+ }
+
+ /**
* Called when there has been user activity.
*/
public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event,
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index e053964..21ab781 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -125,7 +125,6 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.util.Preconditions;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.EventLogTags;
import com.android.server.LockGuard;
import com.android.server.ServiceThread;
@@ -133,6 +132,7 @@
import com.android.server.UiThread;
import com.android.server.Watchdog;
import com.android.server.am.BatteryStatsService;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
@@ -2445,6 +2445,8 @@
mClock.uptimeMillis());
} else if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_REMOVED) {
mNotifier.onGroupRemoved(groupId);
+ } else if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_CHANGED) {
+ mNotifier.onGroupChanged();
}
if (oldWakefulness != newWakefulness) {
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 1346a29..dc48242 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 @@
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/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
index 539c415..d7aa987 100644
--- a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
@@ -30,6 +30,7 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.BluetoothPowerStatsLayout;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -142,6 +143,7 @@
return null;
}
+ Arrays.fill(mDeviceStats, 0);
mPowerStats.uidStats.clear();
collectBluetoothActivityInfo();
diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
index 128f14a..dd6484d 100644
--- a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
@@ -34,6 +34,7 @@
import com.android.server.power.stats.format.CpuPowerStatsLayout;
import java.io.PrintWriter;
+import java.util.Arrays;
import java.util.Locale;
/**
@@ -330,7 +331,9 @@
return null;
}
+ Arrays.fill(mCpuPowerStats.stats, 0);
mCpuPowerStats.uidStats.clear();
+
// TODO(b/305120724): additionally retrieve time-in-cluster for each CPU cluster
long newTimestampNanos = mKernelCpuStatsReader.readCpuStats(this::processUidStats,
mLayout.getScalingStepToPowerBracketMap(), mLastUpdateTimestampNanos,
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
index 1d2e388..079fc3c 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
@@ -24,6 +24,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.EnergyConsumerPowerStatsLayout;
+import java.util.Arrays;
+
public class EnergyConsumerPowerStatsCollector extends PowerStatsCollector {
public interface Injector {
@@ -105,6 +107,7 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
mPowerStats.uidStats.clear();
if (!mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout)) {
diff --git a/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
index 8371e66..c38904f 100644
--- a/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
@@ -27,6 +27,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.ScreenPowerStatsLayout;
+import java.util.Arrays;
+
public class ScreenPowerStatsCollector extends PowerStatsCollector {
private static final String TAG = "ScreenPowerStatsCollector";
@@ -115,6 +117,9 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
+ mPowerStats.uidStats.clear();
+
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
for (int display = 0; display < mDisplayCount; display++) {
@@ -142,8 +147,6 @@
mLastDozeTime[display] = screenDozeTimeMs;
}
- mPowerStats.uidStats.clear();
-
mScreenUsageTimeRetriever.retrieveTopActivityTimes((uid, topActivityTimeMs) -> {
long topActivityDuration = topActivityTimeMs - mLastTopActivityTime.get(uid);
if (topActivityDuration == 0) {
diff --git a/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
index 9e4a391..e36c994 100644
--- a/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
@@ -25,6 +25,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.WakelockPowerStatsLayout;
+import java.util.Arrays;
+
class WakelockPowerStatsCollector extends PowerStatsCollector {
public interface WakelockDurationRetriever {
@@ -89,6 +91,9 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
+ mPowerStats.uidStats.clear();
+
long elapsedRealtime = mClock.elapsedRealtime();
mPowerStats.durationMs = elapsedRealtime - mLastCollectionTime;
@@ -101,7 +106,6 @@
mLastWakelockDurationMs = wakelockDurationMillis;
- mPowerStats.uidStats.clear();
mWakelockDurationRetriever.retrieveUidWakelockDuration((uid, durationMs) -> {
if (!mFirstCollection) {
long[] uidStats = mPowerStats.uidStats.get(uid);
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index 7a84b05..1fdeac9 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -30,6 +30,7 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.WifiPowerStatsLayout;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -151,6 +152,9 @@
return null;
}
+ Arrays.fill(mDeviceStats, 0);
+ mPowerStats.uidStats.clear();
+
WifiActivityEnergyInfo activityInfo = null;
if (mPowerReportingSupported) {
activityInfo = collectWifiActivityInfo();
@@ -224,8 +228,6 @@
}
private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
- mPowerStats.uidStats.clear();
-
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 52e8285..c062f5a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1073,7 +1073,8 @@
final String systemUiPermission =
mService.isCallerVirtualDeviceOwner(mDisplayContent.getDisplayId(), callingUid)
- // Allow virtual device owners to add system windows on their displays.
+ && mDisplayContent.isTrusted()
+ // Virtual device owners can add system windows on their trusted displays.
? android.Manifest.permission.CREATE_VIRTUAL_DEVICE
: android.Manifest.permission.STATUS_BAR_SERVICE;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c48590f..188b368 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1947,7 +1947,8 @@
mCleanupTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
buildCleanupTransaction(mCleanupTransaction, info);
if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
- mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
+ mController.dispatchLegacyAppTransitionStarting(participantDisplays,
+ mStatusBarTransitionDelay);
try {
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
"Calling onTransitionReady: %s", info);
@@ -2188,32 +2189,34 @@
for (int i = onTopTasksEnd.size() - 1; i >= 0; --i) {
final Task task = onTopTasksEnd.get(i);
if (task.getDisplayId() != displayId) continue;
- if (!enableDisplayFocusInShellTransitions()
- || mOnTopDisplayStart == onTopDisplayEnd
- || displayId != onTopDisplayEnd.mDisplayId) {
- // If it didn't change since last report, don't report
- if (reportedOnTop == null) {
- if (mOnTopTasksStart.contains(task)) continue;
- } else if (reportedOnTop.contains(task)) {
- continue;
- }
+ if (reportedOnTop == null) {
+ if (mOnTopTasksStart.contains(task)) continue;
+ } else if (reportedOnTop.contains(task)) {
+ continue;
}
- // Need to report it.
- mParticipants.add(task);
- int changeIdx = mChanges.indexOfKey(task);
- if (changeIdx < 0) {
- mChanges.put(task, new ChangeInfo(task));
- changeIdx = mChanges.indexOfKey(task);
- }
- mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP;
+ addToTopChange(task);
}
// Swap in the latest on-top tasks.
mController.mLatestOnTopTasksReported.put(displayId, onTopTasksEnd);
onTopTasksEnd = reportedOnTop != null ? reportedOnTop : new ArrayList<>();
onTopTasksEnd.clear();
+
+ if (enableDisplayFocusInShellTransitions()
+ && mOnTopDisplayStart != onTopDisplayEnd
+ && displayId == onTopDisplayEnd.mDisplayId) {
+ addToTopChange(onTopDisplayEnd);
+ }
}
}
+ private void addToTopChange(@NonNull WindowContainer wc) {
+ mParticipants.add(wc);
+ if (!mChanges.containsKey(wc)) {
+ mChanges.put(wc, new ChangeInfo(wc));
+ }
+ mChanges.get(wc).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP;
+ }
+
private void postCleanupOnFailure() {
mController.mAtm.mH.post(() -> {
synchronized (mController.mAtm.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 37c3ce80..b7fe327 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -1356,12 +1356,13 @@
}
}
- void dispatchLegacyAppTransitionStarting(TransitionInfo info, long statusBarTransitionDelay) {
+ void dispatchLegacyAppTransitionStarting(DisplayContent[] participantDisplays,
+ long statusBarTransitionDelay) {
final long now = SystemClock.uptimeMillis();
for (int i = 0; i < mLegacyListeners.size(); ++i) {
final WindowManagerInternal.AppTransitionListener listener = mLegacyListeners.get(i);
- for (int j = 0; j < info.getRootCount(); ++j) {
- final int displayId = info.getRoot(j).getDisplayId();
+ for (int j = 0; j < participantDisplays.length; ++j) {
+ final int displayId = participantDisplays[j].mDisplayId;
if (shouldDispatchLegacyListener(listener, displayId)) {
listener.onAppTransitionStartingLocked(
now + statusBarTransitionDelay,
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 7925220..e1e4714 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -10172,6 +10172,22 @@
}
}
+ /**
+ * Returns whether the display with the given ID is trusted.
+ */
+ @Override
+ public boolean isDisplayTrusted(int displayId) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ DisplayContent dc = mRoot.getDisplayContent(displayId);
+ return dc != null && dc.isTrusted();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
@RequiresPermission(ACCESS_SURFACE_FLINGER)
@Override
public boolean replaceContentOnDisplay(int displayId, SurfaceControl sc) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 6d7396f..21ed8d7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.os.Build.IS_USER;
import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
@@ -30,6 +33,7 @@
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import android.app.WindowConfiguration;
import android.content.res.Resources.NotFoundException;
import android.graphics.Color;
import android.graphics.Point;
@@ -157,6 +161,10 @@
return runReset(pw);
case "disable-blur":
return runSetBlurDisabled(pw);
+ case "set-display-windowing-mode":
+ return runSetDisplayWindowingMode(pw);
+ case "get-display-windowing-mode":
+ return runGetDisplayWindowingMode(pw);
case "shell":
return runWmShellCommand(pw);
default:
@@ -1434,6 +1442,35 @@
return 0;
}
+ private int runSetDisplayWindowingMode(PrintWriter pw) throws RemoteException {
+ int displayId = Display.DEFAULT_DISPLAY;
+ String arg = getNextArgRequired();
+ if ("-d".equals(arg)) {
+ displayId = Integer.parseInt(getNextArgRequired());
+ arg = getNextArgRequired();
+ }
+
+ final int windowingMode = Integer.parseInt(arg);
+ mInterface.setWindowingMode(displayId, windowingMode);
+
+ return 0;
+ }
+
+ private int runGetDisplayWindowingMode(PrintWriter pw) throws RemoteException {
+ int displayId = Display.DEFAULT_DISPLAY;
+ final String arg = getNextArg();
+ if ("-d".equals(arg)) {
+ displayId = Integer.parseInt(getNextArgRequired());
+ }
+
+ final int windowingMode = mInterface.getWindowingMode(displayId);
+ pw.println("display windowing mode="
+ + WindowConfiguration.windowingModeToString(windowingMode) + " for displayId="
+ + displayId);
+
+ return 0;
+ }
+
private int runWmShellCommand(PrintWriter pw) {
String arg = getNextArg();
@@ -1512,6 +1549,9 @@
// set-multi-window-config
runResetMultiWindowConfig();
+ // set-display-windowing-mode
+ mInternal.setWindowingMode(displayId, WINDOWING_MODE_UNDEFINED);
+
pw.println("Reset all settings for displayId=" + displayId);
return 0;
}
@@ -1552,6 +1592,12 @@
printLetterboxHelp(pw);
printMultiWindowConfigHelp(pw);
+ pw.println(" set-display-windowing-mode [-d DISPLAY_ID] [mode_id]");
+ pw.println(" As mode_id, use " + WINDOWING_MODE_UNDEFINED + " for undefined, "
+ + WINDOWING_MODE_FREEFORM + " for freeform, " + WINDOWING_MODE_FULLSCREEN + " for"
+ + " fullscreen");
+ pw.println(" get-display-windowing-mode [-d DISPLAY_ID]");
+
pw.println(" reset [-d DISPLAY_ID]");
pw.println(" Reset all override settings.");
if (!IS_USER) {
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
index b4e2544..a8016a0 100644
--- a/services/profcollect/src/com/android/server/profcollect/Utils.java
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -25,10 +25,14 @@
import com.android.internal.os.BackgroundThread;
+import java.time.Instant;
import java.util.concurrent.ThreadLocalRandom;
public final class Utils {
+ private static Instant lastTraceTime = Instant.EPOCH;
+ private static final int TRACE_COOLDOWN_SECONDS = 30;
+
public static boolean withFrequency(String configName, int defaultFrequency) {
int threshold = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, configName, defaultFrequency);
@@ -40,6 +44,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().post(() -> {
try {
mIProfcollect.trace_system(eventName);
@@ -54,6 +61,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
mIProfcollect.trace_system(eventName);
@@ -69,6 +79,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().post(() -> {
try {
mIProfcollect.trace_process(eventName,
@@ -80,4 +93,16 @@
});
return true;
}
+
+ /**
+ * Returns true if the last trace is within the cooldown period. If the last trace is outside
+ * the cooldown period, the last trace time is reset to the current time.
+ */
+ private static boolean isInCooldownOrReset() {
+ if (!Instant.now().isBefore(lastTraceTime.plusSeconds(TRACE_COOLDOWN_SECONDS))) {
+ lastTraceTime = Instant.now();
+ return false;
+ }
+ return true;
+ }
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 255dcb0..342c87a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -37,6 +37,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -1524,6 +1525,47 @@
}
@Test
+ public void testGetDisplayIdsForGroup() throws Exception {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ // Create display 1
+ FakeDisplayDevice displayDevice1 =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ LogicalDisplay display1 = logicalDisplayMapper.getDisplayLocked(displayDevice1);
+ final int groupId1 = display1.getDisplayInfoLocked().displayGroupId;
+ // Create display 2
+ FakeDisplayDevice displayDevice2 =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ LogicalDisplay display2 = logicalDisplayMapper.getDisplayLocked(displayDevice2);
+ final int groupId2 = display2.getDisplayInfoLocked().displayGroupId;
+ // Both displays should be in the same display group
+ assertEquals(groupId1, groupId2);
+ final int[] expectedDisplayIds = new int[]{
+ display1.getDisplayIdLocked(), display2.getDisplayIdLocked()};
+
+ final int[] displayIds = localService.getDisplayIdsForGroup(groupId1);
+
+ assertArrayEquals(expectedDisplayIds, displayIds);
+ }
+
+ @Test
+ public void testGetDisplayIdsForUnknownGroup() throws Exception {
+ final int unknownDisplayGroupId = 999;
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ // Verify that display manager does not have display group
+ assertNull(logicalDisplayMapper.getDisplayGroupLocked(unknownDisplayGroupId));
+
+ final int[] displayIds = localService.getDisplayIdsForGroup(unknownDisplayGroupId);
+
+ assertEquals(0, displayIds.length);
+ }
+
+ @Test
public void testCreateVirtualDisplay_isValidProjection_notValid()
throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
index e863f15..e678acc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
@@ -39,6 +39,7 @@
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Parcel;
import android.os.Process;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
@@ -580,6 +581,50 @@
assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() >= originalMonotonicTime);
}
+ /**
+ * Test to confirm that parcel read and write implementations match, correctly loading records
+ * with the same values and leaving no data unread.
+ */
+ @Test
+ public void testParcelReadWriteMatch() throws Exception {
+ // Create a start info records with all fields set.
+ ApplicationStartInfo startInfo = new ApplicationStartInfo(1234L);
+ startInfo.setPid(123);
+ startInfo.setRealUid(987);
+ startInfo.setPackageUid(654);
+ startInfo.setDefiningUid(321);
+ startInfo.setReason(ApplicationStartInfo.START_REASON_LAUNCHER);
+ startInfo.setStartupState(ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN);
+ startInfo.setStartType(ApplicationStartInfo.START_TYPE_WARM);
+ startInfo.setLaunchMode(ApplicationStartInfo.LAUNCH_MODE_SINGLE_TOP);
+ startInfo.setPackageName(APP_1_PACKAGE_NAME);
+ startInfo.setProcessName(APP_1_PROCESS_NAME);
+ startInfo.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_LAUNCH, 999L);
+ startInfo.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME, 888L);
+ startInfo.setForceStopped(true);
+ startInfo.setStartComponent(ApplicationStartInfo.START_COMPONENT_OTHER);
+ startInfo.setIntent(buildIntent(COMPONENT));
+
+ // Write the start info to a parcel.
+ Parcel parcel = Parcel.obtain();
+ startInfo.writeToParcel(parcel, 0 /* flags */);
+
+ // Set the data position back to 0 so it's ready to be read.
+ parcel.setDataPosition(0);
+
+ // Now load the record from the parcel.
+ ApplicationStartInfo startInfoFromParcel = new ApplicationStartInfo(parcel);
+
+ // Make sure there is no unread data remaining in the parcel, and confirm that the loaded
+ // start info object is equal to the one it was written from. Check dataAvail first as if
+ // that check fails then the next check will fail too, but knowing the status of this check
+ // will tell us that we're missing a read or write. Check the objects are equals second as
+ // if the avail check passes and equals fails, then we know we're reading all the data just
+ // not to the correct fields.
+ assertEquals(0, parcel.dataAvail());
+ assertTrue(startInfo.equals(startInfoFromParcel));
+ }
+
private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) {
try {
Field field = clazz.getDeclaredField(fieldName);
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 0702926..a1db182 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -268,7 +268,7 @@
}
@Test
- public void testOnGlobalWakefulnessChangeStarted() throws Exception {
+ public void testOnGlobalWakefulnessChangeStarted() {
createNotifier();
// GIVEN system is currently non-interactive
when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
@@ -294,6 +294,96 @@
}
@Test
+ public void testOnGroupWakefulnessChangeStarted_newPowerGroup_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not yet known to Notifier and per-display wake by touch is disabled
+ final int groupId = 123;
+ final int changeReason = PowerManager.WAKE_REASON_TAP;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+
+ // WHEN a power group wakefulness change starts
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN window manager policy is informed that device has started waking up
+ verify(mPolicy).startedWakingUp(groupId, changeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_interactivityNoChange_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not interactive and per-display wake by touch is disabled
+ final int groupId = 234;
+ final int changeReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+ verify(mPolicy, times(1)).startedGoingToSleep(groupId, changeReason);
+
+ // WHEN a power wakefulness change to not interactive starts
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN policy is only informed once of non-interactive wakefulness change
+ verify(mPolicy, times(1)).startedGoingToSleep(groupId, changeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_interactivityChange_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not interactive and per-display wake by touch is disabled
+ final int groupId = 345;
+ final int firstChangeReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, firstChangeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // WHEN a power wakefulness change to interactive starts
+ final int secondChangeReason = PowerManager.WAKE_REASON_TAP;
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, secondChangeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN policy is informed of the change
+ verify(mPolicy).startedWakingUp(groupId, secondChangeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_perDisplayWakeByTouchEnabled() {
+ createNotifier();
+ // GIVEN per-display wake by touch flag is enabled
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(true);
+ final int groupId = 456;
+ final int displayId1 = 1001;
+ final int displayId2 = 1002;
+ final int[] displays = new int[]{displayId1, displayId2};
+ when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displays));
+ when(mDisplayManagerInternal.getDisplayIdsForGroup(groupId)).thenReturn(displays);
+ final int changeReason = PowerManager.WAKE_REASON_TAP;
+
+ // WHEN power group wakefulness change started
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN native input manager is updated that the displays are interactive
+ final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+ expectedDisplayInteractivities.put(displayId1, true);
+ expectedDisplayInteractivities.put(displayId2, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+ }
+
+ @Test
public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
createNotifier();
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index cbe6700..6ede334 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -230,6 +230,20 @@
}
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/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 585df84..22a4f85 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2662,6 +2662,17 @@
when(n.isColorized()).thenReturn(true);
when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+
+ NotificationRecord notification_media = spy(getNotificationRecord(mPkg, 0, "", mUser,
+ "", false, IMPORTANCE_LOW));
+ n = mock(Notification.class);
+ sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+ when(notification_media.isConversation()).thenReturn(false);
+ when(notification_media.getNotification()).thenReturn(n);
+ when(notification_media.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isMediaNotification()).thenReturn(true);
+ assertThat(GroupHelper.getSection(notification_media)).isNull();
}
@Test
@@ -2756,7 +2767,7 @@
@Test
@EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS})
public void testNonGroupableNotifications_forceGroupConversations() {
- // Check that there is no valid section for: calls, foreground services
+ // Check that there is no valid section for: calls, foreground services, media notifications
NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser,
"", false, IMPORTANCE_LOW));
Notification n = mock(Notification.class);
@@ -2780,6 +2791,17 @@
when(n.isColorized()).thenReturn(true);
when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+
+ NotificationRecord notification_media = spy(getNotificationRecord(mPkg, 0, "", mUser,
+ "", false, IMPORTANCE_LOW));
+ n = mock(Notification.class);
+ sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+ when(notification_media.isConversation()).thenReturn(false);
+ when(notification_media.getNotification()).thenReturn(n);
+ when(notification_media.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isMediaNotification()).thenReturn(true);
+ assertThat(GroupHelper.getSection(notification_media)).isNull();
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index a45b102..797b95b5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -15,12 +15,15 @@
*/
package com.android.server.notification;
+import static android.os.UserHandle.USER_ALL;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
@@ -33,6 +36,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.app.ActivityManager;
import android.app.INotificationManager;
import android.content.ComponentName;
@@ -42,24 +46,28 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
-import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.Adjustment;
import android.testing.TestableContext;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
import android.util.Xml;
-import android.Manifest;
+
+import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.function.TriPredicate;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.UiServiceTestCase;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -71,8 +79,12 @@
import java.util.Arrays;
import java.util.List;
+@RunWith(AndroidJUnit4.class)
public class NotificationAssistantsTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private PackageManager mPm;
@Mock
@@ -98,6 +110,35 @@
ComponentName mCn = new ComponentName("a", "b");
+
+ // Helper function to hold mApproved lock, avoid GuardedBy lint errors
+ private boolean isUserSetServicesEmpty(NotificationAssistants assistant, int userId) {
+ synchronized (assistant.mApproved) {
+ return assistant.mUserSetServices.get(userId).isEmpty();
+ }
+ }
+
+ private void writeXmlAndReload(int userId) throws Exception {
+ TypedXmlSerializer serializer = Xml.newFastSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ serializer.startDocument(null, true);
+ mAssistants.writeXml(serializer, false, userId);
+ serializer.endDocument();
+ serializer.flush();
+
+ //fail(baos.toString("UTF-8"));
+
+ final TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+
+ parser.nextTag();
+ mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
+ }
+
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -164,25 +205,7 @@
mAssistants.setPackageOrComponentEnabled(current.flattenToString(), userId, true, false,
true);
- TypedXmlSerializer serializer = Xml.newFastSerializer();
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
- serializer.startDocument(null, true);
- mAssistants.writeXml(serializer, true, userId);
- serializer.endDocument();
- serializer.flush();
-
- //fail(baos.toString("UTF-8"));
-
- final TypedXmlPullParser parser = Xml.newFastPullParser();
- parser.setInput(new BufferedInputStream(
- new ByteArrayInputStream(baos.toByteArray())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
-
- parser.nextTag();
- mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ writeXmlAndReload(USER_ALL);
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
// approved should not be null
@@ -203,11 +226,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
@@ -226,11 +247,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, true,
+ mAssistants.readXml(parser, mNm::canUseManagedServices, true,
ActivityManager.getCurrentUser());
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
@@ -253,11 +272,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(mAssistants.mIsUserChanged.get(0));
@@ -273,11 +290,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(0)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -294,11 +309,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(0)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -314,11 +327,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -334,11 +345,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -361,7 +370,7 @@
new ByteArrayInputStream(xml.toString().getBytes())), null);
parser.nextTag();
- mAssistants.readXml(parser, null, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, null, false, USER_ALL);
assertEquals(1, mAssistants.getAllowedComponents(0).size());
assertEquals(new ArrayList(Arrays.asList(new ComponentName("a", "a"))),
@@ -378,7 +387,7 @@
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
parser.nextTag();
- mAssistants.readXml(parser, null, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, null, false, USER_ALL);
verify(mNm, never()).setDefaultAssistantForUser(anyInt());
verify(mAssistants, times(1)).addApprovedList(
@@ -529,10 +538,66 @@
assertEquals(new ArraySet<>(), mAssistants.getDefaultComponents());
}
- // Helper function to hold mApproved lock, avoid GuardedBy lint errors
- private boolean isUserSetServicesEmpty(NotificationAssistants assistant, int userId) {
- synchronized (assistant.mApproved) {
- return assistant.mUserSetServices.get(userId).isEmpty();
- }
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0);
+
+ ManagedServices.ManagedServiceInfo info =
+ mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
+ mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains(
+ Adjustment.KEY_NOT_CONVERSATION);
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(1);
}
-}
+
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState_readWriteXml_entries() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ ManagedServices.ManagedServiceInfo info =
+ mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
+ mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+
+ writeXmlAndReload(USER_ALL);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains(
+ Adjustment.KEY_NOT_CONVERSATION);
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(1);
+ }
+
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState_readWriteXml_empty() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ writeXmlAndReload(USER_ALL);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0);
+ }
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index bbf2cbd..3c120e1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -33,6 +33,7 @@
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_CAN_COLORIZE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
+import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_NO_DISMISS;
@@ -2872,6 +2873,131 @@
}
@Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_forceGrouped_clearsSummaryFlag() throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ // Old record was a summary and it was auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+ mService.addNotification(r);
+ mService.convertSummaryToNotificationLocked(r.getKey());
+ mService.addAutogroupKeyLocked(r.getKey(), aggregateGroupName, true);
+
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_forceGroupedRegular_updatedAsSummary_clearsSummaryFlag()
+ throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ // Old record was not summary and it was auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+ mService.addNotification(r);
+ mService.addAutogroupKeyLocked(r.getKey(), aggregateGroupName, true);
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_notForceGrouped_dontClearSummaryFlag()
+ throws Exception {
+ final String originalGroupName = "originalGroup";
+
+ // Old record was a summary and it was not auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+ mService.addNotification(r);
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was not removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(
+ FLAG_GROUP_SUMMARY);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testRemoveFGSFlagFromNotification_enqueued_forceGrouped_clearsSummaryFlag() {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null,
+ originalGroupName, true);
+ r.getSbn().getNotification().flags &= ~FLAG_GROUP_SUMMARY;
+ r.setOverrideGroupKey(aggregateGroupName);
+ mService.addEnqueuedNotification(r);
+
+ mInternalService.removeForegroundServiceFlagFromNotification(
+ mPkg, r.getSbn().getId(), r.getSbn().getUserId());
+ waitForIdle();
+
+ assertThat(mService.mEnqueuedNotifications).hasSize(1);
+ assertThat(mService.mEnqueuedNotifications.get(0).getFlags() & FLAG_GROUP_SUMMARY)
+ .isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testRemoveFGSFlagFromNotification_posted_forceGrouped_clearsSummaryFlag() {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null,
+ originalGroupName, true);
+ r.getSbn().getNotification().flags &= ~FLAG_GROUP_SUMMARY;
+ r.setOverrideGroupKey(aggregateGroupName);
+ mService.addNotification(r);
+
+ mInternalService.removeForegroundServiceFlagFromNotification(
+ mPkg, r.getSbn().getId(), r.getSbn().getUserId());
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
public void testCancelAllNotifications_IgnoreForegroundService() throws Exception {
when(mAmi.applyForegroundServiceNotification(
any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY);
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 91eb2ed..c6cc941 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -60,6 +60,7 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
@@ -69,6 +70,7 @@
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
@@ -127,7 +129,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.ZenRule.class, getZenRuleExemptFields());
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
@@ -145,6 +147,337 @@
}
}
+ @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();
+
+ // Both sides same rule
+ ZenModeDiff.DeviceEffectsDiff dSame = new ZenModeDiff.DeviceEffectsDiff(effects, effects);
+ assertFalse(dSame.hasDiff());
+
+ // from existent rule to null: expect deleted
+ ZenModeDiff.DeviceEffectsDiff deleted = new ZenModeDiff.DeviceEffectsDiff(effects, null);
+ assertTrue(deleted.hasDiff());
+ assertTrue(deleted.wasRemoved());
+
+ // from null to new rule: expect added
+ ZenModeDiff.DeviceEffectsDiff added = new ZenModeDiff.DeviceEffectsDiff(null, effects);
+ assertTrue(added.hasDiff());
+ assertTrue(added.wasAdded());
+ }
+
+ @Test
+ public void testDeviceEffectsDiff_fieldDiffs() throws Exception {
+ // Start these the same
+ 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);
+
+ ZenModeDiff.DeviceEffectsDiff d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ assertTrue(d.hasDiff());
+
+ // Now diff them and check that each of the fields has a diff
+ for (Field f : fieldsForDiff) {
+ String name = f.getName();
+ assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
+ assertTrue(d.getDiffForField(name).hasDiff());
+ assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
+ assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
+ assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
+ assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
+ }
+ }
+
+ @Test
+ public void testDeviceEffectsDiff_toString() throws Exception {
+ // Ensure device effects toString is readable.
+ ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build();
+ ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build();
+
+ ZenModeDiff.DeviceEffectsDiff d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{no changes}");
+
+ d = new ZenModeDiff.DeviceEffectsDiff(effects1, null);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{removed}");
+
+ d = new ZenModeDiff.DeviceEffectsDiff(null, effects2);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{added}");
+
+ 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);
+
+ d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ 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]}");
+ }
+
+
+ @Test
+ public void testPolicyDiff_addRemoveSame() {
+ // Test add, remove, and both sides same
+ ZenPolicy effects = new ZenPolicy.Builder().build();
+
+ // Both sides same rule
+ ZenModeDiff.PolicyDiff dSame = new ZenModeDiff.PolicyDiff(effects, effects);
+ assertFalse(dSame.hasDiff());
+
+ // from existent rule to null: expect deleted
+ ZenModeDiff.PolicyDiff deleted = new ZenModeDiff.PolicyDiff(effects, null);
+ assertTrue(deleted.hasDiff());
+ assertTrue(deleted.wasRemoved());
+
+ // from null to new rule: expect added
+ ZenModeDiff.PolicyDiff added = new ZenModeDiff.PolicyDiff(null, effects);
+ assertTrue(added.hasDiff());
+ assertTrue(added.wasAdded());
+ }
+
+ @Test
+ public void testPolicyDiff_fieldDiffs() throws Exception {
+ // Start these the same
+ 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);
+
+ ZenModeDiff.PolicyDiff d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ assertTrue(d.hasDiff());
+
+ // Now diff them and check that each of the fields has a diff.
+ // Because ZenPolicy consolidates priority category and visual effect fields in a list,
+ // we cannot use reflection on ZenPolicy to get the list of fields.
+ ArrayList<String> diffFields = new ArrayList<>();
+ Field[] fields = ZenModeDiff.PolicyDiff.class.getDeclaredFields();
+
+ for (Field field : fields) {
+ int m = field.getModifiers();
+ if (Modifier.isStatic(m) && Modifier.isFinal(m)) {
+ diffFields.add((String) field.get(policy1));
+ }
+ }
+
+ for (String name : diffFields) {
+ assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
+ assertTrue(d.getDiffForField(name).hasDiff());
+ assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
+ assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
+ assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
+ assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
+ }
+ }
+
+ @Test
+ public void testPolicyDiff_toString() throws Exception {
+ // Ensure device effects toString is readable.
+ ZenPolicy policy1 = new ZenPolicy.Builder().build();
+ ZenPolicy policy2 = new ZenPolicy.Builder().build();
+
+ ZenModeDiff.PolicyDiff d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{no changes}");
+
+ d = new ZenModeDiff.PolicyDiff(policy1, null);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{removed}");
+
+ d = new ZenModeDiff.PolicyDiff(null, policy2);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{added}");
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenPolicy.class, Collections.emptySet() /*no exempt fields*/, false);
+ generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, expectedTo);
+
+ d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ 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() {
// "Metadata" fields are never compared.
Set<String> exemptFields = new LinkedHashSet<>(
@@ -194,7 +527,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.class, getConfigExemptAndFlaggedFields());
+ ZenModeConfig.class, getConfigExemptAndFlaggedFields(), false);
generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
@@ -223,7 +556,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS);
+ ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS, false);
generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
@@ -359,17 +692,23 @@
// Get the fields on which we would want to check a diff. The requirements are: not final or/
// static (as these should/can never change), and not in a specific list that's exempted.
- private List<Field> getFieldsForDiffCheck(Class<?> c, Set<String> exemptNames)
+ private List<Field> getFieldsForDiffCheck(Class<?> c, Set<String> exemptNames,
+ boolean includeFinal)
throws SecurityException {
Field[] fields = c.getDeclaredFields();
ArrayList<Field> out = new ArrayList<>();
for (Field field : fields) {
// Check for exempt reasons
+ // Anything in provided exemptNames is skipped.
+ if (exemptNames.contains(field.getName())) {
+ continue;
+ }
int m = field.getModifiers();
- if (Modifier.isFinal(m)
- || Modifier.isStatic(m)
- || exemptNames.contains(field.getName())) {
+ if (Modifier.isStatic(m)) {
+ continue;
+ }
+ if (!includeFinal && Modifier.isFinal(m)) {
continue;
}
out.add(field);
@@ -377,6 +716,106 @@
return out;
}
+ // Generate a set of diffs for two ZenPolicy objects. Store the results in the provided
+ // expectation maps.
+ private void generateFieldDiffsForZenPolicy(ZenPolicy a, ZenPolicy b, List<Field> fields,
+ ArrayMap<String, Object> expectedA, ArrayMap<String, Object> expectedB)
+ throws Exception {
+ // Loop through fields for which we want to check diffs, set a diff and keep track of
+ // what we set.
+ for (Field f : fields) {
+ f.setAccessible(true);
+ // Just double-check also that the fields actually are for the class declared
+ assertEquals(f.getDeclaringClass(), a.getClass());
+ Class<?> t = f.getType();
+
+ if (int.class.equals(t)) {
+ // these will not be valid for arbitrary int enums, but should suffice for a diff.
+ f.setInt(a, 2);
+ expectedA.put(f.getName(), 2);
+ f.setInt(b, 1);
+ expectedB.put(f.getName(), 1);
+ } else if (List.class.equals(t)) {
+ // Fieds mPriorityCategories and mVisualEffects store multiple values and
+ // must be treated separately.
+ List<Integer> aList = (ArrayList<Integer>) f.get(a);
+ List<Integer> bList = (ArrayList<Integer>) f.get(b);
+ if (f.getName().equals("mPriorityCategories")) {
+ // PRIORITY_CATEGORY_REMINDERS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 0,
+ "mPriorityCategories_Reminders");
+ // PRIORITY_CATEGORY_EVENTS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 1,
+ "mPriorityCategories_Events");
+ // PRIORITY_CATEGORY_MESSAGES
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 2,
+ "mPriorityCategories_Messages");
+ // PRIORITY_CATEGORY_CALLS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 3,
+ "mPriorityCategories_Calls");
+ // PRIORITY_CATEGORY_REPEAT_CALLERS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 4,
+ "mPriorityCategories_RepeatCallers");
+ // PRIORITY_CATEGORY_ALARMS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 5,
+ "mPriorityCategories_Alarms");
+ // PRIORITY_CATEGORY_MEDIA
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 6,
+ "mPriorityCategories_Media");
+ // PRIORITY_CATEGORY_SYSTEM
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 7,
+ "mPriorityCategories_System");
+ // PRIORITY_CATEGORY_CONVERSATIONS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 8,
+ "mPriorityCategories_Conversations");
+ // Assert that we've set every PriorityCategory enum value.
+ assertThat(Collections.frequency(aList, ZenPolicy.STATE_ALLOW))
+ .isEqualTo(ZenPolicy.NUM_PRIORITY_CATEGORIES);
+ } else if (f.getName().equals("mVisualEffects")) {
+ // VISUAL_EFFECT_FULL_SCREEN_INTENT
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 0,
+ "mVisualEffects_FullScreenIntent");
+ // VISUAL_EFFECT_LIGHTS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 1,
+ "mVisualEffects_Lights");
+ // VISUAL_EFFECT_PEEK
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 2,
+ "mVisualEffects_Peek");
+ // VISUAL_EFFECT_STATUS_BAR
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 3,
+ "mVisualEffects_StatusBar");
+ // VISUAL_EFFECT_BADGE
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 4,
+ "mVisualEffects_Badge");
+ // VISUAL_EFFECT_AMBIENT
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 5,
+ "mVisualEffects_Ambient");
+ // VISUAL_EFFECT_NOTIFICATION_LIST
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 6,
+ "mVisualEffects_NotificationList");
+ // Assert that we've set every VisualeEffect enum value.
+ assertThat(Collections.frequency(aList, ZenPolicy.STATE_ALLOW))
+ .isEqualTo(ZenPolicy.NUM_VISUAL_EFFECTS);
+ } else {
+ // Any other lists that are added should be added to the diff.
+ fail("could not generate field diffs for policy list: " + f.getName());
+ }
+ }
+ }
+ }
+
+ // Helper function to create a diff in two list values at a given index, and record that
+ // diff's values in the associated expected maps under the provided field name.
+ private void setPolicyListValueDiff(List<Integer> aList, List<Integer> bList,
+ ArrayMap<String, Object> expectedA,
+ ArrayMap<String, Object> expectedB,
+ int index, String fieldName) {
+ aList.set(index, ZenPolicy.STATE_ALLOW);
+ expectedA.put(fieldName, ZenPolicy.STATE_ALLOW);
+ bList.set(index, ZenPolicy.STATE_DISALLOW);
+ expectedB.put(fieldName, ZenPolicy.STATE_DISALLOW);
+ }
+
// Generate a set of generic diffs for the specified two objects and the fields to generate
// diffs for, and store the results in the provided expectation maps to be able to check the
// output later.
@@ -420,6 +859,44 @@
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 9b92ff4..3ea3235 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.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 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 @@
}
@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 1aa9087..a85f866 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 @@
mTestLooper.dispatchAll();
Assert.assertEquals(expectEnabled, mIsTalkBackEnabled);
}
+
+ void assertKeyGestureEventSentToKeyGestureController(int gestureType) {
+ verify(mInputManagerInternal)
+ .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType));
+ }
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 2bb86bc..1a42e80 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -687,6 +687,7 @@
@Override
public int startRecognitionForService(ParcelUuid soundModelId, Bundle params,
ComponentName detectionService, SoundTrigger.RecognitionConfig config) {
+ final UserHandle userHandle = Binder.getCallingUserHandle();
mEventLogger.enqueue(new SessionEvent(Type.START_RECOGNITION_SERVICE,
getUuid(soundModelId)));
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
@@ -699,7 +700,7 @@
IRecognitionStatusCallback callback =
new RemoteSoundTriggerDetectionService(soundModelId.getUuid(), params,
- detectionService, Binder.getCallingUserHandle(), config);
+ detectionService, userHandle, config);
synchronized (mLock) {
SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 92effe0..ff966ae 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -2298,13 +2298,9 @@
*
* 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 @@
* </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 @@
/**
* 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 @@
* 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();
@@ -10613,20 +10597,31 @@
return null;
}
- /** @hide */
+ /**
+ * Get the names of packages with carrier privileges for the current subscription.
+ *
+ * @throws UnsupportedOperationException If the device does not have {@link
+ * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}
+ * @hide
+ */
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- public List<String> getPackagesWithCarrierPrivileges() {
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+ @NonNull
+ public Set<String> getPackagesWithCarrierPrivileges() {
+ final Set<String> result = new HashSet<>();
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- return telephony.getPackagesWithCarrierPrivileges(getPhoneId());
+ result.addAll(telephony.getPackagesWithCarrierPrivileges(getPhoneId()));
}
} catch (RemoteException ex) {
Rlog.e(TAG, "getPackagesWithCarrierPrivileges RemoteException", ex);
} catch (NullPointerException ex) {
Rlog.e(TAG, "getPackagesWithCarrierPrivileges NPE", ex);
}
- return Collections.EMPTY_LIST;
+ return result;
}
/**
@@ -19367,12 +19362,9 @@
* </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();
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index c61a250..9f4df90 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -39,6 +39,7 @@
import junit.framework.Assert.fail
import org.hamcrest.Matchers.allOf
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -107,6 +108,7 @@
parser = InputJsonParser(instrumentation.context)
}
+ @Ignore("b/366602644")
@Test
fun testEvemuRecording() {
VirtualDisplayActivityScenario.AutoClose<CaptureEventActivity>(
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
index 9d56a92..8ecddaa 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
@@ -16,6 +16,8 @@
package com.android.internal.protolog;
+import static org.junit.Assert.assertThrows;
+
import android.platform.test.annotations.Presubmit;
import com.android.internal.protolog.common.IProtoLogGroup;
@@ -44,8 +46,29 @@
.containsExactly(TEST_GROUP_1, TEST_GROUP_2);
}
+ @Test
+ public void throwOnRegisteringDuplicateGroup() {
+ final var assertion = assertThrows(RuntimeException.class,
+ () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2));
+
+ Truth.assertThat(assertion).hasMessageThat().contains("" + TEST_GROUP_1.getId());
+ Truth.assertThat(assertion).hasMessageThat().contains("duplicate");
+ }
+
+ @Test
+ public void throwOnRegisteringGroupsWithIdCollisions() {
+ final var assertion = assertThrows(RuntimeException.class,
+ () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_WITH_COLLISION, TEST_GROUP_2));
+
+ Truth.assertThat(assertion).hasMessageThat()
+ .contains("" + TEST_GROUP_WITH_COLLISION.getId());
+ Truth.assertThat(assertion).hasMessageThat().contains("collision");
+ }
+
private static final IProtoLogGroup TEST_GROUP_1 = new ProtoLogGroup("TEST_TAG_1", 1);
private static final IProtoLogGroup TEST_GROUP_2 = new ProtoLogGroup("TEST_TAG_2", 2);
+ private static final IProtoLogGroup TEST_GROUP_WITH_COLLISION =
+ new ProtoLogGroup("TEST_TAG_WITH_COLLISION", 1);
private static class ProtoLogGroup implements IProtoLogGroup {
private final boolean mEnabled;
diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
index 81814b6..7bc9970 100644
--- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import android.net.NetworkCapabilities;
import android.net.wifi.WifiConfiguration;
@@ -39,6 +40,7 @@
private static final int SUB_ID = 1;
private static final int NETWORK_ID = 5;
private static final int MIN_UDP_PORT_4500_NAT_TIMEOUT = 120;
+ private static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_INVALID = 119;
private static final WifiInfo WIFI_INFO =
new WifiInfo.Builder().setNetworkId(NETWORK_ID).build();
@@ -48,6 +50,27 @@
new VcnTransportInfo(WIFI_INFO, MIN_UDP_PORT_4500_NAT_TIMEOUT);
@Test
+ public void testBuilder() {
+ final VcnTransportInfo transportInfo =
+ new VcnTransportInfo.Builder()
+ .setMinUdpPort4500NatTimeoutSeconds(MIN_UDP_PORT_4500_NAT_TIMEOUT)
+ .build();
+
+ assertEquals(
+ MIN_UDP_PORT_4500_NAT_TIMEOUT, transportInfo.getMinUdpPort4500NatTimeoutSeconds());
+ }
+
+ @Test
+ public void testBuilder_withInvalidNatTimeout() {
+ try {
+ new VcnTransportInfo.Builder()
+ .setMinUdpPort4500NatTimeoutSeconds(MIN_UDP_PORT_4500_NAT_TIMEOUT_INVALID);
+ fail("Expected to fail due to invalid NAT timeout");
+ } catch (Exception expected) {
+ }
+ }
+
+ @Test
public void testGetWifiInfo() {
assertEquals(WIFI_INFO, WIFI_UNDERLYING_INFO.getWifiInfo());
diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
new file mode 100644
index 0000000..3ce6c8f
--- /dev/null
+++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.vcn;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.wifi.WifiInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class VcnUtilsTest {
+ private static final int SUB_ID = 1;
+
+ private static final WifiInfo WIFI_INFO = new WifiInfo.Builder().build();
+ private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+ new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+ private static final VcnTransportInfo VCN_TRANSPORT_INFO =
+ new VcnTransportInfo.Builder().build();
+
+ private ConnectivityManager mMockConnectivityManager;
+ private Network mMockWifiNetwork;
+ private Network mMockCellNetwork;
+
+ private NetworkCapabilities mVcnCapsWithUnderlyingWifi;
+ private NetworkCapabilities mVcnCapsWithUnderlyingCell;
+
+ @Before
+ public void setUp() {
+ mMockConnectivityManager = mock(ConnectivityManager.class);
+
+ mMockWifiNetwork = mock(Network.class);
+ mVcnCapsWithUnderlyingWifi = newVcnCaps(VCN_TRANSPORT_INFO, mMockWifiNetwork);
+ final NetworkCapabilities wifiCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .setTransportInfo(WIFI_INFO)
+ .build();
+ when(mMockConnectivityManager.getNetworkCapabilities(mMockWifiNetwork))
+ .thenReturn(wifiCaps);
+
+ mMockCellNetwork = mock(Network.class);
+ mVcnCapsWithUnderlyingCell = newVcnCaps(VCN_TRANSPORT_INFO, mMockCellNetwork);
+ final NetworkCapabilities cellCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+ .build();
+ when(mMockConnectivityManager.getNetworkCapabilities(mMockCellNetwork))
+ .thenReturn(cellCaps);
+ }
+
+ private static NetworkCapabilities newVcnCaps(
+ VcnTransportInfo vcnTransportInfo, Network underlyingNetwork) {
+ return new NetworkCapabilities.Builder()
+ .setTransportInfo(vcnTransportInfo)
+ .setUnderlyingNetworks(Collections.singletonList(underlyingNetwork))
+ .build();
+ }
+
+ @Test
+ public void getWifiInfoFromVcnCaps() {
+ assertEquals(
+ WIFI_INFO,
+ VcnUtils.getWifiInfoFromVcnCaps(
+ mMockConnectivityManager, mVcnCapsWithUnderlyingWifi));
+ }
+
+ @Test
+ public void getWifiInfoFromVcnCaps_onVcnWithUnderlyingCell() {
+ assertNull(
+ VcnUtils.getWifiInfoFromVcnCaps(
+ mMockConnectivityManager, mVcnCapsWithUnderlyingCell));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps() {
+ assertEquals(
+ SUB_ID,
+ VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, mVcnCapsWithUnderlyingCell));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_onVcnWithUnderlyingWifi() {
+ assertEquals(
+ INVALID_SUBSCRIPTION_ID,
+ VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, mVcnCapsWithUnderlyingWifi));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_onNonVcnNetwork() {
+ assertEquals(
+ INVALID_SUBSCRIPTION_ID,
+ VcnUtils.getSubIdFromVcnCaps(
+ mMockConnectivityManager, new NetworkCapabilities.Builder().build()));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_withMultipleUnderlyingNetworks() {
+ final NetworkCapabilities vcnCaps =
+ new NetworkCapabilities.Builder(mVcnCapsWithUnderlyingCell)
+ .setUnderlyingNetworks(
+ Arrays.asList(
+ new Network[] {mMockCellNetwork, mock(Network.class)}))
+ .build();
+ assertEquals(SUB_ID, VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, vcnCaps));
+ }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index b5cc553..f1f74bc 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -206,7 +206,7 @@
.getAllSubscriptionInfoList();
doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
- setPrivilegedPackagesForMock(Collections.singletonList(PACKAGE_NAME));
+ setPrivilegedPackagesForMock(Collections.singleton(PACKAGE_NAME));
}
private IntentFilter getIntentFilter() {
@@ -293,7 +293,7 @@
Collections.singletonMap(TEST_SUBSCRIPTION_ID_1, TEST_CARRIER_CONFIG_WRAPPER));
}
- private void setPrivilegedPackagesForMock(@NonNull List<String> privilegedPackages) {
+ private void setPrivilegedPackagesForMock(@NonNull Set<String> privilegedPackages) {
doReturn(privilegedPackages).when(mTelephonyManager).getPackagesWithCarrierPrivileges();
}
@@ -390,7 +390,7 @@
@Test
public void testOnSubscriptionsChangedFired_onActiveSubIdsChanged() throws Exception {
setupReadySubIds();
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
doReturn(TEST_SUBSCRIPTION_ID_2).when(mDeps).getActiveDataSubscriptionId();
final ActiveDataSubscriptionIdListener listener = getActiveDataSubscriptionIdListener();
@@ -411,7 +411,7 @@
public void testOnSubscriptionsChangedFired_WithReadySubidsNoPrivilegedPackages()
throws Exception {
setupReadySubIds();
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
listener.onSubscriptionsChanged();
@@ -567,7 +567,7 @@
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
// Simulate a loss of carrier privileges
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();