application_package.dart 21.7 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
import 'dart:async';
6
import 'dart:collection';
7

8
import 'package:meta/meta.dart';
Dan Field's avatar
Dan Field committed
9
import 'package:xml/xml.dart';
10

11
import 'android/gradle.dart';
12
import 'base/common.dart';
13
import 'base/context.dart';
14
import 'base/file_system.dart';
15
import 'base/io.dart';
16
import 'base/process.dart';
17
import 'base/user_messages.dart';
18
import 'build_info.dart';
19
import 'fuchsia/application_package.dart';
20
import 'globals.dart' as globals;
21
import 'ios/plist_parser.dart';
22
import 'linux/application_package.dart';
23
import 'macos/application_package.dart';
24
import 'project.dart';
25
import 'tester/flutter_tester.dart';
26
import 'web/web_device.dart';
27
import 'windows/application_package.dart';
28

29
class ApplicationPackageFactory {
30
  static ApplicationPackageFactory get instance => context.get<ApplicationPackageFactory>();
31 32

  Future<ApplicationPackage> getPackageForPlatform(
33
    TargetPlatform platform, {
34
    BuildInfo buildInfo,
35 36
    File applicationBinary,
  }) async {
37
    switch (platform) {
38
      case TargetPlatform.android:
39 40 41 42
      case TargetPlatform.android_arm:
      case TargetPlatform.android_arm64:
      case TargetPlatform.android_x64:
      case TargetPlatform.android_x86:
43
        if (globals.androidSdk?.licensesAvailable == true  && globals.androidSdk?.latestVersion == null) {
44 45
          await checkGradleDependencies();
        }
46
        return applicationBinary == null
47
            ? await AndroidApk.fromAndroidProject(FlutterProject.current().android)
48 49 50
            : AndroidApk.fromApk(applicationBinary);
      case TargetPlatform.ios:
        return applicationBinary == null
51
            ? await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo)
52 53
            : IOSApp.fromPrebuiltApp(applicationBinary);
      case TargetPlatform.tester:
54
        return FlutterTesterApp.fromCurrentDirectory(globals.fs);
55
      case TargetPlatform.darwin_x64:
56
        return applicationBinary == null
57
            ? MacOSApp.fromMacOSProject(FlutterProject.current().macos)
58
            : MacOSApp.fromPrebuiltApp(applicationBinary);
59
      case TargetPlatform.web_javascript:
60 61 62
        if (!FlutterProject.current().web.existsSync()) {
          return null;
        }
63
        return WebApplicationPackage(FlutterProject.current());
64
      case TargetPlatform.linux_x64:
65
        return applicationBinary == null
66
            ? LinuxApp.fromLinuxProject(FlutterProject.current().linux)
67
            : LinuxApp.fromPrebuiltApp(applicationBinary);
68
      case TargetPlatform.windows_x64:
69
        return applicationBinary == null
70
            ? WindowsApp.fromWindowsProject(FlutterProject.current().windows)
71
            : WindowsApp.fromPrebuiltApp(applicationBinary);
72 73
      case TargetPlatform.fuchsia_arm64:
      case TargetPlatform.fuchsia_x64:
74 75 76
        return applicationBinary == null
            ? FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia)
            : FuchsiaApp.fromPrebuiltApp(applicationBinary);
77 78 79 80 81 82
    }
    assert(platform != null);
    return null;
  }
}

83
abstract class ApplicationPackage {
84 85
  ApplicationPackage({ @required this.id })
    : assert(id != null);
86

87 88 89
  /// Package ID from the Android Manifest or equivalent.
  final String id;

90 91
  String get name;

92 93
  String get displayName => name;

94
  File get packagesFile => null;
95

96
  @override
97
  String toString() => displayName ?? id;
98 99 100
}

