diff --git a/framework/res/xml/config.xml b/framework/res/xml/config.xml index da9c1f46..b6900078 100644 --- a/framework/res/xml/config.xml +++ b/framework/res/xml/config.xml @@ -51,6 +51,7 @@ + diff --git a/framework/src/org/apache/cordova/InAppBrowser.java b/framework/src/org/apache/cordova/InAppBrowser.java new file mode 100644 index 00000000..9b747d3a --- /dev/null +++ b/framework/src/org/apache/cordova/InAppBrowser.java @@ -0,0 +1,515 @@ +/* + 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.cordova; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.StringTokenizer; + +import org.apache.cordova.api.CallbackContext; +import org.apache.cordova.api.CordovaPlugin; +import org.apache.cordova.api.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.InputType; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +public class InAppBrowser extends CordovaPlugin { + + private static final String NULL = "null"; + protected static final String LOG_TAG = "InAppBrowser"; + private static final String SELF = "_self"; + private static final String SYSTEM = "_system"; + private static final String BLANK = "_blank"; + private static final String LOCATION = "location"; + private static int CLOSE_EVENT = 0; + private static int LOCATION_CHANGED_EVENT = 1; + + private String browserCallbackId = null; + + private Dialog dialog; + private WebView inAppWebView; + private EditText edittext; + private boolean showLocationBar = true; + private CallbackContext callbackContext; + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + this.callbackContext = callbackContext; + + try { + if (action.equals("open")) { + String url = args.getString(0); + String target = args.optString(1); + if (target == null || target.equals("") || target.equals(NULL)) { + target = SELF; + } + HashMap features = parseFeature(args.optString(2)); + + Log.d(LOG_TAG, "target = " + target); + + url = updateUrl(url); + + // SELF + if (SELF.equals(target)) { + Log.d(LOG_TAG, "in self"); + // load in webview + if (url.startsWith("file://") || url.startsWith("javascript:") + || this.webView.isUrlWhiteListed(url)) { + this.webView.loadUrl(url); + } + // load in InAppBrowser + else { + result = this.showWebPage(url, features); + } + } + // SYSTEM + else if (SYSTEM.equals(target)) { + Log.d(LOG_TAG, "in system"); + result = this.openExternal(url); + } + // BLANK - or anything else + else { + Log.d(LOG_TAG, "in blank"); + result = this.showWebPage(url, features); + } + } + else if (action.equals("close")) { + closeDialog(); + + JSONObject obj = new JSONObject(); + obj.put("type", CLOSE_EVENT); + + PluginResult pluginResult = new PluginResult(status, obj); + pluginResult.setKeepCallback(false); + this.callbackContext.sendPluginResult(pluginResult); + } + else { + status = PluginResult.Status.INVALID_ACTION; + } + this.callbackContext.sendPluginResult(new PluginResult(status, result)); + } catch (JSONException e) { + this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); + } + return true; + } + + /** + * Put the list of features into a hash map + * + * @param optString + * @return + */ + private HashMap parseFeature(String optString) { + if (optString.equals(NULL)) { + return null; + } else { + HashMap map = new HashMap(); + StringTokenizer features = new StringTokenizer(optString, ","); + StringTokenizer option; + while(features.hasMoreElements()) { + option = new StringTokenizer(features.nextToken(), "="); + if (option.hasMoreElements()) { + String key = option.nextToken(); + Boolean value = option.nextToken().equals("no") ? Boolean.FALSE : Boolean.TRUE; + map.put(key, value); + } + } + return map; + } + } + + /** + * Convert relative URL to full path + * + * @param url + * @return + */ + private String updateUrl(String url) { + Uri newUrl = Uri.parse(url); + if (newUrl.isRelative()) { + url = this.webView.getUrl().substring(0, this.webView.getUrl().lastIndexOf("/")+1) + url; + } + return url; + } + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load. + * @param usePhoneGap Load url in PhoneGap webview + * @return "" if ok, or error message. + */ + public String openExternal(String url) { + try { + Intent intent = null; + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + this.cordova.getActivity().startActivity(intent); + return ""; + } catch (android.content.ActivityNotFoundException e) { + Log.d(LOG_TAG, "InAppBrowser: Error loading url "+url+":"+ e.toString()); + return e.toString(); + } + } + + /** + * Closes the dialog + */ + private void closeDialog() { + // TODO: fire 'exit' event + if (dialog != null) { + dialog.dismiss(); + } + } + + /** + * Checks to see if it is possible to go back one page in history, then does so. + */ + private void goBack() { + if (this.inAppWebView.canGoBack()) { + this.inAppWebView.goBack(); + } + } + + /** + * Checks to see if it is possible to go forward one page in history, then does so. + */ + private void goForward() { + if (this.inAppWebView.canGoForward()) { + this.inAppWebView.goForward(); + } + } + + /** + * Navigate to the new page + * + * @param url to load + */ + private void navigate(String url) { + InputMethodManager imm = (InputMethodManager)this.cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(edittext.getWindowToken(), 0); + + if (!url.startsWith("http") && !url.startsWith("file:")) { + this.inAppWebView.loadUrl("http://" + url); + } else { + this.inAppWebView.loadUrl(url); + } + this.inAppWebView.requestFocus(); + } + + + /** + * Should we show the location bar? + * + * @return boolean + */ + private boolean getShowLocationBar() { + return this.showLocationBar; + } + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load. + * @param jsonObject + */ + public String showWebPage(final String url, HashMap features) { + // Determine if we should hide the location bar. + showLocationBar = true; + if (features != null) { + showLocationBar = features.get(LOCATION).booleanValue(); + } + + // Create dialog in new thread + Runnable runnable = new Runnable() { + /** + * Convert our DIP units to Pixels + * + * @return int + */ + private int dpToPixels(int dipValue) { + int value = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, + (float) dipValue, + cordova.getActivity().getResources().getDisplayMetrics() + ); + + return value; + } + + public void run() { + // Let's create the main dialog + dialog = new Dialog(cordova.getActivity(), android.R.style.Theme_NoTitleBar); + dialog.getWindow().getAttributes().windowAnimations = android.R.style.Animation_Dialog; + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCancelable(true); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + public void onDismiss(DialogInterface dialog) { + try { + JSONObject obj = new JSONObject(); + obj.put("type", CLOSE_EVENT); + + sendUpdate(obj, false); + } catch (JSONException e) { + Log.d(LOG_TAG, "Should never happen"); + } + } + }); + + // Main container layout + LinearLayout main = new LinearLayout(cordova.getActivity()); + main.setOrientation(LinearLayout.VERTICAL); + + // Toolbar layout + RelativeLayout toolbar = new RelativeLayout(cordova.getActivity()); + toolbar.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, this.dpToPixels(44))); + toolbar.setPadding(this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2), this.dpToPixels(2)); + toolbar.setHorizontalGravity(Gravity.LEFT); + toolbar.setVerticalGravity(Gravity.TOP); + + // Action Button Container layout + RelativeLayout actionButtonContainer = new RelativeLayout(cordova.getActivity()); + actionButtonContainer.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + actionButtonContainer.setHorizontalGravity(Gravity.LEFT); + actionButtonContainer.setVerticalGravity(Gravity.CENTER_VERTICAL); + actionButtonContainer.setId(1); + + // Back button + Button back = new Button(cordova.getActivity()); + RelativeLayout.LayoutParams backLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT); + backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT); + back.setLayoutParams(backLayoutParams); + back.setContentDescription("Back Button"); + back.setId(2); + back.setText("<"); + back.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + goBack(); + } + }); + + // Forward button + Button forward = new Button(cordova.getActivity()); + RelativeLayout.LayoutParams forwardLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT); + forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2); + forward.setLayoutParams(forwardLayoutParams); + forward.setContentDescription("Forward Button"); + forward.setId(3); + forward.setText(">"); + forward.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + goForward(); + } + }); + + // Edit Text Box + edittext = new EditText(cordova.getActivity()); + RelativeLayout.LayoutParams textLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + textLayoutParams.addRule(RelativeLayout.RIGHT_OF, 1); + textLayoutParams.addRule(RelativeLayout.LEFT_OF, 5); + edittext.setLayoutParams(textLayoutParams); + edittext.setId(4); + edittext.setSingleLine(true); + edittext.setText(url); + edittext.setInputType(InputType.TYPE_TEXT_VARIATION_URI); + edittext.setImeOptions(EditorInfo.IME_ACTION_GO); + edittext.setInputType(InputType.TYPE_NULL); // Will not except input... Makes the text NON-EDITABLE + edittext.setOnKeyListener(new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + // If the event is a key-down event on the "enter" button + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + navigate(edittext.getText().toString()); + return true; + } + return false; + } + }); + + // Close button + Button close = new Button(cordova.getActivity()); + RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT); + closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + close.setLayoutParams(closeLayoutParams); + forward.setContentDescription("Close Button"); + close.setId(5); + close.setText("Done"); + close.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + closeDialog(); + } + }); + + // WebView + inAppWebView = new WebView(cordova.getActivity()); + inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + inAppWebView.setWebChromeClient(new WebChromeClient()); + WebViewClient client = new InAppBrowserClient(edittext); + inAppWebView.setWebViewClient(client); + WebSettings settings = inAppWebView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setBuiltInZoomControls(true); + settings.setPluginsEnabled(true); + settings.setDomStorageEnabled(true); + inAppWebView.loadUrl(url); + inAppWebView.setId(6); + inAppWebView.getSettings().setLoadWithOverviewMode(true); + inAppWebView.getSettings().setUseWideViewPort(true); + inAppWebView.requestFocus(); + inAppWebView.requestFocusFromTouch(); + + // Add the back and forward buttons to our action button container layout + actionButtonContainer.addView(back); + actionButtonContainer.addView(forward); + + // Add the views to our toolbar + toolbar.addView(actionButtonContainer); + toolbar.addView(edittext); + toolbar.addView(close); + + // Don't add the toolbar if its been disabled + if (getShowLocationBar()) { + // Add our toolbar to our main view/layout + main.addView(toolbar); + } + + // Add our webview to our main view/layout + main.addView(inAppWebView); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.FILL_PARENT; + lp.height = WindowManager.LayoutParams.FILL_PARENT; + + dialog.setContentView(main); + dialog.show(); + dialog.getWindow().setAttributes(lp); + } + + private Bitmap loadDrawable(String filename) throws java.io.IOException { + InputStream input = cordova.getActivity().getAssets().open(filename); + return BitmapFactory.decodeStream(input); + } + }; + this.cordova.getActivity().runOnUiThread(runnable); + return ""; + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + */ + private void sendUpdate(JSONObject obj, boolean keepCallback) { + if (this.browserCallbackId != null) { + PluginResult result = new PluginResult(PluginResult.Status.OK, obj); + result.setKeepCallback(keepCallback); + this.callbackContext.sendPluginResult(result); + } + } + + /** + * The webview client receives notifications about appView + */ + public class InAppBrowserClient extends WebViewClient { + EditText edittext; + + /** + * Constructor. + * + * @param mContext + * @param edittext + */ + public InAppBrowserClient(EditText mEditText) { + this.edittext = mEditText; + } + + /** + * Notify the host application that a page has started loading. + * + * @param view The webview initiating the callback. + * @param url The url of the page. + */ + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + String newloc; + if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) { + newloc = url; + } else { + newloc = "http://" + url; + } + + if (!newloc.equals(edittext.getText().toString())) { + edittext.setText(newloc); + } + + // TODO: Fire 'loadstart' event + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOCATION_CHANGED_EVENT); + obj.put("location", url); + + sendUpdate(obj, true); + } catch (JSONException e) { + Log.d("InAppBrowser", "This should never happen"); + } + } + + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + // TODO: Fire 'loadstop' event + } + } +} \ No newline at end of file