application_package_test.dart 41.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'package:file/file.dart';
import 'package:file/memory.dart';
7
import 'package:flutter_tools/src/android/android_sdk.dart';
8
import 'package:flutter_tools/src/android/application_package.dart';
9 10
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/logger.dart';
12
import 'package:flutter_tools/src/base/os.dart';
13
import 'package:flutter_tools/src/base/process.dart';
14
import 'package:flutter_tools/src/base/user_messages.dart';
15 16
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
17
import 'package:flutter_tools/src/fuchsia/application_package.dart';
18
import 'package:flutter_tools/src/globals.dart' as globals;
19
import 'package:flutter_tools/src/ios/application_package.dart';
20
import 'package:flutter_tools/src/ios/plist_parser.dart';
21
import 'package:flutter_tools/src/project.dart';
22
import 'package:test/fake.dart';
23

24 25
import '../src/common.dart';
import '../src/context.dart';
26
import '../src/fake_process_manager.dart';
27
import '../src/fakes.dart';
28 29

void main() {
30
  group('Apk with partial Android SDK works', () {
31 32 33 34
    late FakeAndroidSdk sdk;
    late FakeProcessManager fakeProcessManager;
    late MemoryFileSystem fs;
    late Cache cache;
35

36 37
    final Map<Type, Generator> overrides = <Type, Generator>{
      AndroidSdk: () => sdk,
38
      ProcessManager: () => fakeProcessManager,
39
      FileSystem: () => fs,
40
      Cache: () => cache,
41 42 43
    };

    setUp(() async {
44
      sdk = FakeAndroidSdk();
45
      fakeProcessManager = FakeProcessManager.empty();
46
      fs = MemoryFileSystem.test();
47
      cache = Cache.test(
48
        processManager: FakeProcessManager.any(),
49
      );
50
      Cache.flutterRoot = '../..';
51
      sdk.licensesAvailable = true;
52
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.currentDirectory);
53
      fs.file(project.android.hostAppGradleRoot.childFile(
54
        globals.platform.isWindows ? 'gradlew.bat' : 'gradlew',
55
      ).path).createSync(recursive: true);
56 57
    });

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    testUsingContext('correct debug filename in module projects', () async {
      const String aaptPath = 'aaptPath';
      final File apkFile = globals.fs.file('app-debug.apk');
      final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion();
      sdkVersion.aaptPath = aaptPath;
      sdk.latestVersion = sdkVersion;
      sdk.platformToolsAvailable = true;
      sdk.licensesAvailable = false;

      fakeProcessManager.addCommand(
        FakeCommand(
          command: <String>[
            aaptPath,
            'dump',
            'xmltree',
             apkFile.path,
            'AndroidManifest.xml',
          ],
          stdout: _aaptDataWithDefaultEnabledAndMainLauncherActivity
        )
      );

      fakeProcessManager.addCommand(
        FakeCommand(
          command: <String>[
            aaptPath,
            'dump',
            'xmltree',
             fs.path.join('module_project', 'build', 'host', 'outputs', 'apk', 'debug', 'app-debug.apk'),
            'AndroidManifest.xml',
          ],
          stdout: _aaptDataWithDefaultEnabledAndMainLauncherActivity
        )
      );

      await ApplicationPackageFactory.instance!.getPackageForPlatform(
        TargetPlatform.android_arm,
        applicationBinary: apkFile,
      );
      final BufferLogger logger = BufferLogger.test();
      final FlutterProject project = await aModuleProject();
      project.android.hostAppGradleRoot.childFile('build.gradle').createSync(recursive: true);
      final File appGradle = project.android.hostAppGradleRoot.childFile(
        fs.path.join('app', 'build.gradle'));
      appGradle.createSync(recursive: true);
      appGradle.writeAsStringSync("def flutterPluginVersion = 'managed'");
      final File apkDebugFile = project.directory
        .childDirectory('build')
        .childDirectory('host')
        .childDirectory('outputs')
        .childDirectory('apk')
        .childDirectory('debug')
        .childFile('app-debug.apk');
      apkDebugFile.createSync(recursive: true);
      final AndroidApk? androidApk = await AndroidApk.fromAndroidProject(
        project.android,
        androidSdk: sdk,
        processManager: fakeProcessManager,
        userMessages:  UserMessages(),
        processUtils: ProcessUtils(processManager: fakeProcessManager, logger: logger),
        logger: logger,
        fileSystem: fs,
        buildInfo: const BuildInfo(BuildMode.debug, null, treeShakeIcons: false),
      );
      expect(androidApk, isNotNull);
    }, overrides: overrides);

125 126
    testUsingContext('Licenses not available, platform and buildtools available, apk exists', () async {
      const String aaptPath = 'aaptPath';
127
      final File apkFile = globals.fs.file('app-debug.apk');
128 129 130 131 132
      final FakeAndroidSdkVersion sdkVersion = FakeAndroidSdkVersion();
      sdkVersion.aaptPath = aaptPath;
      sdk.latestVersion = sdkVersion;
      sdk.platformToolsAvailable = true;
      sdk.licensesAvailable = false;
133 134 135 136

      fakeProcessManager.addCommand(
        FakeCommand(
          command: <String>[
137 138 139
            aaptPath,
            'dump',
            'xmltree',
140
             apkFile.path,
141
            'AndroidManifest.xml',
142 143 144 145
          ],
          stdout: _aaptDataWithDefaultEnabledAndMainLauncherActivity
        )
      );
146

147
      final ApplicationPackage applicationPackage = (await ApplicationPackageFactory.instance!.getPackageForPlatform(
148 149
        TargetPlatform.android_arm,
        applicationBinary: apkFile,
150
      ))!;
151
      expect(applicationPackage.name, 'app-debug.apk');
152 153
      expect(applicationPackage, isA<PrebuiltApplicationPackage>());
      expect((applicationPackage as PrebuiltApplicationPackage).applicationPackage.path, apkFile.path);
154
      expect(fakeProcessManager, hasNoRemainingExpectations);
155 156
    }, overrides: overrides);

157
    testUsingContext('Licenses available, build tools not, apk exists', () async {
158
      sdk.latestVersion = null;
159
      final FlutterProject project = FlutterProject.fromDirectoryTest(fs.currentDirectory);
Emmanuel Garcia's avatar
Emmanuel Garcia committed
160 161 162 163
      project.android.hostAppGradleRoot
        .childFile('gradle.properties')
        .writeAsStringSync('irrelevant');

164
      final Directory gradleWrapperDir = cache.getArtifactDirectory('gradle_wrapper');
165

166
      gradleWrapperDir.fileSystem.directory(gradleWrapperDir.childDirectory('gradle').childDirectory('wrapper'))
167
          .createSync(recursive: true);
168 169
      gradleWrapperDir.childFile('gradlew').writeAsStringSync('irrelevant');
      gradleWrapperDir.childFile('gradlew.bat').writeAsStringSync('irrelevant');
170

171
      await ApplicationPackageFactory.instance!.getPackageForPlatform(
172
        TargetPlatform.android_arm,
173
        applicationBinary: globals.fs.file('app-debug.apk'),
174
      );
175
      expect(fakeProcessManager, hasNoRemainingExpectations);
176 177 178
    }, overrides: overrides);

    testUsingContext('Licenses available, build tools available, does not call gradle dependencies', () async {
179 180
      final AndroidSdkVersion sdkVersion = FakeAndroidSdkVersion();
      sdk.latestVersion = sdkVersion;
181

182
      await ApplicationPackageFactory.instance!.getPackageForPlatform(
183 184
        TargetPlatform.android_arm,
      );
185
      expect(fakeProcessManager, hasNoRemainingExpectations);
186
    }, overrides: overrides);
187

188
    testWithoutContext('returns null when failed to extract manifest', () async {
189
      final Logger logger = BufferLogger.test();
190 191
      final AndroidApk? androidApk = AndroidApk.fromApk(
        fs.file(''),
192
        processManager: fakeProcessManager,
193
        logger: logger,
194 195
        userMessages: UserMessages(),
        androidSdk: sdk,
196
        processUtils: ProcessUtils(processManager: fakeProcessManager, logger: logger),
197
      );
198

199
      expect(androidApk, isNull);
200
      expect(fakeProcessManager, hasNoRemainingExpectations);
201
    });
202 203
  });