class AndroidApk extends ApplicationPackage {
101
  AndroidApk({
Adam Barth's avatar
Adam Barth committed
102
    String id,
103
    @required this.file,
104
    @required this.versionCode,
105
    @required this.launchActivity,
106
  }) : assert(file != null),
107 108
       assert(launchActivity != null),
       super(id: id);
109

110
  /// Creates a new AndroidApk from an existing APK.
111
  factory AndroidApk.fromApk(File apk) {
112
    final String aaptPath = globals.androidSdk?.latestVersion?.aaptPath;
113
    if (aaptPath == null) {
114
      globals.printError(userMessages.aaptNotFound);
115 116 117
      return null;
    }

118 119
    String apptStdout;
    try {
120
      apptStdout = processUtils.runSync(
121 122 123 124 125 126 127 128 129 130
        <String>[
          aaptPath,
          'dump',
          'xmltree',
          apk.path,
          'AndroidManifest.xml',
        ],
        throwOnError: true,
      ).stdout.trim();
    } on ProcessException catch (error) {
131
      globals.printError('Failed to extract manifest from APK: $error.');
132 133 134 135
      return null;
    }

    final ApkManifestData data = ApkManifestData.parseFromXmlDump(apptStdout);
136 137

    if (data == null) {
138
      globals.printError('Unable to read manifest info from ${apk.path}.');
139 140 141 142
      return null;
    }

    if (data.packageName == null || data.launchableActivityName == null) {
143
      globals.printError('Unable to read manifest info from ${apk.path}.');
144 145 146
      return null;
    }

147
    return AndroidApk(
148
      id: data.packageName,
149
      file: apk,
150
      versionCode: int.tryParse(data.versionCode),
151
      launchActivity: '${data.packageName}/${data.launchableActivityName}',
152 153 154
    );
  }

155 156 157 158 159 160
  /// Path to the actual apk file.
  final File file;

  /// The path to the activity that should be launched.
  final String launchActivity;

161 162 163
  /// The version code of the APK.
  final int versionCode;

164
  /// Creates a new AndroidApk based on the information in the Android manifest.
165 166
  static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
    File apkFile;
167

168
    if (androidProject.isUsingGradle) {
169 170
      apkFile = await getGradleAppOut(androidProject);
      if (apkFile.existsSync()) {
171 172
        // Grab information from the .apk. The gradle build script might alter
        // the application Id, so we need to look at what was actually built.
173
        return AndroidApk.fromApk(apkFile);
174 175 176 177
      }
      // The .apk hasn't been built yet, so we work with what we have. The run
      // command will grab a new AndroidApk after building, to get the updated
      // IDs.
178
    } else {
179
      apkFile = globals.fs.file(globals.fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
180 181
    }

182
    final File manifest = androidProject.appManifestFile;
183

184
    if (!manifest.existsSync()) {
185 186
      globals.printError('AndroidManifest.xml could not be found.');
      globals.printError('Please check ${manifest.path} for errors.');
187
      return null;
188
    }
189

190
    final String manifestString = manifest.readAsStringSync();
Dan Field's avatar
Dan Field committed
191
    XmlDocument document;
192
    try {
Dan Field's avatar
Dan Field committed
193 194
      document = XmlDocument.parse(manifestString);
    } on XmlParserException catch (exception) {
195 196
      String manifestLocation;
      if (androidProject.isUsingGradle) {
197
        manifestLocation = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml');
198
      } else {
199
        manifestLocation = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'AndroidManifest.xml');
200
      }
201 202
      globals.printError('AndroidManifest.xml is not a valid XML document.');
      globals.printError('Please check $manifestLocation for errors.');
203 204
      throwToolExit('XML Parser error message: ${exception.toString()}');
    }
205

Dan Field's avatar
Dan Field committed
206
    final Iterable<XmlElement> manifests = document.findElements('manifest');
207
    if (manifests.isEmpty) {
208 209
      globals.printError('AndroidManifest.xml has no manifest element.');
      globals.printError('Please check ${manifest.path} for errors.');
210
      return null;
211
    }
212
    final String packageId = manifests.first.getAttribute('package');
213 214

