application_package.dart 14 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Copyright 2014 The Flutter 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 'dart:collection';

import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:xml/xml.dart';

import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../build_info.dart';
import '../project.dart';
import 'android_sdk.dart';
import 'gradle.dart';

/// An application package created from an already built Android APK.
24
class AndroidApk extends ApplicationPackage implements PrebuiltApplicationPackage {
25
  AndroidApk({
26
    required String id,
27
    required this.applicationPackage,
28 29
    required this.versionCode,
    required this.launchActivity,
30
  }) : assert(applicationPackage != null),
31 32 33 34 35 36
       assert(launchActivity != null),
       super(id: id);

  /// Creates a new AndroidApk from an existing APK.
  ///
  /// Returns `null` if the APK was invalid or any required tooling was missing.
37 38 39 40 41 42 43
  static AndroidApk? fromApk(
    File apk, {
    required AndroidSdk androidSdk,
    required ProcessManager processManager,
    required UserMessages userMessages,
    required Logger logger,
    required ProcessUtils processUtils,
44
  }) {
45
    final String? aaptPath = androidSdk.latestVersion?.aaptPath;
46 47 48 49 50 51 52
    if (aaptPath == null || !processManager.canRun(aaptPath)) {
      logger.printError(userMessages.aaptNotFound);
      return null;
    }

    String apptStdout;
    try {
53
      apptStdout = processUtils.runSync(
54 55 56 57 58 59 60 61 62 63
        <String>[
          aaptPath,
          'dump',
          'xmltree',
          apk.path,
          'AndroidManifest.xml',
        ],
        throwOnError: true,
      ).stdout.trim();
    } on ProcessException catch (error) {
64
      logger.printError('Failed to extract manifest from APK: $error.');
65 66 67
      return null;
    }

68
    final ApkManifestData? data = ApkManifestData.parseFromXmlDump(apptStdout, logger);
69 70 71 72 73 74

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

75 76
    final String? packageName = data.packageName;
    if (packageName == null || data.launchableActivityName == null) {
77 78 79 80 81
      logger.printError('Unable to read manifest info from ${apk.path}.');
      return null;
    }

    return AndroidApk(
82
      id: packageName,
83
      applicationPackage: apk,
84
      versionCode: data.versionCode == null ? null : int.tryParse(data.versionCode!),
85 86 87 88
      launchActivity: '${data.packageName}/${data.launchableActivityName}',
    );
  }

89 90
  @override
  final FileSystemEntity applicationPackage;
91 92 93 94 95

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

  /// The version code of the APK.
96
  final int? versionCode;
97 98