204
  group('ApkManifestData', () {
205 206 207 208
    testWithoutContext('Parses manifest with an Activity that has enabled set to true, action set to android.intent.action.MAIN and category set to android.intent.category.LAUNCHER', () {
      final ApkManifestData data = ApkManifestData.parseFromXmlDump(
        _aaptDataWithExplicitEnabledAndMainLauncherActivity,
        BufferLogger.test(),
209
      )!;
210

211
      expect(data, isNotNull);
212 213
      expect(data.packageName, 'io.flutter.examples.hello_world');
      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
214 215 216 217 218 219
    });

    testWithoutContext('Parses manifest with an Activity that has no value for its enabled field, action set to android.intent.action.MAIN and category set to android.intent.category.LAUNCHER', () {
      final ApkManifestData data = ApkManifestData.parseFromXmlDump(
        _aaptDataWithDefaultEnabledAndMainLauncherActivity,
        BufferLogger.test(),
220
      )!;
221

222 223 224
      expect(data, isNotNull);
      expect(data.packageName, 'io.flutter.examples.hello_world');
      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
225 226 227 228 229 230
    });

    testWithoutContext('Parses manifest with a dist namespace', () {
      final ApkManifestData data = ApkManifestData.parseFromXmlDump(
        _aaptDataWithDistNamespace,
        BufferLogger.test(),
231
      )!;
232 233 234 235

      expect(data, isNotNull);
      expect(data.packageName, 'io.flutter.examples.hello_world');
      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity');
236 237 238 239
    });

    testWithoutContext('Error when parsing manifest with no Activity that has enabled set to true nor has no value for its enabled field', () {
      final BufferLogger logger = BufferLogger.test();
240
      final ApkManifestData? data = ApkManifestData.parseFromXmlDump(
241 242 243
        _aaptDataWithNoEnabledActivity,
        logger,
      );
244

245 246
      expect(data, isNull);
      expect(
247 248 249 250 251 252 253
        logger.errorText,
        'Error running io.flutter.examples.hello_world. Default activity not found\n',
      );
    });

    testWithoutContext('Error when parsing manifest with no Activity that has action set to android.intent.action.MAIN', () {
      final BufferLogger logger = BufferLogger.test();
254
      final ApkManifestData? data = ApkManifestData.parseFromXmlDump(
255 256 257
        _aaptDataWithNoMainActivity,
        logger,
      );
258

259 260
      expect(data, isNull);
      expect(
261 262 263 264 265 266 267
        logger.errorText,
        'Error running io.flutter.examples.hello_world. Default activity not found\n',
      );
    });

    testWithoutContext('Error when parsing manifest with no Activity that has category set to android.intent.category.LAUNCHER', () {
      final BufferLogger logger = BufferLogger.test();
268
      final ApkManifestData? data = ApkManifestData.parseFromXmlDump(
269 270 271
        _aaptDataWithNoLauncherActivity,
        logger,
      );
272

273 274
      expect(data, isNull);
      expect(
275 276 277 278 279 280 281 282 283
        logger.errorText,
        'Error running io.flutter.examples.hello_world. Default activity not found\n',
      );
    });

    testWithoutContext('Parsing manifest with Activity that has multiple category, android.intent.category.LAUNCHER and android.intent.category.DEFAULT', () {
      final ApkManifestData data = ApkManifestData.parseFromXmlDump(
        _aaptDataWithLauncherAndDefaultActivity,
        BufferLogger.test(),
284
      )!;
285 286 287 288

      expect(data, isNotNull);
      expect(data.packageName, 'io.flutter.examples.hello_world');
      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity');
289
    });
290

291
    testWithoutContext('Parses manifest with missing application tag', () async {
292
      final ApkManifestData? data = ApkManifestData.parseFromXmlDump(
293 294 295
        _aaptDataWithoutApplication,
        BufferLogger.test(),
      );
296 297 298

      expect(data, isNull);
    });
299
  });
