1 /* 2 * Copyright (C) 2017 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 com.android.timezone.xts; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.tradefed.build.IBuildInfo; 20 import com.android.tradefed.config.Option; 21 import com.android.tradefed.device.ITestDevice; 22 import com.android.tradefed.log.LogUtil; 23 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 24 import com.android.tradefed.testtype.IBuildReceiver; 25 import com.android.tradefed.testtype.IDeviceTest; 26 import com.android.tradefed.util.FileUtil; 27 28 import org.junit.After; 29 import org.junit.Before; 30 import org.junit.Test; 31 import org.junit.runner.RunWith; 32 33 import java.io.File; 34 import java.util.function.BooleanSupplier; 35 36 import static org.junit.Assert.assertEquals; 37 import static org.junit.Assert.assertFalse; 38 import static org.junit.Assert.assertNotNull; 39 import static org.junit.Assert.assertTrue; 40 41 /** 42 * Class for host-side tests that the time zone rules update feature works as intended. This is 43 * intended to give confidence to OEMs that they have implemented / configured the OEM parts of the 44 * feature correctly. 45 * 46 * <p>There are two main operations involved in time zone updates: 47 * <ol> 48 * <li>Package installs/uninstalls - asynchronously stage operations for install</li> 49 * <li>Reboots - perform the staged operations / delete bad installed data</li> 50 * </ol> 51 * Both these operations are time consuming and there's a degree of non-determinism involved. 52 * 53 * <p>A "clean" device can also be in one of two main states depending on whether it has been wiped 54 * and/or rebooted before this test runs: 55 * <ul> 56 * <li>A device may have nothing staged / installed in /data/misc/zoneinfo at all.</li> 57 * <li>A device may have the time zone data from the default system image version of the time 58 * zone data app staged or installed.</li> 59 * </ul> 60 * This test attempts to handle both of these cases. 61 * 62 */ 63 @RunWith(DeviceJUnit4ClassRunner.class) 64 public class TimeZoneUpdateHostTest implements IDeviceTest, IBuildReceiver { 65 66 // These must match equivalent values in RulesManagerService dumpsys code. 67 private static final String STAGED_OPERATION_NONE = "None"; 68 private static final String STAGED_OPERATION_INSTALL = "Install"; 69 private static final String STAGED_OPERATION_UNINSTALL = "Uninstall"; 70 private static final String INSTALL_STATE_INSTALLED = "Installed"; 71 72 private IBuildInfo mBuildInfo; 73 private ITestDevice mDevice; 74 private File mTempDir; 75 76 @Option(name = "oem-data-app-package-name", 77 description="The OEM-specific package name for the data app", 78 mandatory = true) 79 private String mOemDataAppPackageName; 80 getTimeZoneDataPackageName()81 private String getTimeZoneDataPackageName() { 82 assertNotNull(mOemDataAppPackageName); 83 return mOemDataAppPackageName; 84 } 85 86 @Option(name = "oem-data-app-apk-prefix", 87 description="The OEM-specific APK name for the data app test files, e.g." 88 + "for TimeZoneDataOemCorp_test1.apk the prefix would be" 89 + "\"TimeZoneDataOemCorp\"", 90 mandatory = true) 91 private String mOemDataAppApkPrefix; 92 getTimeZoneDataApkName(String testId)93 private String getTimeZoneDataApkName(String testId) { 94 assertNotNull(mOemDataAppApkPrefix); 95 return mOemDataAppApkPrefix + "_" + testId + ".apk"; 96 } 97 98 @Override setBuild(IBuildInfo buildInfo)99 public void setBuild(IBuildInfo buildInfo) { 100 mBuildInfo = buildInfo; 101 } 102 103 @Override setDevice(ITestDevice device)104 public void setDevice(ITestDevice device) { 105 mDevice = device; 106 } 107 108 @Override getDevice()109 public ITestDevice getDevice() { 110 return mDevice; 111 } 112 113 @Before setUp()114 public void setUp() throws Exception { 115 createTempDir(); 116 resetDeviceToClean(); 117 } 118 119 @After tearDown()120 public void tearDown() throws Exception { 121 resetDeviceToClean(); 122 deleteTempDir(); 123 } 124 createTempDir()125 private void createTempDir() throws Exception { 126 mTempDir = File.createTempFile("timeZoneUpdateTest", null); 127 assertTrue(mTempDir.delete()); 128 assertTrue(mTempDir.mkdir()); 129 } 130 deleteTempDir()131 private void deleteTempDir() throws Exception { 132 FileUtil.recursiveDelete(mTempDir); 133 } 134 135 /** 136 * Reset the device to having no installed time zone data outside of the /system/priv-app 137 * version that came with the system image. 138 */ resetDeviceToClean()139 private void resetDeviceToClean() throws Exception { 140 // If this fails the data app isn't present on device. No point in starting. 141 assertTrue(getTimeZoneDataPackageName() + " not installed", 142 isPackageInstalled(getTimeZoneDataPackageName())); 143 144 // Reboot as needed to apply any staged operation. 145 if (!STAGED_OPERATION_NONE.equals(getStagedOperationType())) { 146 rebootDeviceAndWaitForRestart(); 147 } 148 149 // A "clean" device means no time zone data .apk installed in /data at all, try to get to 150 // that state. 151 for (int i = 0; i < 2; i++) { 152 logDeviceTimeZoneState(); 153 154 // Even if there's no distro installed, there may be an updated APK installed, so try to 155 // remove it unconditionally. 156 String errorCode = uninstallPackage(getTimeZoneDataPackageName()); 157 if (errorCode != null) { 158 // Failed to uninstall, which we take to mean the device is "clean". 159 break; 160 } 161 // Success, meaning there was an APK that could be uninstalled. 162 // If there is a distro installed we need wait for the distro uninstall that should now 163 // become staged. 164 boolean distroIsInstalled = INSTALL_STATE_INSTALLED.equals(getCurrentInstallState()); 165 if (distroIsInstalled) { 166 // It may take a short while before we can detect anything: the package manager 167 // should have triggered an intent, and the PackageTracker has to receive that and 168 // send its own intent, which then has to be acted on before we could detect an 169 // operation in progress. We expect the device eventually to get to the staged state 170 // "UNINSTALL", meaning it will try to revert to no distro installed on next boot. 171 waitForStagedUninstall(); 172 173 rebootDeviceAndWaitForRestart(); 174 } else { 175 // There was an apk installed, but no time zone distro was installed. It was 176 // probably a "bad" .apk that was rejected. The update app will request an uninstall 177 // anyway just to be sure, so we'll give it a chance to do that before continuing 178 // otherwise we could get an "operation in progress" later on when we're not 179 // expecting it. 180 Thread.sleep(10000); 181 } 182 } 183 assertActiveRulesVersion(getBaseRulesVersion()); 184 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 185 } 186 187 @Test testInstallNewerRulesVersion()188 public void testInstallNewerRulesVersion() throws Exception { 189 // This information must match the rules version in test1: IANA version=2030a, revision=1 190 String test1VersionInfo = "2030a,1"; 191 192 // Confirm the staged / install state before we start. 193 assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion())); 194 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 195 196 File appFile = getTimeZoneDataApkFile("test1"); 197 getDevice().installPackage(appFile, true /* reinstall */); 198 199 waitForStagedInstall(test1VersionInfo); 200 201 // Confirm the install state hasn't changed. 202 assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion())); 203 204 // Now reboot, and the staged version should become the installed version. 205 rebootDeviceAndWaitForRestart(); 206 207 // After reboot, check the state. 208 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 209 assertEquals(INSTALL_STATE_INSTALLED, getCurrentInstallState()); 210 assertEquals(test1VersionInfo, getCurrentInstalledVersion()); 211 } 212 213 @Test testInstallNewerRulesVersion_secondaryUser()214 public void testInstallNewerRulesVersion_secondaryUser() throws Exception { 215 ITestDevice device = getDevice(); 216 if (!device.isMultiUserSupported()) { 217 // Just pass on non-multi-user devices. 218 return; 219 } 220 221 int userId = device.createUser("TimeZoneTest", false /* guest */, false /* ephemeral */); 222 try { 223 224 // This information must match the rules version in test1: IANA version=2030a, revision=1 225 String test1VersionInfo = "2030a,1"; 226 227 // Confirm the staged / install state before we start. 228 assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion())); 229 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 230 231 File appFile = getTimeZoneDataApkFile("test1"); 232 233 // Install the app for the test user. It should still all work. 234 device.installPackageForUser(appFile, true /* reinstall */, userId); 235 236 waitForStagedInstall(test1VersionInfo); 237 238 // Confirm the install state hasn't changed. 239 assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion())); 240 241 // Now reboot, and the staged version should become the installed version. 242 rebootDeviceAndWaitForRestart(); 243 244 // After reboot, check the state. 245 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 246 assertEquals(INSTALL_STATE_INSTALLED, getCurrentInstallState()); 247 assertEquals(test1VersionInfo, getCurrentInstalledVersion()); 248 } 249 finally { 250 // If this fails, the device may be left in a bad state. 251 device.removeUser(userId); 252 } 253 } 254 255 @Test testInstallOlderRulesVersion()256 public void testInstallOlderRulesVersion() throws Exception { 257 File appFile = getTimeZoneDataApkFile("test2"); 258 getDevice().installPackage(appFile, true /* reinstall */); 259 260 // The attempt to install a version of the data that is older than the version in the system 261 // image should be rejected and nothing should be staged. There's currently no way (short of 262 // looking at logs) to tell this has happened, but combined with other tests and given a 263 // suitable delay it gives us some confidence that the attempt has been made and it was 264 // rejected. 265 266 Thread.sleep(30000); 267 268 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType()); 269 } 270 rebootDeviceAndWaitForRestart()271 private void rebootDeviceAndWaitForRestart() throws Exception { 272 log("Rebooting device"); 273 getDevice().reboot(); 274 } 275 logDeviceTimeZoneState()276 private void logDeviceTimeZoneState() throws Exception { 277 log("Initial device state: " + dumpEntireTimeZoneStatusToString()); 278 } 279 log(String msg)280 private static void log(String msg) { 281 LogUtil.CLog.i(msg); 282 } 283 assertActiveRulesVersion(String expectedRulesVersion)284 private void assertActiveRulesVersion(String expectedRulesVersion) throws Exception { 285 // Dumpsys reports the version reported by ICU, ZoneInfoDb and TimeZoneFinder and they 286 // should always match. 287 String expectedActiveRulesVersion = 288 expectedRulesVersion + "," + expectedRulesVersion + "," + expectedRulesVersion; 289 290 String actualActiveRulesVersion = 291 waitForNoOperationInProgressAndReturn(StateType.ACTIVE_RULES_VERSION); 292 assertEquals(expectedActiveRulesVersion, actualActiveRulesVersion); 293 } 294 getCurrentInstalledVersion()295 private String getCurrentInstalledVersion() throws Exception { 296 return waitForNoOperationInProgressAndReturn(StateType.CURRENTLY_INSTALLED_VERSION); 297 } 298 getCurrentInstallState()299 private String getCurrentInstallState() throws Exception { 300 return waitForNoOperationInProgressAndReturn(StateType.CURRENT_INSTALL_STATE); 301 } 302 getStagedInstallVersion()303 private String getStagedInstallVersion() throws Exception { 304 return waitForNoOperationInProgressAndReturn(StateType.STAGED_INSTALL_VERSION); 305 } 306 getStagedOperationType()307 private String getStagedOperationType() throws Exception { 308 return waitForNoOperationInProgressAndReturn(StateType.STAGED_OPERATION_TYPE); 309 } 310 getBaseRulesVersion()311 private String getBaseRulesVersion() throws Exception { 312 return waitForNoOperationInProgressAndReturn(StateType.BASE_RULES_VERSION); 313 } 314 isOperationInProgress()315 private boolean isOperationInProgress() { 316 try { 317 String operationInProgressString = 318 getDeviceTimeZoneState(StateType.OPERATION_IN_PROGRESS); 319 return Boolean.parseBoolean(operationInProgressString); 320 } catch (Exception e) { 321 throw new AssertionError("Failed to read staged status", e); 322 } 323 } 324 waitForNoOperationInProgressAndReturn(StateType stateType)325 private String waitForNoOperationInProgressAndReturn(StateType stateType) throws Exception { 326 waitForCondition(() -> !isOperationInProgress()); 327 return getDeviceTimeZoneState(stateType); 328 } 329 waitForStagedUninstall()330 private void waitForStagedUninstall() throws Exception { 331 waitForCondition(() -> isStagedUninstall()); 332 } 333 waitForStagedInstall(String versionString)334 private void waitForStagedInstall(String versionString) throws Exception { 335 waitForCondition(() -> isStagedInstall(versionString)); 336 } 337 isStagedUninstall()338 private boolean isStagedUninstall() { 339 try { 340 return getStagedOperationType().equals(STAGED_OPERATION_UNINSTALL); 341 } catch (Exception e) { 342 throw new AssertionError("Failed to read staged status", e); 343 } 344 } 345 isStagedInstall(String versionString)346 private boolean isStagedInstall(String versionString) { 347 try { 348 return getStagedOperationType().equals(STAGED_OPERATION_INSTALL) 349 && getStagedInstallVersion().equals(versionString); 350 } catch (Exception e) { 351 throw new AssertionError("Failed to read staged status", e); 352 } 353 } 354 waitForCondition(BooleanSupplier condition)355 private static void waitForCondition(BooleanSupplier condition) throws Exception { 356 int count = 0; 357 boolean lastResult; 358 while (!(lastResult = condition.getAsBoolean()) && count++ < 120) { 359 Thread.sleep(1000); 360 } 361 // Some conditions may not be stable so using the lastResult instead of 362 // condition.getAsBoolean() ensures we understand why we exited the loop. 363 assertTrue("Failed condition: " + condition, lastResult); 364 } 365 366 private enum StateType { 367 OPERATION_IN_PROGRESS, 368 BASE_RULES_VERSION, 369 CURRENT_INSTALL_STATE, 370 CURRENTLY_INSTALLED_VERSION, 371 STAGED_OPERATION_TYPE, 372 STAGED_INSTALL_VERSION, 373 ACTIVE_RULES_VERSION; 374 getFormatStateChar()375 public String getFormatStateChar() { 376 // This switch must match values in com.android.server.timezone.RulesManagerService. 377 switch (this) { 378 case OPERATION_IN_PROGRESS: 379 return "p"; 380 case BASE_RULES_VERSION: 381 return "b"; 382 case CURRENT_INSTALL_STATE: 383 return "c"; 384 case CURRENTLY_INSTALLED_VERSION: 385 return "i"; 386 case STAGED_OPERATION_TYPE: 387 return "o"; 388 case STAGED_INSTALL_VERSION: 389 return "t"; 390 case ACTIVE_RULES_VERSION: 391 return "a"; 392 default: 393 throw new AssertionError("Unknown state type: " + this); 394 } 395 } 396 } 397 getDeviceTimeZoneState(StateType stateType)398 private String getDeviceTimeZoneState(StateType stateType) throws Exception { 399 String output = getDevice().executeShellCommand( 400 "dumpsys timezone -format_state " + stateType.getFormatStateChar()); 401 assertNotNull(output); 402 // Output will be "Foo: bar\n". We want the "bar". 403 String value = output.split(":")[1]; 404 return value.substring(1, value.length() - 1); 405 } 406 dumpEntireTimeZoneStatusToString()407 private String dumpEntireTimeZoneStatusToString() throws Exception { 408 String output = getDevice().executeShellCommand("dumpsys timezone"); 409 assertNotNull(output); 410 return output; 411 } 412 getTimeZoneDataApkFile(String testId)413 private File getTimeZoneDataApkFile(String testId) throws Exception { 414 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuildInfo); 415 String fileName = getTimeZoneDataApkName(testId); 416 return buildHelper.getTestFile(fileName); 417 } 418 isPackageInstalled(String pkg)419 private boolean isPackageInstalled(String pkg) throws Exception { 420 for (String installedPackage : getDevice().getInstalledPackageNames()) { 421 if (pkg.equals(installedPackage)) { 422 return true; 423 } 424 } 425 return false; 426 } 427 uninstallPackage(String packageName)428 private String uninstallPackage(String packageName) throws Exception { 429 return getDevice().uninstallPackage(packageName); 430 } 431 } 432