1 /* 2 * Copyright (C) 2019 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.apkverity; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assert.fail; 24 25 import android.platform.test.annotations.RootPermissionTest; 26 27 import com.android.blockdevicewriter.BlockDeviceWriter; 28 import com.android.fsverity.AddFsVerityCertRule; 29 import com.android.tradefed.device.DeviceNotAvailableException; 30 import com.android.tradefed.device.ITestDevice; 31 import com.android.tradefed.log.LogUtil.CLog; 32 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 33 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 34 import com.android.tradefed.util.CommandResult; 35 import com.android.tradefed.util.CommandStatus; 36 37 import org.junit.After; 38 import org.junit.Before; 39 import org.junit.Rule; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 import java.io.FileNotFoundException; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.HashSet; 47 48 /** 49 * This test makes sure app installs with fs-verity signature, and on-access verification works. 50 * 51 * <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig 52 * signature file. Otherwise, install will fail. 53 * 54 * <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded 55 * from disk to memory. The file is immutable by design, enforced by filesystem. 56 * 57 * <p>In order to make sure a block of the file is readable only if the underlying block on disk 58 * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical 59 * address against the block device. 60 * 61 * <p>Requirements to run this test: 62 * <ul> 63 * <li>Device is rootable</li> 64 * <li>The filesystem supports fs-verity</li> 65 * <li>The feature flag is enabled</li> 66 * </ul> 67 */ 68 @RootPermissionTest 69 @RunWith(DeviceJUnit4ClassRunner.class) 70 public class ApkVerityTest extends BaseHostJUnit4Test { 71 private static final String TARGET_PACKAGE = "com.android.apkverity"; 72 73 private static final String BASE_APK = "ApkVerityTestApp.apk"; 74 private static final String BASE_APK_DM = "ApkVerityTestApp.dm"; 75 private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk"; 76 private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm"; 77 78 private static final String INSTALLED_BASE_APK = "base.apk"; 79 private static final String INSTALLED_BASE_DM = "base.dm"; 80 private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk"; 81 private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm"; 82 private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig"; 83 private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig"; 84 private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig"; 85 private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig"; 86 87 private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer"; 88 private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der"; 89 90 /** Only 4K page is supported by fs-verity currently. */ 91 private static final int FSVERITY_PAGE_SIZE = 4096; 92 93 @Rule 94 public final AddFsVerityCertRule mAddFsVerityCertRule = 95 new AddFsVerityCertRule(this, CERT_PATH); 96 97 private ITestDevice mDevice; 98 private boolean mDmRequireFsVerity; 99 100 @Before setUp()101 public void setUp() throws DeviceNotAvailableException { 102 mDevice = getDevice(); 103 mDmRequireFsVerity = "true".equals( 104 mDevice.getProperty("pm.dexopt.dm.require_fsverity")); 105 106 uninstallPackage(TARGET_PACKAGE); 107 } 108 109 @After tearDown()110 public void tearDown() throws DeviceNotAvailableException { 111 uninstallPackage(TARGET_PACKAGE); 112 } 113 114 @Test testFsverityKernelSupports()115 public void testFsverityKernelSupports() throws DeviceNotAvailableException { 116 ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); 117 expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity"); 118 } 119 120 @Test testInstallBase()121 public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException { 122 new InstallMultiple() 123 .addFileAndSignature(BASE_APK) 124 .run(); 125 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 126 127 verifyInstalledFiles( 128 INSTALLED_BASE_APK, 129 INSTALLED_BASE_APK_FSV_SIG); 130 verifyInstalledFilesHaveFsverity(INSTALLED_BASE_APK); 131 } 132 133 @Test testInstallBaseWithWrongSignature()134 public void testInstallBaseWithWrongSignature() 135 throws DeviceNotAvailableException, FileNotFoundException { 136 new InstallMultiple() 137 .addFile(BASE_APK) 138 .addFile(SPLIT_APK_DM + ".fsv_sig", 139 BASE_APK + ".fsv_sig") 140 .runExpectingFailure(); 141 } 142 143 @Test testInstallBaseWithSplit()144 public void testInstallBaseWithSplit() 145 throws DeviceNotAvailableException, FileNotFoundException { 146 new InstallMultiple() 147 .addFileAndSignature(BASE_APK) 148 .addFileAndSignature(SPLIT_APK) 149 .run(); 150 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 151 152 verifyInstalledFiles( 153 INSTALLED_BASE_APK, 154 INSTALLED_BASE_APK_FSV_SIG, 155 INSTALLED_SPLIT_APK, 156 INSTALLED_SPLIT_APK_FSV_SIG); 157 verifyInstalledFilesHaveFsverity( 158 INSTALLED_BASE_APK, 159 INSTALLED_SPLIT_APK); 160 } 161 162 @Test testInstallBaseWithDm()163 public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException { 164 new InstallMultiple() 165 .addFileAndSignature(BASE_APK) 166 .addFileAndSignature(BASE_APK_DM) 167 .run(); 168 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 169 170 verifyInstalledFiles( 171 INSTALLED_BASE_APK, 172 INSTALLED_BASE_APK_FSV_SIG, 173 INSTALLED_BASE_DM, 174 INSTALLED_BASE_DM_FSV_SIG); 175 verifyInstalledFilesHaveFsverity( 176 INSTALLED_BASE_APK, 177 INSTALLED_BASE_DM); 178 } 179 180 @Test testInstallEverything()181 public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException { 182 new InstallMultiple() 183 .addFileAndSignature(BASE_APK) 184 .addFileAndSignature(BASE_APK_DM) 185 .addFileAndSignature(SPLIT_APK) 186 .addFileAndSignature(SPLIT_APK_DM) 187 .run(); 188 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 189 190 verifyInstalledFiles( 191 INSTALLED_BASE_APK, 192 INSTALLED_BASE_APK_FSV_SIG, 193 INSTALLED_BASE_DM, 194 INSTALLED_BASE_DM_FSV_SIG, 195 INSTALLED_SPLIT_APK, 196 INSTALLED_SPLIT_APK_FSV_SIG, 197 INSTALLED_SPLIT_DM, 198 INSTALLED_SPLIT_DM_FSV_SIG); 199 verifyInstalledFilesHaveFsverity( 200 INSTALLED_BASE_APK, 201 INSTALLED_BASE_DM, 202 INSTALLED_SPLIT_APK, 203 INSTALLED_SPLIT_DM); 204 } 205 206 @Test testInstallSplitOnly()207 public void testInstallSplitOnly() 208 throws DeviceNotAvailableException, FileNotFoundException { 209 new InstallMultiple() 210 .addFileAndSignature(BASE_APK) 211 .run(); 212 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 213 verifyInstalledFiles( 214 INSTALLED_BASE_APK, 215 INSTALLED_BASE_APK_FSV_SIG); 216 217 new InstallMultiple() 218 .inheritFrom(TARGET_PACKAGE) 219 .addFileAndSignature(SPLIT_APK) 220 .run(); 221 222 verifyInstalledFiles( 223 INSTALLED_BASE_APK, 224 INSTALLED_BASE_APK_FSV_SIG, 225 INSTALLED_SPLIT_APK, 226 INSTALLED_SPLIT_APK_FSV_SIG); 227 verifyInstalledFilesHaveFsverity( 228 INSTALLED_BASE_APK, 229 INSTALLED_SPLIT_APK); 230 } 231 232 @Test testInstallSplitOnlyMissingSignature()233 public void testInstallSplitOnlyMissingSignature() 234 throws DeviceNotAvailableException, FileNotFoundException { 235 new InstallMultiple() 236 .addFileAndSignature(BASE_APK) 237 .run(); 238 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 239 verifyInstalledFiles( 240 INSTALLED_BASE_APK, 241 INSTALLED_BASE_APK_FSV_SIG); 242 243 new InstallMultiple() 244 .inheritFrom(TARGET_PACKAGE) 245 .addFile(SPLIT_APK) 246 .runExpectingFailure(); 247 } 248 249 @Test testInstallSplitOnlyWithoutBaseSignature()250 public void testInstallSplitOnlyWithoutBaseSignature() 251 throws DeviceNotAvailableException, FileNotFoundException { 252 new InstallMultiple() 253 .addFile(BASE_APK) 254 .run(); 255 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 256 verifyInstalledFiles(INSTALLED_BASE_APK); 257 258 new InstallMultiple() 259 .inheritFrom(TARGET_PACKAGE) 260 .addFileAndSignature(SPLIT_APK) 261 .run(); 262 verifyInstalledFiles( 263 INSTALLED_BASE_APK, 264 INSTALLED_SPLIT_APK, 265 INSTALLED_SPLIT_APK_FSV_SIG); 266 } 267 268 @Test testInstallOnlyDmHasFsvSig()269 public void testInstallOnlyDmHasFsvSig() 270 throws DeviceNotAvailableException, FileNotFoundException { 271 new InstallMultiple() 272 .addFile(BASE_APK) 273 .addFileAndSignature(BASE_APK_DM) 274 .addFile(SPLIT_APK) 275 .addFileAndSignature(SPLIT_APK_DM) 276 .run(); 277 verifyInstalledFiles( 278 INSTALLED_BASE_APK, 279 INSTALLED_BASE_DM, 280 INSTALLED_BASE_DM_FSV_SIG, 281 INSTALLED_SPLIT_APK, 282 INSTALLED_SPLIT_DM, 283 INSTALLED_SPLIT_DM_FSV_SIG); 284 verifyInstalledFilesHaveFsverity( 285 INSTALLED_BASE_DM, 286 INSTALLED_SPLIT_DM); 287 } 288 289 @Test testInstallDmWithoutFsvSig_Base()290 public void testInstallDmWithoutFsvSig_Base() 291 throws DeviceNotAvailableException, FileNotFoundException { 292 InstallMultiple installer = new InstallMultiple() 293 .addFile(BASE_APK) 294 .addFile(BASE_APK_DM) 295 .addFile(SPLIT_APK) 296 .addFileAndSignature(SPLIT_APK_DM); 297 if (mDmRequireFsVerity) { 298 installer.runExpectingFailure(); 299 } else { 300 installer.run(); 301 verifyInstalledFiles( 302 INSTALLED_BASE_APK, 303 INSTALLED_BASE_DM, 304 INSTALLED_SPLIT_APK, 305 INSTALLED_SPLIT_DM, 306 INSTALLED_SPLIT_DM_FSV_SIG); 307 verifyInstalledFilesHaveFsverity(INSTALLED_SPLIT_DM); 308 } 309 } 310 311 @Test testInstallDmWithoutFsvSig_Split()312 public void testInstallDmWithoutFsvSig_Split() 313 throws DeviceNotAvailableException, FileNotFoundException { 314 InstallMultiple installer = new InstallMultiple() 315 .addFile(BASE_APK) 316 .addFileAndSignature(BASE_APK_DM) 317 .addFile(SPLIT_APK) 318 .addFile(SPLIT_APK_DM); 319 if (mDmRequireFsVerity) { 320 installer.runExpectingFailure(); 321 } else { 322 installer.run(); 323 verifyInstalledFiles( 324 INSTALLED_BASE_APK, 325 INSTALLED_BASE_DM, 326 INSTALLED_BASE_DM_FSV_SIG, 327 INSTALLED_SPLIT_APK, 328 INSTALLED_SPLIT_DM); 329 verifyInstalledFilesHaveFsverity(INSTALLED_BASE_DM); 330 } 331 } 332 333 @Test testInstallSomeApkIsMissingFsvSig_Base()334 public void testInstallSomeApkIsMissingFsvSig_Base() 335 throws DeviceNotAvailableException, FileNotFoundException { 336 new InstallMultiple() 337 .addFileAndSignature(BASE_APK) 338 .addFileAndSignature(BASE_APK_DM) 339 .addFile(SPLIT_APK) 340 .addFileAndSignature(SPLIT_APK_DM) 341 .runExpectingFailure(); 342 } 343 344 @Test testInstallSomeApkIsMissingFsvSig_Split()345 public void testInstallSomeApkIsMissingFsvSig_Split() 346 throws DeviceNotAvailableException, FileNotFoundException { 347 new InstallMultiple() 348 .addFile(BASE_APK) 349 .addFileAndSignature(BASE_APK_DM) 350 .addFileAndSignature(SPLIT_APK) 351 .addFileAndSignature(SPLIT_APK_DM) 352 .runExpectingFailure(); 353 } 354 355 @Test testInstallBaseWithFsvSigThenSplitWithout()356 public void testInstallBaseWithFsvSigThenSplitWithout() 357 throws DeviceNotAvailableException, FileNotFoundException { 358 new InstallMultiple() 359 .addFileAndSignature(BASE_APK) 360 .run(); 361 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 362 verifyInstalledFiles( 363 INSTALLED_BASE_APK, 364 INSTALLED_BASE_APK_FSV_SIG); 365 366 new InstallMultiple() 367 .addFile(SPLIT_APK) 368 .runExpectingFailure(); 369 } 370 371 @Test testInstallBaseWithoutFsvSigThenSplitWith()372 public void testInstallBaseWithoutFsvSigThenSplitWith() 373 throws DeviceNotAvailableException, FileNotFoundException { 374 new InstallMultiple() 375 .addFile(BASE_APK) 376 .run(); 377 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 378 verifyInstalledFiles(INSTALLED_BASE_APK); 379 380 new InstallMultiple() 381 .addFileAndSignature(SPLIT_APK) 382 .runExpectingFailure(); 383 } 384 385 @Test testFsverityFileIsImmutableAndReadable()386 public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException { 387 new InstallMultiple().addFileAndSignature(BASE_APK).run(); 388 String apkPath = getApkPath(TARGET_PACKAGE); 389 390 assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); 391 expectRemoteCommandToFail("echo -n '' >> " + apkPath); 392 expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null"); 393 } 394 395 @Test testFsverityFailToReadModifiedBlockAtFront()396 public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException { 397 new InstallMultiple().addFileAndSignature(BASE_APK).run(); 398 String apkPath = getApkPath(TARGET_PACKAGE); 399 400 long apkSize = getFileSizeInBytes(apkPath); 401 long offsetFirstByte = 0; 402 403 // The first two pages should be both readable at first. 404 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); 405 if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { 406 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, 407 offsetFirstByte + FSVERITY_PAGE_SIZE)); 408 } 409 410 // Damage the file directly against the block device. 411 damageFileAgainstBlockDevice(apkPath, offsetFirstByte); 412 413 // Expect actual read from disk to fail but only at damaged page. 414 BlockDeviceWriter.dropCaches(mDevice); 415 assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); 416 if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { 417 long lastByteOfTheSamePage = 418 offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1; 419 assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage)); 420 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage + 1)); 421 } 422 } 423 424 @Test testFsverityFailToReadModifiedBlockAtBack()425 public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException { 426 new InstallMultiple().addFileAndSignature(BASE_APK).run(); 427 String apkPath = getApkPath(TARGET_PACKAGE); 428 429 long apkSize = getFileSizeInBytes(apkPath); 430 long offsetOfLastByte = apkSize - 1; 431 432 // The first two pages should be both readable at first. 433 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); 434 if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { 435 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, 436 offsetOfLastByte - FSVERITY_PAGE_SIZE)); 437 } 438 439 // Damage the file directly against the block device. 440 damageFileAgainstBlockDevice(apkPath, offsetOfLastByte); 441 442 // Expect actual read from disk to fail but only at damaged page. 443 BlockDeviceWriter.dropCaches(mDevice); 444 assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); 445 if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { 446 long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE; 447 assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage)); 448 assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage - 1)); 449 } 450 } 451 verifyInstalledFilesHaveFsverity(String... filenames)452 private void verifyInstalledFilesHaveFsverity(String... filenames) 453 throws DeviceNotAvailableException { 454 // Verify that all files are protected by fs-verity 455 String apkPath = getApkPath(TARGET_PACKAGE); 456 String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); 457 long kTargetOffset = 0; 458 for (String basename : filenames) { 459 String path = appDir + "/" + basename; 460 damageFileAgainstBlockDevice(path, kTargetOffset); 461 462 // Retry is sometimes needed to pass the test. Package manager may have FD leaks 463 // (see b/122744005 as example) that prevents the file in question to be evicted 464 // from filesystem cache. Forcing GC workarounds the problem. 465 int retry = 5; 466 for (; retry > 0; retry--) { 467 BlockDeviceWriter.dropCaches(mDevice); 468 if (!BlockDeviceWriter.canReadByte(mDevice, path, kTargetOffset)) { 469 break; 470 } 471 try { 472 CLog.d("lsof: " + expectRemoteCommandToSucceed("lsof " + apkPath)); 473 Thread.sleep(1000); 474 String pid = expectRemoteCommandToSucceed("pidof system_server"); 475 mDevice.executeShellV2Command("kill -10 " + pid); // force GC 476 } catch (InterruptedException e) { 477 Thread.currentThread().interrupt(); 478 return; 479 } 480 } 481 assertTrue("Read from " + path + " should fail", retry > 0); 482 } 483 } 484 verifyInstalledFiles(String... filenames)485 private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException { 486 String apkPath = getApkPath(TARGET_PACKAGE); 487 String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); 488 // Exclude directories since we only care about files. 489 HashSet<String> actualFiles = new HashSet<>(Arrays.asList( 490 expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n"))); 491 492 HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames)); 493 assertEquals(expectedFiles, actualFiles); 494 } 495 damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte)496 private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte) 497 throws DeviceNotAvailableException { 498 assertTrue(path.startsWith("/data/")); 499 ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); 500 ArrayList<String> args = new ArrayList<>(); 501 args.add(DAMAGING_EXECUTABLE); 502 if ("f2fs".equals(mountPoint.type)) { 503 args.add("--use-f2fs-pinning"); 504 } 505 args.add(mountPoint.filesystem); 506 args.add(path); 507 args.add(Long.toString(offsetOfTargetingByte)); 508 expectRemoteCommandToSucceed(String.join(" ", args)); 509 } 510 getApkPath(String packageName)511 private String getApkPath(String packageName) throws DeviceNotAvailableException { 512 String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk"); 513 int index = line.trim().indexOf(":"); 514 assertTrue(index >= 0); 515 return line.substring(index + 1); 516 } 517 getFileSizeInBytes(String packageName)518 private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException { 519 return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim()); 520 } 521 expectRemoteCommandToSucceed(String cmd)522 private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException { 523 CommandResult result = mDevice.executeShellV2Command(cmd); 524 assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS, 525 result.getStatus()); 526 return result.getStdout(); 527 } 528 expectRemoteCommandToFail(String cmd)529 private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException { 530 CommandResult result = mDevice.executeShellV2Command(cmd); 531 assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(), 532 result.getStatus() != CommandStatus.SUCCESS); 533 } 534 535 private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { InstallMultiple()536 InstallMultiple() { 537 super(getDevice(), getBuild()); 538 } 539 addFileAndSignature(String filename)540 InstallMultiple addFileAndSignature(String filename) { 541 try { 542 addFile(filename); 543 addFile(filename + ".fsv_sig"); 544 } catch (FileNotFoundException e) { 545 fail("Missing test file: " + e); 546 } 547 return this; 548 } 549 } 550 } 551