application_package.dart 17.9 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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/os.dart' show os;
17
import 'base/process.dart';
18
import 'base/user_messages.dart';
19
import 'build_info.dart';
20
import 'globals.dart';
21
import 'ios/ios_workflow.dart';
22
import 'ios/plist_utils.dart' as plist;
23
import 'macos/application_package.dart';
24
import 'project.dart';
25
import 'tester/flutter_tester.dart';
26
import 'web/web_device.dart';
27

28 29 30 31
class ApplicationPackageFactory {
  static ApplicationPackageFactory get instance => context[ApplicationPackageFactory];

  Future<ApplicationPackage> getPackageForPlatform(
32 33 34
    TargetPlatform platform, {
    File applicationBinary,
  }) async {
35 36 37 38 39
    switch (platform) {
      case TargetPlatform.android_arm:
      case TargetPlatform.android_arm64:
      case TargetPlatform.android_x64:
      case TargetPlatform.android_x86:
40 41 42
        if (androidSdk?.licensesAvailable == true  && androidSdk.latestVersion == null) {
          await checkGradleDependencies();
        }
43 44 45 46 47 48 49 50 51 52
        return applicationBinary == null
            ? await AndroidApk.fromAndroidProject((await FlutterProject.current()).android)
            : AndroidApk.fromApk(applicationBinary);
      case TargetPlatform.ios:
        return applicationBinary == null
            ? IOSApp.fromIosProject((await FlutterProject.current()).ios)
            : IOSApp.fromPrebuiltApp(applicationBinary);
      case TargetPlatform.tester:
        return FlutterTesterApp.fromCurrentDirectory();
      case TargetPlatform.darwin_x64:
53 54 55
        return applicationBinary != null
          ? MacOSApp.fromPrebuiltApp(applicationBinary)
          : null;
56 57
      case TargetPlatform.web:
        return WebApplicationPackage(await FlutterProject.current());
58 59 60 61 62 63 64 65 66 67
      case TargetPlatform.linux_x64:
      case TargetPlatform.windows_x64:
      case TargetPlatform.fuchsia:
        return null;
    }
    assert(platform != null);
    return null;
  }
}

68
abstract class ApplicationPackage {
69 70
  ApplicationPackage({ @required this.id })
    : assert(id != null);
71

72 73 74
  /// Package ID from the Android Manifest or equivalent.
  final String id;

75 76
  String get name;

77 78
  String get displayName => name;

79
  File get packagesFile => null;
80

81
  @override
82
  String toString() => displayName ?? id;
83 84 85
}

class AndroidApk extends ApplicationPackage {
86
  AndroidApk({
Adam Barth's avatar
Adam Barth committed
87
    String id,
88
    @required this.file,
89
    @required this.versionCode,
90
    @required this.launchActivity,
91
  }) : assert(file != null),
92 93
       assert(launchActivity != null),
       super(id: id);
94

95
  /// Creates a new AndroidApk from an existing APK.
96
  factory AndroidApk.fromApk(File apk) {
97
    final String aaptPath = androidSdk?.latestVersion?.aaptPath;
98
    if (aaptPath == null) {
99
      printError(userMessages.aaptNotFound);
100 101 102
      return null;
    }

103
    final List<String> aaptArgs = <String>[
104 105 106
       aaptPath,
      'dump',
      'xmltree',
107
      apk.path,
108 109 110 111 112
      'AndroidManifest.xml',
    ];

    final ApkManifestData data = ApkManifestData
        .parseFromXmlDump(runCheckedSync(aaptArgs));
113 114

    if (data == null) {
115
      printError('Unable to read manifest info from ${apk.path}.');
116 117 118 119
      return null;
    }

    if (data.packageName == null || data.launchableActivityName == null) {
120
      printError('Unable to read manifest info from ${apk.path}.');
121 122 123
      return null;
    }

124
    return AndroidApk(
125
      id: data.packageName,
126
      file: apk,
127
      versionCode: int.tryParse(data.versionCode),
128
      launchActivity: '${data.packageName}/${data.launchableActivityName}',
129 130 131
    );
  }

132 133 134 135 136 137
  /// Path to the actual apk file.
  final File file;

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

138 139 140
  /// The version code of the APK.
  final int versionCode;

141
  /// Creates a new AndroidApk based on the information in the Android manifest.
142 143
  static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
    File apkFile;
144

145
    if (androidProject.isUsingGradle) {
146 147
      apkFile = await getGradleAppOut(androidProject);
      if (apkFile.existsSync()) {
148 149
        // Grab information from the .apk. The gradle build script might alter
        // the application Id, so we need to look at what was actually built.
150
        return AndroidApk.fromApk(apkFile);
151 152 153 154
      }
      // 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.
155
    } else {
156
      apkFile = fs.file(fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
157 158
    }

159
    final File manifest = androidProject.appManifestFile;
160 161

    if (!manifest.existsSync())
162
      return null;
163

164
    final String manifestString = manifest.readAsStringSync();
165 166 167 168 169 170 171 172 173 174 175 176 177 178
    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()}');
    }
