/*
* Copyright 2012-2014 eBay Software Foundation and selendroid committers.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.selendroid.server;
import android.Manifest;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.CallLog;
import android.view.View;
import io.selendroid.server.android.ActivitiesReporter;
import io.selendroid.server.android.AndroidWait;
import io.selendroid.server.common.exceptions.PermissionDeniedException;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.server.common.utils.CallLogEntry;
import io.selendroid.server.extension.ExtensionLoader;
import io.selendroid.server.model.ExternalStorage;
import io.selendroid.server.util.Intents;
import io.selendroid.server.util.SelendroidLogger;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DefaultServerInstrumentation implements ServerInstrumentation {
public static final int DEFAULT_SERVER_PORT = 8080;
private Instrumentation instrumentation;
protected InstrumentationArguments args;
private AndroidWait androidWait;
private ActivitiesReporter activitiesReporter;
protected int serverPort;
private HttpdThread serverThread;
protected PowerManager.WakeLock wakeLock;
private ExtensionLoader extensionLoader;
public DefaultServerInstrumentation(Instrumentation instrumentation,
InstrumentationArguments args) {
this.instrumentation = instrumentation;
this.args = args;
if (args.isLoadExtensions()) {
extensionLoader = new ExtensionLoader(instrumentation.getTargetContext(),
ExternalStorage.getExtensionDex().getAbsolutePath());
} else {
extensionLoader = new ExtensionLoader(instrumentation.getTargetContext());
}
activitiesReporter = new ActivitiesReporter();
androidWait = new AndroidWait();
}
@Override
public void onCreate() {
Handler mainThreadHandler = new Handler();
serverPort = parseServerPort(args.getServerPort());
callBeforeApplicationCreateBootstraps();
// Queue bootstrapping and starting of the main activity on the main thread.
mainThreadHandler.post(new Runnable() {
@Override
public void run() {
callAfterApplicationCreateBootstraps();
if (args.getServiceClassName() != null) {
startService();
} else {
startMainActivity();
}
try {
startServer();
} catch (Exception e) {
SelendroidLogger.error("Failed to start Selendroid server", e);
}
}
});
}
@Override
public void onDestroy() {
try {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
} catch (Exception e) {
SelendroidLogger.error("Error shutting down: ", e);
}
stopServer();
}
@Override
public void startService() {
if (args.getServiceClassName() != null) {
startService(args.getServiceClassName(), args.getIntentAction());
}
}
@Override
public void startService(String serviceClassName, String intentAction) {
instrumentation.getTargetContext().startService(
Intents.createStartServiceIntent(instrumentation.getTargetContext(), serviceClassName, intentAction));
}
@Override
public void startMainActivity() {
doFinishAllActivities();
if (args.getActivityClassName() != null) {
startActivity(args.getActivityClassName());
} else {
instrumentation
.getTargetContext()
.startActivity(Intents.createUriIntent(args.getIntentAction(), args.getIntentUri()));
}
}
@Override
public void startActivity(String activityClassName) {
doFinishAllActivities();
// Start the new activity
Intent intent = Intents.createStartActivityIntent(instrumentation.getTargetContext(), activityClassName);
instrumentation.getTargetContext().startActivity(intent);
}
@Override
public void startServer() {
if (serverThread != null && serverThread.isAlive()) {
return;
}
if (serverThread != null) {
SelendroidLogger.info("Stopping selendroid http server");
stopServer();
}
serverThread = new HttpdThread(this, serverPort);
serverThread.startServer();
}
@Override
public void stopServer() {
if (serverThread == null) {
return;
}
if (!serverThread.isAlive()) {
serverThread = null;
return;
}
SelendroidLogger.info("Stopping selendroid http server");
serverThread.stopLooping();
serverThread.interrupt();
try {
serverThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
serverThread = null;
}
@Override
public View getRootView() {
try {
View decorView = getCurrentActivity().getWindow().getDecorView();
if (decorView != null) {
View rootView = null;// decorView.findViewById(android.R.id.content);
if (rootView != null) {
return rootView;
}
}
return decorView;
} catch (Exception e) {
SelendroidLogger.error("Error searching for root view: ", e);
}
throw new SelendroidException("Could not find any views");
}
@Override
public ActivitiesReporter getActivitiesReporter() {
return activitiesReporter;
}
@Override
public Activity getCurrentActivity() {
return activitiesReporter.getCurrentActivity();
}
@Override
public void setImplicitWait(long millis) {
androidWait.setTimeoutInMillis(millis);
}
@Override
public void finishAllActivities() {
instrumentation.runOnMainSync(new Runnable() {
@Override
public void run() {
doFinishAllActivities();
}
});
}
@Override
public AndroidWait getAndroidWait() {
return androidWait;
}
@Override
public Map<String, String> getExtraArgs() {
return args.getExtraArgs();
}
@Override
public String getExtraArg(String key) {
return args.getExtraArg(key);
}
@Override
public void backgroundActivity() {
activitiesReporter.setBackgroundActivity(activitiesReporter.getCurrentActivity());
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
homeIntent.addCategory(Intent.CATEGORY_HOME);
homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
instrumentation.getTargetContext().startActivity(homeIntent);
}
@Override
public void resumeActivity() {
Activity activity = activitiesReporter.getBackgroundActivity();
SelendroidLogger.info("got background activity");
if (activity == null) {
SelendroidLogger
.error("activity class is empty", new NullPointerException(
"Activity class to start is null."));
return;
}
// start now the new activity
SelendroidLogger.info("background activity is not null");
Intent intent = new Intent(instrumentation.getTargetContext(), activity.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
SelendroidLogger.info("created intent and got target context");
instrumentation.getTargetContext().startActivity(intent);
SelendroidLogger.info("got target context and started activity");
activitiesReporter.setBackgroundActivity(null);
}
@Override
public void addCallLog(CallLogEntry log) {
String permission = Manifest.permission.WRITE_CALL_LOG;
if (instrumentation.getTargetContext().checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
ContentValues values = new ContentValues();
values.put(CallLog.Calls.CACHED_NUMBER_TYPE, 0);
values.put(CallLog.Calls.TYPE, log.getDirection());
values.put(CallLog.Calls.DATE, log.getDate().getTime());
values.put(CallLog.Calls.DURATION, log.getDuration());
values.put(CallLog.Calls.NUMBER, log.getNumber());
instrumentation.getTargetContext().getContentResolver().insert(CallLog.Calls.CONTENT_URI, values);
} else {
throw new PermissionDeniedException("Application Under Test does not have the required WRITE_CALL_LOGS permission for this feature..");
}
}
@Override
public List<CallLogEntry> readCallLog() {
if (instrumentation.getTargetContext().checkCallingOrSelfPermission(Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
List<CallLogEntry> logs = new ArrayList<CallLogEntry>();
Cursor managedCursor = instrumentation.getTargetContext().getContentResolver().query(CallLog.Calls.CONTENT_URI, null, null, null, null);
int number = managedCursor.getColumnIndex(CallLog.Calls.NUMBER);
int type = managedCursor.getColumnIndex(CallLog.Calls.TYPE);
int date = managedCursor.getColumnIndex(CallLog.Calls.DATE);
int duration = managedCursor.getColumnIndex(CallLog.Calls.DURATION);
while (managedCursor.moveToNext()) {
String phNumber = managedCursor.getString(number);
String callType = managedCursor.getString(type);
String callDate = managedCursor.getString(date);
Date callDayTime = new Date(Long.valueOf(callDate));
String callDuration = managedCursor.getString(duration);
logs.add(new CallLogEntry(phNumber, Integer.parseInt(callDuration), callDayTime, Integer.parseInt(callType)));
}
managedCursor.close();
return logs;
} else {
throw new PermissionDeniedException("Application under test does not have required READ_CALL_LOG permission for this feature.");
}
}
@Override
public Instrumentation getInstrumentation() {
return instrumentation;
}
@Override
public ExtensionLoader getExtensionLoader() {
return extensionLoader;
}
@Override
public String getServerVersion() {
Context context = instrumentation.getContext();
String versionName = "0.3";
try {
versionName =
context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
}
return versionName;
}
@Override
public String getCpuArch() {
return android.os.Build.CPU_ABI;
}
@Override
public String getOsName() {
return "Android";
}
@Override
public String getOsVersion() {
return String.valueOf(android.os.Build.VERSION.SDK_INT);
}
@Override
public JSONArray getSupportedApps() {
return new JSONArray();
}
@Override
public JSONArray getSupportedDevices() {
return new JSONArray();
}
private void doFinishAllActivities() {
Set<Activity> activities = activitiesReporter.getActivities();
if (activities != null && !activities.isEmpty()) {
for (Activity activity : activities) {
activity.finish();
}
}
}
public void callBeforeApplicationCreateBootstraps() {
if (!args.isLoadExtensions() || args.getBootstrapClassNames() == null) {
return;
}
extensionLoader.runBeforeApplicationCreateBootstrap(instrumentation, args.getBootstrapClassNames().split(","));
}
public void callAfterApplicationCreateBootstraps() {
if (!args.isLoadExtensions() || args.getBootstrapClassNames() == null) {
return;
}
extensionLoader.runAfterApplicationCreateBootstrap(instrumentation, args.getBootstrapClassNames().split(","));
}
private class HttpdThread extends Thread {
private final AndroidServer server;
private ServerInstrumentation instrumentation = null;
private Looper looper;
public HttpdThread(ServerInstrumentation instrumentation, int serverPort) {
this.instrumentation = instrumentation;
// Create the server but absolutely do not start it here
server = new AndroidServer(this.instrumentation, serverPort);
}
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
startServer();
Looper.loop();
}
public AndroidServer getServer() {
return server;
}
private void startServer() {
try {
// Get a wake lock to stop the cpu going to sleep
PowerManager pm = (PowerManager) instrumentation.getInstrumentation().getContext().getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "Selendroid");
try {
wakeLock.acquire();
} catch (SecurityException e) {
}
server.start();
SelendroidLogger.info("Started selendroid http server on port " + server.getPort());
} catch (Exception e) {
SelendroidLogger.error("Error starting httpd.", e);
throw new SelendroidException("Httpd failed to start!");
}
}
public void stopLooping() {
if (looper == null) {
return;
}
looper.quit();
}
}
protected int parseServerPort(String port) {
int parsedServerPort;
try {
parsedServerPort = Integer.parseInt(port);
} catch (NumberFormatException e) {
SelendroidLogger.info("Failed to parse server port, defaulting to 8080");
parsedServerPort = DEFAULT_SERVER_PORT;
}
if (!isValidPort(parsedServerPort)) {
SelendroidLogger.info("Invalid port " + parsedServerPort + ", defaulting to " + DEFAULT_SERVER_PORT);
}
return parsedServerPort;
}
private boolean isValidPort(int port) {
return port >= 1024 && port <= 65535;
}
}