1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.car.cluster.cts; 17 18 import static android.car.CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION; 19 import static android.car.feature.Flags.FLAG_CLUSTER_HEALTH_MONITORING; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static org.junit.Assume.assumeFalse; 24 import static org.junit.Assume.assumeTrue; 25 26 import android.app.Activity; 27 import android.app.Instrumentation; 28 import android.app.UiAutomation; 29 import android.car.Car; 30 import android.car.CarAppFocusManager; 31 import android.car.CarAppFocusManager.OnAppFocusOwnershipCallback; 32 import android.car.cluster.ClusterHomeManager; 33 import android.car.cluster.ClusterHomeManager.ClusterNavigationStateListener; 34 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 35 import android.car.cts.utils.DumpUtils; 36 import android.car.navigation.CarNavigationStatusManager; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.os.Bundle; 41 import android.platform.test.annotations.RequiresFlagsEnabled; 42 import android.view.WindowInsets; 43 import android.view.WindowInsetsController; 44 45 import androidx.test.platform.app.InstrumentationRegistry; 46 47 import com.android.compatibility.common.util.ApiTest; 48 import com.android.compatibility.common.util.PollingCheck; 49 50 import org.junit.After; 51 import org.junit.Before; 52 import org.junit.Test; 53 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.TimeUnit; 56 57 public final class ClusterHomeManagerTest { 58 private static final long TIMEOUT_MS = 10_000; 59 private static final String FEATURE_CAR_SPLITSCREEN_MULTITASKING = 60 "android.software.car.splitscreen_multitasking"; 61 private static final String CLUSTER_HOME_SERVICE = "ClusterHomeService"; 62 private static final String DUMP_TPL_COUNT = "mTrustedPresentationListenerCount"; 63 private static final String DUMP_CLUSTER_SURFACE = "mClusterActivitySurface"; 64 private static final String DUMP_CLUSTER_VISIBLE = "mClusterActivityVisible"; 65 private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2"; 66 private static final NavigationStateProto NAVIGATION_STATE_1 = 67 NavigationStateProto.newBuilder().setServiceStatus( 68 NavigationStateProto.ServiceStatus.NORMAL).build();; 69 private static final NavigationStateProto NAVIGATION_STATE_2 = 70 NavigationStateProto.newBuilder().setServiceStatus( 71 NavigationStateProto.ServiceStatus.REROUTING).build(); 72 private static final Bundle NAVIGATION_STATE_BUNDLE_1 = new Bundle(); 73 private static final Bundle NAVIGATION_STATE_BUNDLE_2 = new Bundle(); 74 75 static { NAVIGATION_STATE_BUNDLE_1.putByteArray( NAV_STATE_PROTO_BUNDLE_KEY, NAVIGATION_STATE_1.toByteArray())76 NAVIGATION_STATE_BUNDLE_1.putByteArray( 77 NAV_STATE_PROTO_BUNDLE_KEY, NAVIGATION_STATE_1.toByteArray()); NAVIGATION_STATE_BUNDLE_2.putByteArray( NAV_STATE_PROTO_BUNDLE_KEY, NAVIGATION_STATE_2.toByteArray())78 NAVIGATION_STATE_BUNDLE_2.putByteArray( 79 NAV_STATE_PROTO_BUNDLE_KEY, NAVIGATION_STATE_2.toByteArray()); 80 } 81 82 private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); 83 private final Context mContext = mInstrumentation.getContext(); 84 private final Context mTargetContext = mInstrumentation.getTargetContext(); 85 private final UiAutomation mUiAutomation = mInstrumentation.getUiAutomation(); 86 private final ComponentName mTestActivityName = 87 new ComponentName(mTargetContext, ClusterHomeManagerTest.TestActivity.class); 88 private ClusterHomeManager mClusterHomeManager; 89 private CarAppFocusManager mCarAppFocusManager; 90 private CarNavigationStatusManager mCarNavigationStatusManager; 91 private TestActivity mTestActivity; 92 private String mTestMonitoringSurface; 93 94 @Before setUp()95 public void setUp() { 96 mUiAutomation.adoptShellPermissionIdentity( 97 Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL, 98 Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE, 99 Car.PERMISSION_CAR_NAVIGATION_MANAGER); 100 101 Car car = Car.createCar(mContext); 102 mClusterHomeManager = car.getCarManager(ClusterHomeManager.class); 103 assumeTrue(mClusterHomeManager != null); 104 mCarAppFocusManager = car.getCarManager(CarAppFocusManager.class); 105 mCarNavigationStatusManager = car.getCarManager(CarNavigationStatusManager.class); 106 } 107 108 @After tearDown()109 public void tearDown() throws Exception { 110 // Destroy the test activity. 111 if (mTestActivity != null) { 112 mTestActivity.finishAndRemoveTask(); 113 mTestActivity.waitForDestroyed(); 114 mTestActivity = null; 115 } 116 if (mTestMonitoringSurface != null && !mTestMonitoringSurface.equals("null")) { 117 // Ensure that visibility monitoring has stopped. 118 PollingCheck.waitFor(TIMEOUT_MS, () -> { 119 String monitoringSurface = DumpUtils.executeDumpShellCommand(CLUSTER_HOME_SERVICE) 120 .get(DUMP_CLUSTER_SURFACE); 121 return !monitoringSurface.equals(mTestMonitoringSurface); 122 }); 123 } 124 125 mUiAutomation.dropShellPermissionIdentity(); 126 } 127 128 @Test 129 @RequiresFlagsEnabled(FLAG_CLUSTER_HEALTH_MONITORING) 130 @ApiTest(apis = {"android.car.cluster.ClusterHomeManager#startVisibilityMonitoring(Activity)"}) testStartVisibilityMonitoring()131 public void testStartVisibilityMonitoring() throws Exception { 132 // TODO(b/338221434) Explicitly skip the test until the RRO issue is resolved. 133 assumeFalse(mContext.getPackageManager().hasSystemFeature( 134 FEATURE_CAR_SPLITSCREEN_MULTITASKING)); 135 136 var oldDump = DumpUtils.executeDumpShellCommand(CLUSTER_HOME_SERVICE); 137 int oldCount = Integer.valueOf(oldDump.get(DUMP_TPL_COUNT)); 138 139 mTestActivity = (TestActivity) mInstrumentation.startActivitySync( 140 Intent.makeMainActivity(mTestActivityName) 141 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 142 143 // The callback will be called with 'true' as soon as the system bars disappear. 144 PollingCheck.waitFor(TIMEOUT_MS, () -> { 145 var dump = DumpUtils.executeDumpShellCommand(CLUSTER_HOME_SERVICE); 146 int count = Integer.valueOf(dump.get(DUMP_TPL_COUNT)); 147 boolean visible = Boolean.parseBoolean(dump.get(DUMP_CLUSTER_VISIBLE)); 148 return count > oldCount && visible; 149 }); 150 151 var oldDump2 = DumpUtils.executeDumpShellCommand(CLUSTER_HOME_SERVICE); 152 int oldCount2 = Integer.valueOf(oldDump2.get(DUMP_TPL_COUNT)); 153 mTestMonitoringSurface = oldDump2.get(DUMP_CLUSTER_SURFACE); 154 155 // Insets can be accessible only in the Activity's thread. 156 mTestActivity.getMainExecutor().execute(() -> { 157 WindowInsetsController insets = 158 mTestActivity.getWindow().getDecorView().getWindowInsetsController(); 159 insets.show(WindowInsets.Type.systemBars()); 160 } 161 ); 162 163 // The callback will be called with 'false' as soon as the system bars appear. 164 PollingCheck.waitFor(TIMEOUT_MS, () -> { 165 var dump = DumpUtils.executeDumpShellCommand(CLUSTER_HOME_SERVICE); 166 int count = Integer.valueOf(dump.get(DUMP_TPL_COUNT)); 167 boolean visible = Boolean.parseBoolean(dump.get(DUMP_CLUSTER_VISIBLE)); 168 return count > oldCount2 && !visible; 169 }); 170 } 171 172 @Test 173 @RequiresFlagsEnabled(FLAG_CLUSTER_HEALTH_MONITORING) 174 @ApiTest(apis = { 175 "android.car.cluster.ClusterHomeManager#registerClusterNavigationStateListener", 176 "android.car.cluster.ClusterHomeManager#unregisterClusterNavigationStateListener"}) testRegisterAndUnregisterClusterNavigationStateListener()177 public void testRegisterAndUnregisterClusterNavigationStateListener() { 178 TestNavigationStateListener listener1 = new TestNavigationStateListener(); 179 TestNavigationStateListener listener2 = new TestNavigationStateListener(); 180 mClusterHomeManager.registerClusterNavigationStateListener( 181 mContext.getMainExecutor(), listener1); 182 mClusterHomeManager.registerClusterNavigationStateListener( 183 mContext.getMainExecutor(), listener2); 184 TestAppFocusCallback focusCallback = new TestAppFocusCallback(); 185 mCarAppFocusManager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION, focusCallback); 186 focusCallback.waitForFocusGranted(); 187 188 // Send the 1st navigation state. 189 mCarNavigationStatusManager.sendNavigationStateChange(NAVIGATION_STATE_BUNDLE_1); 190 // Both listeners should receive the 1st navigation state change. 191 PollingCheck.waitFor(TIMEOUT_MS, 192 () -> (listener1.getNavigationState().equals(NAVIGATION_STATE_1) 193 && listener2.getNavigationState().equals(NAVIGATION_STATE_1))); 194 195 // Unregister listener1. 196 mClusterHomeManager.unregisterClusterNavigationStateListener(listener1); 197 // Send the 2nd navigation state. 198 mCarNavigationStatusManager.sendNavigationStateChange(NAVIGATION_STATE_BUNDLE_2); 199 200 // Only listener2 is expected to receive the new navigation state. 201 PollingCheck.waitFor(TIMEOUT_MS, 202 () -> (listener2.getNavigationState().equals(NAVIGATION_STATE_2))); 203 assertThat(listener1.getNavigationState()).isEqualTo(NAVIGATION_STATE_1); 204 } 205 206 private static class TestAppFocusCallback implements OnAppFocusOwnershipCallback { 207 private boolean mHasFocus = false; 208 @Override onAppFocusOwnershipGranted(int appType)209 public void onAppFocusOwnershipGranted(int appType) { 210 mHasFocus = true; 211 } 212 213 @Override onAppFocusOwnershipLost(int appType)214 public void onAppFocusOwnershipLost(int appType) { 215 mHasFocus = false; 216 } 217 waitForFocusGranted()218 public void waitForFocusGranted() { 219 PollingCheck.waitFor(TIMEOUT_MS, () -> mHasFocus); 220 } 221 } 222 223 private static class TestNavigationStateListener implements ClusterNavigationStateListener { 224 private NavigationStateProto mReceivedSate = NavigationStateProto.getDefaultInstance(); 225 226 getNavigationState()227 public NavigationStateProto getNavigationState() { 228 return mReceivedSate; 229 } 230 231 @Override onNavigationStateChanged(byte[] navigationState)232 public void onNavigationStateChanged(byte[] navigationState) { 233 try { 234 mReceivedSate = NavigationStateProto.parseFrom(navigationState); 235 } catch (Exception e) { 236 // This should never happen. 237 throw new AssertionError("Received an invalid byte stream ", e); 238 } 239 } 240 } 241 242 public static final class TestActivity extends Activity { 243 private final CountDownLatch mDestroyed = new CountDownLatch(1); 244 245 @Override onStart()246 protected void onStart() { 247 super.onStart(); 248 Car car = Car.createCar(this); 249 ClusterHomeManager clusterHomeManager = car.getCarManager(ClusterHomeManager.class); 250 clusterHomeManager.startVisibilityMonitoring(this); 251 252 // SystemBar also hides ActivitySurface, so hide the system bars. 253 WindowInsetsController insets = getWindow().getDecorView().getWindowInsetsController(); 254 insets.hide(WindowInsets.Type.systemBars()); 255 } 256 257 @Override onDestroy()258 protected void onDestroy() { 259 super.onDestroy(); 260 mDestroyed.countDown(); 261 } 262 waitForDestroyed()263 private boolean waitForDestroyed() throws InterruptedException { 264 return mDestroyed.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); 265 } 266 } 267 } 268