make flutter run work with a pre-built apk (#5307)

* make flutter run work with a pre-built apk

* refactor to remove the buildDir param
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index b15dde6..47e32c3 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -8,18 +8,16 @@
 import 'package:xml/xml.dart' as xml;
 
 import 'android/gradle.dart';
+import 'base/process.dart';
 import 'build_info.dart';
+import 'globals.dart';
 import 'ios/plist_utils.dart';
 
 abstract class ApplicationPackage {
-  /// Path to the package's root folder.
-  final String rootPath;
-
   /// Package ID from the Android Manifest or equivalent.
   final String id;
 
-  ApplicationPackage({this.rootPath, this.id}) {
-    assert(rootPath != null);
+  ApplicationPackage({ this.id }) {
     assert(id != null);
   }
 
@@ -39,15 +37,42 @@
   final String launchActivity;
 
   AndroidApk({
-    String buildDir,
     String id,
     this.apkPath,
     this.launchActivity
-  }) : super(rootPath: buildDir, id: id) {
+  }) : super(id: id) {
     assert(apkPath != null);
     assert(launchActivity != null);
   }
 
+  /// 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}'
+    );
+  }
+
   /// Creates a new AndroidApk based on the information in the Android manifest.
   factory AndroidApk.fromCurrentDirectory() {
     String manifestPath;
@@ -70,23 +95,23 @@
     Iterable<xml.XmlElement> manifests = document.findElements('manifest');
     if (manifests.isEmpty)
       return null;
-    String id = manifests.first.getAttribute('package');
+    String packageId = manifests.first.getAttribute('package');
 
     String launchActivity;
     for (xml.XmlElement category in document.findAllElements('category')) {
       if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
         xml.XmlElement activity = category.parent.parent;
         String activityName = activity.getAttribute('android:name');
-        launchActivity = "$id/$activityName";
+        launchActivity = "$packageId/$activityName";
         break;
       }
     }
-    if (id == null || launchActivity == null)
+
+    if (packageId == null || launchActivity == null)
       return null;
 
     return new AndroidApk(
-      buildDir: 'build',
-      id: id,
+      id: packageId,
       apkPath: apkPath,
       launchActivity: launchActivity
     );
@@ -100,9 +125,9 @@
   static final String kBundleName = 'Runner.app';
 
   IOSApp({
-    String projectDir,
+    this.appDirectory,
     String projectBundleId
-  }) : super(rootPath: projectDir, id: projectBundleId);
+  }) : super(id: projectBundleId);
 
   factory IOSApp.fromCurrentDirectory() {
     if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
@@ -114,7 +139,7 @@
       return null;
 
     return new IOSApp(
-      projectDir: path.join('ios'),
+      appDirectory: path.join('ios'),
       projectBundleId: value
     );
   }
@@ -125,20 +150,26 @@
   @override
   String get displayName => id;
 
+  final String appDirectory;
+
   String get simulatorBundlePath => _buildAppPath('iphonesimulator');
 
   String get deviceBundlePath => _buildAppPath('iphoneos');
 
   String _buildAppPath(String type) {
-    return path.join(rootPath, 'build', 'Release-$type', kBundleName);
+    return path.join(appDirectory, 'build', 'Release-$type', kBundleName);
   }
 }
 
-ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform) {
+ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, {
+  String applicationBinary
+}) {
   switch (platform) {
     case TargetPlatform.android_arm:
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
+      if (applicationBinary != null)
+        return new AndroidApk.fromApk(applicationBinary);
       return new AndroidApk.fromCurrentDirectory();
     case TargetPlatform.ios:
       return new IOSApp.fromCurrentDirectory();
@@ -173,3 +204,52 @@
     return null;
   }
 }