  /// Creates a new AndroidApk based on the information in the Android manifest.
99 100 101 102 103 104 105 106
  static Future<AndroidApk?> fromAndroidProject(
    AndroidProject androidProject, {
    required AndroidSdk androidSdk,
    required ProcessManager processManager,
    required UserMessages userMessages,
    required ProcessUtils processUtils,
    required Logger logger,
    required FileSystem fileSystem,
107 108 109 110 111 112 113 114 115 116 117 118 119 120
  }) async {
    File apkFile;

    if (androidProject.isUsingGradle && androidProject.isSupportedVersion) {
      apkFile = getApkDirectory(androidProject.parent).childFile('app.apk');
      if (apkFile.existsSync()) {
        // Grab information from the .apk. The gradle build script might alter
        // the application Id, so we need to look at what was actually built.
        return AndroidApk.fromApk(
          apkFile,
          androidSdk: androidSdk,
          processManager: processManager,
          logger: logger,
          userMessages: userMessages,
121
          processUtils: processUtils,
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
        );
      }
      // 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.
    } else {
      apkFile = fileSystem.file(fileSystem.path.join(getAndroidBuildDirectory(), 'app.apk'));
    }

    final File manifest = androidProject.appManifestFile;

    if (!manifest.existsSync()) {
      logger.printError('AndroidManifest.xml could not be found.');
      logger.printError('Please check ${manifest.path} for errors.');
      return null;
    }

    final String manifestString = manifest.readAsStringSync();
    XmlDocument document;
    try {
      document = XmlDocument.parse(manifestString);
    } on XmlParserException catch (exception) {
      String manifestLocation;
      if (androidProject.isUsingGradle) {
        manifestLocation = fileSystem.path.join(androidProject.hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml');
      } else {
        manifestLocation = fileSystem.path.join(androidProject.hostAppGradleRoot.path, 'AndroidManifest.xml');
      }
      logger.printError('AndroidManifest.xml is not a valid XML document.');
      logger.printError('Please check $manifestLocation for errors.');
      throwToolExit('XML Parser error message: ${exception.toString()}');
    }

    final Iterable<XmlElement> manifests = document.findElements('manifest');
    if (manifests.isEmpty) {
      logger.printError('AndroidManifest.xml has no manifest element.');
      logger.printError('Please check ${manifest.path} for errors.');
      return null;
    }
161
    final String? packageId = manifests.first.getAttribute('package');
162

163
    String? launchActivity;
164
    for (final XmlElement activity in document.findAllElements('activity')) {
165
      final String? enabled = activity.getAttribute('android:enabled');
166 167 168 169 170
      if (enabled != null && enabled == 'false') {
        continue;
      }

      for (final XmlElement element in activity.findElements('intent-filter')) {
171 172
        String? actionName = '';
        String? categoryName = '';
173 174 175 176
        for (final XmlNode node in element.children) {
          if (node is! XmlElement) {
            continue;
          }
177
          final String? name = node.getAttribute('android:name');
178 179 180 181 182 183
          if (name == 'android.intent.action.MAIN') {
            actionName = name;
          } else if (name == 'android.intent.category.LAUNCHER') {
            categoryName = name;
          }
        }
184 185
        if (actionName != null && categoryName != null && actionName.isNotEmpty && categoryName.isNotEmpty) {
          final String? activityName = activity.getAttribute('android:name');
186 187 188 189 190 191 192 193 194 195 196 197 198 199
          launchActivity = '$packageId/$activityName';
          break;
        }
      }
    }

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

    return AndroidApk(
      id: packageId,
200
      applicationPackage: apkFile,
201 202 203 204 205 206
      versionCode: null,
      launchActivity: launchActivity,
    );
  }

  @override
207
  String get name => applicationPackage.basename;
208 209
}

210 211
abstract class _Entry {
  const _Entry(this.parent, this.level);
212

213
  final _Element? parent;
214
  final int level;
215 216 217
}

class _Element extends _Entry {
218
  _Element._(this.name, _Element? parent, int level) : super(parent, level);
219

220
  factory _Element.fromLine(String line, _Element? parent) {
221 222
    //      E: application (line=29)
    final List<String> parts = line.trimLeft().split(' ');
223
    return _Element._(parts[1], parent, line.length - line.trimLeft().length);
224 225
  }

226
  final List<_Entry> children = <_Entry>[];
227
  final String? name;
228 229 230 231 232

  void addChild(_Entry child) {
    children.add(child);
  }

233 234
  _Attribute? firstAttribute(String name) {
    for (final _Attribute child in children.whereType<_Attribute>()) {
235
      if (child.key?.startsWith(name) ?? false) {
236 237 238 239
        return child;
      }
    }
    return null;
240 241
  }

242 243
  _Element? firstElement(String name) {
    for (final _Element child in children.whereType<_Element>()) {
244
      if (child.name?.startsWith(name) ?? false) {
245 246 247 248
        return child;
      }
    }
    return null;
249 250 251
  }

  Iterable<_Element> allElements(String name) {
252
    return children.whereType<_Element>().where((_Element e) => e.name?.startsWith(name) ?? false);
253 254 255 256
  }
}

class _Attribute extends _Entry {
257
  const _Attribute._(this.key, this.value, _Element? parent, int level) : super(parent, level);
258 259

