application_package.dart 21 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';
9
import 'package:xml/xml.dart' as xml;
10

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

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

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

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

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

91 92
  String get name;

93 94
  String get displayName => name;

95
  File get packagesFile => null;
96

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

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

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

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

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

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

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

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

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

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

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

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

169
    if (androidProject.isUsingGradle) {
170 171
      apkFile = await getGradleAppOut(androidProject);
      if (apkFile.existsSync()) {
172 173
        // Grab information from the .apk. The gradle build script might alter
        // the application Id, so we need to look at what was actually built.
174
        return AndroidApk.fromApk(apkFile);
175 176 177 178
      }
      // 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.
179
    } else {
180
      apkFile = fs.file(fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
181 182
    }

183
    final File manifest = androidProject.appManifestFile;
184

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

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

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

    String launchActivity;
216 217 218 219 220 221 222 223 224 225
    for (xml.XmlElement activity in document.findAllElements('activity')) {
      final String enabled = activity.getAttribute('android:enabled');
      if (enabled != null && enabled == 'false') {
        continue;
      }

      for (xml.XmlElement element in activity.findElements('intent-filter')) {
        String actionName = '';
        String categoryName = '';
        for (xml.XmlNode node in element.children) {
226
          if (node is! xml.XmlElement) {
227 228
            continue;
          }
229
          final xml.XmlElement xmlElement = node as xml.XmlElement;
230 231 232 233 234 235 236 237
          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) {
238 239 240 241
          final String activityName = activity.getAttribute('android:name');
          launchActivity = '$packageId/$activityName';
          break;
        }
242 243
      }
    }
244

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

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

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

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

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

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

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

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

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

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

355 356 357 358 359 360
  String get simulatorBundlePath;

  String get deviceBundlePath;
}

class BuildableIOSApp extends IOSApp {
361
  BuildableIOSApp(this.project, String projectBundleId)
362 363 364 365
    : super(projectBundleId: projectBundleId);

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

369
  final IosProject project;
370

371
  @override
372
  String get name => project.hostAppBundleName;
373 374

  @override
375 376
  String get simulatorBundlePath => _buildAppPath('iphonesimulator');

377
  @override
378 379 380
  String get deviceBundlePath => _buildAppPath('iphoneos');

  String _buildAppPath(String type) {
381
    return fs.path.join(getIosBuildDirectory(), type, name);
382
  }
383 384
}

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

392 393 394
  final Directory bundleDir;
  final String bundleName;

395 396 397 398 399 400 401 402 403 404 405 406
  @override
  String get name => bundleName;

  @override
  String get simulatorBundlePath => _bundlePath;

  @override
  String get deviceBundlePath => _bundlePath;

  String get _bundlePath => bundleDir.path;
}

407
class ApplicationPackageStore {
408
  ApplicationPackageStore({ this.android, this.iOS, this.fuchsia });
409

410 411
  AndroidApk android;
  IOSApp iOS;
412
  FuchsiaApp fuchsia;
413 414 415
  LinuxApp linux;
  MacOSApp macOS;
  WindowsApp windows;
416

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

450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
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>[];
  }

465 466 467
  List<_Entry> children;
  String name;

468 469 470 471 472
  void addChild(_Entry child) {
    children.add(child);
  }

  _Attribute firstAttribute(String name) {
473 474
    return children.whereType<_Attribute>().firstWhere(
        (_Attribute e) => e.key.startsWith(name),
475 476 477 478 479
        orElse: () => null,
    );
  }

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

486 487
  Iterable<_Element> allElements(String name) {
    return children.whereType<_Element>().where((_Element e) => e.name.startsWith(name));
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
  }
}

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;
  }
503 504 505

  String key;
  String value;
506 507
}