+
+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'
+    // launchable-activity: name='org.domokit.sky.shell.SkyActivity'  label='' icon=''
+    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();
+}
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index c31c861..944b10c 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -25,7 +25,6 @@
 abstract class RunCommandBase extends FlutterCommand {
   RunCommandBase() {
     addBuildModeFlags(defaultToRelease: false);
-
     argParser.addFlag('trace-startup',
         negatable: true,
         defaultsTo: false,
@@ -59,6 +58,9 @@
     argParser.addFlag('build',
         defaultsTo: true,
         help: 'If necessary, build the app before running.');
+    argParser.addOption('use-application-binary',
+        hide: true,
+        help: 'Specify a pre-built application binary to use when running.');
     usesPubOption();
 
     // Option to enable hot reloading.
@@ -172,7 +174,8 @@
         target: targetFile,
         debuggingOptions: options,
         traceStartup: traceStartup,
-        benchmark: argResults['benchmark']
+        benchmark: argResults['benchmark'],
+        applicationBinary: argResults['use-application-binary']
       );
     }
 
diff --git a/packages/flutter_tools/lib/src/dart/package_map.dart b/packages/flutter_tools/lib/src/dart/package_map.dart
index 13152c4..c0652c8 100644
--- a/packages/flutter_tools/lib/src/dart/package_map.dart
+++ b/packages/flutter_tools/lib/src/dart/package_map.dart
@@ -23,6 +23,8 @@
     _globalPackagesPath = value;
   }
 
