Commit c5ea4098 authored by Ian Fischer's avatar Ian Fischer

Most of the infrastructure needed to install an APK on Android.

parent d8d87f18
...@@ -40,13 +40,13 @@ abstract class _Device { ...@@ -40,13 +40,13 @@ abstract class _Device {
_Device._(this.id); _Device._(this.id);
/// Install an app package on the current device /// Install an app package on the current device
bool installApp(String path); bool installApp(String appPath, String appPackageID, String appFileName);
/// Check if the current device needs an installation
bool needsInstall();
/// Check if the device is currently connected /// Check if the device is currently connected
bool isConnected(); bool isConnected();
/// Check if the current version of the given app is already installed
bool isAppInstalled(String appPath, String appPackageID, String appFileName);
} }
class AndroidDevice extends _Device { class AndroidDevice extends _Device {
...@@ -57,54 +57,44 @@ class AndroidDevice extends _Device { ...@@ -57,54 +57,44 @@ class AndroidDevice extends _Device {
String _adbPath; String _adbPath;
String get adbPath => _adbPath; String get adbPath => _adbPath;
bool _hasAdb = false;
bool _hasValidAndroid = false;
factory AndroidDevice([String id = null]) { factory AndroidDevice([String id = null]) {
return new _Device(className, id); return new _Device(className, id);
} }
AndroidDevice._(id) : super._(id) { AndroidDevice._(id) : super._(id) {
_updatePaths(); _adbPath = _getAdbPath();
_hasAdb = _checkForAdb();
// Checking for lollipop only needs to be done if we are starting an // Checking for lollipop only needs to be done if we are starting an
// app, but it has an important side effect, which is to discard any // app, but it has an important side effect, which is to discard any
// progress messages if the adb server is restarted. // progress messages if the adb server is restarted.
if (!_checkForAdb() || !_checkForLollipopOrLater()) { _hasValidAndroid = _checkForLollipopOrLater();
_logging.severe('Unable to run on Android.');
}
}
@override if (!_hasAdb || !_hasValidAndroid) {
bool installApp(String path) { _logging.severe('Unable to run on Android.');
return true;
}
@override
bool needsInstall() {
return true;
} }
@override
bool isConnected() {
return true;
} }
void _updatePaths() { String _getAdbPath() {
if (Platform.environment.containsKey('ANDROID_HOME')) { if (Platform.environment.containsKey('ANDROID_HOME')) {
String androidHomeDir = Platform.environment['ANDROID_HOME']; String androidHomeDir = Platform.environment['ANDROID_HOME'];
String adbPath1 = String adbPath1 =
path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb'); path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb'); String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
if (FileSystemEntity.isFileSync(adbPath1)) { if (FileSystemEntity.isFileSync(adbPath1)) {
_adbPath = adbPath1; return adbPath1;
} else if (FileSystemEntity.isFileSync(adbPath2)) { } else if (FileSystemEntity.isFileSync(adbPath2)) {
_adbPath = adbPath2; return adbPath2;
} else { } else {
_logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' + _logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' +
'using default path "$_ADB_PATH"'); 'using default path "$_ADB_PATH"');
_adbPath = _ADB_PATH; return _ADB_PATH;
} }
} else { } else {
_adbPath = _ADB_PATH; return _ADB_PATH;
} }
} }
...@@ -184,4 +174,74 @@ class AndroidDevice extends _Device { ...@@ -184,4 +174,74 @@ class AndroidDevice extends _Device {
} }
return false; return false;
} }
String _getDeviceSha1Path(String appPackageID, String appFileName) {
return '/sdcard/$appPackageID/$appFileName.sha1';
}
String _getDeviceApkSha1(String appPackageID, String appFileName) {
return runCheckedSync([
adbPath,
'shell',
'cat',
_getDeviceSha1Path(appPackageID, appFileName)
]);
}
String _getSourceSha1(String apkPath) {
String sha1 =
runCheckedSync(['shasum', '-a', '1', '-p', apkPath]).split(' ')[0];
return sha1;
}
@override
bool isAppInstalled(String appPath, String appPackageID, String appFileName) {
if (!isConnected()) {
return false;
}
if (runCheckedSync([adbPath, 'shell', 'pm', 'path', appPackageID]) == '') {
_logging.info(
'TODO(iansf): move this log to the caller. $appFileName is not on the device. Installing now...');
return false;
}
if (_getDeviceApkSha1(appPackageID, appFileName) !=
_getSourceSha1(appPath)) {
_logging.info(
'TODO(iansf): move this log to the caller. $appFileName is out of date. Installing now...');
return false;
}
return true;
}
@override
bool installApp(String appPath, String appPackageID, String appFileName) {
if (!isConnected()) {
_logging.info('Android device not connected. Not installing.');
return false;
}
if (!FileSystemEntity.isFileSync(appPath)) {
_logging.severe('"$appPath" does not exist.');
return false;
}
runCheckedSync([adbPath, 'install', '-r', appPath]);
Directory tempDir = Directory.systemTemp;
String sha1Path = path.join(
tempDir.path, appPath.replaceAll(path.separator, '_'), '.sha1');
File sha1TempFile = new File(sha1Path);
sha1TempFile.writeAsStringSync(_getSourceSha1(appPath), flush: true);
runCheckedSync([
adbPath,
'push',
sha1Path,
_getDeviceSha1Path(appPackageID, appFileName)
]);
sha1TempFile.deleteSync();
return true;
}
@override
bool isConnected() => _hasValidAndroid;
}
} }
...@@ -12,7 +12,8 @@ import 'common.dart'; ...@@ -12,7 +12,8 @@ import 'common.dart';
import 'device.dart'; import 'device.dart';
class InstallCommandHandler extends CommandHandler { class InstallCommandHandler extends CommandHandler {
InstallCommandHandler() AndroidDevice android = null;
InstallCommandHandler([this.android])
: super('install', 'Install your Sky app on attached devices.'); : super('install', 'Install your Sky app on attached devices.');
@override @override
...@@ -32,9 +33,11 @@ class InstallCommandHandler extends CommandHandler { ...@@ -32,9 +33,11 @@ class InstallCommandHandler extends CommandHandler {
bool installedSomewhere = false; bool installedSomewhere = false;
AndroidDevice android = new AndroidDevice(); if (android == null) {
android = new AndroidDevice();
}
if (android.isConnected()) { if (android.isConnected()) {
installedSomewhere = installedSomewhere || android.installApp(''); installedSomewhere = installedSomewhere || android.installApp('', '', '');
} }
if (installedSomewhere) { if (installedSomewhere) {
......
...@@ -14,10 +14,14 @@ main() => defineTests(); ...@@ -14,10 +14,14 @@ main() => defineTests();
defineTests() { defineTests() {
group('install', () { group('install', () {
test('install returns 0', () { test('returns 0 when Android is connected and ready for an install', () {
MockAndroidDevice android = new MockAndroidDevice();
when(android.isConnected()).thenReturn(true);
when(android.installApp(any, any, any)).thenReturn(true);
InstallCommandHandler handler = new InstallCommandHandler(android);
MockArgResults results = new MockArgResults(); MockArgResults results = new MockArgResults();
when(results['help']).thenReturn(false); when(results['help']).thenReturn(false);
InstallCommandHandler handler = new InstallCommandHandler();
handler handler
.processArgResults(results) .processArgResults(results)
.then((int code) => expect(code, equals(0))); .then((int code) => expect(code, equals(0)));
......
...@@ -4,9 +4,14 @@ ...@@ -4,9 +4,14 @@
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:sky_tools/src/device.dart';
@proxy
class MockArgResults extends Mock implements ArgResults { class MockArgResults extends Mock implements ArgResults {
@override @override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
} }
class MockAndroidDevice extends Mock implements AndroidDevice {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment