"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter_tools/lib/src/application_package.dart" (13 Nov 2020, 22159 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "application_package.dart": 1.22.3_vs_1.22.4.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 import 'dart:async';
    6 import 'dart:collection';
    7 
    8 import 'package:meta/meta.dart';
    9 import 'package:xml/xml.dart';
   10 
   11 import 'android/gradle.dart';
   12 import 'base/common.dart';
   13 import 'base/context.dart';
   14 import 'base/file_system.dart';
   15 import 'base/io.dart';
   16 import 'base/process.dart';
   17 import 'base/user_messages.dart';
   18 import 'build_info.dart';
   19 import 'fuchsia/application_package.dart';
   20 import 'globals.dart' as globals;
   21 import 'ios/plist_parser.dart';
   22 import 'linux/application_package.dart';
   23 import 'macos/application_package.dart';
   24 import 'project.dart';
   25 import 'tester/flutter_tester.dart';
   26 import 'web/web_device.dart';
   27 import 'windows/application_package.dart';
   28 
   29 class ApplicationPackageFactory {
   30   static ApplicationPackageFactory get instance => context.get<ApplicationPackageFactory>();
   31 
   32   Future<ApplicationPackage> getPackageForPlatform(
   33     TargetPlatform platform, {
   34     BuildInfo buildInfo,
   35     File applicationBinary,
   36   }) async {
   37     switch (platform) {
   38       case TargetPlatform.android:
   39       case TargetPlatform.android_arm:
   40       case TargetPlatform.android_arm64:
   41       case TargetPlatform.android_x64:
   42       case TargetPlatform.android_x86:
   43         if (globals.androidSdk?.licensesAvailable == true  && globals.androidSdk?.latestVersion == null) {
   44           await checkGradleDependencies();
   45         }
   46         return applicationBinary == null
   47             ? await AndroidApk.fromAndroidProject(FlutterProject.current().android)
   48             : AndroidApk.fromApk(applicationBinary);
   49       case TargetPlatform.ios:
   50         return applicationBinary == null
   51             ? await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo)
   52             : IOSApp.fromPrebuiltApp(applicationBinary);
   53       case TargetPlatform.tester:
   54         return FlutterTesterApp.fromCurrentDirectory(globals.fs);
   55       case TargetPlatform.darwin_x64:
   56         return applicationBinary == null
   57             ? MacOSApp.fromMacOSProject(FlutterProject.current().macos)
   58             : MacOSApp.fromPrebuiltApp(applicationBinary);
   59       case TargetPlatform.web_javascript:
   60         if (!FlutterProject.current().web.existsSync()) {
   61           return null;
   62         }
   63         return WebApplicationPackage(FlutterProject.current());
   64       case TargetPlatform.linux_x64:
   65         return applicationBinary == null
   66             ? LinuxApp.fromLinuxProject(FlutterProject.current().linux)
   67             : LinuxApp.fromPrebuiltApp(applicationBinary);
   68       case TargetPlatform.windows_x64:
   69         return applicationBinary == null
   70             ? WindowsApp.fromWindowsProject(FlutterProject.current().windows)
   71             : WindowsApp.fromPrebuiltApp(applicationBinary);
   72       case TargetPlatform.fuchsia_arm64:
   73       case TargetPlatform.fuchsia_x64:
   74         return applicationBinary == null
   75             ? FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia)
   76             : FuchsiaApp.fromPrebuiltApp(applicationBinary);
   77     }
   78     assert(platform != null);
   79     return null;
   80   }
   81 }
   82 
   83 abstract class ApplicationPackage {
   84   ApplicationPackage({ @required this.id })
   85     : assert(id != null);
   86 
   87   /// Package ID from the Android Manifest or equivalent.
   88   final String id;
   89 
   90   String get name;
   91 
   92   String get displayName => name;
   93 
   94   File get packagesFile => null;
   95 
   96   @override
   97   String toString() => displayName ?? id;
   98 }
   99 
  100 class AndroidApk extends ApplicationPackage {
  101   AndroidApk({
  102     String id,
  103     @required this.file,
  104     @required this.versionCode,
  105     @required this.launchActivity,
  106   }) : assert(file != null),
  107        assert(launchActivity != null),
  108        super(id: id);
  109 
  110   /// Creates a new AndroidApk from an existing APK.
  111   factory AndroidApk.fromApk(File apk) {
  112     final String aaptPath = globals.androidSdk?.latestVersion?.aaptPath;
  113     if (aaptPath == null) {
  114       globals.printError(userMessages.aaptNotFound);
  115       return null;
  116     }
  117 
  118     String apptStdout;
  119     try {
  120       apptStdout = processUtils.runSync(
  121         <String>[
  122           aaptPath,
  123           'dump',
  124           'xmltree',
  125           apk.path,
  126           'AndroidManifest.xml',
  127         ],
  128         throwOnError: true,
  129       ).stdout.trim();
  130     } on ProcessException catch (error) {
  131       globals.printError('Failed to extract manifest from APK: $error.');
  132       return null;
  133     }
  134 
  135     final ApkManifestData data = ApkManifestData.parseFromXmlDump(apptStdout);
  136 
  137     if (data == null) {
  138       globals.printError('Unable to read manifest info from ${apk.path}.');
  139       return null;
  140     }
  141 
  142     if (data.packageName == null || data.launchableActivityName == null) {
  143       globals.printError('Unable to read manifest info from ${apk.path}.');
  144       return null;
  145     }
  146 
  147     return AndroidApk(
  148       id: data.packageName,
  149       file: apk,
  150       versionCode: int.tryParse(data.versionCode),
  151       launchActivity: '${data.packageName}/${data.launchableActivityName}',
  152     );
  153   }
  154 
  155   /// Path to the actual apk file.
  156   final File file;
  157 
  158   /// The path to the activity that should be launched.
  159   final String launchActivity;
  160 
  161   /// The version code of the APK.
  162   final int versionCode;
  163 
  164   /// Creates a new AndroidApk based on the information in the Android manifest.
  165   static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
  166     File apkFile;
  167 
  168     if (androidProject.isUsingGradle) {
  169       apkFile = await getGradleAppOut(androidProject);
  170       if (apkFile.existsSync()) {
  171         // Grab information from the .apk. The gradle build script might alter
  172         // the application Id, so we need to look at what was actually built.
  173         return AndroidApk.fromApk(apkFile);
  174       }
  175       // The .apk hasn't been built yet, so we work with what we have. The run
  176       // command will grab a new AndroidApk after building, to get the updated
  177       // IDs.
  178     } else {
  179       apkFile = globals.fs.file(globals.fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
  180     }
  181 
  182     final File manifest = androidProject.appManifestFile;
  183 
  184     if (!manifest.existsSync()) {
  185       globals.printError('AndroidManifest.xml could not be found.');
  186       globals.printError('Please check ${manifest.path} for errors.');
  187       return null;
  188     }
  189 
  190     final String manifestString = manifest.readAsStringSync();
  191     XmlDocument document;
  192     try {
  193       document = XmlDocument.parse(manifestString);
  194     } on XmlParserException catch (exception) {
  195       String manifestLocation;
  196       if (androidProject.isUsingGradle) {
  197         manifestLocation = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml');
  198       } else {
  199         manifestLocation = globals.fs.path.join(androidProject.hostAppGradleRoot.path, 'AndroidManifest.xml');
  200       }
  201       globals.printError('AndroidManifest.xml is not a valid XML document.');
  202       globals.printError('Please check $manifestLocation for errors.');
  203       throwToolExit('XML Parser error message: ${exception.toString()}');
  204     }
  205 
  206     final Iterable<XmlElement> manifests = document.findElements('manifest');
  207     if (manifests.isEmpty) {
  208       globals.printError('AndroidManifest.xml has no manifest element.');
  209       globals.printError('Please check ${manifest.path} for errors.');
  210       return null;
  211     }
  212     final String packageId = manifests.first.getAttribute('package');
  213 
  214     String launchActivity;
  215     for (final XmlElement activity in document.findAllElements('activity')) {
  216       final String enabled = activity.getAttribute('android:enabled');
  217       if (enabled != null && enabled == 'false') {
  218         continue;
  219       }
  220 
  221       for (final XmlElement element in activity.findElements('intent-filter')) {
  222         String actionName = '';
  223         String categoryName = '';
  224         for (final XmlNode node in element.children) {
  225           if (node is! XmlElement) {
  226             continue;
  227           }
  228           final XmlElement xmlElement = node as XmlElement;
  229           final String name = xmlElement.getAttribute('android:name');
  230           if (name == 'android.intent.action.MAIN') {
  231             actionName = name;
  232           } else if (name == 'android.intent.category.LAUNCHER') {
  233             categoryName = name;
  234           }
  235         }
  236         if (actionName.isNotEmpty && categoryName.isNotEmpty) {
  237           final String activityName = activity.getAttribute('android:name');
  238           launchActivity = '$packageId/$activityName';
  239           break;
  240         }
  241       }
  242     }
  243 
  244     if (packageId == null || launchActivity == null) {
  245       globals.printError('package identifier or launch activity not found.');
  246       globals.printError('Please check ${manifest.path} for errors.');
  247       return null;
  248     }
  249 
  250     return AndroidApk(
  251       id: packageId,
  252       file: apkFile,
  253       versionCode: null,
  254       launchActivity: launchActivity,
  255     );
  256   }
  257 
  258   @override
  259   File get packagesFile => file;
  260 
  261   @override
  262   String get name => file.basename;
  263 }
  264 
  265 /// Tests whether a [Directory] is an iOS bundle directory.
  266 bool _isBundleDirectory(Directory dir) => dir.path.endsWith('.app');
  267 
  268 abstract class IOSApp extends ApplicationPackage {
  269   IOSApp({@required String projectBundleId}) : super(id: projectBundleId);
  270 
  271   /// Creates a new IOSApp from an existing app bundle or IPA.
  272   factory IOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
  273     final FileSystemEntityType entityType = globals.fs.typeSync(applicationBinary.path);
  274     if (entityType == FileSystemEntityType.notFound) {
  275       globals.printError(
  276           'File "${applicationBinary.path}" does not exist. Use an app bundle or an ipa.');
  277       return null;
  278     }
  279     Directory bundleDir;
  280     if (entityType == FileSystemEntityType.directory) {
  281       final Directory directory = globals.fs.directory(applicationBinary);
  282       if (!_isBundleDirectory(directory)) {
  283         globals.printError('Folder "${applicationBinary.path}" is not an app bundle.');
  284         return null;
  285       }
  286       bundleDir = globals.fs.directory(applicationBinary);
  287     } else {
  288       // Try to unpack as an ipa.
  289       final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_app.');
  290       shutdownHooks.addShutdownHook(() async {
  291         await tempDir.delete(recursive: true);
  292       }, ShutdownStage.STILL_RECORDING);
  293       globals.os.unzip(globals.fs.file(applicationBinary), tempDir);
  294       final Directory payloadDir = globals.fs.directory(
  295         globals.fs.path.join(tempDir.path, 'Payload'),
  296       );
  297       if (!payloadDir.existsSync()) {
  298         globals.printError(
  299             'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.');
  300         return null;
  301       }
  302       try {
  303         bundleDir = payloadDir.listSync().whereType<Directory>().singleWhere(_isBundleDirectory);
  304       } on StateError {
  305         globals.printError(
  306             'Invalid prebuilt iOS ipa. Does not contain a single app bundle.');
  307         return null;
  308       }
  309     }
  310     final String plistPath = globals.fs.path.join(bundleDir.path, 'Info.plist');
  311     if (!globals.fs.file(plistPath).existsSync()) {
  312       globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
  313       return null;
  314     }
  315     final String id = globals.plistParser.getValueFromFile(
  316       plistPath,
  317       PlistParser.kCFBundleIdentifierKey,
  318     );
  319     if (id == null) {
  320       globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
  321       return null;
  322     }
  323 
  324     return PrebuiltIOSApp(
  325       bundleDir: bundleDir,
  326       bundleName: globals.fs.path.basename(bundleDir.path),
  327       projectBundleId: id,
  328     );
  329   }
  330 
  331   static Future<IOSApp> fromIosProject(IosProject project, BuildInfo buildInfo) {
  332     if (!globals.platform.isMacOS) {
  333       return null;
  334     }
  335     if (!project.exists) {
  336       // If the project doesn't exist at all the current hint to run flutter
  337       // create is accurate.
  338       return null;
  339     }
  340     if (!project.xcodeProject.existsSync()) {
  341       globals.printError('Expected ios/Runner.xcodeproj but this file is missing.');
  342       return null;
  343     }
  344     if (!project.xcodeProjectInfoFile.existsSync()) {
  345       globals.printError('Expected ios/Runner.xcodeproj/project.pbxproj but this file is missing.');
  346       return null;
  347     }
  348     return BuildableIOSApp.fromProject(project, buildInfo);
  349   }
  350 
  351   @override
  352   String get displayName => id;
  353 
  354   String get simulatorBundlePath;
  355 
  356   String get deviceBundlePath;
  357 }
  358 
  359 class BuildableIOSApp extends IOSApp {
  360   BuildableIOSApp(this.project, String projectBundleId, String hostAppBundleName)
  361     : _hostAppBundleName = hostAppBundleName,
  362       super(projectBundleId: projectBundleId);
  363 
  364   static Future<BuildableIOSApp> fromProject(IosProject project, BuildInfo buildInfo) async {
  365     final String projectBundleId = await project.productBundleIdentifier(buildInfo);
  366     final String hostAppBundleName = await project.hostAppBundleName(buildInfo);
  367     return BuildableIOSApp(project, projectBundleId, hostAppBundleName);
  368   }
  369 
  370   final IosProject project;
  371 
  372   final String _hostAppBundleName;
  373 
  374   @override
  375   String get name => _hostAppBundleName;
  376 
  377   @override
  378   String get simulatorBundlePath => _buildAppPath('iphonesimulator');
  379 
  380   @override
  381   String get deviceBundlePath => _buildAppPath('iphoneos');
  382 
  383   String _buildAppPath(String type) {
  384     return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
  385   }
  386 }
  387 
  388 class PrebuiltIOSApp extends IOSApp {
  389   PrebuiltIOSApp({
  390     this.bundleDir,
  391     this.bundleName,
  392     @required String projectBundleId,
  393   }) : super(projectBundleId: projectBundleId);
  394 
  395   final Directory bundleDir;
  396   final String bundleName;
  397 
  398   @override
  399   String get name => bundleName;
  400 
  401   @override
  402   String get simulatorBundlePath => _bundlePath;
  403 
  404   @override
  405   String get deviceBundlePath => _bundlePath;
  406 
  407   String get _bundlePath => bundleDir.path;
  408 }
  409 
  410 class ApplicationPackageStore {
  411   ApplicationPackageStore({ this.android, this.iOS, this.fuchsia });
  412 
  413   AndroidApk android;
  414   IOSApp iOS;
  415   FuchsiaApp fuchsia;
  416   LinuxApp linux;
  417   MacOSApp macOS;
  418   WindowsApp windows;
  419 
  420   Future<ApplicationPackage> getPackageForPlatform(
  421     TargetPlatform platform,
  422     BuildInfo buildInfo,
  423   ) async {
  424     switch (platform) {
  425       case TargetPlatform.android:
  426       case TargetPlatform.android_arm:
  427       case TargetPlatform.android_arm64:
  428       case TargetPlatform.android_x64:
  429       case TargetPlatform.android_x86:
  430         android ??= await AndroidApk.fromAndroidProject(FlutterProject.current().android);
  431         return android;
  432       case TargetPlatform.ios:
  433         iOS ??= await IOSApp.fromIosProject(FlutterProject.current().ios, buildInfo);
  434         return iOS;
  435       case TargetPlatform.fuchsia_arm64:
  436       case TargetPlatform.fuchsia_x64:
  437         fuchsia ??= FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia);
  438         return fuchsia;
  439       case TargetPlatform.darwin_x64:
  440         macOS ??= MacOSApp.fromMacOSProject(FlutterProject.current().macos);
  441         return macOS;
  442       case TargetPlatform.linux_x64:
  443         linux ??= LinuxApp.fromLinuxProject(FlutterProject.current().linux);
  444         return linux;
  445       case TargetPlatform.windows_x64:
  446         windows ??= WindowsApp.fromWindowsProject(FlutterProject.current().windows);
  447         return windows;
  448       case TargetPlatform.tester:
  449       case TargetPlatform.web_javascript:
  450         return null;
  451     }
  452     return null;
  453   }
  454 }
  455 
  456 class _Entry {
  457   _Element parent;
  458   int level;
  459 }
  460 
  461 class _Element extends _Entry {
  462   _Element.fromLine(String line, _Element parent) {
  463     //      E: application (line=29)
  464     final List<String> parts = line.trimLeft().split(' ');
  465     name = parts[1];
  466     level = line.length - line.trimLeft().length;
  467     this.parent = parent;
  468     children = <_Entry>[];
  469   }
  470 
  471   List<_Entry> children;
  472   String name;
  473 
  474   void addChild(_Entry child) {
  475     children.add(child);
  476   }
  477 
  478   _Attribute firstAttribute(String name) {
  479     return children.whereType<_Attribute>().firstWhere(
  480         (_Attribute e) => e.key.startsWith(name),
  481         orElse: () => null,
  482     );
  483   }
  484 
  485   _Element firstElement(String name) {
  486     return children.whereType<_Element>().firstWhere(
  487         (_Element e) => e.name.startsWith(name),
  488         orElse: () => null,
  489     );
  490   }
  491 
  492   Iterable<_Element> allElements(String name) {
  493     return children.whereType<_Element>().where((_Element e) => e.name.startsWith(name));
  494   }
  495 }
  496 
  497 class _Attribute extends _Entry {
  498   _Attribute.fromLine(String line, _Element parent) {
  499     //     A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
  500     const String attributePrefix = 'A: ';
  501     final List<String> keyVal = line
  502         .substring(line.indexOf(attributePrefix) + attributePrefix.length)
  503         .split('=');
  504     key = keyVal[0];
  505     value = keyVal[1];
  506     level = line.length - line.trimLeft().length;
  507     this.parent = parent;
  508   }
  509 
  510   String key;
  511   String value;
  512 }
  513 
  514 class ApkManifestData {
  515   ApkManifestData._(this._data);
  516 
  517   static bool isAttributeWithValuePresent(_Element baseElement,
  518       String childElement, String attributeName, String attributeValue) {
  519     final Iterable<_Element> allElements = baseElement.allElements(childElement);
  520     for (final _Element oneElement in allElements) {
  521       final String elementAttributeValue = oneElement
  522           ?.firstAttribute(attributeName)
  523           ?.value;
  524       if (elementAttributeValue != null &&
  525           elementAttributeValue.startsWith(attributeValue)) {
  526         return true;
  527       }
  528     }
  529     return false;
  530   }
  531 
  532   static ApkManifestData parseFromXmlDump(String data) {
  533     if (data == null || data.trim().isEmpty) {
  534       return null;
  535     }
  536 
  537     final List<String> lines = data.split('\n');
  538     assert(lines.length > 3);
  539 
  540     final int manifestLine = lines.indexWhere((String line) => line.contains('E: manifest'));
  541     final _Element manifest = _Element.fromLine(lines[manifestLine], null);
  542     _Element currentElement = manifest;
  543 
  544     for (final String line in lines.skip(manifestLine)) {
  545       final String trimLine = line.trimLeft();
  546       final int level = line.length - trimLine.length;
  547 
  548       // Handle level out
  549       while (currentElement.parent != null && level <= currentElement.level) {
  550         currentElement = currentElement.parent;
  551       }
  552 
  553       if (level > currentElement.level) {
  554         switch (trimLine[0]) {
  555           case 'A':
  556             currentElement
  557                 .addChild(_Attribute.fromLine(line, currentElement));
  558             break;
  559           case 'E':
  560             final _Element element = _Element.fromLine(line, currentElement);
  561             currentElement.addChild(element);
  562             currentElement = element;
  563         }
  564       }
  565     }
  566 
  567     final _Element application = manifest.firstElement('application');
  568     if (application == null) {
  569       return null;
  570     }
  571 
  572     final Iterable<_Element> activities = application.allElements('activity');
  573 
  574     _Element launchActivity;
  575     for (final _Element activity in activities) {
  576       final _Attribute enabled = activity.firstAttribute('android:enabled');
  577       final Iterable<_Element> intentFilters = activity.allElements('intent-filter');
  578       final bool isEnabledByDefault = enabled == null;
  579       final bool isExplicitlyEnabled = enabled != null && enabled.value.contains('0xffffffff');
  580       if (!(isEnabledByDefault || isExplicitlyEnabled)) {
  581         continue;
  582       }
  583 
  584       for (final _Element element in intentFilters) {
  585         final bool isMainAction = isAttributeWithValuePresent(
  586             element, 'action', 'android:name', '"android.intent.action.MAIN"');
  587         if (!isMainAction) {
  588           continue;
  589         }
  590         final bool isLauncherCategory = isAttributeWithValuePresent(
  591             element, 'category', 'android:name',
  592             '"android.intent.category.LAUNCHER"');
  593         if (!isLauncherCategory) {
  594           continue;
  595         }
  596         launchActivity = activity;
  597         break;
  598       }
  599       if (launchActivity != null) {
  600         break;
  601       }
  602     }
  603 
  604     final _Attribute package = manifest.firstAttribute('package');
  605     // "io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
  606     final String packageName = package.value.substring(1, package.value.indexOf('" '));
  607 
  608     if (launchActivity == null) {
  609       globals.printError('Error running $packageName. Default activity not found');
  610       return null;
  611     }
  612 
  613     final _Attribute nameAttribute = launchActivity.firstAttribute('android:name');
  614     // "io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
  615     final String activityName = nameAttribute
  616         .value.substring(1, nameAttribute.value.indexOf('" '));
  617 
  618     // Example format: (type 0x10)0x1
  619     final _Attribute versionCodeAttr = manifest.firstAttribute('android:versionCode');
  620     if (versionCodeAttr == null) {
  621       globals.printError('Error running $packageName. Manifest versionCode not found');
  622       return null;
  623     }
  624     if (!versionCodeAttr.value.startsWith('(type 0x10)')) {
  625       globals.printError('Error running $packageName. Manifest versionCode invalid');
  626       return null;
  627     }
  628     final int versionCode = int.tryParse(versionCodeAttr.value.substring(11));
  629     if (versionCode == null) {
  630       globals.printError('Error running $packageName. Manifest versionCode invalid');
  631       return null;
  632     }
  633 
  634     final Map<String, Map<String, String>> map = <String, Map<String, String>>{};
  635     map['package'] = <String, String>{'name': packageName};
  636     map['version-code'] = <String, String>{'name': versionCode.toString()};
  637     map['launchable-activity'] = <String, String>{'name': activityName};
  638 
  639     return ApkManifestData._(map);
  640   }
  641 
  642   final Map<String, Map<String, String>> _data;
  643 
  644   @visibleForTesting
  645   Map<String, Map<String, String>> get data =>
  646       UnmodifiableMapView<String, Map<String, String>>(_data);
  647 
  648   String get packageName => _data['package'] == null ? null : _data['package']['name'];
  649 
  650   String get versionCode => _data['version-code'] == null ? null : _data['version-code']['name'];
  651 
  652   String get launchableActivityName {
  653     return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name'];
  654   }
  655 
  656   @override
  657   String toString() => _data.toString();
  658 }