300

301
  group('PrebuiltIOSApp', () {
302 303
    late FakeOperatingSystemUtils os;
    late FakePlistParser testPlistParser;
304

305
    final Map<Type, Generator> overrides = <Type, Generator>{
306
      FileSystem: () => MemoryFileSystem.test(),
307
      ProcessManager: () => FakeProcessManager.any(),
308
      PlistParser: () => testPlistParser,
309
      OperatingSystemUtils: () => os,
310
    };
311

312
    setUp(() {
313
      os = FakeOperatingSystemUtils();
314
      testPlistParser = FakePlistParser();
315 316
    });

317
    testUsingContext('Error on non-existing file', () {
318 319
      final PrebuiltIOSApp? iosApp =
          IOSApp.fromPrebuiltApp(globals.fs.file('not_existing.ipa')) as PrebuiltIOSApp?;
320 321
      expect(iosApp, isNull);
      expect(
322
        testLogger.errorText,
323 324 325
        'File "not_existing.ipa" does not exist. Use an app bundle or an ipa.\n',
      );
    }, overrides: overrides);
326

327
    testUsingContext('Error on non-app-bundle folder', () {
328
      globals.fs.directory('regular_folder').createSync();
329 330
      final PrebuiltIOSApp? iosApp =
          IOSApp.fromPrebuiltApp(globals.fs.file('regular_folder')) as PrebuiltIOSApp?;
331 332
      expect(iosApp, isNull);
      expect(
333
          testLogger.errorText, 'Folder "regular_folder" is not an app bundle.\n');
334
    }, overrides: overrides);
335

336
    testUsingContext('Error on no info.plist', () {
337
      globals.fs.directory('bundle.app').createSync();
338
      final PrebuiltIOSApp? iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('bundle.app')) as PrebuiltIOSApp?;
339 340
      expect(iosApp, isNull);
      expect(
341
        testLogger.errorText,
342 343 344
        'Invalid prebuilt iOS app. Does not contain Info.plist.\n',
      );
    }, overrides: overrides);
345

346
    testUsingContext('Error on bad info.plist', () {
347
      globals.fs.directory('bundle.app').createSync();
348
      globals.fs.file('bundle.app/Info.plist').createSync();
349
      final PrebuiltIOSApp? iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('bundle.app')) as PrebuiltIOSApp?;
350 351
      expect(iosApp, isNull);
      expect(
352
        testLogger.errorText,
353 354 355 356
        contains(
            'Invalid prebuilt iOS app. Info.plist does not contain bundle identifier\n'),
      );
    }, overrides: overrides);
