application_package.dart 15.1 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/file_system.dart';
14
import 'base/os.dart' show os;
15
import 'base/process.dart';
16
import 'build_info.dart';
17
import 'globals.dart';
18
import 'ios/ios_workflow.dart';
19
import 'ios/plist_utils.dart' as plist;
20
import 'ios/xcodeproj.dart';
21
import 'tester/flutter_tester.dart';
22

23 24
abstract class ApplicationPackage {
  /// Package ID from the Android Manifest or equivalent.
25
  final String id;
26

27 28
  ApplicationPackage({ @required this.id })
    : assert(id != null);
29

30 31
  String get name;

32 33
  String get displayName => name;

34
  String get packagePath => null;
35

36
  @override
37
  String toString() => displayName;
38 39 40
}

class AndroidApk extends ApplicationPackage {
41
  /// Path to the actual apk file.
42
  final String apkPath;
43

44
  /// The path to the activity that should be launched.
45 46 47
  final String launchActivity;

  AndroidApk({
Adam Barth's avatar
Adam Barth committed
48
    String id,
49
    @required this.apkPath,
50
    @required this.launchActivity
51
  }) : assert(apkPath != null),
52 53
       assert(launchActivity != null),
       super(id: id);
54

55
  /// Creates a new AndroidApk from an existing APK.
56
  factory AndroidApk.fromApk(String applicationBinary) {
57
    final String aaptPath = androidSdk?.latestVersion?.aaptPath;
58 59 60 61 62
    if (aaptPath == null) {
      printError('Unable to locate the Android SDK; please run \'flutter doctor\'.');
      return null;
    }

63 64 65 66
     final List<String> aaptArgs = <String>[
       aaptPath,
      'dump',
      'xmltree',
67
      applicationBinary,
68 69 70 71 72
      'AndroidManifest.xml',
    ];

    final ApkManifestData data = ApkManifestData
        .parseFromXmlDump(runCheckedSync(aaptArgs));
73 74

    if (data == null) {
75
      printError('Unable to read manifest info from $applicationBinary.');
76 77 78 79
      return null;
    }

    if (data.packageName == null || data.launchableActivityName == null) {
80
      printError('Unable to read manifest info from $applicationBinary.');
81 82 83 84 85
      return null;
    }

    return new AndroidApk(
      id: data.packageName,
86
      apkPath: applicationBinary,
87 88 89 90
      launchActivity: '${data.packageName}/${data.launchableActivityName}'
    );
  }

91
  /// Creates a new AndroidApk based on the information in the Android manifest.
92 93 94
  static Future<AndroidApk> fromCurrentDirectory() async {
    String manifestPath;
    String apkPath;
95

96 97 98
    if (isProjectUsingGradle()) {
      apkPath = await getGradleAppOut();
      if (fs.file(apkPath).existsSync()) {
99 100
        // Grab information from the .apk. The gradle build script might alter
        // the application Id, so we need to look at what was actually built.
101
        return new AndroidApk.fromApk(apkPath);
102 103 104 105
      }
      // 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.
106
      manifestPath = gradleManifestPath;
107
    } else {
108 109
      manifestPath = fs.path.join('android', 'AndroidManifest.xml');
      apkPath = fs.path.join(getAndroidBuildDirectory(), 'app.apk');
110 111
    }

112
    if (!fs.isFileSync(manifestPath))
113
      return null;
114

115
    final String manifestString = fs.file(manifestPath).readAsStringSync();
116
    final xml.XmlDocument document = xml.parse(manifestString);
117

118
    final Iterable<xml.XmlElement> manifests = document.findElements('manifest');
119 120
    if (manifests.isEmpty)
      return null;
121
    final String packageId = manifests.first.getAttribute('package');
122 123 124 125

    String launchActivity;
    for (xml.XmlElement category in document.findAllElements('category')) {
      if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
126
        final xml.XmlElement activity = category.parent.parent;
127 128 129 130 131 132
        final String enabled = activity.getAttribute('android:enabled');
        if (enabled == null || enabled == 'true') {
          final String activityName = activity.getAttribute('android:name');
          launchActivity = '$packageId/$activityName';
          break;
        }
133 134
      }
    }
135 136

    if (packageId == null || launchActivity == null)
137 138
      return null;

139
    return new AndroidApk(
140
      id: packageId,
141
      apkPath: apkPath,
142 143
      launchActivity: launchActivity
    );
144
  }
145

146
  @override