+  static bool get isUsingCustomPackagesPath => _globalPackagesPath != null;
+
   static String _globalPackagesPath;
 
   final String packagesPath;
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 8e72015..e6e8ebe 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -98,7 +98,7 @@
 }
 
 Future<XcodeBuildResult> buildXcodeProject({
-  ApplicationPackage app,
+  IOSApp app,
   BuildMode mode,
   String target: flx.defaultMainPath,
   bool buildForDevice,
@@ -113,7 +113,7 @@
   // Before the build, all service definitions must be updated and the dylibs
   // copied over to a location that is suitable for Xcodebuild to find them.
 
-  await _addServicesToBundle(new Directory(app.rootPath));
+  await _addServicesToBundle(new Directory(app.appDirectory));
 
   List<String> commands = <String>[
     '/usr/bin/env',
@@ -125,13 +125,13 @@
     'ONLY_ACTIVE_ARCH=YES',
   ];
 
-  List<FileSystemEntity> contents = new Directory(app.rootPath).listSync();
+  List<FileSystemEntity> contents = new Directory(app.appDirectory).listSync();
   for (FileSystemEntity entity in contents) {
     if (path.extension(entity.path) == '.xcworkspace') {
       commands.addAll(<String>[
         '-workspace', path.basename(entity.path),
         '-scheme', path.basenameWithoutExtension(entity.path),
-        "BUILD_DIR=${path.absolute(app.rootPath, 'build')}",
+        "BUILD_DIR=${path.absolute(app.appDirectory, 'build')}",
       ]);
       break;
     }
@@ -154,7 +154,7 @@
 
   RunResult result = await runAsync(
     commands,
-    workingDirectory: app.rootPath,
+    workingDirectory: app.appDirectory,
     allowReentrantFlutter: true
   );
 
@@ -170,7 +170,7 @@
     Match match = regexp.firstMatch(result.stdout);
     String outputDir;
     if (match != null)
-      outputDir = path.join(app.rootPath, match.group(1));
+      outputDir = path.join(app.appDirectory, match.group(1));
     return new XcodeBuildResult(true, outputDir);
   }
 }
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index ad1d3bf..7108ef1 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -23,7 +23,8 @@
     DebuggingOptions debuggingOptions,
     bool usesTerminalUI: true,
     this.traceStartup: false,
-    this.benchmark: false
+    this.benchmark: false,
+    this.applicationBinary
   }) : super(device,
              target: target,
              debuggingOptions: debuggingOptions,
@@ -32,8 +33,9 @@
   ApplicationPackage _package;
   String _mainPath;
   LaunchResult _result;
-  bool traceStartup;
-  bool benchmark;
+  final bool traceStartup;
+  final bool benchmark;
+  final String applicationBinary;
 
   @override
   Future<int> run({
@@ -105,7 +107,7 @@
       return 1;
     }
 
-    _package = getApplicationPackageForPlatform(device.platform);
+    _package = getApplicationPackageForPlatform(device.platform, applicationBinary: applicationBinary);
 
     if (_package == null) {
       String message = 'No application found for ${device.platform}.';
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 1e63160..b2f14f6 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -185,11 +185,14 @@
   Validator commandValidator;
 
   bool _commandValidator() {
-    if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
-      printError('Error: No pubspec.yaml file found.\n'
-        'This command should be run from the root of your Flutter project.\n'
-        'Do not run this command from the root of your git clone of Flutter.');
-      return false;
+    if (!PackageMap.isUsingCustomPackagesPath) {
+      // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
+      if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
+        printError('Error: No pubspec.yaml file found.\n'
+          'This command should be run from the root of your Flutter project.\n'
+          'Do not run this command from the root of your git clone of Flutter.');
+        return false;
+      }
     }
 
     if (_usesTargetOption) {
diff --git a/packages/flutter_tools/test/all.dart b/packages/flutter_tools/test/all.dart
index 6d5ba70..93867c5 100644
--- a/packages/flutter_tools/test/all.dart
+++ b/packages/flutter_tools/test/all.dart
@@ -15,6 +15,7 @@
 import 'analyze_test.dart' as analyze_test;
 import 'android_device_test.dart' as android_device_test;
 import 'android_sdk_test.dart' as android_sdk_test;
+import 'application_package_test.dart' as application_package_test;
 import 'base_utils_test.dart' as base_utils_test;
 import 'config_test.dart' as config_test;
 import 'context_test.dart' as context_test;
@@ -44,6 +45,7 @@
   analyze_test.main();
   android_device_test.main();
   android_sdk_test.main();
+  application_package_test.main();
   base_utils_test.main();
   config_test.main();
   context_test.main();
diff --git a/packages/flutter_tools/test/application_package_test.dart b/packages/flutter_tools/test/application_package_test.dart
new file mode 100644
index 0000000..65987ba
--- /dev/null
+++ b/packages/flutter_tools/test/application_package_test.dart
@@ -0,0 +1,46 @@
+// Copyright 2016 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:flutter_tools/src/application_package.dart';
+import 'package:test/test.dart';
+
+import 'src/context.dart';
+
+void main() {
+  group('ApkManifestData', () {
+    testUsingContext('parse sdk', () {
+      ApkManifestData data = ApkManifestData.parseFromAaptBadging(_aaptData);
+      expect(data, isNotNull);
+      expect(data.packageName, 'io.flutter.gallery');
+      expect(data.launchableActivityName, 'org.domokit.sky.shell.SkyActivity');
+    });
+  });
+}
+
+final String _aaptData = '''
+package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1'
+sdkVersion:'14'
+targetSdkVersion:'21'
+uses-permission: name='android.permission.INTERNET'
+application-label:'Flutter Gallery'
+application-icon-160:'res/mipmap-mdpi-v4/ic_launcher.png'
+application-icon-240:'res/mipmap-hdpi-v4/ic_launcher.png'
+application-icon-320:'res/mipmap-xhdpi-v4/ic_launcher.png'
+application-icon-480:'res/mipmap-xxhdpi-v4/ic_launcher.png'
+application-icon-640:'res/mipmap-xxxhdpi-v4/ic_launcher.png'
+application: label='Flutter Gallery' icon='res/mipmap-mdpi-v4/ic_launcher.png'
+application-debuggable
+launchable-activity: name='org.domokit.sky.shell.SkyActivity'  label='' icon=''
+feature-group: label=''
+  uses-feature: name='android.hardware.screen.portrait'
+  uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation'
+  uses-feature: name='android.hardware.touchscreen'
+  uses-implied-feature: name='android.hardware.touchscreen' reason='default feature for all apps'
+main
+supports-screens: 'small' 'normal' 'large' 'xlarge'
+supports-any-density: 'true'
+locales: '--_--'
+densities: '160' '240' '320' '480' '640'
+native-code: 'armeabi-v7a'
+''';
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index a384ea1..9633a9a 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -17,13 +17,12 @@
 class MockApplicationPackageStore extends ApplicationPackageStore {
   MockApplicationPackageStore() : super(
     android: new AndroidApk(
-      buildDir: '/mock/path/to/android',
       id: 'io.flutter.android.mock',
       apkPath: '/mock/path/to/android/SkyShell.apk',
       launchActivity: 'io.flutter.android.mock.MockActivity'
     ),
     iOS: new IOSApp(
-      projectDir: '/mock/path/to/iOS/SkyShell.app',
+      appDirectory: '/mock/path/to/iOS/SkyShell.app',
       projectBundleId: 'io.flutter.ios.mock'
     )
   );