357

358
    testUsingContext('Success with app bundle', () {
359
      globals.fs.directory('bundle.app').createSync();
360 361
      globals.fs.file('bundle.app/Info.plist').createSync();
      testPlistParser.setProperty('CFBundleIdentifier', 'fooBundleId');
362
      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('bundle.app'))! as PrebuiltIOSApp;
363
      expect(testLogger.errorText, isEmpty);
364
      expect(iosApp.uncompressedBundle.path, 'bundle.app');
365 366
      expect(iosApp.id, 'fooBundleId');
      expect(iosApp.bundleName, 'bundle.app');
367
      expect(iosApp.applicationPackage.path, globals.fs.directory('bundle.app').path);
368
    }, overrides: overrides);
369

370
    testUsingContext('Bad ipa zip-file, no payload dir', () {
371
      globals.fs.file('app.ipa').createSync();
372
      final PrebuiltIOSApp? iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('app.ipa')) as PrebuiltIOSApp?;
373 374
      expect(iosApp, isNull);
      expect(
375
        testLogger.errorText,
376 377 378
        'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.\n',
      );
    }, overrides: overrides);
379

380
    testUsingContext('Bad ipa zip-file, two app bundles', () {
381
      globals.fs.file('app.ipa').createSync();
382
      os.onUnzip = (File zipFile, Directory targetDirectory) {
383
        if (zipFile.path != 'app.ipa') {
384
          return;
385 386
        }
        final String bundlePath1 =
387
            globals.fs.path.join(targetDirectory.path, 'Payload', 'bundle1.app');
388
        final String bundlePath2 =
389 390 391
            globals.fs.path.join(targetDirectory.path, 'Payload', 'bundle2.app');
        globals.fs.directory(bundlePath1).createSync(recursive: true);
        globals.fs.directory(bundlePath2).createSync(recursive: true);
392
      };
393
      final PrebuiltIOSApp? iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('app.ipa')) as PrebuiltIOSApp?;
394
      expect(iosApp, isNull);
395
      expect(testLogger.errorText,
396 397
          'Invalid prebuilt iOS ipa. Does not contain a single app bundle.\n');
    }, overrides: overrides);
