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