147
  String get packagePath => apkPath;
148

149
  @override
150
  String get name => fs.path.basename(apkPath);
151 152
}

153 154 155 156 157
/// 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 {
158
  IOSApp({@required String projectBundleId}) : super(id: projectBundleId);
159

160
  /// Creates a new IOSApp from an existing app bundle or IPA.
161 162
  factory IOSApp.fromPrebuiltApp(String applicationBinary) {
    final FileSystemEntityType entityType = fs.typeSync(applicationBinary);
163 164
    if (entityType == FileSystemEntityType.notFound) {
      printError(
165
          'File "$applicationBinary" does not exist. Use an app bundle or an ipa.');
166 167
      return null;
    }
168
    Directory bundleDir;
169 170 171
    if (entityType == FileSystemEntityType.directory) {
      final Directory directory = fs.directory(applicationBinary);
      if (!_isBundleDirectory(directory)) {
172
        printError('Folder "$applicationBinary" is not an app bundle.');
173 174 175 176 177 178 179
        return null;
      }
      bundleDir = fs.directory(applicationBinary);
    } else {
      // Try to unpack as an ipa.
      final Directory tempDir = fs.systemTempDirectory.createTempSync(
          'flutter_app_');
180 181 182
      addShutdownHook(() async {
        await tempDir.delete(recursive: true);
      }, ShutdownStage.STILL_RECORDING);
183
      os.unzip(fs.file(applicationBinary), tempDir);
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
      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;
      }
199
    }
200
    final String plistPath = fs.path.join(bundleDir.path, 'Info.plist');
201 202 203 204 205 206 207 208 209 210
    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');
211
      return null;
212
    }
213 214 215

    return new PrebuiltIOSApp(
      bundleDir: bundleDir,
216
      bundleName: fs.path.basename(bundleDir.path),
217 218 219
      projectBundleId: id,
    );
  }
220

221 222
  factory IOSApp.fromCurrentDirectory() {
    if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
223 224
      return null;

225
    final String plistPath = fs.path.join('ios', 'Runner', 'Info.plist');
226 227 228 229
    String id = iosWorkflow.getPlistValueFromFile(
      plistPath,
      plist.kCFBundleIdentifierKey,
    );
230
    if (id == null || !xcodeProjectInterpreter.isInstalled)
231
      return null;
232
    final String projectPath = fs.path.join('ios', 'Runner.xcodeproj');
233
    final Map<String, String> buildSettings = xcodeProjectInterpreter.getBuildSettings(projectPath, 'Runner');
234
    id = substituteXcodeVariables(id, buildSettings);
235

236
    return new BuildableIOSApp(
237
      appDirectory: 'ios',
238 239
      projectBundleId: id,
      buildSettings: buildSettings,
240
    );
241
  }
242

243
  @override
244
  String get displayName => id;
245

246 247 248 249 250 251
  String get simulatorBundlePath;

  String get deviceBundlePath;
}

class BuildableIOSApp extends IOSApp {
252
  static const String kBundleName = 'Runner.app';
253 254 255 256

  BuildableIOSApp({
    this.appDirectory,
    String projectBundleId,
257
    this.buildSettings,
258 259
  }) : super(projectBundleId: projectBundleId);

260 261
  final String appDirectory;

xster's avatar
xster committed
262 263 264 265 266
  /// Build settings of the app's Xcode project.
  ///
  /// These are the build settings as specified in the Xcode project files.
  ///
  /// Build settings may change depending on the parameters passed while building.
267 268
  final Map<String, String> buildSettings;

269 270 271 272
  @override
  String get name => kBundleName;

  @override
273 274
  String get simulatorBundlePath => _buildAppPath('iphonesimulator');

275
  @override
276 277
  String get deviceBundlePath => _buildAppPath('iphoneos');

278 279 280
  /// True if the app is built from a Swift project. Null if unknown.
  bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION');

281
  String _buildAppPath(String type) {
282
    return fs.path.join(getIosBuildDirectory(), type, kBundleName);
283
  }
284 285
}

286 287 288 289 290 291 292
class PrebuiltIOSApp extends IOSApp {
  final Directory bundleDir;
  final String bundleName;

  PrebuiltIOSApp({
    this.bundleDir,
    this.bundleName,
293
    @required String projectBundleId,
294 295 296 297 298 299 300 301 302 303 304 305 306 307
  }) : super(projectBundleId: projectBundleId);

  @override
  String get name => bundleName;

