application_package.dart 9.4 KB
Newer Older
1 2 3 4 5
// 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.

import 'package:path/path.dart' as path;
6
import 'package:xml/xml.dart' as xml;
7

8
import 'android/android_sdk.dart';
9
import 'android/gradle.dart';
10
import 'base/file_system.dart';
11
import 'base/os.dart' show os;
12
import 'base/process.dart';
13
import 'build_info.dart';
14
import 'globals.dart';
15
import 'ios/plist_utils.dart' as plist;
16
import 'ios/xcodeproj.dart';
17

18 19
abstract class ApplicationPackage {
  /// Package ID from the Android Manifest or equivalent.
20
  final String id;
21

22
  ApplicationPackage({ this.id }) {
23 24
    assert(id != null);
  }
25

26 27
  String get name;

28 29
  String get displayName => name;

30 31
  String get packagePath => null;

32
  @override
33
  String toString() => displayName;
34 35 36
}

class AndroidApk extends ApplicationPackage {
37 38 39
  /// Path to the actual apk file.
  final String apkPath;

40
  /// The path to the activity that should be launched.
41 42 43
  final String launchActivity;

  AndroidApk({
Adam Barth's avatar
Adam Barth committed
44
    String id,
45
    this.apkPath,
Adam Barth's avatar
Adam Barth committed
46
    this.launchActivity
47
  }) : super(id: id) {
48
    assert(apkPath != null);
49 50
    assert(launchActivity != null);
  }
51

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  /// Creates a new AndroidApk from an existing APK.
  factory AndroidApk.fromApk(String applicationBinary) {
    String aaptPath = androidSdk?.latestVersion?.aaptPath;
    if (aaptPath == null) {
      printError('Unable to locate the Android SDK; please run \'flutter doctor\'.');
      return null;
    }

    List<String> aaptArgs = <String>[aaptPath, 'dump', 'badging', applicationBinary];
    ApkManifestData data = ApkManifestData.parseFromAaptBadging(runCheckedSync(aaptArgs));

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

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

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

80
  /// Creates a new AndroidApk based on the information in the Android manifest.
81
  factory AndroidApk.fromCurrentDirectory() {
82 83 84 85 86 87 88 89
    String manifestPath;
    String apkPath;

    if (isProjectUsingGradle()) {
      manifestPath = gradleManifestPath;
      apkPath = gradleAppOut;
    } else {
      manifestPath = path.join('android', 'AndroidManifest.xml');
90
      apkPath = path.join(getAndroidBuildDirectory(), 'app.apk');
91 92
    }

93
    if (!fs.isFileSync(manifestPath))
94
      return null;
95

96
    String manifestString = fs.file(manifestPath).readAsStringSync();
97 98 99 100 101
    xml.XmlDocument document = xml.parse(manifestString);

    Iterable<xml.XmlElement> manifests = document.findElements('manifest');
    if (manifests.isEmpty)
      return null;
102
    String packageId = manifests.first.getAttribute('package');
103 104 105 106

    String launchActivity;
    for (xml.XmlElement category in document.findAllElements('category')) {
      if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
Hixie's avatar
Hixie committed
107
        xml.XmlElement activity = category.parent.parent;
108
        String activityName = activity.getAttribute('android:name');
109
        launchActivity = "$packageId/$activityName";
110 111 112
        break;
      }
    }
113 114

    if (packageId == null || launchActivity == null)
115 116
      return null;

117
    return new AndroidApk(
118
      id: packageId,
119
      apkPath: apkPath,
120 121
      launchActivity: launchActivity
    );
122
  }
123

124 125 126
  @override
  String get packagePath => apkPath;

127 128
  @override
  String get name => path.basename(apkPath);
129 130
}

131 132 133 134 135 136 137 138 139 140 141
/// 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 {
  IOSApp({String projectBundleId}) : super(id: projectBundleId);

  /// Creates a new IOSApp from an existing IPA.
  factory IOSApp.fromIpa(String applicationBinary) {
    Directory bundleDir;
    try {
142
      Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_app_');
143
      addShutdownHook(() async => await tempDir.delete(recursive: true));
144 145
      os.unzip(fs.file(applicationBinary), tempDir);
      Directory payloadDir = fs.directory(path.join(tempDir.path, 'Payload'));
146 147 148 149 150
      bundleDir = payloadDir.listSync().singleWhere(_isBundleDirectory);
    } on StateError catch (e, stackTrace) {
      printError('Invalid prebuilt iOS binary: ${e.toString()}', stackTrace);
      return null;
    }
151

152 153 154 155 156 157 158 159 160 161 162 163
    String plistPath = path.join(bundleDir.path, 'Info.plist');
    String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
    if (id == null)
      return null;

    return new PrebuiltIOSApp(
      ipaPath: applicationBinary,
      bundleDir: bundleDir,
      bundleName: path.basename(bundleDir.path),
      projectBundleId: id,
    );
  }
