Merge "Move all bouncer messages domain logic to the interactor" into main
diff --git a/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java b/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
index 7927aa9..885000f 100644
--- a/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
+++ b/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
@@ -22,7 +22,6 @@
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.perftests.utils.PerfTestActivity;
-import android.platform.test.annotations.LargeTest;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
@@ -32,6 +31,7 @@
import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
diff --git a/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java b/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
index 898111f..436ee16 100644
--- a/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
+++ b/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
@@ -22,13 +22,13 @@
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.perftests.utils.PerfTestActivity;
-import android.platform.test.annotations.LargeTest;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.CursorAnchorInfo;
import android.widget.TextView;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
index d9c4632..8504b1f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
@@ -4,7 +4,7 @@
"name": "CtsJobSchedulerTestCases",
"options": [
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.LargeTest"}
]
@@ -14,7 +14,7 @@
"options": [
{"include-filter": "com.android.server.job"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"}
]
},
@@ -23,7 +23,7 @@
"options": [
{"include-filter": "com.android.server.job"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"}
]
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 660859d..ad65806 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3232,6 +3232,7 @@
method public int getDefaultActivityPolicy();
method public int getDefaultNavigationPolicy();
method public int getDevicePolicy(int);
+ method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @Nullable public android.content.ComponentName getHomeComponent();
method public int getLockState();
method @Nullable public String getName();
method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
@@ -3263,6 +3264,7 @@
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
+ method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName);
method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setUsersWithMatchingAccounts(@NonNull java.util.Set<android.os.UserHandle>);
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 93107ce..315a055 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -138,7 +138,7 @@
"include-annotation": "android.platform.test.annotations.Presubmit"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 213e5cb..7704486 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -10335,11 +10335,14 @@
* @return the current credential manager policy if null then this policy has not been
* configured.
*/
+ @UserHandleAware(
+ enabledSinceTargetSdkVersion = UPSIDE_DOWN_CAKE,
+ requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
public @Nullable PackagePolicy getCredentialManagerPolicy() {
throwIfParentInstance("getCredentialManagerPolicy");
if (mService != null) {
try {
- return mService.getCredentialManagerPolicy();
+ return mService.getCredentialManagerPolicy(myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index c49b820..58f9d57 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -346,7 +346,7 @@
boolean hasManagedProfileCallerIdAccess(int userId, String packageName);
void setCredentialManagerPolicy(in PackagePolicy policy);
- PackagePolicy getCredentialManagerPolicy();
+ PackagePolicy getCredentialManagerPolicy(int userId);
void setManagedProfileContactsAccessPolicy(in PackagePolicy policy);
PackagePolicy getManagedProfileContactsAccessPolicy();
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index b4c740ec..0fa78c8 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -229,6 +229,7 @@
@Nullable private final String mName;
// Mapping of @PolicyType to @DevicePolicy
@NonNull private final SparseIntArray mDevicePolicies;
+ @Nullable private final ComponentName mHomeComponent;
@NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
@Nullable private final IVirtualSensorCallback mVirtualSensorCallback;
private final int mAudioPlaybackSessionId;
@@ -243,6 +244,7 @@
@NonNull Set<ComponentName> activityPolicyExemptions,
@Nullable String name,
@NonNull SparseIntArray devicePolicies,
+ @Nullable ComponentName homeComponent,
@NonNull List<VirtualSensorConfig> virtualSensorConfigs,
@Nullable IVirtualSensorCallback virtualSensorCallback,
int audioPlaybackSessionId,
@@ -258,6 +260,7 @@
new ArraySet<>(Objects.requireNonNull(activityPolicyExemptions));
mName = name;
mDevicePolicies = Objects.requireNonNull(devicePolicies);
+ mHomeComponent = homeComponent;
mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
mVirtualSensorCallback = virtualSensorCallback;
mAudioPlaybackSessionId = audioPlaybackSessionId;
@@ -280,6 +283,7 @@
IVirtualSensorCallback.Stub.asInterface(parcel.readStrongBinder());
mAudioPlaybackSessionId = parcel.readInt();
mAudioRecordingSessionId = parcel.readInt();
+ mHomeComponent = parcel.readTypedObject(ComponentName.CREATOR);
}
/**
@@ -291,6 +295,19 @@
}
/**
+ * Returns the custom component used as home on all displays owned by this virtual device that
+ * support home activities.
+ *
+ * @see Builder#setHomeComponent
+ */
+ // TODO(b/297168328): Link to the relevant API for creating displays with home support.
+ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
+ @Nullable
+ public ComponentName getHomeComponent() {
+ return mHomeComponent;
+ }
+
+ /**
* Returns the user handles with matching managed accounts on the remote device to which
* this virtual device is streaming.
*
@@ -468,6 +485,7 @@
mVirtualSensorCallback != null ? mVirtualSensorCallback.asBinder() : null);
dest.writeInt(mAudioPlaybackSessionId);
dest.writeInt(mAudioRecordingSessionId);
+ dest.writeTypedObject(mHomeComponent, flags);
}
@Override
@@ -508,7 +526,7 @@
int hashCode = Objects.hash(
mLockState, mUsersWithMatchingAccounts, mCrossTaskNavigationExemptions,
mDefaultNavigationPolicy, mActivityPolicyExemptions, mDefaultActivityPolicy, mName,
- mDevicePolicies, mAudioPlaybackSessionId, mAudioRecordingSessionId);
+ mDevicePolicies, mHomeComponent, mAudioPlaybackSessionId, mAudioRecordingSessionId);
for (int i = 0; i < mDevicePolicies.size(); i++) {
hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -528,6 +546,7 @@
+ " mActivityPolicyExemptions=" + mActivityPolicyExemptions
+ " mName=" + mName
+ " mDevicePolicies=" + mDevicePolicies
+ + " mHomeComponent=" + mHomeComponent
+ " mAudioPlaybackSessionId=" + mAudioPlaybackSessionId
+ " mAudioRecordingSessionId=" + mAudioRecordingSessionId
+ ")";
@@ -588,6 +607,7 @@
@Nullable private VirtualSensorCallback mVirtualSensorCallback;
@Nullable private Executor mVirtualSensorDirectChannelCallbackExecutor;
@Nullable private VirtualSensorDirectChannelCallback mVirtualSensorDirectChannelCallback;
+ @Nullable private ComponentName mHomeComponent;
private static class VirtualSensorCallbackDelegate extends IVirtualSensorCallback.Stub {
@NonNull
@@ -665,6 +685,23 @@
}
/**
+ * Specifies a component to be used as home on all displays owned by this virtual device
+ * that support home activities.
+ * *
+ * <p>Note: Only relevant for virtual displays that support home activities.</p>
+ *
+ * @param homeComponent The component name to be used as home. If unset, then the system-
+ * default secondary home activity will be used.
+ */
+ // TODO(b/297168328): Link to the relevant API for creating displays with home support.
+ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
+ @NonNull
+ public Builder setHomeComponent(@Nullable ComponentName homeComponent) {
+ mHomeComponent = homeComponent;
+ return this;
+ }
+
+ /**
* Sets the user handles with matching managed accounts on the remote device to which
* this virtual device is streaming. The caller is responsible for verifying the presence
* and legitimacy of a matching managed account on the remote device.
@@ -1031,6 +1068,7 @@
mActivityPolicyExemptions,
mName,
mDevicePolicies,
+ mHomeComponent,
mVirtualSensorConfigs,
virtualSensorCallbackDelegate,
mAudioPlaybackSessionId,
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index ee36f18..3e96c96 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -15,6 +15,13 @@
}
flag {
+ name: "vdm_custom_home"
+ namespace: "virtual_devices"
+ description: "Enable custom home API"
+ bug: "297168328"
+}
+
+flag {
name: "vdm_public_apis"
namespace: "virtual_devices"
description: "Enable public VDM API for device capabilities"
diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING
index 01a9373..addede4 100644
--- a/core/java/android/content/TEST_MAPPING
+++ b/core/java/android/content/TEST_MAPPING
@@ -7,7 +7,7 @@
"include-annotation": "android.platform.test.annotations.Presubmit"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 60622f18..ad3abd9 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -7,7 +7,7 @@
],
"name": "FrameworksVibratorCoreTests",
"options": [
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "org.junit.Ignore"}
@@ -20,7 +20,7 @@
],
"name": "FrameworksVibratorServicesTests",
"options": [
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "org.junit.Ignore"}
@@ -33,7 +33,7 @@
],
"name": "CtsVibratorTestCases",
"options": [
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "org.junit.Ignore"}
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index febe6f7..a95e66d 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -16,7 +16,7 @@
flag {
name: "allow_private_profile"
- namespace: "private_profile"
+ namespace: "profile_experiences"
description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
bug: "299069460"
}
diff --git a/core/java/android/service/notification/TEST_MAPPING b/core/java/android/service/notification/TEST_MAPPING
index 59b2bc1..7b8d52f 100644
--- a/core/java/android/service/notification/TEST_MAPPING
+++ b/core/java/android/service/notification/TEST_MAPPING
@@ -13,9 +13,6 @@
"exclude-annotation": "org.junit.Ignore"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
- },
- {
"exclude-annotation": "androidx.test.filters.LargeTest"
}
]
@@ -33,9 +30,6 @@
"exclude-annotation": "org.junit.Ignore"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
- },
- {
"exclude-annotation": "androidx.test.filters.LargeTest"
}
]
diff --git a/core/java/android/view/TEST_MAPPING b/core/java/android/view/TEST_MAPPING
index 1e39716..db35908 100644
--- a/core/java/android/view/TEST_MAPPING
+++ b/core/java/android/view/TEST_MAPPING
@@ -10,7 +10,7 @@
"include-annotation": "android.platform.test.annotations.Presubmit"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ff6165b..0ba5d06 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1151,14 +1151,10 @@
}
private boolean isInTouchMode() {
- IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService();
- if (windowManager != null) {
- try {
- return windowManager.isInTouchMode(getDisplayId());
- } catch (RemoteException e) {
- }
+ if (mAttachInfo == null) {
+ return mContext.getResources().getBoolean(R.bool.config_defaultInTouchMode);
}
- return false;
+ return mAttachInfo.mInTouchMode;
}
/**
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index 2747233a..8d71a8e 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -109,6 +109,12 @@
}
/**
+ * @return the custom home component specified for the relevant display, if any.
+ */
+ @Nullable
+ public abstract ComponentName getCustomHomeComponent();
+
+ /**
* Returns {@code true} if all of the given activities can be launched on this virtual display
* in the configuration defined by the rest of the arguments.
*
diff --git a/core/java/com/android/internal/policy/AttributeCache.java b/core/java/com/android/internal/policy/AttributeCache.java
index 1bdad25..970f511 100644
--- a/core/java/com/android/internal/policy/AttributeCache.java
+++ b/core/java/com/android/internal/policy/AttributeCache.java
@@ -16,12 +16,18 @@
package com.android.internal.policy;
+import android.annotation.RequiresPermission;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Handler;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.LruCache;
@@ -46,6 +52,8 @@
@GuardedBy("this")
private final Configuration mConfiguration = new Configuration();
+ private PackageMonitor mPackageMonitor;
+
public final static class Package {
public final Context context;
private final SparseArray<ArrayMap<int[], Entry>> mMap = new SparseArray<>();
@@ -77,6 +85,34 @@
}
}
+ /**
+ * Start monitor package change, so the resources can be loaded correctly.
+ */
+ void monitorPackageRemove(Handler handler) {
+ if (mPackageMonitor == null) {
+ mPackageMonitor = new PackageMonitor(mContext, handler);
+ }
+ }
+
+ static class PackageMonitor extends BroadcastReceiver {
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ PackageMonitor(Context context, Handler handler) {
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
+ context.registerReceiverAsUser(this, UserHandle.ALL, filter,
+ null /* broadcastPermission */, handler);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final Uri packageUri = intent.getData();
+ if (packageUri != null) {
+ final String packageName = packageUri.getEncodedSchemeSpecificPart();
+ AttributeCache.instance().removePackage(packageName);
+ }
+ }
+ }
+
public static AttributeCache instance() {
return sInstance;
}
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 8f4df80..40a437f 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -48,6 +48,7 @@
import android.hardware.HardwareBuffer;
import android.media.Image;
import android.media.ImageReader;
+import android.os.Handler;
import android.os.SystemProperties;
import android.util.Slog;
import android.view.InflateException;
@@ -1399,4 +1400,14 @@
// Approximation of WCAG 2.0 relative luminance.
return ((r * 8) + (g * 22) + (b * 2)) >> 5;
}
+
+ /**
+ * For non-system server process, it must call this method to initialize the AttributeCache and
+ * start monitor package change, so the resources can be loaded correctly.
+ */
+ public static void initAttributeCache(Context context, Handler handler) {
+ AttributeCache.init(context);
+ AttributeCache.instance().monitorPackageRemove(handler);
+ }
+
}
diff --git a/core/tests/coretests/src/android/colormodel/CamTest.java b/core/tests/coretests/src/android/colormodel/CamTest.java
index 5bcc593..05fc0e0 100644
--- a/core/tests/coretests/src/android/colormodel/CamTest.java
+++ b/core/tests/coretests/src/android/colormodel/CamTest.java
@@ -18,7 +18,7 @@
import static org.junit.Assert.assertEquals;
-import android.platform.test.annotations.LargeTest;
+import androidx.test.filters.LargeTest;
import org.junit.Assert;
import org.junit.Test;
diff --git a/core/tests/coretests/src/android/provider/NameValueCacheTest.java b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
index 87e4a42..989c992 100644
--- a/core/tests/coretests/src/android/provider/NameValueCacheTest.java
+++ b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
@@ -55,7 +55,6 @@
* Due to how the classes are structured, we have to test it in a somewhat roundabout way. We're
* mocking out the contentProvider and are handcrafting very specific Bundles to answer the queries.
*/
-@Ignore("b/297724333")
@Presubmit
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -229,6 +228,8 @@
@After
public void cleanUp() throws IOException {
+ Settings.Config.clearProviderForTest();
+ Settings.Secure.clearProviderForTest();
mConfigsStorage.clear();
mSettingsStorage.clear();
mSettingsCacheGenerationStore.close();
diff --git a/core/tests/vibrator/TEST_MAPPING b/core/tests/vibrator/TEST_MAPPING
index f3333d8..2f3afa6 100644
--- a/core/tests/vibrator/TEST_MAPPING
+++ b/core/tests/vibrator/TEST_MAPPING
@@ -3,7 +3,7 @@
{
"name": "FrameworksVibratorCoreTests",
"options": [
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "org.junit.Ignore"}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index 13c0ac4..b71c48e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -76,6 +76,10 @@
private IRemoteTransition mOccludeByDreamTransition = null;
private IRemoteTransition mUnoccludeTransition = null;
+ // While set true, Keyguard has created a remote animation runner to handle the open app
+ // transition.
+ private boolean mIsLaunchingActivityOverLockscreen;
+
private final class StartedTransition {
final TransitionInfo mInfo;
final SurfaceControl.Transaction mFinishT;
@@ -120,7 +124,7 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull TransitionFinishCallback finishCallback) {
- if (!handles(info)) {
+ if (!handles(info) || mIsLaunchingActivityOverLockscreen) {
return false;
}
@@ -313,5 +317,11 @@
mUnoccludeTransition = unoccludeTransition;
});
}
+
+ @Override
+ public void setLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
+ mMainExecutor.execute(() ->
+ mIsLaunchingActivityOverLockscreen = isLaunchingActivityOverLockscreen);
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
index b4b327f..33c299f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
@@ -38,4 +38,9 @@
@NonNull IRemoteTransition occludeTransition,
@NonNull IRemoteTransition occludeByDreamTransition,
@NonNull IRemoteTransition unoccludeTransition) {}
+
+ /**
+ * Notify whether keyguard has created a remote animation runner for next app launch.
+ */
+ default void setLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 00f6a1c..83dc7fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -111,6 +111,14 @@
WindowContainerTransaction mFinishWCT = null;
/**
+ * Whether the transition has request for remote transition while mLeftoversHandler
+ * isn't remote transition handler.
+ * If true and the mLeftoversHandler can handle the transition, need to notify remote
+ * transition handler to consume the transition.
+ */
+ boolean mHasRequestToRemote;
+
+ /**
* Mixed transitions are made up of multiple "parts". This keeps track of how many
* parts are currently animating.
*/
@@ -200,6 +208,10 @@
MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
mixed.mLeftoversHandler = handler.first;
mActiveTransitions.add(mixed);
+ if (mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+ mixed.mHasRequestToRemote = true;
+ mPlayer.getRemoteTransitionHandler().handleRequest(transition, request);
+ }
return handler.second;
} else if (mSplitHandler.isSplitScreenVisible()
&& isOpeningType(request.getType())
@@ -316,12 +328,22 @@
// the time of handleRequest, but we need more information than is available at that time.
if (KeyguardTransitionHandler.handles(info)) {
if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Converting mixed transition into a keyguard transition");
- onTransitionConsumed(transition, false, null);
+ final MixedTransition keyguardMixed =
+ new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+ mActiveTransitions.add(keyguardMixed);
+ final boolean hasAnimateKeyguard = animateKeyguard(keyguardMixed, info,
+ startTransaction, finishTransaction, finishCallback);
+ if (hasAnimateKeyguard) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Converting mixed transition into a keyguard transition");
+ // Consume the original mixed transition
+ onTransitionConsumed(transition, false, null);
+ return true;
+ } else {
+ // Keyguard handler cannot handle it, process through original mixed
+ mActiveTransitions.remove(keyguardMixed);
+ }
}
- mixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
- mActiveTransitions.add(mixed);
}
if (mixed == null) return false;
@@ -332,8 +354,17 @@
} else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
return false;
} else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
- return animateOpenIntentWithRemoteAndPip(mixed, info, startTransaction,
- finishTransaction, finishCallback);
+ final boolean handledToPip = animateOpenIntentWithRemoteAndPip(mixed, info,
+ startTransaction, finishTransaction, finishCallback);
+ // Consume the transition on remote handler if the leftover handler already handle this
+ // transition. And if it cannot, the transition will be handled by remote handler, so
+ // don't consume here.
+ // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
+ if (handledToPip && mixed.mHasRequestToRemote
+ && mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+ mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
+ }
+ return handledToPip;
} else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -799,5 +830,8 @@
} else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
}
+ if (mixed.mHasRequestToRemote) {
+ mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 7df658e..de03f58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -99,7 +99,6 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.AttributeCache;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
@@ -182,7 +181,7 @@
/* broadcastPermission = */ null,
mMainHandler);
- AttributeCache.init(mContext);
+ TransitionAnimation.initAttributeCache(mContext, mMainHandler);
}
private void updateEnterpriseThumbnailDrawable() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 77f14f1..adf92d8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -50,7 +50,7 @@
}
@Before
- fun before() {
+ fun setUp() {
Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
new file mode 100644
index 0000000..ba2b3e7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.wm.shell.flicker.appcompat
+
+import android.os.Build
+import android.tools.common.datatypes.Rect
+import android.platform.test.annotations.Postsubmit
+import android.system.helpers.CommandsHelper
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.helpers.FIND_TIMEOUT
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.LetterboxAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test rotating an immersive app in fullscreen.
+ *
+ * To run this test: `atest WMShellFlickerTestsOther:RotateImmersiveAppInFullscreenTest`
+ *
+ * Actions:
+ * ```
+ * Rotate the device by 90 degrees to trigger a rotation through sensors
+ * Verify that the button exists
+ * ```
+ *
+ * Notes:
+ * ```
+ * Some default assertions that are inherited from
+ * the `BaseTest` are ignored due to the nature of the immersive apps.
+ *
+ * This test only works with Cuttlefish devices.
+ * ```
+ */
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class RotateImmersiveAppInFullscreenTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
+
+ private val immersiveApp = LetterboxAppHelper(instrumentation,
+ launcherName = ActivityOptions.PortraitImmersiveActivity.LABEL,
+ component =
+ ActivityOptions.PortraitImmersiveActivity.COMPONENT.toFlickerComponent())
+
+ private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+ private val execAdb: (String) -> String = { cmd -> cmdHelper.executeShellCommand(cmd) }
+
+ protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
+
+ private val isCuttlefishDevice: Boolean = Build.MODEL.contains("Cuttlefish")
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ setStartRotation()
+ immersiveApp.launchViaIntent(wmHelper)
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds
+ ?: error("Display not found")
+ }
+ transitions {
+ if (isCuttlefishDevice) {
+ // Simulates a device rotation through sensors because the rotation button
+ // only appears in a rotation event through sensors
+ execAdb("/vendor/bin/cuttlefish_sensor_injection rotate 0")
+ // verify rotation button existence
+ val rotationButtonSelector = By.res(LAUNCHER_PACKAGE, "rotate_suggestion")
+ uiDevice.wait(Until.hasObject(rotationButtonSelector), FIND_TIMEOUT)
+ uiDevice.findObject(rotationButtonSelector)
+ ?: error("rotation button not found")
+ }
+ }
+ teardown {
+ immersiveApp.exit(wmHelper)
+ }
+ }
+
+ @Before
+ fun setUpForImmersiveAppTests() {
+ Assume.assumeTrue(isCuttlefishDevice)
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun taskBarLayerIsVisibleAtStartAndEnd() {
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun navBarLayerIsVisibleAtStartAndEnd() {
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun statusBarLayerIsVisibleAtStartAndEnd() {
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun taskBarWindowIsAlwaysVisible() {
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun navBarWindowIsAlwaysVisible() {
+ }
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun statusBarWindowIsAlwaysVisible() {
+ }
+
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun statusBarLayerPositionAtStartAndEnd() {
+ }
+
+ @Test
+ @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+ }
+
+ /** Test that app is fullscreen by checking status bar and task bar visibility. */
+ @Postsubmit
+ @Test
+ fun appWindowFullScreen() {
+ flicker.assertWmEnd {
+ this.isAppWindowInvisible(ComponentNameMatcher.STATUS_BAR)
+ .isAppWindowInvisible(ComponentNameMatcher.TASK_BAR)
+ .visibleRegion(immersiveApp).coversExactly(startDisplayBounds)
+ }
+ }
+
+ /** Test that app is in the original rotation we have set up. */
+ @Postsubmit
+ @Test
+ fun appInOriginalRotation() {
+ flicker.assertWmEnd {
+ this.hasRotation(Rotation.ROTATION_90)
+ }
+ }
+
+ companion object {
+ private var startDisplayBounds = Rect.EMPTY
+ const val LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher"
+
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_90),
+ // TODO(b/292403378): 3 button mode not added as rotation button is hidden in taskbar
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 8c81733..d1067a9 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -105,6 +105,13 @@
screen. -->
<item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
+ <!-- Proportion of the screen height to use to set the maximum height of the bouncer to when
+ the device is in the DEVICE_POSTURE_HALF_OPENED posture.
+
+ This value is only used when motion layout bouncer is used - when flag
+ landscape.enable_lockscreen (b/293252410) is on -->
+ <item name="motion_layout_half_fold_bouncer_height_ratio" type="dimen" format="float">0.55</item>
+
<!-- The actual amount of translation that is applied to the security when it animates from one
side of the screen to the other in one-handed or user switcher mode. Note that it will
always translate from the side of the screen to the other (it will "jump" closer to the
diff --git a/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml b/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
index 6112411..751d6d8 100644
--- a/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
+++ b/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
@@ -10,9 +10,34 @@
motion:duration="0"
motion:autoTransition="none"/>
+ <Transition
+ motion:constraintSetStart="@id/single_constraints"
+ motion:constraintSetEnd="@+id/half_folded_single_constraints"
+ motion:duration="@integer/material_motion_duration_short_1"
+ motion:autoTransition="none"/>
+
<!-- No changes to default layout -->
<ConstraintSet android:id="@+id/single_constraints"/>
+ <ConstraintSet android:id="@+id/half_folded_single_constraints">
+
+ <Constraint
+ android:id="@+id/pattern_top_guideline"
+ androidprv:layout_constraintGuide_percent=
+ "@dimen/motion_layout_half_fold_bouncer_height_ratio"/>
+
+ <Constraint
+ android:id="@+id/keyguard_selector_fade_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="0dp"
+ android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toBottomOf="parent"
+ androidprv:layout_constraintTop_toBottomOf="@+id/flow1"/>
+
+ </ConstraintSet>
+
<ConstraintSet android:id="@+id/split_constraints">
<Constraint
diff --git a/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml b/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
index 2a1270c..cc498f4 100644
--- a/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
+++ b/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
@@ -26,11 +26,35 @@
motion:constraintSetStart="@id/single_constraints"
motion:constraintSetEnd="@+id/split_constraints"
motion:duration="0"
- motion:autoTransition="none"/>
+ motion:autoTransition="none" />
+
+ <Transition
+ motion:constraintSetStart="@id/single_constraints"
+ motion:constraintSetEnd="@+id/half_folded_single_constraints"
+ motion:duration="@integer/material_motion_duration_short_1" />
<!-- No changes to default layout -->
<ConstraintSet android:id="@+id/single_constraints"/>
+ <ConstraintSet android:id="@+id/half_folded_single_constraints">
+
+ <Constraint
+ android:id="@+id/pin_pad_top_guideline"
+ androidprv:layout_constraintGuide_percent=
+ "@dimen/motion_layout_half_fold_bouncer_height_ratio"/>
+
+ <Constraint
+ android:id="@+id/keyguard_selector_fade_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="0dp"
+ android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+ android:orientation="vertical"
+ androidprv:layout_constraintBottom_toBottomOf="parent"
+ androidprv:layout_constraintTop_toBottomOf="@+id/flow1"/>
+
+ </ConstraintSet>
+
<ConstraintSet android:id="@+id/split_constraints">
<Constraint
@@ -68,4 +92,5 @@
android:layout_marginTop="@dimen/keyguard_eca_top_margin" />
</ConstraintSet>
+
</MotionScene>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bluetooth_device_item.xml b/packages/SystemUI/res/layout/bluetooth_device_item.xml
new file mode 100644
index 0000000..6dd44fb
--- /dev/null
+++ b/packages/SystemUI/res/layout/bluetooth_device_item.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+
+<!-- TODO(b/298124674) remove this root -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/bluetooth_device_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginBottom="4dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/bluetooth_device_row"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_height="@dimen/bluetooth_dialog_device_height"
+ android:paddingEnd="24dp"
+ android:paddingStart="20dp"
+ android:baselineAligned="false">
+
+ <ImageView
+ android:id="@+id/bluetooth_device_icon"
+ android:contentDescription="@string/accessibility_bluetooth_device_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_gravity="center_vertical" />
+
+ <View
+ android:id="@+id/bluetooth_device"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="@+id/bluetooth_device_name"
+ app:layout_constraintBottom_toBottomOf="@+id/bluetooth_device_summary"
+ app:layout_constraintStart_toStartOf="@+id/bluetooth_device_name"
+ app:layout_constraintEnd_toEndOf="@+id/bluetooth_device_name" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/bluetooth_device_name"
+ style="@style/BluetoothTileDialog.DeviceName"
+ android:paddingStart="20dp"
+ android:paddingTop="10dp"
+ app:layout_constraintWidth_percent="0.7"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/bluetooth_device_icon"
+ app:layout_constraintEnd_toStartOf="@+id/gear_icon"
+ app:layout_constraintBottom_toTopOf="@+id/bluetooth_device_summary"
+ android:gravity="center_vertical"
+ android:textSize="14sp" />
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/bluetooth_device_summary"
+ style="@style/BluetoothTileDialog.DeviceSummary"
+ android:paddingStart="20dp"
+ android:paddingBottom="10dp"
+ app:layout_constraintWidth_percent="0.7"
+ app:layout_constraintTop_toBottomOf="@+id/bluetooth_device_name"
+ app:layout_constraintStart_toEndOf="@+id/bluetooth_device_icon"
+ app:layout_constraintEnd_toStartOf="@+id/gear_icon"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical" />
+
+ <ImageView
+ android:id="@+id/gear_icon"
+ android:src="@drawable/ic_settings_24dp"
+ android:contentDescription="@string/accessibility_bluetooth_device_settings_gear"
+ android:layout_width="0dp"
+ android:layout_height="24dp"
+ app:layout_constraintStart_toEndOf="@+id/bluetooth_device_name"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintWidth_percent="0.3"
+ android:gravity="center_vertical"
+ android:paddingStart="10dp" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
new file mode 100644
index 0000000..16aeb95
--- /dev/null
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ style="@style/Widget.SliceView.Panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/bluetooth_tile_dialog_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="24dp"
+ android:ellipsize="end"
+ android:gravity="center_vertical|center_horizontal"
+ android:text="@string/quick_settings_bluetooth_label"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ android:textSize="24sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/bluetooth_tile_dialog_subtitle"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/bluetooth_tile_dialog_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin"
+ android:ellipsize="end"
+ android:gravity="center_vertical|center_horizontal"
+ android:maxLines="1"
+ android:text="@string/quick_settings_bluetooth_tile_subtitle"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/bluetooth_toggle_title"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_title" />
+
+ <TextView
+ android:id="@+id/bluetooth_toggle_title"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_width="0dp"
+ android:layout_height="64dp"
+ android:gravity="center_vertical"
+ android:layout_marginTop="4dp"
+ android:text="@string/turn_on_bluetooth"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ android:textSize="16sp"
+ app:layout_constraintEnd_toStartOf="@+id/bluetooth_toggle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle" />
+
+ <Switch
+ android:id="@+id/bluetooth_toggle"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:gravity="center|center_vertical"
+ android:paddingEnd="24dp"
+ android:layout_marginTop="10dp"
+ android:contentDescription="@string/turn_on_bluetooth"
+ android:switchMinWidth="@dimen/settingslib_switch_track_width"
+ android:theme="@style/MainSwitch.Settingslib"
+ android:thumb="@drawable/settingslib_thumb_selector"
+ android:track="@drawable/settingslib_track_selector"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle" />
+
+ <androidx.constraintlayout.widget.Group
+ android:id="@+id/pair_new_device_layout_group"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:constraint_referenced_ids="ic_add,pair_new_device_text" />
+
+ <ImageView
+ android:id="@+id/ic_add"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginStart="36dp"
+ android:gravity="center_vertical"
+ android:importantForAccessibility="no"
+ android:src="@drawable/ic_add"
+ app:layout_constraintBottom_toTopOf="@id/device_list"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/pair_new_device_text"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_toggle_title"
+ android:tint="?android:attr/textColorPrimary" />
+
+ <TextView
+ android:id="@+id/pair_new_device_text"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/bluetooth_dialog_device_height"
+ android:gravity="center_vertical"
+ android:layout_marginStart="0dp"
+ android:paddingStart="20dp"
+ android:text="@string/pair_new_bluetooth_devices"
+ android:textSize="14sp"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ app:layout_constraintBottom_toTopOf="@id/device_list"
+ app:layout_constraintStart_toEndOf="@+id/ic_add"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_toggle_title"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/device_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:nestedScrollingEnabled="false"
+ android:overScrollMode="never"
+ android:scrollbars="vertical"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pair_new_device_text"
+ app:layout_constraintBottom_toTopOf="@+id/see_all_text" />
+
+ <androidx.constraintlayout.widget.Group
+ android:id="@+id/see_all_layout_group"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:constraint_referenced_ids="ic_arrow,see_all_text" />
+
+ <ImageView
+ android:id="@+id/ic_arrow"
+ android:layout_marginStart="36dp"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:importantForAccessibility="no"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_arrow_forward"
+ app:layout_constraintBottom_toTopOf="@+id/done_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/see_all_text"
+ app:layout_constraintTop_toBottomOf="@id/device_list" />
+
+ <TextView
+ android:id="@+id/see_all_text"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/bluetooth_dialog_device_height"
+ android:gravity="center_vertical"
+ android:layout_marginStart="0dp"
+ android:paddingStart="20dp"
+ android:text="@string/see_all_bluetooth_devices"
+ android:textSize="14sp"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ app:layout_constraintBottom_toTopOf="@+id/done_button"
+ app:layout_constraintStart_toEndOf="@+id/ic_arrow"
+ app:layout_constraintTop_toBottomOf="@id/device_list"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <Button
+ android:id="@+id/done_button"
+ style="@style/Widget.Dialog.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/dialog_bottom_padding"
+ android:layout_marginEnd="@dimen/dialog_side_padding"
+ android:layout_marginStart="@dimen/dialog_side_padding"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:focusable="true"
+ android:maxLines="1"
+ android:text="@string/inline_done_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/see_all_text" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7e9d3f5..88726af 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1643,6 +1643,11 @@
<!-- Radius of switch track -->
<dimen name="settingslib_switch_track_radius">35dp</dimen>
+ <!-- Bluetooth dialog related dimensions -->
+ <dimen name="bluetooth_dialog_layout_margin">16dp</dimen>
+ <!-- The height of the bluetooth device in bluetooth dialog. -->
+ <dimen name="bluetooth_dialog_device_height">72dp</dimen>
+
<!-- Height percentage of the parent container occupied by the communal view -->
<item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 29c9767..5860806 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -459,7 +459,12 @@
<!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_bluetooth_connected">Bluetooth connected.</string>
- <!-- Content description of the bluetooth icon when connecting for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+
+ <!-- Content description of the bluetooth device icon. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_bluetooth_device_icon">Bluetooth device icon</string>
+
+ <!-- Content description of the bluetooth device settings gear icon. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_bluetooth_device_settings_gear">Bluetooth device settings gear</string>
<!-- Content description of the battery when battery state is unknown for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_battery_unknown">Battery percentage unknown.</string>
@@ -621,6 +626,19 @@
<!-- QuickSettings: Bluetooth (Off) [CHAR LIMIT=NONE] -->
<!-- QuickSettings: Bluetooth detail panel, text when there are no items [CHAR LIMIT=NONE] -->
<string name="quick_settings_bluetooth_detail_empty_text">No paired devices available</string>
+ <!-- QuickSettings: Bluetooth dialog subtitle [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_tile_subtitle">Tap a device to connect</string>
+ <!-- QuickSettings: Bluetooth dialog pair new devices [CHAR LIMIT=NONE]-->
+ <string name="pair_new_bluetooth_devices">Pair new device</string>
+ <!-- QuickSettings: Bluetooth dialog see all devices [CHAR LIMIT=NONE]-->
+ <string name="see_all_bluetooth_devices">See all</string>
+ <!-- QuickSettings: Bluetooth dialog turn on Bluetooth [CHAR LIMIT=NONE]-->
+ <string name="turn_on_bluetooth">Use Bluetooth</string>
+ <!-- QuickSettings: Bluetooth dialog device connected default summary [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_device_connected">Connected</string>
+ <!-- QuickSettings: Bluetooth dialog device saved default summary [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_device_saved">Saved</string>
+
<!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
<string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
<!-- QuickSettings: Bluetooth secondary label for an audio device being connected [CHAR LIMIT=20]-->
@@ -3086,7 +3104,7 @@
configured. This is shown as part of a dialog that explains to the user why they cannot select
this shortcut for their lock screen right now. [CHAR LIMIT=NONE].
-->
- <string name="home_quick_affordance_unavailable_configure_the_app">• At least one device is available</string>
+ <string name="home_quick_affordance_unavailable_configure_the_app">• At least one device or device panel are available</string>
<!---
Explains that the notes app is not available. This is shown as part of a dialog that explains to
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6991b96..084cb88 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -15,7 +15,7 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<style name="TextAppearance.StatusBar.Clock" parent="@*android:style/TextAppearance.StatusBar.Icon">
<item name="android:textSize">@dimen/status_bar_clock_size</item>
@@ -54,10 +54,10 @@
</style>
<style name="TextAppearance.StatusBar.Expanded.EmergencyCallsOnly"
- parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
+ parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
<style name="TextAppearance.StatusBar.Expanded.ChargingInfo"
- parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
+ parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
<style name="TextAppearance.StatusBar.Expanded.UserSwitcher">
<item name="android:textSize">@dimen/kg_user_switcher_text_size</item>
@@ -645,7 +645,7 @@
</style>
<style name="TextAppearance.HeadsUpStatusBarText"
- parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info">
+ parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info">
</style>
<style name="TextAppearance.QSEdit" >
@@ -699,7 +699,7 @@
</style>
<style name="MediaPlayer.SessionAction"
- parent="@android:style/Widget.Material.Button.Borderless.Small">
+ parent="@android:style/Widget.Material.Button.Borderless.Small">
<item name="android:background">@drawable/qs_media_light_source</item>
<item name="android:tint">?android:attr/textColorPrimary</item>
<item name="android:paddingTop">12dp</item>
@@ -929,12 +929,13 @@
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
</style>
- <style name="Theme.SystemUI.Dialog.Control.DetailPanel" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
- <item name="android:windowFullscreen">false</item>
- <item name="android:windowIsFloating">false</item>
- <item name="android:windowBackground">@color/controls_task_view_bg</item>
- <item name="android:backgroundDimEnabled">false</item>
- <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+ <style name="Theme.SystemUI.Dialog.Control.DetailPanel"
+ parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+ <item name="android:windowFullscreen">false</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowBackground">@color/controls_task_view_bg</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
<style name="Control" />
@@ -1034,17 +1035,17 @@
<style name="Wallet" />
<style name="Wallet.TextAppearance">
- <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
- <item name="android:textColor">?android:attr/textColorPrimary</item>
- <item name="android:singleLine">true</item>
- <item name="android:textSize">14sp</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textSize">14sp</item>
</style>
<style name="Wallet.Theme" parent="@android:style/Theme.DeviceDefault">
- <item name="android:colorBackground">@color/material_dynamic_neutral10</item>
- <item name="android:itemBackground">@color/material_dynamic_neutral20</item>
- <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen. -->
- <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
+ <item name="android:colorBackground">@color/material_dynamic_neutral10</item>
+ <item name="android:itemBackground">@color/material_dynamic_neutral20</item>
+ <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen. -->
+ <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
</style>
<style name="Animation.InternetDialog" parent="@android:style/Animation.InputMethod">
@@ -1169,7 +1170,7 @@
</style>
<style name="TrimmedHorizontalProgressBar"
- parent="android:Widget.Material.ProgressBar.Horizontal">
+ parent="android:Widget.Material.ProgressBar.Horizontal">
<item name="android:indeterminateDrawable">
@drawable/progress_indeterminate_horizontal_material_trimmed
</item>
@@ -1254,6 +1255,36 @@
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
+ <style name="BluetoothTileDialog">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_vertical|start</item>
+ </style>
+
+ <style name="BluetoothTileDialog.Device">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">88dp</item>
+ <item name="android:layout_gravity">center_vertical|start</item>
+ <item name="android:layout_marginStart">@dimen/bluetooth_dialog_layout_margin</item>
+ <item name="android:layout_marginEnd">@dimen/bluetooth_dialog_layout_margin</item>
+ <item name="android:paddingStart">22dp</item>
+ <item name="android:paddingEnd">22dp</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="BluetoothTileDialog.DeviceName">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
+ </style>
+
+ <style name="BluetoothTileDialog.DeviceSummary">
+ <item name="android:ellipsize">end</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:textAppearance">@style/TextAppearance.Dialog.Body.Message</item>
+ </style>
+
<style name="BroadcastDialog">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
@@ -1397,7 +1428,7 @@
</style>
<style name="PermissionGrantButtonTop"
- parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+ parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
<item name="android:layout_width">332dp</item>
<item name="android:layout_height">56dp</item>
<item name="android:layout_marginTop">2dp</item>
@@ -1406,7 +1437,7 @@
</style>
<style name="PermissionGrantButtonBottom"
- parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+ parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
<item name="android:layout_width">332dp</item>
<item name="android:layout_height">56dp</item>
<item name="android:layout_marginTop">2dp</item>
@@ -1465,14 +1496,14 @@
</style>
<style name="TextAppearance.PrivacyDialog.Item.Title"
- parent="@android:style/TextAppearance.DeviceDefault.Medium">
+ parent="@android:style/TextAppearance.DeviceDefault.Medium">
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.PrivacyDialog.Item.Summary"
- parent="@android:style/TextAppearance.DeviceDefault.Small">
+ parent="@android:style/TextAppearance.DeviceDefault.Small">
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
@@ -1481,4 +1512,4 @@
<style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
<item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
</style>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 1741e30..622b67f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -17,6 +17,7 @@
package com.android.keyguard;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_APPEAR;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_DISAPPEAR;
@@ -52,6 +53,8 @@
private final DisappearAnimationUtils mDisappearAnimationUtils;
private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
@Nullable private MotionLayout mContainerMotionLayout;
+ // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
+ // when the flag is enabled/removed
@Nullable private ConstraintLayout mContainerConstraintLayout;
private int mDisappearYTranslation;
private View[][] mViews;
@@ -59,7 +62,7 @@
private int mYTransOffset;
private View mBouncerMessageArea;
private boolean mAlreadyUsingSplitBouncer = false;
- private boolean mIsLockScreenLandscapeEnabled = false;
+ private boolean mIsSmallLockScreenLandscapeEnabled = false;
@DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
public static final long ANIMATION_DURATION = 650;
@@ -87,12 +90,12 @@
/** Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
* enabled, instead of constraint layout (old bouncer implementation) */
public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
- mIsLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
+ mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
findContainerLayout();
}
private void findContainerLayout() {
- if (mIsLockScreenLandscapeEnabled) {
+ if (mIsSmallLockScreenLandscapeEnabled) {
mContainerMotionLayout = findViewById(R.id.pin_container);
} else {
mContainerConstraintLayout = findViewById(R.id.pin_container);
@@ -109,7 +112,7 @@
if (mLastDevicePosture == posture) return;
mLastDevicePosture = posture;
- if (mIsLockScreenLandscapeEnabled) {
+ if (mIsSmallLockScreenLandscapeEnabled) {
boolean useSplitBouncerAfterFold =
mLastDevicePosture == DEVICE_POSTURE_CLOSED
&& getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
@@ -166,21 +169,45 @@
}
}
+ if (mIsSmallLockScreenLandscapeEnabled) {
+ updateHalfFoldedConstraints();
+ } else {
+ updateHalfFoldedGuideline();
+ }
+ }
+
+ private void updateHalfFoldedConstraints() {
+ // Update the constraints based on the device posture...
+ if (mAlreadyUsingSplitBouncer) return;
+
+ boolean shouldCollapsePin =
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+ && mContext.getResources().getConfiguration().orientation
+ == ORIENTATION_PORTRAIT;
+
+ int expectedMotionLayoutState = shouldCollapsePin
+ ? R.id.half_folded_single_constraints
+ : R.id.single_constraints;
+
+ transitionToMotionLayoutState(expectedMotionLayoutState);
+ }
+
+ // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
+ private void updateHalfFoldedGuideline() {
// Update the guideline based on the device posture...
float halfOpenPercentage =
mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
- if (mIsLockScreenLandscapeEnabled) {
- ConstraintSet cs = mContainerMotionLayout.getConstraintSet(R.id.single_constraints);
- cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
- mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
- cs.applyTo(mContainerMotionLayout);
- } else {
- ConstraintSet cs = new ConstraintSet();
- cs.clone(mContainerConstraintLayout);
- cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
- mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
- cs.applyTo(mContainerConstraintLayout);
+ ConstraintSet cs = new ConstraintSet();
+ cs.clone(mContainerConstraintLayout);
+ cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
+ cs.applyTo(mContainerConstraintLayout);
+ }
+
+ private void transitionToMotionLayoutState(int state) {
+ if (mContainerMotionLayout.getCurrentState() != state) {
+ mContainerMotionLayout.transitionToState(state);
}
}
@@ -189,12 +216,24 @@
* Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled. */
@Override
protected void updateConstraints(boolean useSplitBouncer) {
+ if (!mIsSmallLockScreenLandscapeEnabled) return;
+
mAlreadyUsingSplitBouncer = useSplitBouncer;
+
if (useSplitBouncer) {
mContainerMotionLayout.jumpToState(R.id.split_constraints);
mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
} else {
- mContainerMotionLayout.jumpToState(R.id.single_constraints);
+ boolean useHalfFoldedConstraints =
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+ && mContext.getResources().getConfiguration().orientation
+ == ORIENTATION_PORTRAIT;
+
+ if (useHalfFoldedConstraints) {
+ mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
+ } else {
+ mContainerMotionLayout.jumpToState(R.id.single_constraints);
+ }
mContainerMotionLayout.setMaxWidth(getResources()
.getDimensionPixelSize(R.dimen.keyguard_security_width));
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 802e222..5c206e9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -16,6 +16,7 @@
package com.android.keyguard;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
@@ -81,9 +82,11 @@
BouncerKeyguardMessageArea mSecurityMessageDisplay;
private View mEcaView;
@Nullable private MotionLayout mContainerMotionLayout;
+ // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
+ // when the flag is enabled/removed
@Nullable private ConstraintLayout mContainerConstraintLayout;
private boolean mAlreadyUsingSplitBouncer = false;
- private boolean mIsLockScreenLandscapeEnabled = false;
+ private boolean mIsSmallLockScreenLandscapeEnabled = false;
@DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
public KeyguardPatternView(Context context) {
@@ -111,12 +114,12 @@
* enabled, instead of constraint layout (old bouncer implementation)
*/
public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
- mIsLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
+ mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
findContainerLayout();
}
private void findContainerLayout() {
- if (mIsLockScreenLandscapeEnabled) {
+ if (mIsSmallLockScreenLandscapeEnabled) {
mContainerMotionLayout = findViewById(R.id.pattern_container);
} else {
mContainerConstraintLayout = findViewById(R.id.pattern_container);
@@ -132,7 +135,7 @@
if (mLastDevicePosture == posture) return;
mLastDevicePosture = posture;
- if (mIsLockScreenLandscapeEnabled) {
+ if (mIsSmallLockScreenLandscapeEnabled) {
boolean useSplitBouncerAfterFold =
mLastDevicePosture == DEVICE_POSTURE_CLOSED
&& getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
@@ -147,21 +150,45 @@
}
private void updateMargins() {
+ if (mIsSmallLockScreenLandscapeEnabled) {
+ updateHalfFoldedConstraints();
+ } else {
+ updateHalfFoldedGuideline();
+ }
+ }
+
+ private void updateHalfFoldedConstraints() {
+ // Update the constraints based on the device posture...
+ if (mAlreadyUsingSplitBouncer) return;
+
+ boolean shouldCollapsePattern =
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+ && mContext.getResources().getConfiguration().orientation
+ == ORIENTATION_PORTRAIT;
+
+ int expectedMotionLayoutState = shouldCollapsePattern
+ ? R.id.half_folded_single_constraints
+ : R.id.single_constraints;
+
+ transitionToMotionLayoutState(expectedMotionLayoutState);
+ }
+
+ // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
+ private void updateHalfFoldedGuideline() {
// Update the guideline based on the device posture...
float halfOpenPercentage =
mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
- if (mIsLockScreenLandscapeEnabled) {
- ConstraintSet cs = mContainerMotionLayout.getConstraintSet(R.id.single_constraints);
- cs.setGuidelinePercent(R.id.pattern_top_guideline,
- mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
- cs.applyTo(mContainerMotionLayout);
- } else {
- ConstraintSet cs = new ConstraintSet();
- cs.clone(mContainerConstraintLayout);
- cs.setGuidelinePercent(R.id.pattern_top_guideline,
- mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
- cs.applyTo(mContainerConstraintLayout);
+ ConstraintSet cs = new ConstraintSet();
+ cs.clone(mContainerConstraintLayout);
+ cs.setGuidelinePercent(R.id.pattern_top_guideline,
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
+ cs.applyTo(mContainerConstraintLayout);
+ }
+
+ private void transitionToMotionLayoutState(int state) {
+ if (mContainerMotionLayout.getCurrentState() != state) {
+ mContainerMotionLayout.transitionToState(state);
}
}
@@ -172,12 +199,24 @@
*/
@Override
protected void updateConstraints(boolean useSplitBouncer) {
+ if (!mIsSmallLockScreenLandscapeEnabled) return;
+
mAlreadyUsingSplitBouncer = useSplitBouncer;
+
if (useSplitBouncer) {
mContainerMotionLayout.jumpToState(R.id.split_constraints);
mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
} else {
- mContainerMotionLayout.jumpToState(R.id.single_constraints);
+ boolean useHalfFoldedConstraints =
+ mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+ && mContext.getResources().getConfiguration().orientation
+ == ORIENTATION_PORTRAIT;
+
+ if (useHalfFoldedConstraints) {
+ mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
+ } else {
+ mContainerMotionLayout.jumpToState(R.id.single_constraints);
+ }
mContainerMotionLayout.setMaxWidth(getResources()
.getDimensionPixelSize(R.dimen.biometric_auth_pattern_view_max_size));
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index 9c4d224..ec2999f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -139,12 +139,12 @@
onViewInflatedListener.onViewInflated(childController);
// Single bouncer constrains are default
- if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)
- &&
- getResources().getBoolean(R.bool.update_bouncer_constraints)) {
+ if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) {
boolean useSplitBouncer =
- getResources().getConfiguration().orientation
+ getResources().getBoolean(R.bool.update_bouncer_constraints)
+ && getResources().getConfiguration().orientation
== ORIENTATION_LANDSCAPE;
+
updateConstraints(useSplitBouncer);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e170849e..467e84d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -798,7 +798,7 @@
/** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */
@JvmField
- val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog")
+ val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog", teamfood = true)
// TODO(b/300995746): Tracking Bug
/** Enable communal hub features. */
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
index cc51d21..d9b2c39 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
@@ -36,8 +36,8 @@
*
* @param[sliderStateListener] Listener of the slider state.
* @param[sliderEventProducer] Producer of slider events arising from the slider.
- * @property[scope] [CoroutineScope] where the collection of slider events and the launch of timer
- * jobs occur.
+ * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of
+ * slider events and the launch of timer jobs.
* @property[config] Configuration parameters of the slider tracker.
*/
class SeekableSliderTracker(
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
index e1f5708..002b5aa 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
@@ -27,7 +27,8 @@
* The tracker maintains a state machine operated by slider events coming from a
* [SliderEventProducer]. An action is executed in each state via a [SliderListener].
*
- * @param[scope] [CoroutineScope] to launch the collection of [SliderEvent].
+ * @property[scope] [CoroutineScope] to launch the collection of [SliderEvent] and state machine
+ * logic.
* @property[sliderListener] [SliderListener] to execute actions on a given [SliderState].
* @property[eventProducer] Producer of [SliderEvent] to iterate over a state machine.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index efd25d5..b506a36 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3798,6 +3798,13 @@
}
/**
+ * Notify whether keyguard has created a remote animation runner for next app launch.
+ */
+ public void launchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
+ mKeyguardTransitions.setLaunchingActivityOverLockscreen(isLaunchingActivityOverLockscreen);
+ }
+
+ /**
* Implementation of RemoteAnimationRunner that creates a new
* {@link ActivityLaunchAnimator.Runner} whenever onAnimationStart is called, delegating the
* remote animation methods to that runner.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 4e71ef4..cc36961 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -78,6 +78,7 @@
component.getControlsListingController().getOrNull()?.getCurrentServices()
val hasFavorites =
component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
+ val hasPanels = currentServices?.any { it.panelActivity != null } == true
val componentPackageName = component.getPackageName()
when {
currentServices.isNullOrEmpty() && !componentPackageName.isNullOrEmpty() -> {
@@ -100,8 +101,8 @@
),
)
}
- !hasFavorites -> {
- // Home app installed but no favorites selected.
+ !hasFavorites && !hasPanels -> {
+ // Home app installed but no favorites selected or panel activities available.
val activityClass = component.getControlsUiController().get().resolveActivity()
return disabledPickerState(
explanation =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 1be514d..d862f56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -42,6 +42,8 @@
import com.android.systemui.res.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
@@ -50,6 +52,7 @@
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel;
import com.android.systemui.statusbar.policy.BluetoothController;
import java.util.List;
@@ -72,6 +75,10 @@
private final Executor mExecutor;
+ private final BluetoothTileDialogViewModel mDialogViewModel;
+
+ private final FeatureFlags mFeatureFlags;
+
@Inject
public BluetoothTile(
QSHost host,
@@ -83,13 +90,17 @@
StatusBarStateController statusBarStateController,
ActivityStarter activityStarter,
QSLogger qsLogger,
- BluetoothController bluetoothController
+ BluetoothController bluetoothController,
+ FeatureFlags featureFlags,
+ BluetoothTileDialogViewModel dialogViewModel
) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mController = bluetoothController;
mController.observe(getLifecycle(), mCallback);
mExecutor = new HandlerExecutor(mainHandler);
+ mFeatureFlags = featureFlags;
+ mDialogViewModel = dialogViewModel;
}
@Override
@@ -99,11 +110,15 @@
@Override
protected void handleClick(@Nullable View view) {
- // Secondary clicks are header clicks, just toggle.
- final boolean isEnabled = mState.value;
- // Immediately enter transient enabling state when turning bluetooth on.
- refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
- mController.setBluetoothEnabled(!isEnabled);
+ if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
+ mDialogViewModel.showDialog(mContext, view);
+ } else {
+ // Secondary clicks are header clicks, just toggle.
+ final boolean isEnabled = mState.value;
+ // Immediately enter transient enabling state when turning bluetooth on.
+ refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
+ mController.setBluetoothEnabled(!isEnabled);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
new file mode 100644
index 0000000..efad9ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter.STATE_OFF
+import android.bluetooth.BluetoothAdapter.STATE_ON
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
+@SysUISingleton
+internal class BluetoothStateInteractor
+@Inject
+constructor(
+ private val localBluetoothManager: LocalBluetoothManager?,
+ @Application private val coroutineScope: CoroutineScope,
+) {
+
+ internal val updateBluetoothStateFlow: StateFlow<Boolean?> =
+ conflatedCallbackFlow {
+ val listener =
+ object : BluetoothCallback {
+ override fun onBluetoothStateChanged(bluetoothState: Int) {
+ if (bluetoothState == STATE_ON || bluetoothState == STATE_OFF) {
+ super.onBluetoothStateChanged(bluetoothState)
+ trySendWithFailureLogging(
+ bluetoothState == STATE_ON,
+ TAG,
+ "onBluetoothStateChanged"
+ )
+ }
+ }
+ }
+ localBluetoothManager?.eventManager?.registerCallback(listener)
+ awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+ }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
+ initialValue = null
+ )
+
+ internal var isBluetoothEnabled: Boolean
+ get() = localBluetoothManager?.bluetoothAdapter?.isEnabled == true
+ set(value) {
+ if (isBluetoothEnabled != value) {
+ localBluetoothManager?.bluetoothAdapter?.apply {
+ if (value) enable() else disable()
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "BtStateInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
new file mode 100644
index 0000000..6815a73
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.Switch
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Dialog for showing active, connected and saved bluetooth devices. */
+@SysUISingleton
+internal class BluetoothTileDialog
+constructor(
+ private val bluetoothToggleInitialValue: Boolean,
+ private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
+ private val uiEventLogger: UiEventLogger,
+ context: Context,
+) : SystemUIDialog(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK) {
+
+ private val mutableBluetoothStateSwitchedFlow: MutableStateFlow<Boolean> =
+ MutableStateFlow(bluetoothToggleInitialValue)
+ internal val bluetoothStateSwitchedFlow
+ get() = mutableBluetoothStateSwitchedFlow.asStateFlow()
+
+ private val mutableClickedFlow: MutableSharedFlow<Pair<DeviceItem, Int>> =
+ MutableSharedFlow(extraBufferCapacity = 1)
+ internal val deviceItemClickedFlow
+ get() = mutableClickedFlow.asSharedFlow()
+
+ private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback)
+
+ private lateinit var toggleView: Switch
+ private lateinit var doneButton: View
+ private lateinit var seeAllViewGroup: View
+ private lateinit var pairNewDeviceViewGroup: View
+ private lateinit var seeAllText: View
+ private lateinit var pairNewDeviceText: View
+ private lateinit var deviceListView: RecyclerView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+
+ setContentView(LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null))
+
+ toggleView = requireViewById(R.id.bluetooth_toggle)
+ doneButton = requireViewById(R.id.done_button)
+ seeAllViewGroup = requireViewById(R.id.see_all_layout_group)
+ pairNewDeviceViewGroup = requireViewById(R.id.pair_new_device_layout_group)
+ seeAllText = requireViewById(R.id.see_all_text)
+ pairNewDeviceText = requireViewById(R.id.pair_new_device_text)
+ deviceListView = requireViewById<RecyclerView>(R.id.device_list)
+
+ setupToggle()
+ setupRecyclerView()
+
+ doneButton.setOnClickListener { dismiss() }
+ seeAllText.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
+ pairNewDeviceText.setOnClickListener {
+ bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
+ }
+ }
+
+ // TODO(b/298124674): use DiffUtil or AsyncListDiffer to avoid updating the whole list
+ internal fun onDeviceItemUpdated(
+ deviceItem: List<DeviceItem>,
+ showSeeAll: Boolean,
+ showPairNewDevice: Boolean
+ ) {
+ seeAllViewGroup.visibility = if (showSeeAll) VISIBLE else GONE
+ pairNewDeviceViewGroup.visibility = if (showPairNewDevice) VISIBLE else GONE
+ deviceItemAdapter.refreshDeviceItemList(deviceItem)
+ }
+
+ internal fun onDeviceItemUpdatedAtPosition(deviceItem: DeviceItem, position: Int) {
+ deviceItemAdapter.refreshDeviceItem(deviceItem, position)
+ }
+
+ internal fun onBluetoothStateUpdated(isEnabled: Boolean) {
+ toggleView.isChecked = isEnabled
+ }
+
+ private fun setupToggle() {
+ toggleView.isChecked = bluetoothToggleInitialValue
+ toggleView.setOnCheckedChangeListener { _, isChecked ->
+ mutableBluetoothStateSwitchedFlow.value = isChecked
+ uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
+ }
+ }
+
+ private fun setupRecyclerView() {
+ deviceListView.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = deviceItemAdapter
+ }
+ }
+
+ internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
+ RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+
+ private val deviceItem: MutableList<DeviceItem> = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
+ val view =
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.bluetooth_device_item, parent, false)
+ return DeviceItemViewHolder(view)
+ }
+
+ override fun getItemCount() = deviceItem.size
+
+ override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
+ val item = getItem(position)
+ holder.bind(item, position, onClickCallback)
+ }
+
+ internal fun getItem(position: Int) = deviceItem[position]
+
+ internal fun refreshDeviceItemList(updated: List<DeviceItem>) {
+ deviceItem.clear()
+ deviceItem.addAll(updated)
+ notifyDataSetChanged()
+ }
+
+ internal fun refreshDeviceItem(updated: DeviceItem, position: Int) {
+ deviceItem[position] = updated
+ notifyItemChanged(position)
+ }
+
+ internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+ private val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+ private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
+ private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
+ private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
+ private val gearView = view.requireViewById<View>(R.id.gear_icon)
+
+ internal fun bind(
+ item: DeviceItem,
+ position: Int,
+ deviceItemOnClickCallback: BluetoothTileDialogCallback
+ ) {
+ container.apply {
+ isEnabled = item.isEnabled
+ alpha = item.alpha
+ background = item.background
+ }
+ deviceView.setOnClickListener {
+ mutableClickedFlow.tryEmit(Pair(item, position))
+ uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
+ }
+ nameView.text = item.deviceName
+ summaryView.text = item.connectionSummary
+ iconView.apply {
+ item.iconWithDescription?.let {
+ setImageDrawable(it.first)
+ contentDescription = it.second
+ }
+ }
+ gearView.setOnClickListener {
+ deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
+ }
+ }
+ }
+ }
+
+ internal companion object {
+ const val ENABLED_ALPHA = 1.0f
+ const val DISABLED_ALPHA = 0.3f
+ const val MAX_DEVICE_ITEM_ENTRY = 3
+ const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+ "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+ const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
+ "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
+ const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt
new file mode 100644
index 0000000..ea51bee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/** Repository to get CachedBluetoothDevices for the Bluetooth Dialog. */
+@SysUISingleton
+internal class BluetoothTileDialogRepository
+@Inject
+constructor(
+ private val localBluetoothManager: LocalBluetoothManager?,
+ private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
+) {
+ internal val cachedDevices: Collection<CachedBluetoothDevice>
+ get() {
+ return if (
+ localBluetoothManager == null ||
+ bluetoothAdapter == null ||
+ !bluetoothAdapter.isEnabled
+ ) {
+ emptyList()
+ } else {
+ localBluetoothManager.cachedDeviceManager.cachedDevicesCopy
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
new file mode 100644
index 0000000..2865ad7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+
+/** UI Events for the bluetooth tile dialog. */
+enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "The bluetooth tile dialog is shown") BLUETOOTH_TILE_DIALOG_SHOWN(1493),
+ @UiEvent(doc = "The master toggle is clicked") BLUETOOTH_TOGGLE_CLICKED(1494),
+ @UiEvent(doc = "Pair new device is clicked") PAIR_NEW_DEVICE_CLICKED(1495),
+ @UiEvent(doc = "See all is clicked") SEE_ALL_CLICKED(1496),
+ @UiEvent(doc = "Gear icon clicked") DEVICE_GEAR_CLICKED(1497),
+ @UiEvent(doc = "Device clicked") DEVICE_CLICKED(1498),
+ @UiEvent(doc = "Connected device clicked to active") CONNECTED_DEVICE_SET_ACTIVE(1499),
+ @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500);
+
+ override fun getId() = metricId
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
new file mode 100644
index 0000000..012484f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PAIR_NEW_DEVICE
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.MAX_DEVICE_ITEM_ENTRY
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+@SysUISingleton
+internal class BluetoothTileDialogViewModel
+@Inject
+constructor(
+ private val deviceItemInteractor: DeviceItemInteractor,
+ private val bluetoothStateInteractor: BluetoothStateInteractor,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val activityStarter: ActivityStarter,
+ private val uiEventLogger: UiEventLogger,
+ @Application private val coroutineScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+) : BluetoothTileDialogCallback {
+
+ private var job: Job? = null
+
+ @VisibleForTesting internal var dialog: BluetoothTileDialog? = null
+
+ /**
+ * Shows the dialog.
+ *
+ * @param context The context in which the dialog is displayed.
+ * @param view The view from which the dialog is shown.
+ */
+ fun showDialog(context: Context, view: View?) {
+ dismissDialog()
+
+ var updateDeviceItemJob: Job? = null
+
+ job =
+ coroutineScope.launch(mainDispatcher) {
+ dialog = createBluetoothTileDialog(context)
+ view?.let { dialogLaunchAnimator.showFromView(dialog!!, it) } ?: dialog!!.show()
+ updateDeviceItemJob?.cancel()
+ updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems(context) }
+
+ bluetoothStateInteractor.updateBluetoothStateFlow
+ .filterNotNull()
+ .onEach {
+ dialog!!.onBluetoothStateUpdated(it)
+ updateDeviceItemJob?.cancel()
+ updateDeviceItemJob = launch {
+ deviceItemInteractor.updateDeviceItems(context)
+ }
+ }
+ .launchIn(this)
+
+ deviceItemInteractor.updateDeviceItemsFlow
+ .onEach {
+ updateDeviceItemJob?.cancel()
+ updateDeviceItemJob = launch {
+ deviceItemInteractor.updateDeviceItems(context)
+ }
+ }
+ .launchIn(this)
+
+ deviceItemInteractor.deviceItemFlow
+ .filterNotNull()
+ .onEach {
+ dialog!!.onDeviceItemUpdated(
+ it.take(MAX_DEVICE_ITEM_ENTRY),
+ showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY,
+ showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled
+ )
+ }
+ .launchIn(this)
+
+ dialog!!
+ .bluetoothStateSwitchedFlow
+ .onEach { bluetoothStateInteractor.isBluetoothEnabled = it }
+ .launchIn(this)
+
+ dialog!!
+ .deviceItemClickedFlow
+ .onEach {
+ if (deviceItemInteractor.updateDeviceItemOnClick(it.first)) {
+ dialog!!.onDeviceItemUpdatedAtPosition(it.first, it.second)
+ }
+ }
+ .launchIn(this)
+ }
+ }
+
+ private fun createBluetoothTileDialog(context: Context): BluetoothTileDialog {
+ return BluetoothTileDialog(
+ bluetoothStateInteractor.isBluetoothEnabled,
+ this@BluetoothTileDialogViewModel,
+ uiEventLogger,
+ context
+ )
+ .apply { SystemUIDialog.registerDismissListener(this) { dismissDialog() } }
+ }
+
+ override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
+ val intent =
+ Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
+ putExtra(
+ ":settings:show_fragment_args",
+ Bundle().apply {
+ putString("device_address", deviceItem.cachedBluetoothDevice.address)
+ }
+ )
+ }
+ startSettingsActivity(intent, view)
+ }
+
+ override fun onSeeAllClicked(view: View) {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
+ startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
+ }
+
+ override fun onPairNewDeviceClicked(view: View) {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
+ startSettingsActivity(Intent(ACTION_PAIR_NEW_DEVICE), view)
+ }
+
+ private fun dismissDialog() {
+ job?.cancel()
+ job = null
+ dialog?.dismiss()
+ dialog = null
+ }
+
+ private fun startSettingsActivity(intent: Intent, view: View) {
+ dialog?.run {
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ activityStarter.postStartActivityDismissingKeyguard(
+ intent,
+ 0,
+ dialogLaunchAnimator.createActivityLaunchController(view)
+ )
+ }
+ }
+}
+
+internal interface BluetoothTileDialogCallback {
+ fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
+ fun onSeeAllClicked(view: View)
+ fun onPairNewDeviceClicked(view: View)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
new file mode 100644
index 0000000..03ae5e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.graphics.drawable.Drawable
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ENABLED_ALPHA
+
+enum class DeviceItemType {
+ AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ CONNECTED_BLUETOOTH_DEVICE,
+ SAVED_BLUETOOTH_DEVICE,
+}
+
+data class DeviceItem(
+ val type: DeviceItemType,
+ val cachedBluetoothDevice: CachedBluetoothDevice,
+ val deviceName: String = "",
+ val connectionSummary: String = "",
+ val iconWithDescription: Pair<Drawable, String>? = null,
+ val background: Drawable? = null,
+ var isEnabled: Boolean = true,
+ var alpha: Float = ENABLED_ALPHA
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
new file mode 100644
index 0000000..a16a9f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.res.R
+
+private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
+private val connected = R.string.quick_settings_bluetooth_device_connected
+private val saved = R.string.quick_settings_bluetooth_device_saved
+
+/** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */
+internal abstract class DeviceItemFactory {
+ abstract fun isFilterMatched(
+ cachedDevice: CachedBluetoothDevice,
+ audioManager: AudioManager?
+ ): Boolean
+
+ abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem
+}
+
+internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
+ override fun isFilterMatched(
+ cachedDevice: CachedBluetoothDevice,
+ audioManager: AudioManager?
+ ): Boolean {
+ return BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+ }
+
+ // TODO(b/298124674): move create() to the abstract class to reduce duplicate code
+ override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+ return DeviceItem(
+ type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = cachedDevice,
+ deviceName = cachedDevice.name,
+ connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+ ?: context.getString(connected),
+ iconWithDescription =
+ BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+ Pair(p.first, p.second)
+ },
+ background = context.getDrawable(backgroundOn),
+ isEnabled = !cachedDevice.isBusy,
+ alpha =
+ if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+ else BluetoothTileDialog.ENABLED_ALPHA,
+ )
+ }
+}
+
+internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
+ override fun isFilterMatched(
+ cachedDevice: CachedBluetoothDevice,
+ audioManager: AudioManager?
+ ): Boolean {
+ return BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
+ }
+
+ override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+ return DeviceItem(
+ type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = cachedDevice,
+ deviceName = cachedDevice.name,
+ connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+ ?: context.getString(connected),
+ iconWithDescription =
+ BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+ Pair(p.first, p.second)
+ },
+ isEnabled = !cachedDevice.isBusy,
+ alpha =
+ if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+ else BluetoothTileDialog.ENABLED_ALPHA,
+ )
+ }
+}
+
+internal class SavedDeviceItemFactory : DeviceItemFactory() {
+ override fun isFilterMatched(
+ cachedDevice: CachedBluetoothDevice,
+ audioManager: AudioManager?
+ ): Boolean {
+ return cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected
+ }
+
+ override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+ return DeviceItem(
+ type = DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = cachedDevice,
+ deviceName = cachedDevice.name,
+ connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+ ?: context.getString(saved),
+ iconWithDescription =
+ BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+ Pair(p.first, p.second)
+ },
+ isEnabled = !cachedDevice.isBusy,
+ alpha =
+ if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+ else BluetoothTileDialog.ENABLED_ALPHA,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
new file mode 100644
index 0000000..fcd0ce6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+@SysUISingleton
+internal class DeviceItemInteractor
+@Inject
+constructor(
+ private val bluetoothTileDialogRepository: BluetoothTileDialogRepository,
+ private val audioManager: AudioManager,
+ private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
+ private val localBluetoothManager: LocalBluetoothManager?,
+ private val uiEventLogger: UiEventLogger,
+ @Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+ private val mutableDeviceItemFlow: MutableStateFlow<List<DeviceItem>?> = MutableStateFlow(null)
+ internal val deviceItemFlow
+ get() = mutableDeviceItemFlow.asStateFlow()
+
+ internal val updateDeviceItemsFlow: SharedFlow<Unit> =
+ conflatedCallbackFlow {
+ val listener =
+ object : BluetoothCallback {
+ override fun onActiveDeviceChanged(
+ activeDevice: CachedBluetoothDevice?,
+ bluetoothProfile: Int
+ ) {
+ super.onActiveDeviceChanged(activeDevice, bluetoothProfile)
+ trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged")
+ }
+
+ override fun onConnectionStateChanged(
+ cachedDevice: CachedBluetoothDevice?,
+ state: Int
+ ) {
+ super.onConnectionStateChanged(cachedDevice, state)
+ trySendWithFailureLogging(Unit, TAG, "onConnectionStateChanged")
+ }
+
+ override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
+ super.onDeviceAdded(cachedDevice)
+ trySendWithFailureLogging(Unit, TAG, "onDeviceAdded")
+ }
+
+ override fun onProfileConnectionStateChanged(
+ cachedDevice: CachedBluetoothDevice,
+ state: Int,
+ bluetoothProfile: Int
+ ) {
+ super.onProfileConnectionStateChanged(
+ cachedDevice,
+ state,
+ bluetoothProfile
+ )
+ trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged")
+ }
+ }
+ localBluetoothManager?.eventManager?.registerCallback(listener)
+ awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+ }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
+ private var deviceItemFactoryList: List<DeviceItemFactory> =
+ listOf(
+ AvailableMediaDeviceItemFactory(),
+ ConnectedDeviceItemFactory(),
+ SavedDeviceItemFactory()
+ )
+
+ private var displayPriority: List<DeviceItemType> =
+ listOf(
+ DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+ )
+
+ internal suspend fun updateDeviceItems(context: Context) {
+ withContext(backgroundDispatcher) {
+ val mostRecentlyConnectedDevices = bluetoothAdapter?.mostRecentlyConnectedDevices
+
+ mutableDeviceItemFlow.value =
+ bluetoothTileDialogRepository.cachedDevices
+ .mapNotNull { cachedDevice ->
+ deviceItemFactoryList
+ .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) }
+ ?.create(context, cachedDevice)
+ }
+ .sort(displayPriority, mostRecentlyConnectedDevices)
+ }
+ }
+
+ private fun List<DeviceItem>.sort(
+ displayPriority: List<DeviceItemType>,
+ mostRecentlyConnectedDevices: List<BluetoothDevice>?
+ ): List<DeviceItem> {
+ return this.sortedWith(
+ compareBy<DeviceItem> { displayPriority.indexOf(it.type) }
+ .thenBy {
+ mostRecentlyConnectedDevices?.indexOf(it.cachedBluetoothDevice.device) ?: 0
+ }
+ )
+ }
+
+ internal fun updateDeviceItemOnClick(deviceItem: DeviceItem): Boolean {
+ var isClicked = false
+ when (deviceItem.type) {
+ DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+ if (!BluetoothUtils.isActiveMediaDevice(deviceItem.cachedBluetoothDevice)) {
+ deviceItem.cachedBluetoothDevice.setActive()
+ uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
+ isClicked = true
+ }
+ }
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {}
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+ deviceItem.cachedBluetoothDevice.connect()
+ uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+ isClicked = true
+ }
+ }
+ if (isClicked) {
+ deviceItem.isEnabled = false
+ deviceItem.alpha = BluetoothTileDialog.DISABLED_ALPHA
+ }
+ return isClicked
+ }
+
+ internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
+ deviceItemFactoryList = list
+ }
+
+ internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
+ displayPriority = list
+ }
+
+ companion object {
+ private const val TAG = "DeviceItemInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
index 8849d6e..10e7573 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
@@ -13,7 +13,7 @@
"exclude-annotation": "org.junit.Ignore"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.LargeTest"
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 3a88504..6f69ea81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3429,6 +3429,7 @@
@Override
public void setIsLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
mIsLaunchingActivityOverLockscreen = isLaunchingActivityOverLockscreen;
+ mKeyguardViewMediator.launchingActivityOverLockscreen(mIsLaunchingActivityOverLockscreen);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 12fa4c8..477f455 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -176,7 +176,7 @@
!isFeatureEnabled ->
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice::class
.java
- hasServiceInfos && hasFavorites ->
+ hasServiceInfos && (hasFavorites || hasPanels) ->
KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java
else -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 623a8e0..82ee99a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -14,6 +14,7 @@
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
@@ -22,6 +23,7 @@
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel
import com.android.systemui.statusbar.policy.BluetoothController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -50,6 +52,8 @@
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var bluetoothController: BluetoothController
@Mock private lateinit var uiEventLogger: QsEventLogger
+ @Mock private lateinit var featureFlags: FeatureFlagsClassic
+ @Mock private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
private lateinit var testableLooper: TestableLooper
private lateinit var tile: FakeBluetoothTile
@@ -73,6 +77,8 @@
activityStarter,
qsLogger,
bluetoothController,
+ featureFlags,
+ bluetoothTileDialogViewModel
)
tile.initialize()
@@ -220,6 +226,8 @@
activityStarter: ActivityStarter,
qsLogger: QSLogger,
bluetoothController: BluetoothController,
+ featureFlags: FeatureFlagsClassic,
+ bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
) :
BluetoothTile(
qsHost,
@@ -232,6 +240,8 @@
activityStarter,
qsLogger,
bluetoothController,
+ featureFlags,
+ bluetoothTileDialogViewModel
) {
var restrictionChecked: String? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
new file mode 100644
index 0000000..fc2b7a64
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothStateInteractorTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val testScope = TestScope()
+
+ private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
+ @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+ @Before
+ fun setUp() {
+ bluetoothStateInteractor =
+ BluetoothStateInteractor(localBluetoothManager, testScope.backgroundScope)
+ `when`(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
+ }
+
+ @Test
+ fun testGet_isBluetoothEnabled() {
+ testScope.runTest {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(true)
+
+ assertThat(bluetoothStateInteractor.isBluetoothEnabled).isTrue()
+ }
+ }
+
+ @Test
+ fun testGet_isBluetoothDisabled() {
+ testScope.runTest {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+ assertThat(bluetoothStateInteractor.isBluetoothEnabled).isFalse()
+ }
+ }
+
+ @Test
+ fun testSet_bluetoothEnabled() {
+ testScope.runTest {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+ bluetoothStateInteractor.isBluetoothEnabled = true
+ verify(bluetoothAdapter).enable()
+ }
+ }
+
+ @Test
+ fun testSet_bluetoothNoChange() {
+ testScope.runTest {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+ bluetoothStateInteractor.isBluetoothEnabled = false
+ verify(bluetoothAdapter, never()).enable()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt
new file mode 100644
index 0000000..da8f60a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogRepositoryTest : SysuiTestCase() {
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+ @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+ @Mock private lateinit var cachedDeviceManager: CachedBluetoothDeviceManager
+
+ @Mock private lateinit var cachedDevicesCopy: Collection<CachedBluetoothDevice>
+
+ private lateinit var repository: BluetoothTileDialogRepository
+
+ @Before
+ fun setUp() {
+ `when`(localBluetoothManager.cachedDeviceManager).thenReturn(cachedDeviceManager)
+ `when`(cachedDeviceManager.cachedDevicesCopy).thenReturn(cachedDevicesCopy)
+
+ repository = BluetoothTileDialogRepository(localBluetoothManager, bluetoothAdapter)
+ }
+
+ @Test
+ fun testCachedDevices_bluetoothOff_emptyList() {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+ val result = repository.cachedDevices
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun testCachedDevices_bluetoothOn_returnDevice() {
+ `when`(bluetoothAdapter.isEnabled).thenReturn(true)
+
+ val result = repository.cachedDevices
+
+ assertThat(result).isEqualTo(cachedDevicesCopy)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
new file mode 100644
index 0000000..89fa55b3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.graphics.drawable.Drawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.DISABLED_ALPHA
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ENABLED_ALPHA
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogTest : SysuiTestCase() {
+ companion object {
+ const val DEVICE_NAME = "device"
+ const val DEVICE_CONNECTION_SUMMARY = "active"
+ const val ENABLED = true
+ }
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+
+ @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
+
+ @Mock private lateinit var drawable: Drawable
+
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+
+ private lateinit var icon: Pair<Drawable, String>
+ private lateinit var bluetoothTileDialog: BluetoothTileDialog
+ private lateinit var deviceItem: DeviceItem
+
+ @Before
+ fun setUp() {
+ bluetoothTileDialog =
+ BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+ icon = Pair(drawable, DEVICE_NAME)
+ deviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = icon,
+ background = null
+ )
+ `when`(cachedBluetoothDevice.isBusy).thenReturn(false)
+ }
+
+ @Test
+ fun testShowDialog_createRecyclerViewWithAdapter() {
+ bluetoothTileDialog.show()
+
+ val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+
+ assertThat(bluetoothTileDialog.isShowing).isTrue()
+ assertThat(recyclerView).isNotNull()
+ assertThat(recyclerView?.visibility).isEqualTo(VISIBLE)
+ assertThat(recyclerView?.adapter).isNotNull()
+ assertThat(recyclerView?.layoutManager is LinearLayoutManager).isTrue()
+ }
+
+ @Test
+ fun testShowDialog_displayBluetoothDevice() {
+ bluetoothTileDialog =
+ BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+ bluetoothTileDialog.show()
+ bluetoothTileDialog.onDeviceItemUpdated(
+ listOf(deviceItem),
+ showSeeAll = false,
+ showPairNewDevice = false
+ )
+
+ val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+ val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
+ assertThat(adapter.itemCount).isEqualTo(1)
+ assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
+ assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY)
+ assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
+ }
+
+ @Test
+ fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
+ deviceItem.isEnabled = true
+ deviceItem.alpha = ENABLED_ALPHA
+
+ val view =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder =
+ BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+ .Adapter(bluetoothTileDialogCallback)
+ .DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem, 0, bluetoothTileDialogCallback)
+ val container = view.requireViewById<View>(R.id.bluetooth_device)
+ val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+
+ assertThat(container).isNotNull()
+ assertThat(container.isEnabled).isTrue()
+ assertThat(container.alpha).isEqualTo(ENABLED_ALPHA)
+ assertThat(deviceView.hasOnClickListeners()).isTrue()
+ }
+
+ @Test
+ fun testDeviceItemViewHolder_cachedDeviceBusy() {
+ deviceItem.isEnabled = false
+ deviceItem.alpha = DISABLED_ALPHA
+
+ val view =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder =
+ BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+ .Adapter(bluetoothTileDialogCallback)
+ .DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem, 0, bluetoothTileDialogCallback)
+ val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+ val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+
+ assertThat(container).isNotNull()
+ assertThat(container.isEnabled).isFalse()
+ assertThat(container.alpha).isEqualTo(DISABLED_ALPHA)
+ assertThat(deviceView.hasOnClickListeners()).isTrue()
+ }
+
+ @Test
+ fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
+ bluetoothTileDialog =
+ BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+ bluetoothTileDialog.show()
+ bluetoothTileDialog.onDeviceItemUpdated(
+ listOf(deviceItem),
+ showSeeAll = false,
+ showPairNewDevice = true
+ )
+
+ val seeAllLayout = bluetoothTileDialog.requireViewById<View>(R.id.see_all_layout_group)
+ val pairNewLayout =
+ bluetoothTileDialog.requireViewById<View>(R.id.pair_new_device_layout_group)
+ val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+ val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
+
+ assertThat(seeAllLayout).isNotNull()
+ assertThat(seeAllLayout.visibility).isEqualTo(GONE)
+ assertThat(pairNewLayout).isNotNull()
+ assertThat(pairNewLayout.visibility).isEqualTo(VISIBLE)
+ assertThat(adapter.itemCount).isEqualTo(1)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
new file mode 100644
index 0000000..7157cce
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogViewModelTest : SysuiTestCase() {
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val fakeSystemClock = FakeSystemClock()
+ private val backgroundExecutor = FakeExecutor(fakeSystemClock)
+
+ private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
+
+ @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
+ @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+
+ @Mock private lateinit var activityStarter: ActivityStarter
+
+ @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+ @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+
+ @Mock private lateinit var deviceItem: DeviceItem
+
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+
+ private lateinit var scheduler: TestCoroutineScheduler
+ private lateinit var dispatcher: CoroutineDispatcher
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ scheduler = TestCoroutineScheduler()
+ dispatcher = UnconfinedTestDispatcher(scheduler)
+ testScope = TestScope(dispatcher)
+ bluetoothTileDialogViewModel =
+ BluetoothTileDialogViewModel(
+ deviceItemInteractor,
+ bluetoothStateInteractor,
+ dialogLaunchAnimator,
+ activityStarter,
+ uiEventLogger,
+ testScope.backgroundScope,
+ dispatcher,
+ )
+ `when`(deviceItemInteractor.deviceItemFlow).thenReturn(MutableStateFlow(null).asStateFlow())
+ `when`(bluetoothStateInteractor.updateBluetoothStateFlow)
+ .thenReturn(MutableStateFlow(null).asStateFlow())
+ `when`(deviceItemInteractor.updateDeviceItemsFlow)
+ .thenReturn(MutableStateFlow(Unit).asStateFlow())
+ `when`(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true)
+ }
+
+ @Test
+ fun testShowDialog_noAnimation() {
+ testScope.runTest {
+ bluetoothTileDialogViewModel.showDialog(context, null)
+
+ assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+ verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), any())
+ assertThat(bluetoothTileDialogViewModel.dialog?.isShowing).isTrue()
+ verify(uiEventLogger).log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+ }
+ }
+
+ @Test
+ fun testShowDialog_animated() {
+ testScope.runTest {
+ bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+
+ assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+ verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+ }
+ }
+
+ @Test
+ fun testShowDialog_animated_callInBackgroundThread() {
+ testScope.runTest {
+ backgroundExecutor.execute {
+ bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+
+ assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+ verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+ }
+ }
+ }
+
+ @Test
+ fun testShowDialog_fetchDeviceItem() {
+ testScope.runTest {
+ bluetoothTileDialogViewModel.showDialog(context, null)
+
+ assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+ verify(deviceItemInteractor).deviceItemFlow
+ }
+ }
+
+ @Test
+ fun testShowDialog_withBluetoothStateValue() {
+ testScope.runTest {
+ bluetoothTileDialogViewModel.showDialog(context, null)
+
+ assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+ verify(bluetoothStateInteractor).updateBluetoothStateFlow
+ }
+ }
+
+ @Test
+ fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
+ `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+ bluetoothTileDialogViewModel.showDialog(context, null)
+
+ val clickedView = View(context)
+ bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
+
+ verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
+ verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt
new file mode 100644
index 0000000..3451902
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class DeviceItemFactoryTest : SysuiTestCase() {
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+
+ private val availableMediaDeviceItemFactory = AvailableMediaDeviceItemFactory()
+ private val connectedDeviceItemFactory = ConnectedDeviceItemFactory()
+ private val savedDeviceItemFactory = SavedDeviceItemFactory()
+
+ @Before
+ fun setup() {
+ `when`(cachedDevice.name).thenReturn(DEVICE_NAME)
+ `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY)
+ }
+
+ @Test
+ fun testAvailableMediaDeviceItemFactory_createFromCachedDevice() {
+ val deviceItem = availableMediaDeviceItemFactory.create(context, cachedDevice)
+
+ assertDeviceItem(deviceItem, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+ }
+
+ @Test
+ fun testConnectedDeviceItemFactory_createFromCachedDevice() {
+ val deviceItem = connectedDeviceItemFactory.create(context, cachedDevice)
+
+ assertDeviceItem(deviceItem, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ }
+
+ @Test
+ fun testSavedDeviceItemFactory_createFromCachedDevice() {
+ val deviceItem = savedDeviceItemFactory.create(context, cachedDevice)
+
+ assertDeviceItem(deviceItem, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+ assertThat(deviceItem.background).isNull()
+ }
+
+ private fun assertDeviceItem(deviceItem: DeviceItem?, deviceItemType: DeviceItemType) {
+ assertThat(deviceItem).isNotNull()
+ assertThat(deviceItem!!.type).isEqualTo(deviceItemType)
+ assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
+ assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
+ assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY)
+ }
+
+ companion object {
+ const val DEVICE_NAME = "DeviceName"
+ const val CONNECTION_SUMMARY = "ConnectionSummary"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
new file mode 100644
index 0000000..07a95ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class DeviceItemInteractorTest : SysuiTestCase() {
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var bluetoothTileDialogRepository: BluetoothTileDialogRepository
+
+ @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
+
+ @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
+
+ @Mock private lateinit var device1: BluetoothDevice
+
+ @Mock private lateinit var device2: BluetoothDevice
+
+ @Mock private lateinit var deviceItem1: DeviceItem
+
+ @Mock private lateinit var deviceItem2: DeviceItem
+
+ @Mock private lateinit var audioManager: AudioManager
+
+ @Mock private lateinit var adapter: BluetoothAdapter
+
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+
+ private lateinit var interactor: DeviceItemInteractor
+
+ private lateinit var dispatcher: CoroutineDispatcher
+
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ dispatcher = StandardTestDispatcher()
+ testScope = TestScope(dispatcher)
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ uiEventLogger,
+ testScope.backgroundScope,
+ dispatcher
+ )
+
+ `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
+ `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
+ `when`(cachedDevice1.device).thenReturn(device1)
+ `when`(cachedDevice2.device).thenReturn(device2)
+ `when`(bluetoothTileDialogRepository.cachedDevices)
+ .thenReturn(listOf(cachedDevice1, cachedDevice2))
+ }
+
+ @Test
+ fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
+ testScope.runTest {
+ `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(createFactory({ true }, deviceItem1))
+ )
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).isEmpty()
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
+ testScope.runTest {
+ `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(createFactory({ false }, deviceItem1))
+ )
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).isEmpty()
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
+ testScope.runTest {
+ `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(createFactory({ true }, deviceItem1))
+ )
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).hasSize(1)
+ assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem1)
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
+ testScope.runTest {
+ `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
+ )
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).hasSize(2)
+ assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem2)
+ assertThat(interactor.deviceItemFlow.value!![1]).isEqualTo(deviceItem2)
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_sortByDisplayPriority() {
+ testScope.runTest {
+ `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(
+ createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+ createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+ )
+ )
+ interactor.setDisplayPriorityForTesting(
+ listOf(
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+ )
+ )
+ `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+ }
+ }
+
+ @Test
+ fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
+ testScope.runTest {
+ `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
+ interactor.setDeviceItemFactoryListForTesting(
+ listOf(
+ createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+ createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+ )
+ )
+ interactor.setDisplayPriorityForTesting(
+ listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ )
+ `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+
+ interactor.updateDeviceItems(mContext)
+
+ assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+ }
+ }
+
+ private fun createFactory(
+ isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean,
+ deviceItem: DeviceItem
+ ): DeviceItemFactory {
+ return object : DeviceItemFactory() {
+ override fun isFilterMatched(
+ cachedDevice: CachedBluetoothDevice,
+ audioManager: AudioManager?
+ ) = isFilterMatchFunc(cachedDevice)
+
+ override fun create(context: Context, cachedDevice: CachedBluetoothDevice) = deviceItem
+ }
+ }
+}
diff --git a/services/Android.bp b/services/Android.bp
index 9264172..f237095 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -34,17 +34,18 @@
},
}
-// Opt-in config for optimizing and shrinking the services target using R8.
-// Enabled via `export SYSTEM_OPTIMIZE_JAVA=true`, or explicitly in Make via the
+// Config to control optimizing and shrinking the services target using R8.
+// Set via `export SYSTEM_OPTIMIZE_JAVA=true|false`, or explicitly in Make via the
// `SOONG_CONFIG_ANDROID_SYSTEM_OPTIMIZE_JAVA` variable.
-// TODO(b/196084106): Enable optimizations by default after stabilizing and
-// building out retrace infrastructure.
soong_config_module_type {
name: "system_optimized_java_defaults",
module_type: "java_defaults",
config_namespace: "ANDROID",
bool_variables: ["SYSTEM_OPTIMIZE_JAVA"],
- properties: ["optimize"],
+ properties: [
+ "optimize",
+ "dxflags",
+ ],
}
system_optimized_java_defaults {
@@ -75,6 +76,9 @@
// permission subpackage to prune unused jarjar'ed Kotlin dependencies.
proguard_flags_files: ["proguard_permission.flags"],
},
+ // Explicitly configure R8 to preserve debug info, as this path should
+ // really only allow stripping of permission-specific code and deps.
+ dxflags: ["--debug"],
},
},
},
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 102c262..a3ccb16 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -133,6 +133,7 @@
@GuardedBy("mGenericWindowPolicyControllerLock")
private boolean mShowTasksInHostDeviceRecents;
+ @Nullable private final ComponentName mCustomHomeComponent;
/**
* Creates a window policy controller that is generic to the different use cases of virtual
@@ -157,6 +158,10 @@
* @param intentListenerCallback Callback that is called to intercept intents when matching
* passed in filters.
* @param showTasksInHostDeviceRecents whether to show activities in recents on the host device.
+ * @param customHomeComponent The component acting as a home activity on the virtual display. If
+ * {@code null}, then the system-default secondary home activity will be used. This is only
+ * applicable to displays that support home activities, i.e. they're created with the relevant
+ * virtual display flag.
*/
public GenericWindowPolicyController(
int windowFlags,
@@ -172,7 +177,8 @@
@Nullable SecureWindowCallback secureWindowCallback,
@Nullable IntentListenerCallback intentListenerCallback,
@NonNull Set<String> displayCategories,
- boolean showTasksInHostDeviceRecents) {
+ boolean showTasksInHostDeviceRecents,
+ @Nullable ComponentName customHomeComponent) {
super();
mAllowedUsers = allowedUsers;
mActivityLaunchAllowedByDefault = activityLaunchAllowedByDefault;
@@ -187,6 +193,7 @@
mIntentListenerCallback = intentListenerCallback;
mDisplayCategories = displayCategories;
mShowTasksInHostDeviceRecents = showTasksInHostDeviceRecents;
+ mCustomHomeComponent = customHomeComponent;
}
/**
@@ -384,6 +391,11 @@
return false;
}
+ @Override
+ public @Nullable ComponentName getCustomHomeComponent() {
+ return mCustomHomeComponent;
+ }
+
/**
* Returns true if an app with the given UID has an activity running on the virtual display for
* this controller.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index f328b22..203a152 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -53,7 +53,7 @@
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorEvent;
import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
+import android.compat.annotation.EnabledAfter;
import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.Context;
@@ -131,7 +131,7 @@
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
*/
@ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long MAKE_VIRTUAL_DISPLAY_FLAGS_CONSISTENT_WITH_DISPLAY_MANAGER =
294837146L;
@@ -938,6 +938,8 @@
mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED;
final boolean showTasksInHostDeviceRecents =
getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
+ final ComponentName homeComponent =
+ Flags.vdmCustomHome() ? mParams.getHomeComponent() : null;
final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
FLAG_SECURE,
@@ -955,7 +957,8 @@
this::onSecureWindowShown,
this::shouldInterceptIntent,
displayCategories,
- showTasksInHostDeviceRecents);
+ showTasksInHostDeviceRecents,
+ homeComponent);
gwpc.registerRunningAppsChangedListener(/* listener= */ this);
return gwpc;
}
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index cd867f6..2e5f2dc 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -408,12 +408,6 @@
}
}
- void stop() {
- if (mEglContext != null && mEglDisplay != null) {
- EGL14.eglDestroyContext(mEglDisplay, mEglContext);
- }
- }
-
/**
* Draws an animation frame showing the color fade activated at the
* specified level.
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index bc81491..83f4df9 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -3577,8 +3577,7 @@
DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
int displayId, int displayState) {
- return new DisplayPowerState(blanker, colorFade, displayId, displayState,
- new Handler(/*async=*/ true));
+ return new DisplayPowerState(blanker, colorFade, displayId, displayState);
}
DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 90a8490..b0d293a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -324,6 +324,8 @@
// Must only be accessed on the handler thread.
private DisplayPowerState mPowerState;
+
+
// The currently active screen on unblocker. This field is non-null whenever
// we are waiting for a callback to release it and unblock the screen.
private ScreenOnUnblocker mPendingScreenOnUnblocker;
@@ -2914,8 +2916,7 @@
DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
int displayId, int displayState) {
- return new DisplayPowerState(blanker, colorFade, displayId, displayState,
- new Handler(/*async=*/ true));
+ return new DisplayPowerState(blanker, colorFade, displayId, displayState);
}
DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index 85c6a6d..2c257a1 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -74,9 +74,8 @@
private volatile boolean mStopped;
DisplayPowerState(
- DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState,
- Handler handler) {
- mHandler = handler;
+ DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState) {
+ mHandler = new Handler(true /*async*/);
mChoreographer = Choreographer.getInstance();
mBlanker = blanker;
mColorFade = colorFade;
@@ -318,7 +317,6 @@
mStopped = true;
mPhotonicModulator.interrupt();
dismissColorFade();
- stopColorFade();
mCleanListener = null;
mHandler.removeCallbacksAndMessages(null);
}
@@ -378,11 +376,6 @@
}
}
- // Clears up color fade resources.
- private void stopColorFade() {
- if (mColorFade != null) mColorFade.stop();
- }
-
private final Runnable mScreenUpdateRunnable = new Runnable() {
@Override
public void run() {
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index dadcebe..bdd2ab7 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Trace;
import android.util.Slog;
import android.util.SparseArray;
@@ -104,6 +105,9 @@
votes.remove(priority);
}
}
+ Trace.traceCounter(Trace.TRACE_TAG_POWER,
+ TAG + "." + displayId + ":" + Vote.priorityToString(priority),
+ getMaxPhysicalRefreshRate(vote));
if (mLoggingEnabled) {
Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
}
@@ -146,6 +150,15 @@
}
}
+ private int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
+ if (vote == null) {
+ return -1;
+ } else if (vote.refreshRateRanges.physical.max == Float.POSITIVE_INFINITY) {
+ return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
+ }
+ return (int) vote.refreshRateRanges.physical.max;
+ }
+
interface Listener {
void onChanged();
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index 7045e65..c01bc20 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -868,6 +868,28 @@
}
@ServiceThreadOnly
+ void removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices) {
+ ArrayList<Integer> deviceTypesToRemove = new ArrayList<>();
+ for (int i = 0; i < mLocalDevices.size(); i++) {
+ int deviceType = mLocalDevices.keyAt(i);
+ boolean shouldRemoveLocalDevice = allocatedDevices.stream().noneMatch(
+ localDevice -> localDevice.getDeviceInfo() != null
+ && localDevice.getDeviceInfo().getDeviceType() == deviceType);
+ if (shouldRemoveLocalDevice) {
+ deviceTypesToRemove.add(deviceType);
+ }
+ }
+ for (Integer deviceType : deviceTypesToRemove) {
+ mLocalDevices.remove(deviceType);
+ }
+ }
+
+ @ServiceThreadOnly
+ void removeLocalDeviceWithType(int deviceType) {
+ mLocalDevices.remove(deviceType);
+ }
+
+ @ServiceThreadOnly
public void clearDeviceList() {
assertRunOnServiceThread();
for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 232bc47..429db5e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1313,9 +1313,6 @@
localDevice.init();
localDevices.add(localDevice);
}
- // It's now safe to flush existing local devices from mCecController since they were
- // already moved to 'localDevices'.
- clearCecLocalDevices();
mHdmiCecNetwork.clearDeviceList();
allocateLogicalAddress(localDevices, initiatedBy);
}
@@ -1344,6 +1341,7 @@
if (logicalAddress == Constants.ADDR_UNREGISTERED) {
Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType
+ "]");
+ mHdmiCecNetwork.removeLocalDeviceWithType(deviceType);
} else {
// Set POWER_STATUS_ON to all local devices because they share
// lifetime
@@ -1352,6 +1350,8 @@
deviceType,
HdmiControlManager.POWER_STATUS_ON, getCecVersion());
localDevice.setDeviceInfo(deviceInfo);
+ // If a local device of the same type already exists, it will be
+ // replaced.
mHdmiCecNetwork.addLocalDevice(deviceType, localDevice);
mHdmiCecNetwork.addCecDevice(localDevice.getDeviceInfo());
mCecController.addLogicalAddress(logicalAddress);
@@ -1367,6 +1367,10 @@
// since we reallocate the logical address only.
onInitializeCecComplete(initiatedBy);
}
+ // We remove local devices here, instead of before the start of
+ // address allocation, to prevent multiple local devices of the
+ // same type from existing simultaneously.
+ mHdmiCecNetwork.removeUnusedLocalDevices(allocatedDevices);
mAddressAllocated = true;
notifyAddressAllocated(allocatedDevices, initiatedBy);
// Reinvoke the saved display status callback once the local
@@ -1386,9 +1390,19 @@
}
}
+ /**
+ * Notifies local devices that address allocation finished.
+ * @param devices - list of local devices allocated.
+ * @param initiatedBy - reason for the address allocation.
+ */
+ @VisibleForTesting
@ServiceThreadOnly
- private void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
+ public void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
assertRunOnServiceThread();
+ if (devices == null || devices.isEmpty()) {
+ Slog.w(TAG, "No local device to notify.");
+ return;
+ }
List<HdmiCecMessage> bufferedMessages = mCecMessageBuffer.getBuffer();
for (HdmiCecLocalDevice device : devices) {
int address = device.getDeviceInfo().getLogicalAddress();
diff --git a/services/core/java/com/android/server/notification/TEST_MAPPING b/services/core/java/com/android/server/notification/TEST_MAPPING
index 59b2bc1..7db2e8b 100644
--- a/services/core/java/com/android/server/notification/TEST_MAPPING
+++ b/services/core/java/com/android/server/notification/TEST_MAPPING
@@ -13,7 +13,7 @@
"exclude-annotation": "org.junit.Ignore"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.LargeTest"
@@ -33,7 +33,7 @@
"exclude-annotation": "org.junit.Ignore"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "androidx.test.filters.LargeTest"
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 700fae9..33cb85c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4681,9 +4681,12 @@
@UserIdInt int userId, @NonNull String recentCallingPackage,
@NonNull String debugInfo) {
synchronized (mLock) {
- final PackageUserStateInternal userState = mSettings.getPackageLPr(
- packageName).getUserStateOrDefault(userId);
- if (userState.isQuarantined()) {
+ final PackageSetting pkgSetting = mSettings.getPackageLPr(packageName);
+ // If the package doesn't exist, don't need to proceed to setPackageStoppedState.
+ if (pkgSetting == null) {
+ return;
+ }
+ if (pkgSetting.getUserStateOrDefault(userId).isQuarantined()) {
Slog.i(TAG,
"Component is quarantined+suspended but being used: "
+ packageName + " by " + recentCallingPackage + ", debugInfo: "
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 097656c..dc75a98 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1983,6 +1983,7 @@
public void run() {
if (mPendingHomeKeyEvent != null) {
handleShortPressOnHome(mPendingHomeKeyEvent);
+ mPendingHomeKeyEvent.recycle();
mPendingHomeKeyEvent = null;
}
}
@@ -2027,7 +2028,7 @@
if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_PIP_MENU
|| mPictureInPictureVisible) {
mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case
- mPendingHomeKeyEvent = event;
+ mPendingHomeKeyEvent = KeyEvent.obtain(event);
mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
ViewConfiguration.getDoubleTapTimeout());
return true;
@@ -2035,7 +2036,11 @@
}
// Post to main thread to avoid blocking input pipeline.
- mHandler.post(() -> handleShortPressOnHome(event));
+ final KeyEvent shortPressEvent = KeyEvent.obtain(event);
+ mHandler.post(() -> {
+ handleShortPressOnHome(shortPressEvent);
+ shortPressEvent.recycle();
+ });
return true;
}
@@ -2062,9 +2067,14 @@
if (repeatCount == 0) {
mHomePressed = true;
if (mPendingHomeKeyEvent != null) {
+ mPendingHomeKeyEvent.recycle();
mPendingHomeKeyEvent = null;
mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable);
- mHandler.post(() -> handleDoubleTapOnHome(event));
+ final KeyEvent doublePressEvent = KeyEvent.obtain(event);
+ mHandler.post(() -> {
+ handleDoubleTapOnHome(doublePressEvent);
+ doublePressEvent.recycle();
+ });
// TODO(multi-display): Remove display id check once we support recents on
// multi-display
} else if (mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI
@@ -2074,7 +2084,11 @@
} else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
if (!keyguardOn) {
// Post to main thread to avoid blocking input pipeline.
- mHandler.post(() -> handleLongPressOnHome(event));
+ final KeyEvent longPressEvent = KeyEvent.obtain(event);
+ mHandler.post(() -> {
+ handleLongPressOnHome(longPressEvent);
+ longPressEvent.recycle();
+ });
}
}
return true;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d824534..a0c7870 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2887,7 +2887,7 @@
final boolean animate;
if (mStartingData != null) {
if (mStartingData.mWaitForSyncTransactionCommit
- || mTransitionController.inCollectingTransition(startingWindow)) {
+ || mTransitionController.isCollecting(this)) {
mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY;
mStartingData.mPrepareRemoveAnimation = prepareAnimation;
return;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 1ad5798..901975b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -795,10 +795,19 @@
return false;
}
- // Try pausing the existing resumed activity in the Task if any.
final Task task = r.getTask();
- if (task.pauseActivityIfNeeded(r, "realStart")) {
- return false;
+ if (andResume) {
+ // Try pausing the existing resumed activity in the Task if any.
+ if (task.pauseActivityIfNeeded(r, "realStart")) {
+ return false;
+ }
+ final TaskFragment taskFragment = r.getTaskFragment();
+ if (taskFragment != null && taskFragment.getResumedActivity() != null) {
+ if (taskFragment.startPausing(mUserLeaving, false /* uiSleeping */, r,
+ "realStart")) {
+ return false;
+ }
+ }
}
final Task rootTask = task.getRootTask();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4309e72..ca42400 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2160,6 +2160,16 @@
}
/**
+ * @see DisplayWindowPolicyController#getCustomHomeComponent() ()
+ */
+ @Nullable ComponentName getCustomHomeComponent() {
+ if (!supportsSystemDecorations() || mDwpcHelper == null) {
+ return null;
+ }
+ return mDwpcHelper.getCustomHomeComponent();
+ }
+
+ /**
* Applies the rotation transaction. This must be called after {@link #updateRotationUnchecked}
* (if it returned {@code true}) to actually finish the rotation.
*
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 6b33746..e0d69b0 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Process;
@@ -200,6 +201,17 @@
return mDisplayWindowPolicyController.isEnteringPipAllowed(uid);
}
+ /**
+ * @see DisplayWindowPolicyController#getCustomHomeComponent
+ */
+ @Nullable
+ public ComponentName getCustomHomeComponent() {
+ if (mDisplayWindowPolicyController == null) {
+ return null;
+ }
+ return mDisplayWindowPolicyController.getCustomHomeComponent();
+ }
+
void dump(String prefix, PrintWriter pw) {
if (mDisplayWindowPolicyController != null) {
pw.println();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 2fdfec0..2a33918 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1513,10 +1513,30 @@
throw new IllegalArgumentException(
"resolveSecondaryHomeActivity: Should not be default task container");
}
- // Resolve activities in the same package as currently selected primary home activity.
+
Intent homeIntent = mService.getHomeIntent();
ActivityInfo aInfo = resolveHomeActivity(userId, homeIntent);
- if (aInfo != null) {
+ boolean lookForSecondaryHomeActivityInPrimaryHomePackage = aInfo != null;
+
+ if (android.companion.virtual.flags.Flags.vdmCustomHome()) {
+ // Resolve the externally set home activity for this display, if any. If it is unset or
+ // we fail to resolve it, fallback to the default secondary home activity.
+ final ComponentName customHomeComponent =
+ taskDisplayArea.getDisplayContent() != null
+ ? taskDisplayArea.getDisplayContent().getCustomHomeComponent()
+ : null;
+ if (customHomeComponent != null) {
+ homeIntent.setComponent(customHomeComponent);
+ ActivityInfo customHomeActivityInfo = resolveHomeActivity(userId, homeIntent);
+ if (customHomeActivityInfo != null) {
+ aInfo = customHomeActivityInfo;
+ lookForSecondaryHomeActivityInPrimaryHomePackage = false;
+ }
+ }
+ }
+
+ if (lookForSecondaryHomeActivityInPrimaryHomePackage) {
+ // Resolve activities in the same package as currently selected primary home activity.
if (ResolverActivity.class.getName().equals(aInfo.name)) {
// Always fallback to secondary home component if default home is not set.
aInfo = null;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 2d281c4..07ffa69e 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -108,4 +108,13 @@
boolean hasImeSurface() {
return false;
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " waitForSyncTransactionCommit=" + mWaitForSyncTransactionCommit
+ + " removeAfterTransaction= " + mRemoveAfterTransaction
+ + "}";
+ }
}
diff --git a/services/core/jni/TEST_MAPPING b/services/core/jni/TEST_MAPPING
index ea44d06..eb9db70 100644
--- a/services/core/jni/TEST_MAPPING
+++ b/services/core/jni/TEST_MAPPING
@@ -6,7 +6,7 @@
],
"name": "CtsVibratorTestCases",
"options": [
- {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+ {"exclude-annotation": "androidx.test.filters.LargeTest"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"},
{"exclude-annotation": "org.junit.Ignore"}
diff --git a/services/devicepolicy/TEST_MAPPING b/services/devicepolicy/TEST_MAPPING
index 72bba11..fccd1ec 100644
--- a/services/devicepolicy/TEST_MAPPING
+++ b/services/devicepolicy/TEST_MAPPING
@@ -7,7 +7,7 @@
"exclude-annotation": "android.platform.test.annotations.FlakyTest"
},
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
}
]
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 84d1a45..f604932 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -16017,16 +16017,20 @@
}
@Override
- public PackagePolicy getCredentialManagerPolicy() {
+ public PackagePolicy getCredentialManagerPolicy(int userId) {
if (!mHasFeature) {
return null;
}
final CallerIdentity caller = getCallerIdentity();
Preconditions.checkCallAuthorization(
canWriteCredentialManagerPolicy(caller) || canQueryAdminPolicy(caller));
+ if (userId != caller.getUserId()) {
+ Preconditions.checkCallAuthorization(
+ hasCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS));
+ }
synchronized (getLockObject()) {
- ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(caller.getUserId());
+ ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(userId);
return (admin != null) ? admin.mCredentialManagerPolicy : null;
}
}
diff --git a/services/foldables/devicestateprovider/OWNERS b/services/foldables/devicestateprovider/OWNERS
new file mode 100644
index 0000000..b2dcd0c
--- /dev/null
+++ b/services/foldables/devicestateprovider/OWNERS
@@ -0,0 +1,6 @@
+akulian@google.com
+kennethford@google.com
+jiamingliu@google.com
+kchyn@google.com
+nickchameyev@google.com
+nicomazz@google.com
\ No newline at end of file
diff --git a/services/incremental/TEST_MAPPING b/services/incremental/TEST_MAPPING
index 4af880d..4c9403c 100644
--- a/services/incremental/TEST_MAPPING
+++ b/services/incremental/TEST_MAPPING
@@ -12,7 +12,7 @@
"name": "CtsPackageManagerIncrementalStatsHostTestCases",
"options": [
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
}
]
},
@@ -55,7 +55,7 @@
"name": "CtsPackageManagerIncrementalStatsHostTestCases",
"options": [
{
- "include-annotation": "android.platform.test.annotations.LargeTest"
+ "include-annotation": "androidx.test.filters.LargeTest"
}
]
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 9174899..a56b59a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -1255,21 +1255,6 @@
}
@Test
- public void testPowerStateStopsOnDpcStop() {
- // Set up
- DisplayPowerRequest dpr = new DisplayPowerRequest();
- mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
- advanceTime(1);
-
- // Stop dpc
- mHolder.dpc.stop();
- advanceTime(1);
-
- // Ensure dps has stopped
- verify(mHolder.displayPowerState, times(1)).stop();
- }
-
- @Test
public void testRampRateForHdrContent_HdrClamperOff() {
float hdrBrightness = 0.8f;
float clampedBrightness = 0.6f;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 412b65f..0572117 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1191,21 +1191,6 @@
}
@Test
- public void testPowerStateStopsOnDpcStop() {
- // Set up
- DisplayPowerRequest dpr = new DisplayPowerRequest();
- mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
- advanceTime(1);
-
- // Stop dpc
- mHolder.dpc.stop();
- advanceTime(1);
-
- // Ensure dps has stopped
- verify(mHolder.displayPowerState, times(1)).stop();
- }
-
- @Test
public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java
deleted file mode 100644
index 167a412..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.server.display;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-
-import static org.mockito.Mockito.times;
-
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.view.Display;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-
-@SmallTest
-public class DisplayPowerStateTest {
- private static final int DISPLAY_ID = 123;
-
- private DisplayPowerState mDisplayPowerState;
- private TestLooper mTestLooper;
- @Mock
- private DisplayBlanker mDisplayBlankerMock;
- @Mock
- private ColorFade mColorFadeMock;
-
- @Rule
- public final MockitoRule mMockitoRule = MockitoJUnit.rule();
-
- @Before
- public void setUp() {
- mTestLooper = new TestLooper();
- mDisplayPowerState = new DisplayPowerState(
- mDisplayBlankerMock, mColorFadeMock, DISPLAY_ID, Display.STATE_ON,
- new Handler(mTestLooper.getLooper()));
- }
-
- @Test
- public void testColorFadeStopsOnDpsStop() {
- mDisplayPowerState.stop();
- verify(mColorFadeMock, times(1)).stop();
- }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 552b59c..a250ac7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -33,7 +33,6 @@
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -75,11 +74,11 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.platform.test.annotations.LargeTest;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import android.util.SparseBooleanArray;
+import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
@@ -2597,7 +2596,9 @@
@Test
public void testIsWithinEJQuotaLocked_TempAllowlisting_Restricted() {
setDischarging();
- JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
+ JobStatus js =
+ createExpeditedJobStatus(
+ "testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
setStandbyBucket(RESTRICTED_INDEX, js);
setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_FREQUENT_MS, 10 * MINUTE_IN_MILLIS);
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -6088,7 +6089,8 @@
Handler handler = mQuotaController.getHandler();
spyOn(handler);
- JobStatus job = createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
+ JobStatus job =
+ createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
setStandbyBucket(RESTRICTED_INDEX, job);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStartTrackingJobLocked(job, null);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
index 8faddf8..fcfe48e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
@@ -17,7 +17,6 @@
package com.android.server.accessibility.magnification;
import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -27,13 +26,13 @@
import android.graphics.Rect;
import android.os.Handler;
-import android.platform.test.annotations.LargeTest;
import android.testing.TestableContext;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index c40ad28..1c48b8a 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -88,7 +88,8 @@
/* secureWindowCallback= */ null,
/* intentListenerCallback= */ null,
/* displayCategories= */ new ArraySet<>(),
- /* showTasksInHostDeviceRecents= */ true);
+ /* showTasksInHostDeviceRecents= */ true,
+ /* customHomeComponent= */ null);
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 0d172fdb..708ee35 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -21,8 +21,15 @@
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
+import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3;
import static com.android.server.hdmi.HdmiControlService.DEVICE_CLEANUP_TIMEOUT;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_HOTPLUG;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_SCREEN_ON;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_SOUNDBAR_MODE;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
import static com.google.common.truth.Truth.assertThat;
@@ -51,6 +58,7 @@
import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
import android.hardware.hdmi.IHdmiControlStatusChangeListener;
import android.hardware.hdmi.IHdmiVendorCommandListener;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.os.Binder;
import android.os.Looper;
import android.os.RemoteException;
@@ -71,6 +79,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
+import java.util.concurrent.TimeUnit;
/**
* Tests for {@link HdmiControlService} class.
@@ -137,7 +146,7 @@
mLocalDevices.add(mAudioSystemDeviceSpy);
mLocalDevices.add(mPlaybackDeviceSpy);
- mHdmiPortInfo = new HdmiPortInfo[4];
+ mHdmiPortInfo = new HdmiPortInfo[5];
mHdmiPortInfo[0] =
new HdmiPortInfo.Builder(1, HdmiPortInfo.PORT_INPUT, 0x2100)
.setCecSupported(true)
@@ -166,6 +175,13 @@
.setArcSupported(false)
.setEarcSupported(false)
.build();
+ mHdmiPortInfo[4] =
+ new HdmiPortInfo.Builder(4, HdmiPortInfo.PORT_OUTPUT, 0x3000)
+ .setCecSupported(true)
+ .setMhlSupported(false)
+ .setArcSupported(false)
+ .setEarcSupported(false)
+ .build();
mNativeWrapper.setPortInfo(mHdmiPortInfo);
mHdmiControlServiceSpy.initService();
mWakeLockSpy = spy(new FakePowerManagerWrapper.FakeWakeLockWrapper());
@@ -1396,6 +1412,207 @@
}
@Test
+ public void triggerMultipleAddressAllocations_uniqueLocalDevicePerDeviceType() {
+ long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+ mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ // Wake up process that will trigger the address allocation to start.
+ mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mTestLooper.dispatchAll();
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // Hotplug In will trigger the address allocation to start.
+ mHdmiControlServiceSpy.onHotplug(4, true);
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_HOTPLUG));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The first allocation finished. The second allocation is still in progress.
+ HdmiCecLocalDevicePlayback firstAllocatedPlayback = mHdmiControlServiceSpy.playback();
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The second allocation finished.
+ HdmiCecLocalDevicePlayback secondAllocatedPlayback = mHdmiControlServiceSpy.playback();
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_HOTPLUG));
+ // Local devices have the same identity.
+ assertTrue(firstAllocatedPlayback == secondAllocatedPlayback);
+ }
+
+ @Test
+ public void triggerMultipleAddressAllocations_keepLastAllocatedAddress() {
+ // First logical address for playback is free.
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.NACK);
+ mTestLooper.dispatchAll();
+
+ long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+ mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ // Wake up process that will trigger the address allocation to start.
+ mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mTestLooper.dispatchAll();
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+
+ // First logical address for playback is busy.
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mTestLooper.dispatchAll();
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The first allocation finished. The second allocation is still in progress.
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The second allocation finished. Second logical address for playback is used.
+ HdmiCecLocalDevicePlayback allocatedPlayback = mHdmiControlServiceSpy.playback();
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+ assertThat(allocatedPlayback.getDeviceInfo().getLogicalAddress())
+ .isEqualTo(ADDR_PLAYBACK_2);
+ }
+
+ @Test
+ public void triggerMultipleAddressAllocations_toggleSoundbarMode_addThenRemoveAudioSystem() {
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+ long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+ mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ // Enabling Dynamic soundbar mode will trigger address allocation.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // Disabling Dynamic soundbar mode will trigger another address allocation.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_DISABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The first allocation finished. The second allocation is still in progress.
+ // The audio system is present in the network.
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The second allocation finished. The audio system is not present in the network.
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+ }
+
+ @Test
+ public void triggerMultipleAddressAllocations_toggleSoundbarMode_removeThenAddAudioSystem() {
+ mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+ // Enable the setting and check if the audio system local device is found in the network.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+
+ long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+ mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ // Disabling Dynamic soundbar mode will trigger address allocation.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_DISABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // Enabling Dynamic soundbar mode will trigger another address allocation.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1))
+ .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The first allocation finished. The second allocation is still in progress.
+ // The audio system is not present in the network.
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+ mTestLooper.moveTimeForward(allocationDelay / 2);
+ mTestLooper.dispatchAll();
+ // The second allocation finished. The audio system is present in the network.
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+ }
+
+ @Test
+ public void failedAddressAllocation_noLocalDevice() {
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_2, SendMessageResult.SUCCESS);
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_3, SendMessageResult.SUCCESS);
+ mNativeWrapper.setPollAddressResponse(ADDR_AUDIO_SYSTEM, SendMessageResult.SUCCESS);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlServiceSpy.clearCecLocalDevices();
+ mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiControlServiceSpy.playback()).isNull();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+ }
+
+ @Test
public void earcIdle_blocksArcConnection() {
mHdmiControlServiceSpy.clearEarcLocalDevice();
HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index be13753..213e05e 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -18,7 +18,6 @@
import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
import static android.app.job.JobInfo.NETWORK_TYPE_NONE;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -28,13 +27,14 @@
import android.app.job.JobInfo;
import android.content.ComponentName;
-import android.platform.test.annotations.LargeTest;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseLongArray;
+import androidx.test.filters.LargeTest;
+
import com.android.server.job.controllers.JobStatus;
import org.junit.Test;
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 8db09f9..61c4d06 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,7 +46,6 @@
import static java.util.Collections.unmodifiableMap;
import android.content.Context;
-import android.os.Looper;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.view.InputDevice;
@@ -99,10 +98,6 @@
* settings values.
*/
protected final void setUpPhoneWindowManager(boolean supportSettingsUpdate) {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
-
doReturn(mSettingsProviderRule.mockContentResolver(mContext))
.when(mContext).getContentResolver();
mPhoneWindowManager = new TestPhoneWindowManager(mContext, supportSettingsUpdate);
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 ef28ffa..6e2c4bd 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -70,7 +70,6 @@
import android.hardware.input.InputManager;
import android.media.AudioManagerInternal;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
@@ -78,6 +77,7 @@
import android.os.UserHandle;
import android.os.Vibrator;
import android.os.VibratorInfo;
+import android.os.test.TestLooper;
import android.service.dreams.DreamManagerInternal;
import android.telecom.TelecomManager;
import android.util.FeatureFlagUtils;
@@ -160,8 +160,8 @@
@Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
private StaticMockitoSession mMockitoSession;
- private HandlerThread mHandlerThread;
private Handler mHandler;
+ private TestLooper mTestLooper;
private class TestInjector extends PhoneWindowManager.Injector {
TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
@@ -184,12 +184,11 @@
TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
MockitoAnnotations.initMocks(this);
- mHandlerThread = new HandlerThread("fake window manager");
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
+ mTestLooper = new TestLooper();
+ mHandler = new Handler(mTestLooper.getLooper());
mContext = mockingDetails(context).isSpy() ? context : spy(context);
- mHandler.runWithScissors(() -> setUp(supportSettingsUpdate), 0 /* timeout */);
- waitForIdle();
+ mHandler.post(() -> setUp(supportSettingsUpdate));
+ mTestLooper.dispatchAll();
}
private void setUp(boolean supportSettingsUpdate) {
@@ -301,7 +300,6 @@
}
void tearDown() {
- mHandlerThread.quitSafely();
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
Mockito.reset(mPhoneWindowManager);
mMockitoSession.finishMocking();
@@ -328,7 +326,7 @@
}
void waitForIdle() {
- mHandler.runWithScissors(() -> { }, 0 /* timeout */);
+ mTestLooper.dispatchAll();
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index 30a8941..cf620fe 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -275,5 +275,10 @@
public boolean isEnteringPipAllowed(int uid) {
return true;
}
+
+ @Override
+ public ComponentName getCustomHomeComponent() {
+ return null;
+ }
}
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
index 030b292..b3c9c96 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
@@ -31,7 +31,8 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButton3ButtonLandscape :
CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_90) {
- @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+ // TODO: Missing CUJ (b/300078127)
+ @ExpectedScenarios(["ENTIRE_TRACE"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
index 770da143..29c11a4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
@@ -31,7 +31,8 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButton3ButtonPortrait :
CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_0) {
- @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+ // TODO: Missing CUJ (b/300078127)
+ @ExpectedScenarios(["ENTIRE_TRACE"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
index 4b2206d7..1bb4a25 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
@@ -31,7 +31,8 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButtonGesturalNavLandscape :
CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_90) {
- @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+ // TODO: Missing CUJ (b/300078127)
+ @ExpectedScenarios(["ENTIRE_TRACE"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
index 54b471e..17cb6e1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
@@ -31,7 +31,8 @@
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CloseAppBackButtonGesturalNavPortrait :
CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) {
- @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+ // TODO: Missing CUJ (b/300078127)
+ @ExpectedScenarios(["ENTIRE_TRACE"])
@Test
override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
index 93130c4..a1708fe 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
@@ -19,6 +19,7 @@
import android.app.Instrumentation
import android.tools.common.NavBar
import android.tools.common.Rotation
+import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
import android.tools.device.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -46,7 +47,9 @@
tapl.setExpectedRotation(rotation.value)
tapl.setIgnoreTaskbarVisibility(true)
testApp1.launchViaIntent(wmHelper)
+ ChangeDisplayOrientationRule.setRotation(rotation)
testApp2.launchViaIntent(wmHelper)
+ ChangeDisplayOrientationRule.setRotation(rotation)
}
@Test
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index f867c98..9198ae1 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -102,6 +102,20 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:name=".PortraitImmersiveActivity"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitImmersiveActivity"
+ android:immersive="true"
+ android:resizeableActivity="true"
+ android:screenOrientation="portrait"
+ android:theme="@android:style/Theme.NoTitleBar"
+ android:configChanges="screenSize"
+ android:label="PortraitImmersiveActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<activity android:name=".LaunchTransparentActivity"
android:resizeableActivity="false"
android:screenOrientation="portrait"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 7c5e9a3..8b334c0 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -73,6 +73,12 @@
FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
}
+ public static class PortraitImmersiveActivity {
+ public static final String LABEL = "PortraitImmersiveActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".PortraitImmersiveActivity");
+ }
+
public static class TransparentActivity {
public static final String LABEL = "TransparentActivity";
public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java
new file mode 100644
index 0000000..0a7f81c
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java
@@ -0,0 +1,20 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+public class PortraitImmersiveActivity extends GameActivity {
+}
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
index 235bd47..fd888ec 100644
--- a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
Binary files differ
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
index 413e3c0..66f7bfd 100644
--- a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
@@ -1,35 +1,31 @@
-----BEGIN CERTIFICATE-----
-MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT
-MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0
-aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw
-WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE
-AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m
-OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu
-T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c
-JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR
-Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz
-PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm
-aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM
-TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g
-LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO
-BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv
-dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB
-AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL
-NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W
-b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG
-A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
-cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
-MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
-BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
-YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
-ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
-BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
-I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
-CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i
-2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ
-2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ
+MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX
+MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE
+CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx
+OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT
+GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63
+ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS
+iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k
+KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ
+DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk
+j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5
+cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW
+CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499
+iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei
+Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap
+sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b
+9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf
+BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw
+JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH
+MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al
+oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy
+MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF
+AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9
+NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9
+WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw
+9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy
++qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi
+d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=
-----END CERTIFICATE-----
diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
index 5d23d36e..99106ad 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
@@ -5,7 +5,7 @@
</domain>
<domain> developer.android.com </domain>
<pin-set>
- <pin digest="SHA-256"> 7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y= </pin>
+ <pin digest="SHA-256"> zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w= </pin>
</pin-set>
</domain-config>
</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
index d45fd77..232f88f 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
@@ -9,7 +9,7 @@
<domain-config>
<domain>developer.android.com</domain>
<pin-set>
- <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+ <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin>
</pin-set>
</domain-config>
</domain-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
index 1773d280..7cc81b0 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
@@ -3,7 +3,7 @@
<domain-config>
<domain>android.com</domain>
<pin-set>
- <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+ <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin>
</pin-set>
</domain-config>
</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
index 047be16..0494f17 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
@@ -22,23 +22,17 @@
import android.test.ActivityUnitTestCase;
import android.util.ArraySet;
import android.util.Pair;
+
+import com.android.org.conscrypt.TrustedCertificateStore;
+
import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.net.Socket;
-import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.TrustManager;
-import com.android.org.conscrypt.TrustedCertificateStore;
+import javax.net.ssl.SSLContext;
public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
@@ -46,9 +40,9 @@
super(Activity.class);
}
- // SHA-256 of the G2 intermediate CA for android.com (as of 10/2015).
- private static final byte[] G2_SPKI_SHA256
- = hexToBytes("ec722969cb64200ab6638f68ac538e40abab5b19a6485661042a1061c4612776");
+ // SHA-256 of the GTS intermediate CA (CN = GTS CA 1C3) for android.com (as of 09/2023).
+ private static final byte[] GTS_INTERMEDIATE_SPKI_SHA256 =
+ hexToBytes("cc24e77cbc0b29b4bd4b6b1ba7eb85cf82993a8705bd7c64574e827bd3b9336c");
private static final byte[] TEST_CA_BYTES
= hexToBytes(
@@ -161,7 +155,7 @@
public void testGoodPin() throws Exception {
ArraySet<Pin> pins = new ArraySet<Pin>();
- pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+ pins.add(new Pin("SHA-256", GTS_INTERMEDIATE_SPKI_SHA256));
NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
.setPinSet(new PinSet(pins, Long.MAX_VALUE))
.addCertificatesEntryRef(
@@ -247,7 +241,7 @@
public void testWithUrlConnection() throws Exception {
ArraySet<Pin> pins = new ArraySet<Pin>();
- pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+ pins.add(new Pin("SHA-256", GTS_INTERMEDIATE_SPKI_SHA256));
NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
.setPinSet(new PinSet(pins, Long.MAX_VALUE))
.addCertificatesEntryRef(
@@ -304,7 +298,7 @@
} finally {
// Delete the user added CA. We don't know the alias so just delete them all.
for (String alias : store.aliases()) {
- if (store.isUser(alias)) {
+ if (TrustedCertificateStore.isUser(alias)) {
try {
store.deleteCertificateEntry(alias);
} catch (Exception ignored) {
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
index 9dec21b..39b5cb4c 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
@@ -16,19 +16,20 @@
package android.security.net.config;
+import static org.junit.Assert.fail;
+
import android.content.pm.ApplicationInfo;
import android.os.Build;
-import java.net.Socket;
+
import java.net.URL;
+
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.TrustManager;
+import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;
-import junit.framework.Assert;
-
-public final class TestUtils extends Assert {
+public final class TestUtils {
private TestUtils() {
}
@@ -36,8 +37,8 @@
public static void assertConnectionFails(SSLContext context, String host, int port)
throws Exception {
try {
- Socket s = context.getSocketFactory().createSocket(host, port);
- s.getInputStream();
+ SSLSocket s = (SSLSocket) context.getSocketFactory().createSocket(host, port);
+ s.startHandshake();
fail("Expected connection to " + host + ":" + port + " to fail.");
} catch (SSLHandshakeException expected) {
}
@@ -45,7 +46,8 @@
public static void assertConnectionSucceeds(SSLContext context, String host, int port)
throws Exception {
- Socket s = context.getSocketFactory().createSocket(host, port);
+ SSLSocket s = (SSLSocket) context.getSocketFactory().createSocket(host, port);
+ s.startHandshake();
s.getInputStream();
}
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
index 4b7a014..81e05c1 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -16,26 +16,18 @@
package android.security.net.config;
-import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
-import android.util.ArraySet;
-import android.util.Pair;
+
import java.io.IOException;
import java.net.InetAddress;
-import java.net.Socket;
-import java.net.URL;
import java.security.KeyStore;
import java.security.Provider;
-import java.security.Security;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.Set;
-import javax.net.ssl.HttpsURLConnection;
+
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
@@ -52,7 +44,7 @@
NetworkSecurityConfig config = appConfig.getConfigForHostname("");
assertNotNull(config);
// Check defaults.
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
assertFalse(config.getTrustAnchors().isEmpty());
PinSet pinSet = config.getPins();
@@ -72,7 +64,7 @@
NetworkSecurityConfig config = appConfig.getConfigForHostname("");
assertNotNull(config);
// Check defaults.
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
assertTrue(config.getTrustAnchors().isEmpty());
PinSet pinSet = config.getPins();
@@ -91,14 +83,14 @@
NetworkSecurityConfig config = appConfig.getConfigForHostname("");
assertNotNull(config);
// Check defaults.
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
assertTrue(config.getTrustAnchors().isEmpty());
PinSet pinSet = config.getPins();
assertTrue(pinSet.pins.isEmpty());
// Check android.com.
config = appConfig.getConfigForHostname("android.com");
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
assertFalse(config.getTrustAnchors().isEmpty());
pinSet = config.getPins();
@@ -188,7 +180,7 @@
ApplicationConfig appConfig = new ApplicationConfig(source);
assertTrue(appConfig.hasPerDomainConfigs());
NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
assertFalse(config.getTrustAnchors().isEmpty());
PinSet pinSet = config.getPins();
@@ -250,9 +242,9 @@
ApplicationConfig appConfig = new ApplicationConfig(source);
// Check android.com.
NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
- assertEquals(2, config.getTrustAnchors().size());
+ assertEquals(1, config.getTrustAnchors().size());
// Try connections.
SSLContext context = TestUtils.getSSLContext(source);
TestUtils.assertConnectionSucceeds(context, "android.com", 443);
@@ -267,9 +259,9 @@
ApplicationConfig appConfig = new ApplicationConfig(source);
// Check android.com.
NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
- assertTrue(config.isCleartextTrafficPermitted());
+ assertFalse(config.isCleartextTrafficPermitted());
assertFalse(config.isHstsEnforced());
- assertEquals(2, config.getTrustAnchors().size());
+ assertEquals(1, config.getTrustAnchors().size());
// Try connections.
SSLContext context = TestUtils.getSSLContext(source);
TestUtils.assertConnectionSucceeds(context, "android.com", 443);
diff --git a/tests/utils/testutils/TEST_MAPPING b/tests/utils/testutils/TEST_MAPPING
index d9eb44f..6468d8c 100644
--- a/tests/utils/testutils/TEST_MAPPING
+++ b/tests/utils/testutils/TEST_MAPPING
@@ -4,7 +4,7 @@
"name": "frameworks-base-testutils-tests",
"options": [
{
- "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ "exclude-annotation": "androidx.test.filters.LargeTest"
},
{
"exclude-annotation": "android.platform.test.annotations.FlakyTest"