    String launchActivity;
Dan Field's avatar
Dan Field committed
215
    for (final XmlElement activity in document.findAllElements('activity')) {
216 217 218 219 220
      final String enabled = activity.getAttribute('android:enabled');
      if (enabled != null && enabled == 'false') {
        continue;
      }

Dan Field's avatar
Dan Field committed
221
      for (final XmlElement element in activity.findElements('intent-filter')) {
222 223
        String actionName = '';
        String categoryName = '';
Dan Field's avatar
Dan Field committed
224 225
        for (final XmlNode node in element.children) {
          if (node is! XmlElement) {
226 227
            continue;
          }
Dan Field's avatar
Dan Field committed
228
          final XmlElement xmlElement = node as XmlElement;
229 230 231 232 233 234 235 236
          final String name = xmlElement.getAttribute('android:name');
          if (name == 'android.intent.action.MAIN') {
            actionName = name;
          } else if (name == 'android.intent.category.LAUNCHER') {
            categoryName = name;
          }
        }
        if (actionName.isNotEmpty && categoryName.isNotEmpty) {
237 238 239 240
          final String activityName = activity.getAttribute('android:name');
          launchActivity = '$packageId/$activityName';
          break;
        }
241 242
      }
    }
243

244
    if (packageId == null || launchActivity == null) {
245 246
      globals.printError('package identifier or launch activity not found.');
      globals.printError('Please check ${manifest.path} for errors.');
247
      return null;
248
    }
249

250
    return AndroidApk(
251
      id: packageId,
252
      file: apkFile,
253
      versionCode: null,
254
      launchActivity: launchActivity,
255
    );
256
  }
257

258
  @override
259
  File get packagesFile => file;
260

261
  @override
262
  String get name => file.basename;
263 264
}

265
/// Tests whether a [Directory] is an iOS bundle directory.
266
bool _isBundleDirectory(Directory dir) => dir.path.endsWith('.app');
267 268

abstract class IOSApp extends ApplicationPackage {
269
  IOSApp({@required String projectBundleId}) : super(id: projectBundleId);
270

271
  /// Creates a new IOSApp from an existing app bundle or IPA.
272
  factory IOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
273
    final FileSystemEntityType entityType = globals.fs.typeSync(applicationBinary.path);
274
    if (entityType == FileSystemEntityType.notFound) {
275
      globals.printError(
276
          'File "${applicationBinary.path}" does not exist. Use an app bundle or an ipa.');
277 278
      return null;
    }
279
    Directory bundleDir;
280
    if (entityType == FileSystemEntityType.directory) {
281
      final Directory directory = globals.fs.directory(applicationBinary);
282
      if (!_isBundleDirectory(directory)) {
283
        globals.printError('Folder "${applicationBinary.path}" is not an app bundle.');
284 285
        return null;
      }
286
      bundleDir = globals.fs.directory(applicationBinary);
287 288
    } else {
      // Try to unpack as an ipa.
289
      final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_app.');
290
      shutdownHooks.addShutdownHook(() async {
291 292
        await tempDir.delete(recursive: true);
      }, ShutdownStage.STILL_RECORDING);
293
      globals.os.unzip(globals.fs.file(applicationBinary), tempDir);
294 295
      final Directory payloadDir = globals.fs.directory(
        globals.fs.path.join(tempDir.path, 'Payload'),
296 297
      );
      if (!payloadDir.existsSync()) {
298
        globals.printError(
299 300 301 302
            'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.');
        return null;
      }
      try {
303
        bundleDir = payloadDir.listSync().whereType<Directory>().singleWhere(_isBundleDirectory);
304
      } on StateError {
305
        globals.printError(
306 307 308
            'Invalid prebuilt iOS ipa. Does not contain a single app bundle.');
        return null;
      }
309
    }
310 311 312
    final String plistPath = globals.fs.path.join(bundleDir.path, 'Info.plist');
    if (!globals.fs.file(plistPath).existsSync()) {
      globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
313 314
      return null;
    }
315
    final String id = globals.plistParser.getValueFromFile(
316
      plistPath,
317
      PlistParser.kCFBundleIdentifierKey,
318 319
    );
    if (id == null) {
320
      globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
321
      return null;
322
    }
323

324
    return PrebuiltIOSApp(
325
      bundleDir: bundleDir,
326
      bundleName: globals.fs.path.basename(bundleDir.path),
327 328 329
      projectBundleId: id,
    );
  }