164

165 166
  factory IOSApp.fromCurrentDirectory() {
    if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
167 168
      return null;

169
    String plistPath = path.join('ios', 'Runner', 'Info.plist');
170 171
    String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
    if (id == null)
172
      return null;
173
    String projectPath = path.join('ios', 'Runner.xcodeproj');
174
    id = substituteXcodeVariables(id, projectPath, 'Runner');
175

176
    return new BuildableIOSApp(
177
      appDirectory: path.join('ios'),
178
      projectBundleId: id
179
    );
180
  }
181

182
  @override
183
  String get displayName => id;
184

185 186 187 188 189 190 191 192 193 194 195 196 197
  String get simulatorBundlePath;

  String get deviceBundlePath;
}

class BuildableIOSApp extends IOSApp {
  static final String kBundleName = 'Runner.app';

  BuildableIOSApp({
    this.appDirectory,
    String projectBundleId,
  }) : super(projectBundleId: projectBundleId);

198 199
  final String appDirectory;

200 201 202 203
  @override
  String get name => kBundleName;

  @override
204 205
  String get simulatorBundlePath => _buildAppPath('iphonesimulator');

206
  @override
207 208 209
  String get deviceBundlePath => _buildAppPath('iphoneos');

  String _buildAppPath(String type) {
210
    return path.join(getIosBuildDirectory(), 'Release-$type', kBundleName);
211
  }
212 213
}

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
class PrebuiltIOSApp extends IOSApp {
  final String ipaPath;
  final Directory bundleDir;
  final String bundleName;

  PrebuiltIOSApp({
    this.ipaPath,
    this.bundleDir,
    this.bundleName,
    String projectBundleId,
  }) : super(projectBundleId: projectBundleId);

  @override
  String get name => bundleName;

  @override
  String get simulatorBundlePath => _bundlePath;

  @override
  String get deviceBundlePath => _bundlePath;

  String get _bundlePath => bundleDir.path;
}

238 239 240
ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, {
  String applicationBinary
}) {
241 242 243
  switch (platform) {
    case TargetPlatform.android_arm:
    case TargetPlatform.android_x64:
244
    case TargetPlatform.android_x86:
245 246 247
      return applicationBinary == null
          ? new AndroidApk.fromCurrentDirectory()
          : new AndroidApk.fromApk(applicationBinary);
248
    case TargetPlatform.ios:
249 250 251
      return applicationBinary == null
          ? new IOSApp.fromCurrentDirectory()
          : new IOSApp.fromIpa(applicationBinary);
252 253 254 255
    case TargetPlatform.darwin_x64:
    case TargetPlatform.linux_x64:
      return null;
  }
pq's avatar
pq committed
256
  assert(platform != null);
pq's avatar
pq committed
257
  return null;
258 259
}

260
class ApplicationPackageStore {
261 262
  AndroidApk android;
  IOSApp iOS;
263

264
  ApplicationPackageStore({ this.android, this.iOS });
265

266
  ApplicationPackage getPackageForPlatform(TargetPlatform platform) {
267
    switch (platform) {
268
      case TargetPlatform.android_arm:
269
      case TargetPlatform.android_x64:
270
      case TargetPlatform.android_x86:
271
        android ??= new AndroidApk.fromCurrentDirectory();
272
        return android;
273
      case TargetPlatform.ios:
274
        iOS ??= new IOSApp.fromCurrentDirectory();
275
        return iOS;
276 277
      case TargetPlatform.darwin_x64:
      case TargetPlatform.linux_x64:
278 279
        return null;
    }
pq's avatar
pq committed
280
    return null;
281 282
  }
}
283 284 285 286 287 288 289 290 291

class ApkManifestData {
  ApkManifestData._(this._data);

  static ApkManifestData parseFromAaptBadging(String data) {
    if (data == null || data.trim().isEmpty)
      return null;

    // package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1'
292
    // launchable-activity: name='io.flutter.app.FlutterActivity'  label='' icon=''
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
    Map<String, Map<String, String>> map = <String, Map<String, String>>{};

    for (String line in data.split('\n')) {
      int index = line.indexOf(':');
      if (index != -1) {
        String name = line.substring(0, index);
        line = line.substring(index + 1).trim();

        Map<String, String> entries = <String, String>{};
        map[name] = entries;

        for (String entry in line.split(' ')) {
          entry = entry.trim();
          if (entry.isNotEmpty && entry.contains('=')) {
            int split = entry.indexOf('=');
            String key = entry.substring(0, split);
            String value = entry.substring(split + 1);
            if (value.startsWith("'") && value.endsWith("'"))
              value = value.substring(1, value.length - 1);
            entries[key] = value;
          }
        }
      }
    }

    return new ApkManifestData._(map);
  }

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

  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();
}