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