330

331
  static Future<IOSApp> fromIosProject(IosProject project, BuildInfo buildInfo) {
332
    if (getCurrentHostPlatform() != HostPlatform.darwin_x64) {
333
      return null;
334 335
    }
    if (!project.exists) {
336
      // If the project doesn't exist at all the current hint to run flutter
337 338 339 340
      // create is accurate.
      return null;
    }
    if (!project.xcodeProject.existsSync()) {
341
      globals.printError('Expected ios/Runner.xcodeproj but this file is missing.');
342 343
      return null;
    }
344
    if (!project.xcodeProjectInfoFile.existsSync()) {
345
      globals.printError('Expected ios/Runner.xcodeproj/project.pbxproj but this file is missing.');
346 347
      return null;
    }
348
    return BuildableIOSApp.fromProject(project, buildInfo);
349
  }
350

351
  @override
352
  String get displayName => id;
353

354 355 356 357 358 359
  String get simulatorBundlePath;

  String get deviceBundlePath;
}

class BuildableIOSApp extends IOSApp {
360 361 362
  BuildableIOSApp(this.project, String projectBundleId, String hostAppBundleName)
    : _hostAppBundleName = hostAppBundleName,
      super(projectBundleId: projectBundleId);
363

364 365 366
  static Future<BuildableIOSApp> fromProject(IosProject project, BuildInfo buildInfo) async {
    final String projectBundleId = await project.productBundleIdentifier(buildInfo);
    final String hostAppBundleName = await project.hostAppBundleName(buildInfo);
367
    return BuildableIOSApp(project, projectBundleId, hostAppBundleName);
368
  }
369

370
  final IosProject project;
371

372 373
  final String _hostAppBundleName;

374
  @override
375
  String get name => _hostAppBundleName;
376 377

  @override
378 379
  String get simulatorBundlePath => _buildAppPath('iphonesimulator');

380
  @override
381 382 383
  String get deviceBundlePath => _buildAppPath('iphoneos');

  String _buildAppPath(String type) {
384
    return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
385
  }
386 387
}

388 389 390 391
class PrebuiltIOSApp extends IOSApp {
  PrebuiltIOSApp({
    this.bundleDir,
    this.bundleName,
392
    @required String projectBundleId,
393 394
  }) : super(projectBundleId: projectBundleId);

395 396 397
  final Directory bundleDir;
  final String bundleName;

398 399 400 401 402 403 404 405 406 407 408 409
  @override
  String get name => bundleName;

  @override
  String get simulatorBundlePath => _bundlePath;

  @override
  String get deviceBundlePath => _bundlePath;

  String get _bundlePath => bundleDir.path;
}

