mirror of
https://github.com/appium/appium.git
synced 2026-02-13 21:39:49 -06:00
Android mobile :reset now uses adb uninstall/pm install
The old clean.apk approach to resetting state doesn't work well. Using instrumentation means the app is running when the data is deleted. It's better to use the standard adb install/uninstall commands. To make this faster, the apk is pushed to the device once (/data/local/tmp/) and then installed from that location on each reset. Using the regular adb install command is slow due to the file transfer speed. Before uninstalling the app, it's stopped to prevent crashes. Some apps don't like to be uninstalled while running.
This commit is contained in:
267
android/adb.js
267
android/adb.js
@@ -19,6 +19,7 @@ var spawn = require('win-spawn')
|
||||
, rimraf = require('rimraf')
|
||||
, Logcat = require('./logcat')
|
||||
, isWindows = helpers.isWindows()
|
||||
, md5 = require('MD5')
|
||||
, deviceState = require('./device_state');
|
||||
|
||||
var noop = function() {};
|
||||
@@ -64,7 +65,6 @@ var ADB = function(opts, android) {
|
||||
this.emulatorPort = null;
|
||||
this.debugMode = true;
|
||||
this.logcat = null;
|
||||
this.cleanAPK = path.resolve(helpers.getTempPath(), this.appPackage + '.clean.apk');
|
||||
// This is set to true when the bootstrap jar crashes.
|
||||
this.restartBootstrap = false;
|
||||
// The android ref is used to resend the command that
|
||||
@@ -73,6 +73,7 @@ var ADB = function(opts, android) {
|
||||
this.cmdCb = null;
|
||||
this.binaries = {};
|
||||
this.resendLastCommand = function() {};
|
||||
this.appMD5 = null;
|
||||
};
|
||||
|
||||
ADB.prototype.checkSdkBinaryPresent = function(binary, cb) {
|
||||
@@ -152,38 +153,6 @@ ADB.prototype.checkAppPresent = function(cb) {
|
||||
}
|
||||
};
|
||||
|
||||
// Fast reset
|
||||
ADB.prototype.buildFastReset = function(skipAppSign, cb) {
|
||||
logger.info("Building fast reset");
|
||||
// Create manifest
|
||||
var targetAPK = this.apkPath
|
||||
, cleanAPKSrc = path.resolve(__dirname, '..', 'app', 'android', 'Clean.apk')
|
||||
, newPackage = this.appPackage + '.clean'
|
||||
, srcManifest = path.resolve(__dirname, '..', 'app', 'android',
|
||||
'AndroidManifest.xml.src')
|
||||
, dstManifest = path.resolve(getTempPath(), 'AndroidManifest.xml');
|
||||
|
||||
fs.writeFileSync(dstManifest, fs.readFileSync(srcManifest, "utf8"), "utf8");
|
||||
var resignApks = function(cb) {
|
||||
// Resign clean apk and target apk
|
||||
var apks = [ this.cleanAPK ];
|
||||
if (!skipAppSign) {
|
||||
logger.debug("Signing app and clean apk.");
|
||||
apks.push(targetAPK);
|
||||
} else {
|
||||
logger.debug("Skip app sign. Sign clean apk.");
|
||||
}
|
||||
this.sign(apks, cb);
|
||||
}.bind(this);
|
||||
|
||||
async.series([
|
||||
function(cb) { this.checkSdkBinaryPresent("aapt", cb); }.bind(this),
|
||||
function(cb) { this.compileManifest(dstManifest, newPackage, this.appPackage, cb); }.bind(this),
|
||||
function(cb) { this.insertManifest(dstManifest, cleanAPKSrc, this.cleanAPK, cb); }.bind(this),
|
||||
function(cb) { resignApks(cb); }.bind(this)
|
||||
], cb);
|
||||
};
|
||||
|
||||
ADB.prototype.insertSelendroidManifest = function(serverPath, cb) {
|
||||
logger.info("Inserting selendroid manifest");
|
||||
var newServerPath = this.selendroidServerPath
|
||||
@@ -463,41 +432,6 @@ ADB.prototype.checkApkCert = function(apk, cb) {
|
||||
});
|
||||
};
|
||||
|
||||
ADB.prototype.checkFastReset = function(cb) {
|
||||
logger.info("Checking whether we need to run fast reset");
|
||||
// NOP if fast reset is not true.
|
||||
if (!this.fastReset) {
|
||||
logger.info("User doesn't want fast reset, doing nothing");
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
if (this.apkPath === null) {
|
||||
logger.info("Can't run fast reset on an app that's already on the device " +
|
||||
"so doing nothing");
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
if (!this.appPackage) return cb(new Error("appPackage must be set."));
|
||||
|
||||
this.checkApkCert(this.cleanAPK, function(cleanSigned){
|
||||
this.checkApkCert(this.apkPath, function(appSigned){
|
||||
logger.debug("App signed? " + appSigned + " " + this.apkPath);
|
||||
// Only build & resign clean.apk if it doesn't exist or isn't signed.
|
||||
if (!fs.existsSync(this.cleanAPK) || !cleanSigned) {
|
||||
this.buildFastReset(appSigned, function(err){ if (err) return cb(err); cb(null); });
|
||||
} else {
|
||||
if (!appSigned) {
|
||||
// Resign app apk because it's not signed.
|
||||
this.sign([this.apkPath], cb);
|
||||
} else {
|
||||
// App and clean are already existing and signed.
|
||||
cb(null);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
ADB.prototype.getDeviceWithRetry = function(cb, count) {
|
||||
logger.info("Trying to find a connected android device");
|
||||
var error = new Error("Could not find a connected Android device.");
|
||||
@@ -536,8 +470,7 @@ ADB.prototype.prepareDevice = function(onReady) {
|
||||
function(cb) { this.prepareEmulator(cb); }.bind(this),
|
||||
function(cb) { this.getDeviceWithRetry(cb);}.bind(this),
|
||||
function(cb) { this.waitForDevice(cb); }.bind(this),
|
||||
function(cb) { this.startLogcat(cb); }.bind(this),
|
||||
function(cb) { this.checkFastReset(cb); }.bind(this)
|
||||
function(cb) { this.startLogcat(cb); }.bind(this)
|
||||
], onReady);
|
||||
};
|
||||
|
||||
@@ -1411,6 +1344,114 @@ ADB.prototype.installApk = function(apk, cb) {
|
||||
});
|
||||
};
|
||||
|
||||
ADB.prototype.removeOldApks = function(cb) {
|
||||
var listApks = function(cb) {
|
||||
var cmd = this.adbCmd + ' shell "ls /data/local/tmp/*.apk"';
|
||||
logger.info("listApks: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
if (err || stdout.indexOf("No such file") !== -1) {
|
||||
return cb(null, []);
|
||||
}
|
||||
|
||||
var apks = stdout.split("\n");
|
||||
cb(null, apks);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
var removeOtherApks = function(apks, cb) {
|
||||
var matchingApkFound = false;
|
||||
var removeString = "";
|
||||
_.each(apks, function(path) {
|
||||
path = path.trim();
|
||||
if (path.indexOf(this.appMD5) === -1 && path !== '') {
|
||||
removeString += ' rm \\"' + path + '\\";';
|
||||
logger.info("removeOtherApks pushing: " + removeString);
|
||||
} else {
|
||||
matchingApkFound = true;
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// Invoking adb shell with an empty string will open a shell console
|
||||
// so return here if there's nothing to remove.
|
||||
if (removeString === '') {
|
||||
return cb(null, matchingApkFound);
|
||||
}
|
||||
|
||||
var cmd = this.adbCmd + ' shell "' + removeString + '"';
|
||||
logger.info("removeOtherApks: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
if (stdout) logger.info(stdout);
|
||||
if (err) logger.info(err);
|
||||
cb(null, matchingApkFound);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
async.waterfall([
|
||||
function(cb){ listApks(cb); },
|
||||
function(apks, cb) { removeOtherApks(apks, cb); }
|
||||
], function(err, matchingApkFound) { cb(null, matchingApkFound); });
|
||||
};
|
||||
|
||||
// This is only invoked after checking if the app is installed.
|
||||
// installAppApk will always install the apk.
|
||||
ADB.prototype.installAppApk = function(apk, cb) {
|
||||
var getMD5 = function(cb) {
|
||||
fs.readFile(apk, function(err, buffer) {
|
||||
this.appMD5 = md5(buffer);
|
||||
cb(null);
|
||||
}.bind(this));
|
||||
}.bind(this);
|
||||
|
||||
var adbMakeFolder = function(cb) {
|
||||
var cmd = this.adbCmd + ' shell "mkdir /data/local/tmp/"';
|
||||
logger.info("adbMakeFolder: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
cb(null);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
var adbPush = function(cb) {
|
||||
var cmd = this.adbCmd + ' push "' + apk + '" "/data/local/tmp/' + this.appMD5 + '.apk"';
|
||||
logger.info("adbPush: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
cb(err);
|
||||
} else {
|
||||
logger.debug(stdout);
|
||||
cb(null);
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
var adbInstall = function(cb) {
|
||||
var cmd = this.adbCmd + ' shell "pm install -r /data/local/tmp/' + this.appMD5 + '.apk"';
|
||||
logger.info("adbInstall: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
cb(err);
|
||||
} else {
|
||||
logger.debug(stdout);
|
||||
cb(null);
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
async.waterfall([
|
||||
function(cb) { adbMakeFolder(cb); },
|
||||
function(cb) { getMD5(cb); },
|
||||
function(cb) { this.removeOldApks(cb); }.bind(this),
|
||||
// If the apk is already on device, then don't push it again.
|
||||
function(matchingApkFound, cb) {
|
||||
if (matchingApkFound) { cb(null); } else { adbPush(cb); }
|
||||
},
|
||||
function(cb) {
|
||||
adbInstall(cb);
|
||||
}
|
||||
], cb);
|
||||
};
|
||||
|
||||
ADB.prototype.uninstallApp = function(cb) {
|
||||
var next = function() {
|
||||
this.requireDeviceId();
|
||||
@@ -1442,41 +1483,53 @@ ADB.prototype.uninstallApp = function(cb) {
|
||||
};
|
||||
|
||||
ADB.prototype.runFastReset = function(cb) {
|
||||
// list instruments with: adb shell pm list instrumentation
|
||||
// targetPackage + '.clean' / clean.apk.Clear
|
||||
var clearCmd = this.adbCmd + ' shell am instrument ' + this.appPackage + '.clean/clean.apk.Clean';
|
||||
logger.debug("Running fast reset clean: " + clearCmd);
|
||||
exec(clearCmd, { maxBuffer: 524288 }, function(err, stdout, stderr) {
|
||||
if (err) {
|
||||
logger.warn(stderr);
|
||||
cb(err);
|
||||
} else {
|
||||
var stopApp = function(cb) {
|
||||
var cmd = this.adbCmd + ' shell am force-stop ' + this.appPackage;
|
||||
logger.info("stopApp: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
cb(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
var uninstallApp = function(cb) {
|
||||
var cmd = this.adbCmd + ' uninstall ' + this.appPackage;
|
||||
logger.info("uninstallApp: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
cb(null);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
var installApp = function(cb) {
|
||||
var cmd = this.adbCmd + ' shell pm install /data/local/tmp/' + this.appMD5 + '.apk';
|
||||
logger.info("installApp: " + cmd);
|
||||
exec(cmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
cb(null);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
async.series([
|
||||
function(cb) { stopApp(cb); },
|
||||
function(cb) { uninstallApp(cb); },
|
||||
function(cb) { installApp(cb); }
|
||||
], cb);
|
||||
};
|
||||
|
||||
ADB.prototype.checkAppInstallStatus = function(pkg, cb) {
|
||||
var installed = false
|
||||
, cleanInstalled = false;
|
||||
var installed = false;
|
||||
this.requireDeviceId();
|
||||
|
||||
logger.debug("Getting install/clean status for " + pkg);
|
||||
logger.debug("Getting install status for " + pkg);
|
||||
var listPkgCmd = this.adbCmd + " shell pm list packages -3 " + pkg;
|
||||
exec(listPkgCmd, { maxBuffer: 524288 }, function(err, stdout) {
|
||||
var apkInstalledRgx = new RegExp('^package:' +
|
||||
pkg.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
|
||||
installed = apkInstalledRgx.test(stdout);
|
||||
var cleanInstalledRgx = new RegExp('^package:' +
|
||||
(pkg + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
|
||||
cleanInstalled = cleanInstalledRgx.test(stdout);
|
||||
cb(null, installed, cleanInstalled);
|
||||
cb(null, installed);
|
||||
});
|
||||
};
|
||||
|
||||
ADB.prototype.installApp = function(cb) {
|
||||
var installApp = false
|
||||
, installClean = false;
|
||||
var installApp = false;
|
||||
this.requireDeviceId();
|
||||
|
||||
if (this.apkPath === null) {
|
||||
@@ -1487,11 +1540,15 @@ ADB.prototype.installApp = function(cb) {
|
||||
|
||||
this.requireApk();
|
||||
|
||||
var determineInstallAndCleanStatus = function(cb) {
|
||||
logger.info("Determining app install/clean status");
|
||||
this.checkAppInstallStatus(this.appPackage, function(err, installed, cleaned) {
|
||||
var determineInstallStatus = function(cb) {
|
||||
if (this.appMD5 === null) {
|
||||
installApp = true;
|
||||
return cb();
|
||||
}
|
||||
|
||||
logger.info("Determining app install");
|
||||
this.checkAppInstallStatus(this.appPackage, function(err, installed) {
|
||||
installApp = !installed;
|
||||
installClean = !cleaned;
|
||||
cb();
|
||||
});
|
||||
}.bind(this);
|
||||
@@ -1499,29 +1556,13 @@ ADB.prototype.installApp = function(cb) {
|
||||
var doInstall = function(cb) {
|
||||
if (installApp) {
|
||||
this.debug("Installing app apk");
|
||||
this.installApk(this.apkPath, cb);
|
||||
} else { cb(null); }
|
||||
}.bind(this);
|
||||
|
||||
var doClean = function(cb) {
|
||||
if (installClean && this.cleanApp) {
|
||||
this.debug("Installing clean apk");
|
||||
this.installApk(this.cleanAPK, cb);
|
||||
} else { cb(null); }
|
||||
}.bind(this);
|
||||
|
||||
var doFastReset = function(cb) {
|
||||
// App is already installed so reset it.
|
||||
if (!installApp && this.fastReset) {
|
||||
this.runFastReset(cb);
|
||||
this.installAppApk(this.apkPath, cb);
|
||||
} else { cb(null); }
|
||||
}.bind(this);
|
||||
|
||||
async.series([
|
||||
function(cb) { determineInstallAndCleanStatus(cb); },
|
||||
function(cb) { doInstall(cb); },
|
||||
function(cb) { doClean(cb); },
|
||||
function(cb) { doFastReset(cb); }
|
||||
function(cb) { determineInstallStatus(cb); },
|
||||
function(cb) { doInstall(cb); }
|
||||
], cb);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="target.pkg.clean"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="8"
|
||||
android:targetSdkVersion="18" />
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
|
||||
<application android:allowBackup="true">
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="clean.apk.Clean"
|
||||
android:targetPackage="target.pkg" />
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
Binary file not shown.
@@ -68,6 +68,7 @@
|
||||
"adm-zip" : "~0.4.3",
|
||||
"ws": "0.4.25",
|
||||
"socket.io" : "~0.9.14",
|
||||
"MD5" : "~1.1.0",
|
||||
"through": "~2.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user