179

180
    final Iterable<xml.XmlElement> manifests = document.findElements('manifest');
181 182
    if (manifests.isEmpty)
      return null;
183
    final String packageId = manifests.first.getAttribute('package');
184 185

    String launchActivity;
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    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) {
          if (!(node is xml.XmlElement)) {
            continue;
          }
          final xml.XmlElement xmlElement = node;
          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) {
208 209 210 211
          final String activityName = activity.getAttribute('android:name');
          launchActivity = '$packageId/$activityName';
          break;
        }
212 213
      }
    }
214 215

    if (packageId == null || launchActivity == null)
216 217
      return null;

218
    return AndroidApk(
219
      id: packageId,
220
      file: apkFile,
221
      versionCode: null,
222
      launchActivity: launchActivity,
223
    );
224
  }
225

226
  @override
227
  File get packagesFile => file;
228

229
  @override
230
  String get name => file.basename;
231 232
}

233 234 235 236 237
/// Tests whether a [FileSystemEntity] is an iOS bundle directory
bool _isBundleDirectory(FileSystemEntity entity) =>
    entity is Directory && entity.path.endsWith('.app');

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

240
  /// Creates a new IOSApp from an existing app bundle or IPA.
241 242
  factory IOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
    final FileSystemEntityType entityType = fs.typeSync(applicationBinary.path);
243 244
    if (entityType == FileSystemEntityType.notFound) {
      printError(
245
          'File "${applicationBinary.path}" does not exist. Use an app bundle or an ipa.');
246 247
      return null;
    }
248
    Directory bundleDir;
249 250 251
    if (entityType == FileSystemEntityType.directory) {
      final Directory directory = fs.directory(applicationBinary);
      if (!_isBundleDirectory(directory)) {
252
        printError('Folder "${applicationBinary.path}" is not an app bundle.');
253 254 255 256 257
        return null;
      }
      bundleDir = fs.directory(applicationBinary);
    } else {
      // Try to unpack as an ipa.
258
      final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_app.');
259 260 261
      addShutdownHook(() async {
        await tempDir.delete(recursive: true);
      }, ShutdownStage.STILL_RECORDING);
262
      os.unzip(fs.file(applicationBinary), tempDir);
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
      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 {
        bundleDir = payloadDir.listSync().singleWhere(_isBundleDirectory);
      } on StateError {
        printError(
            'Invalid prebuilt iOS ipa. Does not contain a single app bundle.');
        return null;
      }
278
    }
279
    final String plistPath = fs.path.join(bundleDir.path, 'Info.plist');
280 281 282 283 284 285 286 287 288 289
    if (!fs.file(plistPath).existsSync()) {
      printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
      return null;
    }
    final String id = iosWorkflow.getPlistValueFromFile(
      plistPath,
      plist.kCFBundleIdentifierKey,
    );
    if (id == null) {
      printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
290
      return null;
291
    }
292

293
    return PrebuiltIOSApp(
294
      bundleDir: bundleDir,
295
      bundleName: fs.path.basename(bundleDir.path),
296 297 298
      projectBundleId: id,
    );
  }
299

300
  factory IOSApp.fromIosProject(IosProject project) {
301
    if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
302
      return null;
303
    return BuildableIOSApp(project);
304
  }
305

306
  @override
307
  String get displayName => id;
308

309 310 311 312 313 314
  String get simulatorBundlePath;

  String get deviceBundlePath;
}

class BuildableIOSApp extends IOSApp {
315
  BuildableIOSApp(this.project) : super(projectBundleId: project.productBundleIdentifier);
316

317
  final IosProject project;
318

319
  @override
320
  String get name => project.hostAppBundleName;
321 322

  @override
323 324
  String get simulatorBundlePath => _buildAppPath('iphonesimulator');

325
  @override
326 327 328
  String get deviceBundlePath => _buildAppPath('iphoneos');

  String _buildAppPath(String type) {
329
    return fs.path.join(getIosBuildDirectory(), type, name);
330
  }
331 332
}

333 334 335 336
class PrebuiltIOSApp extends IOSApp {
  PrebuiltIOSApp({
    this.bundleDir,
    this.bundleName,
337
    @required String projectBundleId,
338 339
  }) : super(projectBundleId: projectBundleId);

340 341 342
  final Directory bundleDir;
  final String bundleName;

343 344 345 346 347 348 349 350 351 352 353 354
  @override
  String get name => bundleName;

  @override
  String get simulatorBundlePath => _bundlePath;

  @override
  String get deviceBundlePath => _bundlePath;

  String get _bundlePath => bundleDir.path;
}