398

399
    testUsingContext('Success with ipa', () {
400
      globals.fs.file('app.ipa').createSync();
401
      os.onUnzip = (File zipFile, Directory targetDirectory) {
402
        if (zipFile.path != 'app.ipa') {
403
          return;
404
        }
405 406
        final Directory bundleAppDir = globals.fs.directory(
            globals.fs.path.join(targetDirectory.path, 'Payload', 'bundle.app'));
407
        bundleAppDir.createSync(recursive: true);
408
        testPlistParser.setProperty('CFBundleIdentifier', 'fooBundleId');
409 410
        globals.fs
            .file(globals.fs.path.join(bundleAppDir.path, 'Info.plist'))
411
            .createSync();
412
      };
413
      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(globals.fs.file('app.ipa'))! as PrebuiltIOSApp;
414
      expect(testLogger.errorText, isEmpty);
415
      expect(iosApp.uncompressedBundle.path, endsWith('bundle.app'));
416 417
      expect(iosApp.id, 'fooBundleId');
      expect(iosApp.bundleName, 'bundle.app');
418
      expect(iosApp.applicationPackage.path, globals.fs.file('app.ipa').path);
419
    }, overrides: overrides);
420 421

    testUsingContext('returns null when there is no ios or .ios directory', () async {
422 423
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
424 425
      final BuildableIOSApp? iosApp = await IOSApp.fromIosProject(
        FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?;
426 427 428

      expect(iosApp, null);
    }, overrides: overrides);
429 430

    testUsingContext('returns null when there is no Runner.xcodeproj', () async {
431 432 433
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
      globals.fs.file('ios/FooBar.xcodeproj').createSync(recursive: true);
434 435
      final BuildableIOSApp? iosApp = await IOSApp.fromIosProject(
        FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?;
436 437 438

      expect(iosApp, null);
    }, overrides: overrides);
439 440

    testUsingContext('returns null when there is no Runner.xcodeproj/project.pbxproj', () async {
441 442 443
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
      globals.fs.file('ios/Runner.xcodeproj').createSync(recursive: true);
444 445
      final BuildableIOSApp? iosApp = await IOSApp.fromIosProject(
        FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?;
446 447 448

      expect(iosApp, null);
    }, overrides: overrides);
449 450 451 452 453 454

    testUsingContext('returns null when there with no product identifier', () async {
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
      final Directory project = globals.fs.directory('ios/Runner.xcodeproj')..createSync(recursive: true);
      project.childFile('project.pbxproj').createSync();
455
      final BuildableIOSApp? iosApp = await IOSApp.fromIosProject(
456
        FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?;
457 458 459

      expect(iosApp, null);
    }, overrides: overrides);
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545

    testUsingContext('returns project app icon dirname', () async {
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner',
      );
      final String iconDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'AppIcon.appiconset',
      );
      expect(iosApp.projectAppIconDirName, globals.fs.path.join('ios', iconDirSuffix));
    }, overrides: overrides);

    testUsingContext('returns template app icon dirname for Contents.json', () async {
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner',
      );
      final String iconDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'AppIcon.appiconset',
      );
      expect(
        iosApp.templateAppIconDirNameForContentsJson,
        globals.fs.path.join(
          Cache.flutterRoot!,
          'packages',
          'flutter_tools',
          'templates',
          'app_shared',
          'ios.tmpl',
          iconDirSuffix,
        ),
      );
    }, overrides: overrides);

    testUsingContext('returns template app icon dirname for images', () async {
      final String toolsDir = globals.fs.path.join(
        Cache.flutterRoot!,
        'packages',
        'flutter_tools',
      );
      final String packageConfigPath = globals.fs.path.join(
        toolsDir,
        '.dart_tool',
        'package_config.json'
      );
      globals.fs.file(packageConfigPath)
        ..createSync(recursive: true)
        ..writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "flutter_template_images",
      "rootUri": "/flutter_template_images",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner');
      final String iconDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'AppIcon.appiconset',
      );
      expect(
        await iosApp.templateAppIconDirNameForImages,
        globals.fs.path.absolute(
          'flutter_template_images',
          'templates',
          'app_shared',
          'ios.tmpl',
          iconDirSuffix,
        ),
      );
    }, overrides: overrides);
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631

    testUsingContext('returns project launch image dirname', () async {
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner',
      );
      final String launchImageDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'LaunchImage.imageset',
      );
      expect(iosApp.projectLaunchImageDirName, globals.fs.path.join('ios', launchImageDirSuffix));
    }, overrides: overrides);

    testUsingContext('returns template launch image dirname for Contents.json', () async {
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner',
      );
      final String launchImageDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'LaunchImage.imageset',
      );
      expect(
        iosApp.templateLaunchImageDirNameForContentsJson,
        globals.fs.path.join(
          Cache.flutterRoot!,
          'packages',
          'flutter_tools',
          'templates',
          'app_shared',
          'ios.tmpl',
          launchImageDirSuffix,
        ),
      );
    }, overrides: overrides);

    testUsingContext('returns template launch image dirname for images', () async {
      final String toolsDir = globals.fs.path.join(
        Cache.flutterRoot!,
        'packages',
        'flutter_tools',
      );
      final String packageConfigPath = globals.fs.path.join(
          toolsDir,
          '.dart_tool',
          'package_config.json'
      );
      globals.fs.file(packageConfigPath)
        ..createSync(recursive: true)
        ..writeAsStringSync('''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "flutter_template_images",
      "rootUri": "/flutter_template_images",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ]
}
''');
      final BuildableIOSApp iosApp = BuildableIOSApp(
        IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
        'com.foo.bar',
        'Runner');
      final String launchImageDirSuffix = globals.fs.path.join(
        'Runner',
        'Assets.xcassets',
        'LaunchImage.imageset',
      );
      expect(
        await iosApp.templateLaunchImageDirNameForImages,
        globals.fs.path.absolute(
          'flutter_template_images',
          'templates',
          'app_shared',
          'ios.tmpl',
          launchImageDirSuffix,
        ),
      );
    }, overrides: overrides);