  @override
  String get simulatorBundlePath => _bundlePath;

  @override
  String get deviceBundlePath => _bundlePath;

  String get _bundlePath => bundleDir.path;
}

308 309 310
Future<ApplicationPackage> getApplicationPackageForPlatform(TargetPlatform platform, {
  String applicationBinary
}) async {
311 312
  switch (platform) {
    case TargetPlatform.android_arm:
313
    case TargetPlatform.android_arm64:
314
    case TargetPlatform.android_x64:
315
    case TargetPlatform.android_x86:
316
      return applicationBinary == null
317
          ? await AndroidApk.fromCurrentDirectory()
318
          : new AndroidApk.fromApk(applicationBinary);
319
    case TargetPlatform.ios:
320 321
      return applicationBinary == null
          ? new IOSApp.fromCurrentDirectory()
322
          : new IOSApp.fromPrebuiltApp(applicationBinary);
323 324
    case TargetPlatform.tester:
      return new FlutterTesterApp.fromCurrentDirectory();
325 326
    case TargetPlatform.darwin_x64:
    case TargetPlatform.linux_x64:
327
    case TargetPlatform.windows_x64:
328
    case TargetPlatform.fuchsia:
329 330
      return null;
  }
pq's avatar
pq committed
331
  assert(platform != null);
pq's avatar
pq committed
332
  return null;
333 334
}

335
class ApplicationPackageStore {
336 337
  AndroidApk android;
  IOSApp iOS;
338

339
  ApplicationPackageStore({ this.android, this.iOS });
340

341
  Future<ApplicationPackage> getPackageForPlatform(TargetPlatform platform) async {
342
    switch (platform) {
343
      case TargetPlatform.android_arm:
344
      case TargetPlatform.android_arm64:
345
      case TargetPlatform.android_x64:
346
      case TargetPlatform.android_x86:
347
        android ??= await AndroidApk.fromCurrentDirectory();
348
        return android;
349
      case TargetPlatform.ios:
350
        iOS ??= new IOSApp.fromCurrentDirectory();
351
        return iOS;
352 353
      case TargetPlatform.darwin_x64:
      case TargetPlatform.linux_x64:
354
      case TargetPlatform.windows_x64:
355
      case TargetPlatform.fuchsia:
356
      case TargetPlatform.tester:
357 358
        return null;
    }
pq's avatar
pq committed
359
    return null;
360 361
  }
}
362

363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
class _Entry {
  _Element parent;
  int level;
}

class _Element extends _Entry {
  List<_Entry> children;
  String name;

  _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>[];
  }

  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 {
  String key;
  String value;

  _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;
  }
}

422 423 424
class ApkManifestData {
  ApkManifestData._(this._data);

425
  static ApkManifestData parseFromXmlDump(String data) {
426 427 428
    if (data == null || data.trim().isEmpty)
      return null;

429 430
    final List<String> lines = data.split('\n');
    assert(lines.length > 3);
431

432 433
    final _Element manifest = new _Element.fromLine(lines[1], null);
    _Element currentElement = manifest;
434

435 436 437
    for (String line in lines.skip(2)) {
      final String trimLine = line.trimLeft();
      final int level = line.length - trimLine.length;
438

439 440 441 442
      // Handle level out
      while(level <= currentElement.level) {
        currentElement = currentElement.parent;
      }
443

444 445 446 447 448 449 450 451 452 453
      if (level > currentElement.level) {
        switch (trimLine[0]) {
          case 'A':
            currentElement
                .addChild(new _Attribute.fromLine(line, currentElement));
            break;
          case 'E':
            final _Element element = new _Element.fromLine(line, currentElement);
            currentElement.addChild(element);
            currentElement = element;
454 455 456 457
        }
      }
    }

458 459 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
    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');
      if (enabled == null || enabled.value.contains('0xffffffff')) {
        launchActivity = activity;
        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('" '));

    final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
    map['package'] = <String, String>{'name': packageName};
    map['launchable-activity'] = <String, String>{'name': activityName};

490 491 492 493 494
    return new ApkManifestData._(map);
  }

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

495 496 497 498
  @visibleForTesting
  Map<String, Map<String, String>> get data =>
      new UnmodifiableMapView<String, Map<String, String>>(_data);

499 500 501 502 503 504 505 506 507
  String get packageName => _data['package'] == null ? null : _data['package']['name'];

  String get launchableActivityName {
    return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
  }

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