Skip to content

Commit

Permalink
Merge pull request appium#1769 from mobiware/mobile-toggle
Browse files Browse the repository at this point in the history
Add mobile: methods on Android: toggleData, toggleFlightMode, toggleWiFi...
  • Loading branch information
jlipps committed Jan 21, 2014
2 parents 0dceea2 + 1b488f7 commit da0919c
Show file tree
Hide file tree
Showing 31 changed files with 744 additions and 6 deletions.
26 changes: 26 additions & 0 deletions lib/devices/android/adb.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,21 @@ ADB.prototype.isDeviceConnected = function(cb) {
});
};

/*
* Check whether the ADB connection is up
*/
ADB.prototype.ping = function(cb) {
this.shell("echo 'ping'", function(err, stdout) {
if (!err && stdout.indexOf("ping") === 0) {
cb(null, true);
} else if (err) {
cb(err);
} else {
cb(new Error("ADB ping failed, returned: " + stdout));
}
});
};

ADB.prototype.setDeviceId = function(deviceId) {
this.curDeviceId = deviceId;
this.adbCmd = this.adb + " -s " + deviceId;
Expand Down Expand Up @@ -661,6 +676,16 @@ ADB.prototype.restartAdb = function(cb) {
});
};


ADB.prototype.restart = function(cb) {
async.series([
this.stopLogcat.bind(this)
, this.restartAdb.bind(this)
, this.waitForDevice.bind(this)
, this.startLogcat.bind(this)
], cb);
};