632
  });
633 634 635

  group('FuchsiaApp', () {
    final Map<Type, Generator> overrides = <Type, Generator>{
636
      FileSystem: () => MemoryFileSystem.test(),
637
      ProcessManager: () => FakeProcessManager.any(),
638
      OperatingSystemUtils: () => FakeOperatingSystemUtils(),
639
    };
640

641
    testUsingContext('Error on non-existing file', () {
642 643
      final PrebuiltFuchsiaApp? fuchsiaApp =
          FuchsiaApp.fromPrebuiltApp(globals.fs.file('not_existing.far')) as PrebuiltFuchsiaApp?;
644 645
      expect(fuchsiaApp, isNull);
      expect(
646
        testLogger.errorText,
647 648 649 650 651
        'File "not_existing.far" does not exist or is not a .far file. Use far archive.\n',
      );
    }, overrides: overrides);

    testUsingContext('Error on non-far file', () {
652
      globals.fs.directory('regular_folder').createSync();
653 654
      final PrebuiltFuchsiaApp? fuchsiaApp =
          FuchsiaApp.fromPrebuiltApp(globals.fs.file('regular_folder')) as PrebuiltFuchsiaApp?;
655 656
      expect(fuchsiaApp, isNull);
      expect(
657
        testLogger.errorText,
658 659 660 661 662
        'File "regular_folder" does not exist or is not a .far file. Use far archive.\n',
      );
    }, overrides: overrides);

    testUsingContext('Success with far file', () {
663
      globals.fs.file('bundle.far').createSync();
664
      final PrebuiltFuchsiaApp fuchsiaApp = FuchsiaApp.fromPrebuiltApp(globals.fs.file('bundle.far'))! as PrebuiltFuchsiaApp;
665
      expect(testLogger.errorText, isEmpty);
666
      expect(fuchsiaApp.id, 'bundle.far');
667
      expect(fuchsiaApp.applicationPackage.path, globals.fs.file('bundle.far').path);
668 669 670
    }, overrides: overrides);

    testUsingContext('returns null when there is no fuchsia', () async {
671 672
      globals.fs.file('pubspec.yaml').createSync();
      globals.fs.file('.packages').createSync();
673
      final BuildableFuchsiaApp? fuchsiaApp = FuchsiaApp.fromFuchsiaProject(FlutterProject.fromDirectory(globals.fs.currentDirectory).fuchsia) as BuildableFuchsiaApp?;
674 675 676 677

      expect(fuchsiaApp, null);
    }, overrides: overrides);
  });