355
class ApplicationPackageStore {
356 357
  ApplicationPackageStore({ this.android, this.iOS });

358 359
  AndroidApk android;
  IOSApp iOS;
360

361
  Future<ApplicationPackage> getPackageForPlatform(TargetPlatform platform) async {
362
    switch (platform) {
363
      case TargetPlatform.android_arm:
364
      case TargetPlatform.android_arm64:
365
      case TargetPlatform.android_x64:
366
      case TargetPlatform.android_x86:
367
        android ??= await AndroidApk.fromAndroidProject((await FlutterProject.current()).android);
368
        return android;
369
      case TargetPlatform.ios:
370
        iOS ??= IOSApp.fromIosProject((await FlutterProject.current()).ios);
371
        return iOS;
372 373
      case TargetPlatform.darwin_x64:
      case TargetPlatform.linux_x64:
374
      case TargetPlatform.windows_x64:
375
      case TargetPlatform.fuchsia:
376
      case TargetPlatform.tester:
377
      case TargetPlatform.web:
378 379
        return null;
    }
pq's avatar
pq committed
380
    return null;
381 382
  }
}
383

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
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>[];
  }

399 400 401
  List<_Entry> children;
  String name;

402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
  void addChild(_Entry child) {
    children.add(child);
  }

  _Attribute firstAttribute(String name) {
    return children.firstWhere(
        (_Entry e) => e is _Attribute && e.key.startsWith(name),
        orElse: () => null,
    );
  }

  _Element firstElement(String name) {
    return children.firstWhere(
        (_Entry e) => e is _Element && e.name.startsWith(name),
        orElse: () => null,
    );
  }

  Iterable<_Entry> allElements(String name) {
    return children.where(
            (_Entry e) => e is _Element && e.name.startsWith(name));
  }
}

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;
  }
438 439 440

  String key;
  String value;
441 442
}

443 444 445
class ApkManifestData {
  ApkManifestData._(this._data);

446
  static ApkManifestData parseFromXmlDump(String data) {
447 448 449
    if (data == null || data.trim().isEmpty)
      return null;

450 451
    final List<String> lines = data.split('\n');
    assert(lines.length > 3);
452

453
    final _Element manifest = _Element.fromLine(lines[1], null);
454
    _Element currentElement = manifest;
455

456 457 458
    for (String line in lines.skip(2)) {
      final String trimLine = line.trimLeft();
      final int level = line.length - trimLine.length;
459

460
      // Handle level out
461
      while (level <= currentElement.level) {
462 463
        currentElement = currentElement.parent;
      }
464

465 466 467 468
      if (level > currentElement.level) {
        switch (trimLine[0]) {
          case 'A':
            currentElement
469
                .addChild(_Attribute.fromLine(line, currentElement));
470 471
            break;
          case 'E':
472
            final _Element element = _Element.fromLine(line, currentElement);
473 474
            currentElement.addChild(element);
            currentElement = element;
475 476 477 478
        }
      }
    }

479 480 481 482 483 484 485 486
    final _Element application = manifest.firstElement('application');
    assert(application != null);

    final Iterable<_Entry> activities = application.allElements('activity');

    _Element launchActivity;
    for (_Element activity in activities) {
      final _Attribute enabled = activity.firstAttribute('android:enabled');
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
      final Iterable<_Element> intentFilters = activity
          .allElements('intent-filter')
          .cast<_Element>();
      final bool isEnabledByDefault = enabled == null;
      final bool isExplicitlyEnabled = enabled != null && enabled.value.contains('0xffffffff');
      if (!(isEnabledByDefault || isExplicitlyEnabled)) {
        continue;
      }

      for (_Element element in intentFilters) {
        final _Element action = element.firstElement('action');
        final _Element category = element.firstElement('category');
        final String actionAttributeValue = action
            ?.firstAttribute('android:name')
            ?.value;
        final String categoryAttributeValue =
            category?.firstAttribute('android:name')?.value;
        final bool isMainAction = actionAttributeValue != null &&
            actionAttributeValue.startsWith('"android.intent.action.MAIN"');
        final bool isLauncherCategory = categoryAttributeValue != null &&
            categoryAttributeValue.startsWith('"android.intent.category.LAUNCHER"');
        if (isMainAction && isLauncherCategory) {
          launchActivity = activity;
          break;
        }
      }
      if (launchActivity != null) {
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
        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('" '));

532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
    // 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;
    }

548 549
    final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
    map['package'] = <String, String>{'name': packageName};
550
    map['version-code'] = <String, String>{'name': versionCode.toString()};
551 552
    map['launchable-activity'] = <String, String>{'name': activityName};

553
    return ApkManifestData._(map);
554 555 556 557
  }

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

558 559
  @visibleForTesting
  Map<String, Map<String, String>> get data =>
560
      UnmodifiableMapView<String, Map<String, String>>(_data);
561

562 563
  String get packageName => _data['package'] == null ? null : _data['package']['name'];

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

566 567 568 569 570 571 572
  String get launchableActivityName {
    return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
  }

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