410
class ApplicationPackageStore {
411
  ApplicationPackageStore({ this.android, this.iOS, this.fuchsia });
412

413 414
  AndroidApk android;
  IOSApp iOS;
415
  FuchsiaApp fuchsia;
416 417 418
  LinuxApp linux;
  MacOSApp macOS;
  WindowsApp windows;
419

420 421 422 423
  Future<ApplicationPackage> getPackageForPlatform(
    TargetPlatform platform,
    BuildInfo buildInfo,
  ) async {
424
    switch (platform) {
425
      case TargetPlatform.android:
426
      case TargetPlatform.android_arm:
427
      case TargetPlatform.android_arm64:
428
      case TargetPlatform.android_x64:
429
      case TargetPlatform.android_x86:
430
        android ??= await AndroidApk.fromAndroidProject(FlutterProject.current().android);
431
        return android;
432
      case TargetPlatform.ios:
433
        iOS ??= await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo);
434
        return iOS;
435 436
      case TargetPlatform.fuchsia_arm64:
      case TargetPlatform.fuchsia_x64:
437 438
        fuchsia ??= FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia);
        return fuchsia;
439
      case TargetPlatform.darwin_x64:
440 441
        macOS ??= MacOSApp.fromMacOSProject(FlutterProject.current().macos);
        return macOS;
442
      case TargetPlatform.linux_x64:
443 444
        linux ??= LinuxApp.fromLinuxProject(FlutterProject.current().linux);
        return linux;
445
      case TargetPlatform.windows_x64:
446 447
        windows ??= WindowsApp.fromWindowsProject(FlutterProject.current().windows);
        return windows;
448
      case TargetPlatform.tester:
449
      case TargetPlatform.web_javascript:
450 451
        return null;
    }
pq's avatar
pq committed
452
    return null;
453 454
  }
}
455

456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
class _Entry {
  _Element parent;
  int level;
}

class _Element extends _Entry {
  _Element.fromLine(String line, _Element parent) {
    //      E: application (line=29)
    final List<String> parts = line.trimLeft().split(' ');
    name = parts[1];
    level = line.length - line.trimLeft().length;
    this.parent = parent;
    children = <_Entry>[];
  }

471 472 473
  List<_Entry> children;
  String name;

474 475 476 477 478
  void addChild(_Entry child) {
    children.add(child);
  }

  _Attribute firstAttribute(String name) {
479 480
    return children.whereType<_Attribute>().firstWhere(
        (_Attribute e) => e.key.startsWith(name),
481 482 483 484 485
        orElse: () => null,
    );
  }

  _Element firstElement(String name) {
486 487
    return children.whereType<_Element>().firstWhere(
        (_Element e) => e.name.startsWith(name),
488 489 490 491
        orElse: () => null,
    );
  }

492 493
  Iterable<_Element> allElements(String name) {
    return children.whereType<_Element>().where((_Element e) => e.name.startsWith(name));
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
  }
}

class _Attribute extends _Entry {
  _Attribute.fromLine(String line, _Element parent) {
    //     A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
    const String attributePrefix = 'A: ';
    final List<String> keyVal = line
        .substring(line.indexOf(attributePrefix) + attributePrefix.length)
        .split('=');
    key = keyVal[0];
    value = keyVal[1];
    level = line.length - line.trimLeft().length;
    this.parent = parent;
  }
509 510 511

  String key;
  String value;
512 513
}