678 679
}

680 681
const String _aaptDataWithExplicitEnabledAndMainLauncherActivity = '''
N: android=http://schemas.android.com/apk/res/android
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
  E: manifest (line=7)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    E: uses-sdk (line=12)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
    E: uses-permission (line=21)
      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
    E: application (line=29)
      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
      A: android:icon(0x01010002)=@0x7f010000
      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      E: activity (line=34)
        A: android:theme(0x01010000)=@0x1030009
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
        A: android:enabled(0x0101000e)=(type 0x12)0x0
        A: android:launchMode(0x0101001d)=(type 0x10)0x1
        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
        E: intent-filter (line=42)
          E: action (line=43)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=45)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
      E: activity (line=48)
        A: android:theme(0x01010000)=@0x1030009
        A: android:label(0x01010001)="app2" (Raw: "app2")
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
        E: intent-filter (line=53)
          E: action (line=54)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=56)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';


721 722
const String _aaptDataWithDefaultEnabledAndMainLauncherActivity = '''
N: android=http://schemas.android.com/apk/res/android
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
  E: manifest (line=7)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    E: uses-sdk (line=12)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
    E: uses-permission (line=21)
      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
    E: application (line=29)
      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
      A: android:icon(0x01010002)=@0x7f010000
      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      E: activity (line=34)
        A: android:theme(0x01010000)=@0x1030009
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
        A: android:enabled(0x0101000e)=(type 0x12)0x0
        A: android:launchMode(0x0101001d)=(type 0x10)0x1
        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
        E: intent-filter (line=42)
          E: action (line=43)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=45)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
      E: activity (line=48)
        A: android:theme(0x01010000)=@0x1030009
        A: android:label(0x01010001)="app2" (Raw: "app2")
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
        E: intent-filter (line=53)
          E: action (line=54)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=56)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';


761 762
const String _aaptDataWithNoEnabledActivity = '''
N: android=http://schemas.android.com/apk/res/android
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
  E: manifest (line=7)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    E: uses-sdk (line=12)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
    E: uses-permission (line=21)
      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
    E: application (line=29)
      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
      A: android:icon(0x01010002)=@0x7f010000
      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      E: activity (line=34)
        A: android:theme(0x01010000)=@0x1030009
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
        A: android:enabled(0x0101000e)=(type 0x12)0x0
        A: android:launchMode(0x0101001d)=(type 0x10)0x1
        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
        E: intent-filter (line=42)
          E: action (line=43)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
          E: category (line=45)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';

791 792
const String _aaptDataWithNoMainActivity = '''
N: android=http://schemas.android.com/apk/res/android
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
  E: manifest (line=7)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    E: uses-sdk (line=12)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
    E: uses-permission (line=21)
      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
    E: application (line=29)
      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
      A: android:icon(0x01010002)=@0x7f010000
      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      E: activity (line=34)
        A: android:theme(0x01010000)=@0x1030009
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
        A: android:launchMode(0x0101001d)=(type 0x10)0x1
        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
        E: intent-filter (line=42)
          E: category (line=43)
            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';

819 820
const String _aaptDataWithNoLauncherActivity = '''
N: android=http://schemas.android.com/apk/res/android
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
  E: manifest (line=7)
    A: android:versionCode(0x0101021b)=(type 0x10)0x1
    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    E: uses-sdk (line=12)
      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
    E: uses-permission (line=21)
      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
    E: application (line=29)
      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
      A: android:icon(0x01010002)=@0x7f010000
      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
      E: activity (line=34)
        A: android:theme(0x01010000)=@0x1030009
        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
        A: android:launchMode(0x0101001d)=(type 0x10)0x1
        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
        E: intent-filter (line=42)
          E: action (line=43)
            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")''';

