blob: b19ecf2a50cfe11c37a70567f355c09887cc4615 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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 org.apache.appharness;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.CordovaActivity;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.CordovaWebViewImpl;
import org.apache.cordova.PluginEntry;
import org.apache.cordova.PluginResult;
import org.apache.cordova.whitelist.WhitelistPlugin;
import org.json.JSONArray;
import org.json.JSONException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import android.annotation.TargetApi;
import android.content.res.XmlResourceParser;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.DecelerateInterpolator;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class AppHarnessUI extends CordovaPlugin {
private static final String LOG_TAG = "AppHarnessUI";
ViewGroup contentView;
CordovaWebView slaveWebView;
CustomCordovaWebView slaveWebViewEngine;
boolean slaveVisible;
CallbackContext eventsCallback;
@Override
public void pluginInitialize() {
contentView = (ViewGroup)cordova.getActivity().findViewById(android.R.id.content);
}
public boolean isSlaveVisible() {
return slaveVisible;
}
public boolean isSlaveCreated() {
return slaveWebView != null && slaveWebView.getView().getParent() != null;
}
@Override
public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
if ("create".equals(action)) {
final Uri startUri = Uri.parse(args.getString(0));
final Uri configXmlUri = Uri.parse(args.getString(1));
final Set<String> pluginIdWhitelistAsSet = jsonArrayToSet(args.getJSONArray(2));
final String webViewType = args.getString(3);
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
create(startUri, configXmlUri, pluginIdWhitelistAsSet, webViewType, callbackContext);
}
});
} else if ("reload".equals(action)) {
final Uri startUri = Uri.parse(args.getString(0));
final Uri configXmlUri = Uri.parse(args.getString(1));
final Set<String> pluginIdWhitelistAsSet = jsonArrayToSet(args.getJSONArray(2));
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
reload(startUri, configXmlUri, pluginIdWhitelistAsSet, callbackContext);
}
});
} else if ("destroy".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
destroy(callbackContext);
}
});
} else if ("setVisible".equals(action)) {
final boolean value = args.getBoolean(0);
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
setSlaveVisible(value, callbackContext);
}
});
} else if ("evalJs".equals(action)) {
final String code = args.getString(0);
this.cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
evalJs(code, callbackContext);
}
});
} else if ("events".equals(action)) {
eventsCallback = callbackContext;
} else {
return false;
}
return true;
}
private Set<String> jsonArrayToSet(JSONArray jsonArray) throws JSONException {
final Set<String> set = new HashSet<String>(jsonArray.length());
for (int i = 0; i < jsonArray.length(); ++i) {
set.add(jsonArray.getString(i));
}
return set;
}
public void sendEvent(String eventName) {
if (eventsCallback != null) {
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, eventName);
pluginResult.setKeepCallback(true);
eventsCallback.sendPluginResult(pluginResult );
}
}
private void evalJs(String code, CallbackContext callbackContext) {
if (slaveWebViewEngine == null) {
Log.w(LOG_TAG, "Not evaluating JS since no app is active");
} else {
slaveWebViewEngine.evaluateJavascript(code);
}
callbackContext.success();
}
private void create(Uri startUri, Uri configXmlUri, Set<String> pluginIdWhitelist, String webViewType, CallbackContext callbackContext) {
CordovaActivity activity = (CordovaActivity)cordova.getActivity();
if (slaveWebView != null) {
Log.w(LOG_TAG, "create: already exists");
} else {
slaveWebViewEngine = new CustomAndroidWebView(this, activity);
slaveWebView = new CordovaWebViewImpl(webView.getContext(), (CordovaWebViewEngine)slaveWebViewEngine);
// A consistent view ID is needed for plugins that utilize the background-activity plugin.
slaveWebView.getView().setId(200);
// We'll set the plugin entries in initWebView.
slaveWebView.init(cordova, new ArrayList<PluginEntry>(), preferences);
}
setPluginEntries(pluginIdWhitelist, configXmlUri);
slaveWebView.clearCache(true);
slaveWebView.clearHistory();
slaveWebView.loadUrl(startUri.toString());
contentView.addView(slaveWebView.getView());
slaveVisible = true;
// Back button capturing breaks without these:
webView.getView().setEnabled(false);
slaveWebView.getView().requestFocus();
callbackContext.success();
}
private void reload(Uri startUri, Uri configXmlUri, Set<String> pluginIdWhitelist, CallbackContext callbackContext) {
if (slaveWebView == null) {
Log.w(LOG_TAG, "reload: no webview exists");
} else {
// TODO(maxw): If the webview type has changed, create a new webview.
setPluginEntries(pluginIdWhitelist, configXmlUri);
slaveWebView.clearCache(true);
slaveWebView.clearHistory();
slaveWebView.loadUrl(startUri.toString());
}
callbackContext.success();
}
private void destroy(CallbackContext callbackContext) {
if (slaveWebView == null) {
Log.w(LOG_TAG, "destroy: already destroyed");
} else {
slaveWebView.loadUrl("data:text/plain;charset=utf-8,");
contentView.removeView(slaveWebView.getView());
webView.getView().setEnabled(true);
webView.getView().requestFocus();
slaveWebView.getView().setScaleX(1.0f);
slaveWebView.getView().setScaleY(1.0f);
slaveWebViewEngine.setStealTapEvents(false);
slaveVisible = false;
sendEvent("destroyed");
}
if (eventsCallback != null) {
eventsCallback.success("");
eventsCallback = null;
}
callbackContext.success();
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void setSlaveVisible(boolean value, CallbackContext callbackContext) {
if (value == slaveVisible) {
return;
}
if (slaveWebView == null) {
Log.w(LOG_TAG, "setSlaveVisible: slave not created");
} else {
slaveVisible = value;
ViewPropertyAnimator anim = slaveWebView.getView().animate();
// Note: Pivot is set in onSizeChanged.
if (value) {
anim.scaleX(1.0f).scaleY(1.0f);
webView.getView().setEnabled(false);
slaveWebView.getView().setEnabled(true);
slaveWebView.getView().requestFocus();
} else {
anim.scaleX(.25f).scaleY(.25f);
webView.getView().setEnabled(true);
slaveWebView.getView().setEnabled(false);
webView.getView().requestFocus();
}
slaveWebViewEngine.setStealTapEvents(!value);
anim.setDuration(300).setInterpolator(new DecelerateInterpolator(2.0f)).start();
}
if (callbackContext != null) {
callbackContext.success();
}
}
private void setPluginEntries(Set<String> pluginIdWhitelist, Uri configXmlUri) {
CordovaActivity activity = (CordovaActivity)cordova.getActivity();
// Extract the <feature> from CADT's config.xml, and filter out unwanted plugins.
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(activity);
ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(parser.getPluginEntries());
for (int i = 0; i < pluginEntries.size();) {
PluginEntry p = pluginEntries.get(i);
if (!pluginIdWhitelist.contains(p.service)) {
pluginEntries.remove(p);
continue;
} else if (WhitelistPlugin.class.getCanonicalName().equals(p.pluginClass)) {
pluginEntries.set(i, new PluginEntry(p.service, createWhitelistPlugin(configXmlUri)));
}
++i;
}
slaveWebView.getPluginManager().setPluginEntries(pluginEntries);
// This is added by cordova-android in code, so we need to re-add it likewise.
// Note that we re-route navigator.app.exitApp() in JS to close the webview rather than close the Activity.
slaveWebView.getPluginManager().addService("CoreAndroid", "org.apache.cordova.CoreAndroid");
}
private CordovaPlugin createWhitelistPlugin(Uri configXmlUri) {
InputStream istr = null;
try {
istr = new FileInputStream(configXmlUri.getPath());
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(false);
XmlPullParser parser = factory.newPullParser();
parser.setInput(istr, "UTF-8");
return new WhitelistPlugin(parser);
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (istr != null) {
try {
istr.close();
} catch (IOException e) {
}
}
}
return null;
}
}