508 509 510
class ApkManifestData {
  ApkManifestData._(this._data);

511 512
  static bool isAttributeWithValuePresent(_Element baseElement,
      String childElement, String attributeName, String attributeValue) {
513
    final Iterable<_Element> allElements = baseElement.allElements(childElement);
514 515 516 517 518 519 520 521 522 523 524 525
    for (_Element oneElement in allElements) {
      final String elementAttributeValue = oneElement
          ?.firstAttribute(attributeName)
          ?.value;
      if (elementAttributeValue != null &&
          elementAttributeValue.startsWith(attributeValue)) {
        return true;
      }
    }
    return false;
  }

526
  static ApkManifestData parseFromXmlDump(String data) {
527
    if (data == null || data.trim().isEmpty) {
528
      return null;
529
    }
530

531 532
    final List<String> lines = data.split('\n');
    assert(lines.length > 3);
533

534 535
    final int manifestLine = lines.indexWhere((String line) => line.contains('E: manifest'));
    final _Element manifest = _Element.fromLine(lines[manifestLine], null);
536
    _Element currentElement = manifest;
537

538
    for (String line in lines.skip(manifestLine)) {
539 540
      final String trimLine = line.trimLeft();
      final int level = line.length - trimLine.length;
541

542
      // Handle level out
543
      while (currentElement.parent != null && level <= currentElement.level) {
544 545
        currentElement = currentElement.parent;
      }
546

547 548 549 550
      if (level > currentElement.level) {
        switch (trimLine[0]) {
          case 'A':
            currentElement
551
                .addChild(_Attribute.fromLine(line, currentElement));
552 553
            break;
          case 'E':
554
            final _Element element = _Element.fromLine(line, currentElement);
555 556
            currentElement.addChild(element);
            currentElement = element;
557 558 559 560
        }
      }
    }

561 562 563
    final _Element application = manifest.firstElement('application');
    assert(application != null);

564
    final Iterable<_Element> activities = application.allElements('activity');
565 566 567 568

    _Element launchActivity;
    for (_Element activity in activities) {
      final _Attribute enabled = activity.firstAttribute('android:enabled');
569
      final Iterable<_Element> intentFilters = activity.allElements('intent-filter');
570 571 572 573 574 575 576
      final bool isEnabledByDefault = enabled == null;
      final bool isExplicitlyEnabled = enabled != null && enabled.value.contains('0xffffffff');
      if (!(isEnabledByDefault || isExplicitlyEnabled)) {
        continue;
      }

      for (_Element element in intentFilters) {
577 578 579 580 581 582 583 584 585 586
        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;
587
        }
588 589
        launchActivity = activity;
        break;
590 591
      }
      if (launchActivity != null) {
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
        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) {
      printError('Error running $packageName. Default activity not found');
      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('" '));

610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625
    // Example format: (type 0x10)0x1
    final _Attribute versionCodeAttr = manifest.firstAttribute('android:versionCode');
    if (versionCodeAttr == null) {
      printError('Error running $packageName. Manifest versionCode not found');
      return null;
    }
    if (!versionCodeAttr.value.startsWith('(type 0x10)')) {
      printError('Error running $packageName. Manifest versionCode invalid');
      return null;
    }
    final int versionCode = int.tryParse(versionCodeAttr.value.substring(11));
    if (versionCode == null) {
      printError('Error running $packageName. Manifest versionCode invalid');
      return null;
    }

626 627
    final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
    map['package'] = <String, String>{'name': packageName};
628
    map['version-code'] = <String, String>{'name': versionCode.toString()};
629 630
    map['launchable-activity'] = <String, String>{'name': activityName};

631
    return ApkManifestData._(map);
632 633 634 635
  }

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

636 637
  @visibleForTesting
  Map<String, Map<String, String>> get data =>
638
      UnmodifiableMapView<String, Map<String, String>>(_data);
639

640 641
  String get packageName => _data['package'] == null ? null : _data['package']['name'];

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

644 645 646 647 648 649 650
  String get launchableActivityName {
    return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
  }

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