847 848
const String _aaptDataWithLauncherAndDefaultActivity = '''
N: android=http://schemas.android.com/apk/res/android
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
  N: dist=http://schemas.android.com/apk/distribution
    E: manifest (line=7)
      A: android:versionCode(0x0101021b)=(type 0x10)0x1
      A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
      A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1c
      A: android:compileSdkVersionCodename(0x01010573)="9" (Raw: "9")
      A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
      A: platformBuildVersionCode=(type 0x10)0x1
      A: platformBuildVersionName=(type 0x4)0x3f800000
      E: uses-sdk (line=13)
        A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
        A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
      E: dist:module (line=17)
        A: dist:instant=(type 0x12)0xffffffff
      E: uses-permission (line=24)
        A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
      E: application (line=32)
        A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
        A: android:icon(0x01010002)=@0x7f010000
        A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
        E: activity (line=36)
          A: android:theme(0x01010000)=@0x01030009
          A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
          A: android:launchMode(0x0101001d)=(type 0x10)0x1
          A: android:configChanges(0x0101001f)=(type 0x11)0x400037b4
          A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
          A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
          E: intent-filter (line=43)
            E: action (line=44)
              A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
            E: category (line=46)
              A: android:name(0x01010003)="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")
            E: category (line=47)
              A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
''';

885 886
const String _aaptDataWithDistNamespace = '''
N: android=http://schemas.android.com/apk/res/android
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
  N: dist=http://schemas.android.com/apk/distribution
    E: manifest (line=7)
      A: android:versionCode(0x0101021b)=(type 0x10)0x1
      A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
      A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1c
      A: android:compileSdkVersionCodename(0x01010573)="9" (Raw: "9")
      A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
      A: platformBuildVersionCode=(type 0x10)0x1
      A: platformBuildVersionName=(type 0x4)0x3f800000
      E: uses-sdk (line=13)
        A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
        A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
      E: dist:module (line=17)
        A: dist:instant=(type 0x12)0xffffffff
      E: uses-permission (line=24)
        A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
      E: application (line=32)
        A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
        A: android:icon(0x01010002)=@0x7f010000
        A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
        E: activity (line=36)
          A: android:theme(0x01010000)=@0x01030009
          A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
          A: android:launchMode(0x0101001d)=(type 0x10)0x1
          A: android:configChanges(0x0101001f)=(type 0x11)0x400037b4
          A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
          A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
          E: intent-filter (line=43)
            E: action (line=44)
              A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
            E: category (line=46)
              A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
''';

921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
const String _aaptDataWithoutApplication = '''
N: android=http://schemas.android.com/apk/res/android
  N: dist=http://schemas.android.com/apk/distribution
    E: manifest (line=7)
      A: android:versionCode(0x0101021b)=(type 0x10)0x1
      A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
      A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1c
      A: android:compileSdkVersionCodename(0x01010573)="9" (Raw: "9")
      A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
      A: platformBuildVersionCode=(type 0x10)0x1
      A: platformBuildVersionName=(type 0x4)0x3f800000
      E: uses-sdk (line=13)
        A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
        A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
      E: dist:module (line=17)
        A: dist:instant=(type 0x12)0xffffffff
      E: uses-permission (line=24)
        A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
''';

941
class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
942
  void Function(File, Directory)? onUnzip;
943 944 945 946 947 948 949 950 951

  @override
  void unzip(File file, Directory targetDirectory) {
    onUnzip?.call(file, targetDirectory);
  }
}

class FakeAndroidSdk extends Fake implements AndroidSdk {
  @override
952
  late bool platformToolsAvailable;
953 954

  @override
955
  late bool licensesAvailable;
956 957

  @override
958
  AndroidSdkVersion? latestVersion;
959 960 961 962
}

class FakeAndroidSdkVersion extends Fake implements AndroidSdkVersion {
  @override
963
  late String aaptPath;
964
}
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980

Future<FlutterProject> aModuleProject() async {
  final Directory directory = globals.fs.directory('module_project');
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
  directory.childFile('pubspec.yaml').writeAsStringSync('''
name: my_module
flutter:
  module:
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
}