ADB.prototype.startLogcat = function(cb) {
if (this.logcat !== null) {
cb(new Error("Trying to start logcat capture but it's already started!"));
Expand All @@ -678,6 +703,7 @@ ADB.prototype.startLogcat = function(cb) {
ADB.prototype.stopLogcat = function(cb) {
if (this.logcat !== null) {
this.logcat.stopCapture(cb);
this.logcat = null;
}
};

Expand Down
161 changes: 160 additions & 1 deletion lib/devices/android/android-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ var logger = require('../../server/logger.js').get('appium')
, fs = require('fs')
, path = require('path')
, md5 = require('md5calculator')
, async = require('async');
, async = require('async')
, errors = require('../../server/errors.js')
, NotYetImplementedError = errors.NotYetImplementedError;

var logTypesSupported = {
'logcat' : 'Logs for Android applications on real device and emulators ' +
Expand Down Expand Up @@ -34,6 +36,163 @@ androidCommon.background = function(secs, cb) {
}.bind(this));
};

androidCommon.openSettingsActivity = function(setting, cb) {
this.adb.getFocusedPackageAndActivity(function(err, foundPackage,
foundActivity) {
var cmd = 'am start -a android.settings.' + setting;
this.adb.shell(cmd, function(err) {
if (err) {
cb(err);
} else {
this.adb.waitForNotActivity(foundPackage, foundActivity, 5000, cb);
}
}.bind(this));
}.bind(this));
};

androidCommon.toggleSetting = function(setting, preKeySeq, ocb) {
var doKey = function(key) {
return function(cb) {
setTimeout(function() {
this.adb.keyevent(key, cb);
}.bind(this), 2000);
}.bind(this);
}.bind(this);

var settPkg, settAct;

var back = function(cb) {
this.adb.back(function(err) {
if (err) {
cb(err);
} else {
this.adb.waitForNotActivity(settPkg, settAct, 5000, cb);
}
}.bind(this));
}.bind(this);

/*
* preKeySeq is the keyevent sequence to send over ADB in order
* to position the cursor on the right option.
* By default it's [up, up, down] because we usually target the 1st item in
* the screen, and sometimes when opening settings activities the cursor is
* already positionned on the 1st item, but we can't know for sure
*/
if (preKeySeq === null) preKeySeq = [19, 19, 20]; // up, up, down

var sequence = [
function(cb) {
this.openSettingsActivity(setting, cb);
}.bind(this)
];
var len = preKeySeq.length;

for(var i = 0; i < len; i++) {
sequence.push(doKey(preKeySeq[i]));
}

sequence.push(
function(cb) {
this.adb.getFocusedPackageAndActivity(function(err, foundPackage,
foundActivity) {
settPkg = foundPackage;
settAct = foundActivity;
cb(err);
}.bind(this));
}.bind(this)
, function(cb) {
/*
* Click and handle potential ADB disconnect that occurs on official
* emulator when the network connection is disabled
*/
this.wrapActionAndHandleADBDisconnect(doKey(23), cb);
}.bind(this)
, function(cb) {
/*
* In one particular case (enable Location Services), a pop-up is
* displayed on some platforms so the user accepts or refuses that Google
* collects location data. So we wait for that pop-up to open, if it
* doesn't then proceed
*/
this.adb.waitForNotActivity(settPkg, settAct, 5000, function(err) {
if (err) {
cb(null);
} else {
// Click on right button, "Accept"
async.series([
doKey(22) // right
, doKey(23) // click
, function(cb) {
// Wait for pop-up to close
this.adb.waitForActivity(settPkg, settAct, 5000, cb);
}.bind(this)
], function(err) {
cb(err);
}.bind(this));
}
}.bind(this));
}.bind(this)
, back
);

async.series(sequence, function(err) {
if (err) return ocb(err);
ocb(null, { status: status.codes.Success.code });
}.bind(this));
};

androidCommon.toggleData = function(ocb) {
// up, up, down
this.toggleSetting('DATA_ROAMING_SETTINGS', [19, 19, 20], ocb);
};

androidCommon.toggleFlightMode = function(ocb) {
this.adb.getApiLevel(function(err, api) {
var seq = [19, 19]; // up, up
/*
* On Android 4.0 there's no "parent" button in the action bar, so we don't
* need to go down, the cursor is already at the top of the list
*/
if (api > 15) {
seq.push(20); // down
}
this.toggleSetting('AIRPLANE_MODE_SETTINGS', seq, ocb);
}.bind(this));
};

androidCommon.toggleWiFi = function(ocb) {
// right, right
this.toggleSetting('WIFI_SETTINGS', [22, 22], ocb);
};

androidCommon.toggleLocationServices = function(ocb) {
this.adb.getApiLevel(function(err, api) {
if (api > 15) {
var seq = [19, 19]; // up, up
if (api === 16) {
// This version of Android has a "parent" button in its action bar
seq.push(20); // down
} else if (api >= 19) {
// Newer versions of Android have the toggle in the Action bar
seq = [22, 22]; // right, right
/*
* Once the Location services switch is OFF, it won't receive focus
* when going back to the Location Services settings screen unless we
* send a dummy keyevent (UP) *before* opening the settings screen
*/
this.adb.keyevent(19, function(err) {
this.toggleSetting('LOCATION_SOURCE_SETTINGS', seq, ocb);
}.bind(this));
return;
}
this.toggleSetting('LOCATION_SOURCE_SETTINGS', seq, ocb);
} else {
// There's no global location services toggle on older Android versions
ocb(new NotYetImplementedError(), null);
}
}.bind(this));
};

androidCommon.prepareDevice = function(onReady) {
logger.info("Preparing device for session");
async.series([
Expand Down
68 changes: 63 additions & 5 deletions lib/devices/android/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Android.prototype.initialize = function(opts) {
this.shuttingDown = false;
this.adb = null;
this.uiautomator = null;
this.uiautomatorRestartOnExit = false;
this.uiautomatorIgnoreExit = false;
this.swipeStepsPerSec = 28;
this.dragStepsPerSec = 40;
this.asyncWaitMs = 0;
Expand Down Expand Up @@ -99,6 +101,7 @@ Android.prototype.start = function(cb, onDie) {
this.launchCb(err);
}.bind(this));
} else {
this.didLaunch = true;
this.launchCb();
}
}.bind(this));
Expand Down Expand Up @@ -135,6 +138,31 @@ Android.prototype.onLaunch = function(err) {
}
};

Android.prototype.restartUiautomator = function(cb) {
async.series([
this.forwardPort.bind(this)
, this.uiautomator.start.bind(this.uiautomator)
], cb);
};

/*
* Execute an arbitrary function and handle potential ADB disconnection before
* proceeding
*/
Android.prototype.wrapActionAndHandleADBDisconnect = function(action, ocb) {
async.series([
function(cb) {
this.uiautomatorIgnoreExit = true;
action(cb);
}.bind(this)
, this.adb.restart.bind(this.adb)
, this.restartUiautomator.bind(this)
], function(err) {
this.uiautomatorIgnoreExit = false;
ocb(err);
}.bind(this));
};

Android.prototype.onUiautomatorExit = function() {
var respondToClient = function() {
this.cleanup();
Expand All @@ -153,11 +181,41 @@ Android.prototype.onUiautomatorExit = function() {
}.bind(this);

if (this.adb) {
logger.info("Attempting to uninstall app");
this.uninstallApp(function() {
this.shuttingDown = false;
respondToClient();
}.bind(this));
var uninstall = function() {
logger.info("Attempting to uninstall app");
this.uninstallApp(function() {
this.shuttingDown = false;
respondToClient();
}.bind(this));
}.bind(this);

if (!this.uiautomatorIgnoreExit) {
this.adb.ping(function(err, ok) {
if (ok) {
uninstall();
} else {
logger.debug(err);
this.adb.restart(function(err) {
if (err) {
logger.debug(err);
}
if (this.uiautomatorRestartOnExit) {
this.uiautomatorRestartOnExit = false;
this.restartUiautomator(function(err) {
if (err) {
logger.debug(err);
uninstall();
}
}.bind(this));
} else {
uninstall();
}
}.bind(this.adb));
}
}.bind(this));
} else {
this.uiautomatorIgnoreExit = false;
}
} else {
logger.info("We're in uiautomator's exit callback but adb is gone already");
respondToClient();
Expand Down
9 changes: 9 additions & 0 deletions lib/devices/android/logcat.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,21 @@ Logcat.prototype.startCapture = function(cb) {
cb(err);
}
}.bind(this));
this.proc.on('exit', function(code, signal) {
logger.debug('Logcat terminated with code ' + code + ', signal ' + signal);
this.proc = null;
}.bind(this));
this.proc.stdout.pipe(through(this.onStdout.bind(this)));
this.proc.stderr.pipe(through(this.onStderr.bind(this)));
};

Logcat.prototype.stopCapture = function(cb) {
logger.info("Stopping logcat capture");
if(this.proc === null) {
logger.debug("Logcat already stopped");
cb();
return;
}
this.proc.on('exit', function() {
cb();
});
Expand Down
20 changes: 20 additions & 0 deletions lib/devices/android/selendroid.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ var Selendroid = function(opts) {
, 'closeApp'
, 'isAppInstalled'
, 'launchApp'
, 'toggleData'
, 'toggleFlightMode'
, 'toggleWiFi'
, 'toggleLocationServices'
];
this.proxyHost = 'localhost';
this.proxyPort = opts.systemPort;
Expand Down Expand Up @@ -180,6 +184,22 @@ Selendroid.prototype.keyevent = function(keycode, metastate, cb) {
});
};

/*
* Execute an arbitrary function and handle potential ADB disconnection before
* proceeding
*/
Selendroid.prototype.wrapActionAndHandleADBDisconnect = function(action, ocb) {
async.series([
function(cb) {
action(cb);
}.bind(this)
, this.adb.restart.bind(this.adb)
, this.forwardPort.bind(this)
], function(err) {
ocb(err);
}.bind(this));
};

Selendroid.prototype.ensureServerExists = function(cb) {
logger.info("Checking whether selendroid is built yet");
var selBin = path.resolve(__dirname, "..", "..", "..", "build", "selendroid",
Expand Down
1 change: 1 addition & 0 deletions lib/devices/android/uiautomator.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ UiAutomator.prototype.start = function(readyCb) {
return readyCb(new Error("Could not start adb, is it around?"));
}

this.alreadyExited = false;
this.onSocketReady = readyCb;

this.proc.stdout.on('data', this.outputStreamHandler.bind(this));
Expand Down
Loading

0 comments on commit da0919c

Please sign in to comment.