1 /*
2  * Copyright (C) 2021 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 
17 package com.android.cts.scheduling;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.fail;
22 
23 import android.Manifest;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.os.Handler;
29 import android.os.HandlerExecutor;
30 import android.os.HandlerThread;
31 import android.provider.DeviceConfig;
32 import android.scheduling.RebootReadinessManager;
33 import android.scheduling.RebootReadinessManager.RebootReadinessStatus;
34 import android.scheduling.RebootReadinessManager.RequestRebootReadinessStatusListener;
35 import android.util.Log;
36 
37 import androidx.test.InstrumentationRegistry;
38 
39 import org.junit.After;
40 import org.junit.AfterClass;
41 import org.junit.BeforeClass;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.junit.runners.JUnit4;
45 
46 import java.util.Objects;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 
50 /**
51  * Test system RebootReadinessManager APIs.
52  */
53 @RunWith(JUnit4.class)
54 public class RebootReadinessManagerTest {
55 
56     private static class RebootCallback implements RequestRebootReadinessStatusListener {
57         private final boolean mIsReadyToReboot;
58         private final long mEstimatedFinishTime;
59         private final String mSubsystemName;
60 
RebootCallback(boolean isReadyToReboot, long estimatedFinishTime, String subsystemName)61         RebootCallback(boolean isReadyToReboot, long estimatedFinishTime, String subsystemName) {
62             mIsReadyToReboot = isReadyToReboot;
63             mEstimatedFinishTime = estimatedFinishTime;
64             mSubsystemName = subsystemName;
65         }
66 
67         @Override
onRequestRebootReadinessStatus()68         public RebootReadinessStatus onRequestRebootReadinessStatus() {
69             return new RebootReadinessStatus(mIsReadyToReboot, mEstimatedFinishTime,
70                     mSubsystemName);
71         }
72     }
73 
74     /** Utility to ensure that DeviceConfig property is updated */
75     private static class ConfigListener implements DeviceConfig.OnPropertiesChangedListener {
76         private CountDownLatch mLatch;
77         private String mPropertyName;
78         private String mExpectedValue;
79 
ConfigListener(String propertyName, String expectedValue)80         ConfigListener(String propertyName, String expectedValue) {
81             mPropertyName = propertyName;
82             mLatch = new CountDownLatch(1);
83             mExpectedValue = expectedValue;
84         }
85 
awaitPropertyChange(int timeout, TimeUnit unit)86         public void awaitPropertyChange(int timeout, TimeUnit unit) throws InterruptedException {
87             Log.i(TAG, "Waiting for property " + mPropertyName);
88             if (!mLatch.await(timeout, unit)) {
89                 fail("Timed out waiting for properties to get updated");
90             }
91         }
92 
93         @Override
onPropertiesChanged(DeviceConfig.Properties properties)94         public void onPropertiesChanged(DeviceConfig.Properties properties) {
95             Log.d(TAG, "Properties changed: " + properties.getKeyset());
96             if (mLatch != null && properties.getKeyset().contains(mPropertyName)) {
97                 mLatch.countDown();
98             }
99             if (!Objects.equals(properties.getString(mPropertyName, null), mExpectedValue)) {
100                 fail("Property was not set to the expected value: " + mPropertyName + " != "
101                         + mExpectedValue);
102             }
103         }
104     }
105 
106     private static final String TAG = "RebootReadinessManagerTest";
107     private static final String TEST_CALLBACK_PREFIX = "TESTCOMPONENT";
108 
109     private static final RequestRebootReadinessStatusListener BLOCKING_CALLBACK =
110             new RebootCallback(false, 0, TEST_CALLBACK_PREFIX + ": blocking component");
111     private static final RequestRebootReadinessStatusListener READY_CALLBACK = new RebootCallback(
112             true, 0, TEST_CALLBACK_PREFIX + ": non-blocking component");
113 
114     private static final String PROPERTY_ACTIVE_POLLING_INTERVAL_MS = "active_polling_interval_ms";
115     private static final String PROPERTY_DISABLE_INTERACTIVITY_CHECK =
116             "disable_interactivity_check";
117     private static final String PROPERTY_INTERACTIVITY_THRESHOLD_MS = "interactivity_threshold_ms";
118     private static final String PROPERTY_DISABLE_APP_ACTIVITY_CHECK = "disable_app_activity_check";
119     private static final String PROPERTY_DISABLE_SUBSYSTEMS_CHECK = "disable_subsystems_check";
120     private static final int POLLING_INTERVAL_MS_VALUE = 500;
121 
122     RebootReadinessManager mRebootReadinessManager =
123             (RebootReadinessManager) InstrumentationRegistry.getContext().getSystemService(
124                     Context.REBOOT_READINESS_SERVICE);
125 
126     private static final HandlerThread sThread = new HandlerThread("RebootReadinessManagerTest");
127     private static HandlerExecutor sHandlerExecutor;
128     private static Handler sHandler;
129 
130     @BeforeClass
setupClass()131     public static void setupClass() throws Exception {
132         sThread.start();
133         sHandlerExecutor = new HandlerExecutor(sThread.getThreadHandler());
134         sHandler = new Handler(sThread.getLooper());
135         adoptShellPermissions();
136         setPropertyAndWait(PROPERTY_DISABLE_INTERACTIVITY_CHECK, "true");
137         setPropertyAndWait(PROPERTY_DISABLE_APP_ACTIVITY_CHECK, "true");
138         setPropertyAndWait(PROPERTY_DISABLE_SUBSYSTEMS_CHECK, "true");
139         setPropertyAndWait(PROPERTY_ACTIVE_POLLING_INTERVAL_MS,
140                 Integer.toString(POLLING_INTERVAL_MS_VALUE));
141 
142     }
143 
144     @After
tearDown()145     public void tearDown() {
146         mRebootReadinessManager.removeRequestRebootReadinessStatusListener(READY_CALLBACK);
147         mRebootReadinessManager.removeRequestRebootReadinessStatusListener(BLOCKING_CALLBACK);
148         mRebootReadinessManager.cancelPendingReboot();
149     }
150 
151     @AfterClass
teardownClass()152     public static void teardownClass() {
153         sThread.quitSafely();
154         dropShellPermissions();
155     }
156 
157     @Test
testRegisterAndUnregisterCallback()158     public void testRegisterAndUnregisterCallback() throws Exception {
159         assertThat(isReadyToReboot()).isTrue();
160         mRebootReadinessManager.cancelPendingReboot();
161 
162         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
163                 sHandlerExecutor, BLOCKING_CALLBACK);
164         assertThat(isReadyToReboot()).isFalse();
165         mRebootReadinessManager.removeRequestRebootReadinessStatusListener(BLOCKING_CALLBACK);
166         mRebootReadinessManager.cancelPendingReboot();
167         assertThat(isReadyToReboot()).isTrue();
168     }
169 
170     @Test
testCallbackReadyToReboot()171     public void testCallbackReadyToReboot() throws Exception {
172         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
173                 sHandlerExecutor, READY_CALLBACK);
174         CountDownLatch latch = new CountDownLatch(1);
175         final BroadcastReceiver receiver = new BroadcastReceiver() {
176             @Override
177             public void onReceive(Context context, Intent intent) {
178                 boolean extra = intent.getBooleanExtra(
179                         RebootReadinessManager.EXTRA_IS_READY_TO_REBOOT, false);
180                 assertThat(extra).isEqualTo(true);
181                 latch.countDown();
182             }
183         };
184         InstrumentationRegistry.getContext().registerReceiver(receiver,
185                 new IntentFilter(RebootReadinessManager.ACTION_REBOOT_READY));
186         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
187                 sHandlerExecutor, READY_CALLBACK);
188         assertThat(isReadyToReboot()).isTrue();
189         InstrumentationRegistry.getContext().unregisterReceiver(receiver);
190     }
191 
192     @Test
testCallbackNotReadyToReboot()193     public void testCallbackNotReadyToReboot() throws Exception {
194         assertThat(isReadyToReboot()).isTrue();
195         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
196                 sHandlerExecutor, READY_CALLBACK);
197         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
198                 sHandlerExecutor, BLOCKING_CALLBACK);
199         mRebootReadinessManager.cancelPendingReboot();
200         assertThat(isReadyToReboot()).isFalse();
201     }
202 
203     @Test
testRebootPermissionCheck()204     public void testRebootPermissionCheck() {
205         dropShellPermissions();
206         try {
207             mRebootReadinessManager.markRebootPending();
208             fail("Expected to throw SecurityException");
209         } catch (SecurityException expected) {
210         } finally {
211             adoptShellPermissions();
212         }
213     }
214 
215     @Test
testSignalRebootReadinessPermissionCheck()216     public void testSignalRebootReadinessPermissionCheck() {
217         dropShellPermissions();
218         try {
219             mRebootReadinessManager.addRequestRebootReadinessStatusListener(
220                     sHandlerExecutor, READY_CALLBACK);
221             fail("Expected to throw SecurityException");
222         } catch (SecurityException expected) {
223         } finally {
224             adoptShellPermissions();
225         }
226     }
227 
228 
229     @Test
testCancelPendingReboot()230     public void testCancelPendingReboot() throws Exception {
231         mRebootReadinessManager.addRequestRebootReadinessStatusListener(
232                 sHandlerExecutor, BLOCKING_CALLBACK);
233         mRebootReadinessManager.markRebootPending();
234         mRebootReadinessManager.cancelPendingReboot();
235         CountDownLatch latch = new CountDownLatch(1);
236         final BroadcastReceiver receiver = new BroadcastReceiver() {
237             @Override
238             public void onReceive(Context context, Intent intent) {
239                 fail("Reboot readiness checks should be cancelled so no broadcast should be sent.");
240             }
241         };
242         InstrumentationRegistry.getContext().registerReceiver(receiver,
243                 new IntentFilter(RebootReadinessManager.ACTION_REBOOT_READY));
244         mRebootReadinessManager.removeRequestRebootReadinessStatusListener(BLOCKING_CALLBACK);
245 
246         // Ensure that no broadcast is received when reboot readiness checks are canceled.
247         latch.await(10, TimeUnit.SECONDS);
248         assertThat(latch.getCount()).isEqualTo(1);
249         InstrumentationRegistry.getContext().unregisterReceiver(receiver);
250     }
251 
252     @Test
testCancelPendingRebootWhenNotRegistered()253     public void testCancelPendingRebootWhenNotRegistered() {
254         // Ensure that the process does not crash or throw an exception
255         mRebootReadinessManager.cancelPendingReboot();
256     }
257 
258     @Test
testDisableInteractivityCheck()259     public void testDisableInteractivityCheck() throws Exception {
260         setPropertyAndWait(PROPERTY_DISABLE_INTERACTIVITY_CHECK, "false");
261 
262         assertThat(isReadyToReboot()).isFalse();
263 
264         setPropertyAndWait(PROPERTY_DISABLE_INTERACTIVITY_CHECK, "true");
265 
266         assertThat(isReadyToReboot()).isTrue();
267     }
268 
269     @Test
testRebootReadinessStatus()270     public void testRebootReadinessStatus() {
271         RebootReadinessStatus status = new RebootReadinessStatus(false, 1000, "test");
272         assertThat(status.isReadyToReboot()).isFalse();
273         assertThat(status.getEstimatedFinishTime()).isEqualTo(1000);
274         assertThat(status.getLogSubsystemName()).isEqualTo("test");
275     }
276 
277     @Test
testRebootReadinessStatusWithEmptyNameThrowsException()278     public void testRebootReadinessStatusWithEmptyNameThrowsException() {
279         try {
280             RebootReadinessStatus status = new RebootReadinessStatus(false, 1000, "");
281             fail("Expected to throw exception when an empty name is supplied.");
282         } catch (IllegalArgumentException expected) {
283         }
284     }
285 
isReadyToReboot()286     private boolean isReadyToReboot() throws Exception {
287         mRebootReadinessManager.markRebootPending();
288         waitForPolling();
289         return mRebootReadinessManager.isReadyToReboot();
290     }
291 
adoptShellPermissions()292     private static void adoptShellPermissions() {
293         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
294                 Manifest.permission.REBOOT,
295                 Manifest.permission.WRITE_DEVICE_CONFIG, // permission required for T-
296                 Manifest.permission.READ_DEVICE_CONFIG,  // permission required for T-
297                 Manifest.permission.SIGNAL_REBOOT_READINESS,
298                 Manifest.permission.INTERACT_ACROSS_USERS_FULL);
299     }
300 
dropShellPermissions()301     private static void dropShellPermissions() {
302         InstrumentationRegistry
303                 .getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
304     }
305 
setPropertyAndWait(String property, String value)306     private static void setPropertyAndWait(String property, String value)
307             throws InterruptedException {
308         // Since the OnPropertiesChangedListener only detects a change in property, first check if
309         // property is already the desired value.
310         if (DeviceConfig.getString(DeviceConfig.NAMESPACE_REBOOT_READINESS,
311                 property,  /* defaultValue= */ "").equals(value)) {
312             return;
313         }
314 
315         ConfigListener configListener = new ConfigListener(property, value);
316         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_REBOOT_READINESS,
317                 sHandlerExecutor, configListener);
318         try {
319             DeviceConfig.setProperty(DeviceConfig.NAMESPACE_REBOOT_READINESS, property, value,
320                     false);
321             configListener.awaitPropertyChange(10, TimeUnit.SECONDS);
322         } finally {
323             DeviceConfig.removeOnPropertiesChangedListener(configListener);
324         }
325     }
326 
waitForPolling()327     private static void waitForPolling() throws InterruptedException {
328         // TODO(b/333555726): Attempt to fully synchronize execution of polling and
329         //  latch::countDown by running them both on the same thread.
330         // Currently, we synchronize latch:countdown with RebootReadinessStatusListeners.
331         CountDownLatch latch = new CountDownLatch(1);
332         // wait 500 ms longer than polling interval.
333         sHandler.postDelayed(latch::countDown, POLLING_INTERVAL_MS_VALUE + 500);
334 
335         if (!latch.await(POLLING_INTERVAL_MS_VALUE + 2000, TimeUnit.MILLISECONDS)) {
336             fail("Timed out waiting for main executor to finish");
337         }
338     }
339 }
340