514 515 516
class ApkManifestData {
  ApkManifestData._(this._data);

517 518
  static bool isAttributeWithValuePresent(_Element baseElement,
      String childElement, String attributeName, String attributeValue) {
519
    final Iterable<_Element> allElements = baseElement.allElements(childElement);
520
    for (final _Element oneElement in allElements) {
521 522 523 524 525 526 527 528 529 530 531
      final String elementAttributeValue = oneElement
          ?.firstAttribute(attributeName)
          ?.value;
      if (elementAttributeValue != null &&
          elementAttributeValue.startsWith(attributeValue)) {
        return true;
      }
    }
    return false;
  }

532
  static ApkManifestData parseFromXmlDump(String data) {
533
    if (data == null || data.trim().isEmpty) {
534
      return null;
535
    }
536

537 538
    final List<String> lines = data.split('\n');
    assert(lines.length > 3);
539

540 541
    final int manifestLine = lines.indexWhere((String line) => line.contains('E: manifest'));
    final _Element manifest = _Element.fromLine(lines[manifestLine], null);
542
    _Element currentElement = manifest;
543

544
    for (final String line in lines.skip(manifestLine)) {
545 546
      final String trimLine = line.trimLeft();
      final int level = line.length - trimLine.length;
547

548
      // Handle level out
549
      while (currentElement.parent != null && level <= currentElement.level) {
550 551
        currentElement = currentElement.parent;
      }
552

553 554 555 556
      if (level > currentElement.level) {
        switch (trimLine[0]) {
          case 'A':
            currentElement
557
                .addChild(_Attribute.fromLine(line, currentElement));
558 559
            break;
          case 'E':
560
            final _Element element = _Element.fromLine(line, currentElement);
561 562
            currentElement.addChild(element);
            currentElement = element;
563 564 565 566
        }
      }
    }

567
    final _Element application = manifest.firstElement('application');
568 569 570
    if (application == null) {
      return null;
    }
571

572
    final Iterable<_Element> activities = application.allElements('activity');
573 574

    _Element launchActivity;
575
    for (final _Element activity in activities) {
576
      final _Attribute enabled = activity.firstAttribute('android:enabled');
577
      final Iterable<_Element> intentFilters = activity.allElements('intent-filter');
578 579 580 581 582 583
      final bool isEnabledByDefault = enabled == null;
      final bool isExplicitlyEnabled = enabled != null && enabled.value.contains('0xffffffff');
      if (!(isEnabledByDefault || isExplicitlyEnabled)) {
        continue;
      }

584
      for (final _Element element in intentFilters) {
585 586 587 588 589 590 591 592 593 594
        final bool isMainAction = isAttributeWithValuePresent(
            element, 'action', 'android:name', '"android.intent.action.MAIN"');
        if (!isMainAction) {
          continue;
        }
        final bool isLauncherCategory = isAttributeWithValuePresent(
            element, 'category', 'android:name',
            '"android.intent.category.LAUNCHER"');
        if (!isLauncherCategory) {
          continue;
595
        }
596 597
        launchActivity = activity;
        break;
598 599
      }
      if (launchActivity != null) {
600 601 602 603 604 605 606 607 608
        break;
      }
    }

    final _Attribute package = manifest.firstAttribute('package');
    // "io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
    final String packageName = package.value.substring(1, package.value.indexOf('" '));

    if (launchActivity == null) {
609
      globals.printError('Error running $packageName. Default activity not found');
610 611 612 613 614 615 616 617
      return null;
    }

    final _Attribute nameAttribute = launchActivity.firstAttribute('android:name');
    // "io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
    final String activityName = nameAttribute
        .value.substring(1, nameAttribute.value.indexOf('" '));

618 619 620
    // Example format: (type 0x10)0x1
    final _Attribute versionCodeAttr = manifest.firstAttribute('android:versionCode');
    if (versionCodeAttr == null) {
621
      globals.printError('Error running $packageName. Manifest versionCode not found');
622 623 624
      return null;
    }
    if (!versionCodeAttr.value.startsWith('(type 0x10)')) {
625
      globals.printError('Error running $packageName. Manifest versionCode invalid');
626 627 628 629
      return null;
    }
    final int versionCode = int.tryParse(versionCodeAttr.value.substring(11));
    if (versionCode == null) {
630
      globals.printError('Error running $packageName. Manifest versionCode invalid');
631 632 633
      return null;
    }

634 635
    final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
    map['package'] = <String, String>{'name': packageName};
636
    map['version-code'] = <String, String>{'name': versionCode.toString()};
637 638
    map['launchable-activity'] = <String, String>{'name': activityName};

639
    return ApkManifestData._(map);
640 641 642 643
  }

  final Map<String, Map<String, String>> _data;

644 645
  @visibleForTesting
  Map<String, Map<String, String>> get data =>
646
      UnmodifiableMapView<String, Map<String, String>>(_data);
647

648 649
  String get packageName => _data['package'] == null ? null : _data['package']['name'];

650 651
  String get versionCode => _data['version-code'] == null ? null : _data['version-code']['name'];

652 653 654 655 656 657 658
  String get launchableActivityName {
    return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
  }

  @override
  String toString() => _data.toString();
}