  factory _Attribute.fromLine(String line, _Element parent) {
260 261
    //     A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
    const String attributePrefix = 'A: ';
262 263
    final List<String> keyVal = line.substring(line.indexOf(attributePrefix) + attributePrefix.length).split('=');
    return _Attribute._(keyVal[0], keyVal[1], parent, line.length - line.trimLeft().length);
264 265
  }

266 267
  final String? key;
  final String? value;
268 269 270 271 272
}

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

273 274
  static bool _isAttributeWithValuePresent(
      _Element baseElement, String childElement, String attributeName, String attributeValue) {
275 276
    final Iterable<_Element> allElements = baseElement.allElements(childElement);
    for (final _Element oneElement in allElements) {
277 278
      final String? elementAttributeValue = oneElement
          .firstAttribute(attributeName)
279 280 281 282 283 284 285 286 287
          ?.value;
      if (elementAttributeValue != null &&
          elementAttributeValue.startsWith(attributeValue)) {
        return true;
      }
    }
    return false;
  }

288
  static ApkManifestData? parseFromXmlDump(String data, Logger logger) {
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
    if (data == null || data.trim().isEmpty) {
      return null;
    }

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

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

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

      // Handle level out
      while (currentElement.parent != null && level <= currentElement.level) {
306
        currentElement = currentElement.parent!;
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
      }

      if (level > currentElement.level) {
        switch (trimLine[0]) {
          case 'A':
            currentElement
                .addChild(_Attribute.fromLine(line, currentElement));
            break;
          case 'E':
            final _Element element = _Element.fromLine(line, currentElement);
            currentElement.addChild(element);
            currentElement = element;
        }
      }
    }

323
    final _Element? application = manifest.firstElement('application');
324 325 326 327 328 329
    if (application == null) {
      return null;
    }

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

330
    _Element? launchActivity;
331
    for (final _Element activity in activities) {
332
      final _Attribute? enabled = activity.firstAttribute('android:enabled');
333 334
      final Iterable<_Element> intentFilters = activity.allElements('intent-filter');
      final bool isEnabledByDefault = enabled == null;
335
      final bool isExplicitlyEnabled = enabled != null && (enabled.value?.contains('0xffffffff') ?? false);
336 337 338 339 340
      if (!(isEnabledByDefault || isExplicitlyEnabled)) {
        continue;
      }

      for (final _Element element in intentFilters) {
341
        final bool isMainAction = _isAttributeWithValuePresent(
342 343 344 345
            element, 'action', 'android:name', '"android.intent.action.MAIN"');
        if (!isMainAction) {
          continue;
        }
346
        final bool isLauncherCategory = _isAttributeWithValuePresent(
347 348 349 350 351 352 353 354 355 356 357 358 359
            element, 'category', 'android:name',
            '"android.intent.category.LAUNCHER"');
        if (!isLauncherCategory) {
          continue;
        }
        launchActivity = activity;
        break;
      }
      if (launchActivity != null) {
        break;
      }
    }

360
    final _Attribute? package = manifest.firstAttribute('package');
361
    // "io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
362
    final String? packageName = package?.value?.substring(1, package.value?.indexOf('" '));
363 364 365 366 367 368

    if (launchActivity == null) {
      logger.printError('Error running $packageName. Default activity not found');
      return null;
    }

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

    // Example format: (type 0x10)0x1
374
    final _Attribute? versionCodeAttr = manifest.firstAttribute('android:versionCode');
375 376 377 378
    if (versionCodeAttr == null) {
      logger.printError('Error running $packageName. Manifest versionCode not found');
      return null;
    }
379
    if (versionCodeAttr.value?.startsWith('(type 0x10)') != true) {
380 381 382
      logger.printError('Error running $packageName. Manifest versionCode invalid');
      return null;
    }
383
    final int? versionCode = versionCodeAttr.value == null ? null : int.tryParse(versionCodeAttr.value!.substring(11));
384 385 386 387 388
    if (versionCode == null) {
      logger.printError('Error running $packageName. Manifest versionCode invalid');
      return null;
    }

389 390 391 392 393 394 395
    final Map<String, Map<String, String>> map = <String, Map<String, String>>{
      if (packageName != null)
        'package': <String, String>{'name': packageName},
      'version-code': <String, String>{'name': versionCode.toString()},
      if (activityName != null)
        'launchable-activity': <String, String>{'name': activityName},
    };
396 397 398 399 400 401 402 403 404 405

    return ApkManifestData._(map);
  }

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

  @visibleForTesting
  Map<String, Map<String, String>> get data =>
      UnmodifiableMapView<String, Map<String, String>>(_data);

406
  String? get packageName => _data['package'] == null ? null : _data['package']?['name'];
407

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

410 411
  String? get launchableActivityName {
    return _data['launchable-activity'] == null ? null : _data['launchable-activity']?['name'];
412 413 414 415 416
  }

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