diff options
Diffstat (limited to 'plugins/cordova-plugin-inappbrowser/src')
29 files changed, 4780 insertions, 0 deletions
diff --git a/plugins/cordova-plugin-inappbrowser/src/amazon/InAppBrowser.java b/plugins/cordova-plugin-inappbrowser/src/amazon/InAppBrowser.java new file mode 100644 index 00000000..0263ea2c --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/amazon/InAppBrowser.java @@ -0,0 +1,846 @@ +/* + 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.inappbrowser; + +import android.annotation.SuppressLint; +import org.apache.cordova.inappbrowser.InAppBrowserDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +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 com.amazon.android.webkit.AmazonWebChromeClient; +import com.amazon.android.webkit.AmazonGeolocationPermissions.Callback; +import com.amazon.android.webkit.AmazonJsPromptResult; +import com.amazon.android.webkit.AmazonWebSettings; +import com.amazon.android.webkit.AmazonWebStorage; +import com.amazon.android.webkit.AmazonWebView; +import com.amazon.android.webkit.AmazonWebViewClient; +import com.amazon.android.webkit.AmazonCookieManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.Config; +import org.apache.cordova.CordovaArgs; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginResult; +import org.apache.cordova.CordovaActivity; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.StringTokenizer; + +@SuppressLint("SetJavaScriptEnabled") +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 EXIT_EVENT = "exit"; + private static final String LOCATION = "location"; + private static final String HIDDEN = "hidden"; + private static final String ZOOM = "zoom"; + private static final String LOAD_START_EVENT = "loadstart"; + private static final String LOAD_STOP_EVENT = "loadstop"; + private static final String LOAD_ERROR_EVENT = "loaderror"; + private static final String CLEAR_ALL_CACHE = "clearcache"; + private static final String CLEAR_SESSION_CACHE = "clearsessioncache"; + + private InAppBrowserDialog dialog; + private AmazonWebView inAppWebView; + private EditText edittext; + private CallbackContext callbackContext; + private boolean showLocationBar = true; + private boolean showZoomControls = true; + private boolean openWindowHidden = false; + private boolean clearAllCache= false; + private boolean clearSessionCache=false; + + /** + * 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, CordovaArgs args, final CallbackContext callbackContext) throws JSONException { + if (action.equals("open")) { + this.callbackContext = callbackContext; + final String url = args.getString(0); + String t = args.optString(1); + if (t == null || t.equals("") || t.equals(NULL)) { + t = SELF; + } + final String target = t; + final HashMap<String, Boolean> features = parseFeature(args.optString(2)); + + Log.d(LOG_TAG, "target = " + target); + + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + String result = ""; + // SELF + if (SELF.equals(target)) { + Log.d(LOG_TAG, "in self"); + // load in webview + if (url.startsWith("file://") || url.startsWith("javascript:") + || Config.isUrlWhiteListed(url)) { + Log.d(LOG_TAG, "loading in webview"); + webView.loadUrl(url); + } + //Load the dialer + else if (url.startsWith(AmazonWebView.SCHEME_TEL)) + { + try { + Log.d(LOG_TAG, "loading in dialer"); + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } + // load in InAppBrowser + else { + Log.d(LOG_TAG, "loading in InAppBrowser"); + result = showWebPage(url, features); + } + } + // SYSTEM + else if (SYSTEM.equals(target)) { + Log.d(LOG_TAG, "in system"); + result = openExternal(url); + } + // BLANK - or anything else + else { + Log.d(LOG_TAG, "in blank"); + result = showWebPage(url, features); + } + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } + }); + } + else if (action.equals("close")) { + closeDialog(); + } + else if (action.equals("injectScriptCode")) { + String jsWrapper = null; + if (args.getBoolean(1)) { + jsWrapper = String.format("prompt(JSON.stringify([eval(%%s)]), 'gap-iab://%s')", callbackContext.getCallbackId()); + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectScriptFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('script'); c.src = %%s; c.onload = function() { prompt('', 'gap-iab://%s'); }; d.body.appendChild(c); })(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleCode")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('style'); c.innerHTML = %%s; d.body.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('style'); c.innerHTML = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%s; d.head.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %s; d.head.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("show")) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + dialog.show(); + } + }); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); + } + else { + return false; + } + return true; + } + + /** + * Called when the view navigates. + */ + @Override + public void onReset() { + closeDialog(); + } + + /** + * Called by AccelBroker when listener is to be shut down. + * Stop listener. + */ + public void onDestroy() { + closeDialog(); + } + + /** + * Inject an object (script or style) into the InAppBrowser AmazonWebView. + * + * This is a helper method for the inject{Script|Style}{Code|File} API calls, which + * provides a consistent method for injecting JavaScript code into the document. + * + * If a wrapper string is supplied, then the source string will be JSON-encoded (adding + * quotes) and wrapped using string formatting. (The wrapper string should have a single + * '%s' marker) + * + * @param source The source object (filename or script/style text) to inject into + * the document. + * @param jsWrapper A JavaScript string to wrap the source string in, so that the object + * is properly injected, or null if the source string is JavaScript text + * which should be executed directly. + */ + private void injectDeferredObject(String source, String jsWrapper) { + final String scriptToInject; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } else { + scriptToInject = source; + } + final String finalScriptToInject = scriptToInject; + this.cordova.getActivity().runOnUiThread(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // This action will have the side-effect of blurring the currently focused element + inAppWebView.loadUrl("javascript:" + finalScriptToInject); + } /*else { + inAppWebView.evaluateJavascript(finalScriptToInject, null); + }*/ + } + }); + } + + /** + * Put the list of features into a hash map + * + * @param optString + * @return + */ + private HashMap<String, Boolean> parseFeature(String optString) { + if (optString.equals(NULL)) { + return null; + } else { + HashMap<String, Boolean> map = new HashMap<String, Boolean>(); + 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; + } + } + + /** + * 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); + // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". + // Adding the MIME type to http: URLs causes them to not be handled by the downloader. + Uri uri = Uri.parse(url); + if ("file".equals(uri.getScheme())) { + intent.setDataAndType(uri, webView.getResourceApi().getMimeType(uri)); + } else { + intent.setData(uri); + } + 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 + */ + public void closeDialog() { + final AmazonWebView childView = this.inAppWebView; + // The JS protects against multiple calls, so this should happen only when + // closeDialog() is called by other native code. + if (childView == null) { + return; + } + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + childView.setWebViewClient(new AmazonWebViewClient() { + // NB: wait for about:blank before dismissing + public void onPageFinished(AmazonWebView view, String url) { + if (dialog != null) { + dialog.dismiss(); + } + } + }); + // NB: From SDK 19: "If you call methods on WebView from any thread + // other than your app's UI thread, it can cause unexpected results." + // http://developer.android.com/guide/webapps/migrating.html#Threads + childView.loadUrl("about:blank"); + } + }); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", EXIT_EVENT); + sendUpdate(obj, false); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + /** + * Checks to see if it is possible to go back one page in history, then does so. + */ + private void goBack() { + this.cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + if (InAppBrowser.this.inAppWebView.canGoBack()) { + InAppBrowser.this.inAppWebView.goBack(); + } + } + }); + } + + /** + * Checks to see if it is possible to go forward one page in history, then does so. + */ + private void goForward() { + this.cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + if (InAppBrowser.this.inAppWebView.canGoForward()) { + InAppBrowser.this.inAppWebView.goForward(); + } + } + }); + } + + /** + * Navigate to the new page + * + * @param url to load + */ + private void navigate(final String url) { + InputMethodManager imm = (InputMethodManager)this.cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(edittext.getWindowToken(), 0); + + this.cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + if (!url.startsWith("http") && !url.startsWith("file:")) { + InAppBrowser.this.inAppWebView.loadUrl("http://" + url); + } else { + InAppBrowser.this.inAppWebView.loadUrl(url); + } + InAppBrowser.this.inAppWebView.requestFocus(); + } + }); + } + + + /** + * Should we show the location bar? + * + * @return boolean + */ + private boolean getShowLocationBar() { + return this.showLocationBar; + } + + /** + * Should we show the zoom controls? + * + * @return boolean + */ + private boolean getShowZoomControls() { + return this.showZoomControls; + } + + private InAppBrowser getInAppBrowser(){ + return this; + } + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load. + * @param jsonObject + */ + public String showWebPage(final String url, HashMap<String, Boolean> features) { + // Determine if we should hide the location bar. + showLocationBar = true; + showZoomControls = true; + openWindowHidden = false; + if (features != null) { + Boolean show = features.get(LOCATION); + if (show != null) { + showLocationBar = show.booleanValue(); + } + Boolean zoom = features.get(ZOOM); + if (zoom != null) { + showZoomControls = zoom.booleanValue(); + } + Boolean hidden = features.get(HIDDEN); + if (hidden != null) { + openWindowHidden = hidden.booleanValue(); + } + Boolean cache = features.get(CLEAR_ALL_CACHE); + if (cache != null) { + clearAllCache = cache.booleanValue(); + } else { + cache = features.get(CLEAR_SESSION_CACHE); + if (cache != null) { + clearSessionCache = cache.booleanValue(); + } + } + } + + final CordovaWebView thatWebView = this.webView; + + // 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; + } + + @SuppressLint("NewApi") + public void run() { + // Let's create the main dialog + dialog = new InAppBrowserDialog(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.setInAppBroswer(getInAppBrowser()); + + // Main container layout + LinearLayout main = new LinearLayout(cordova.getActivity()); + main.setOrientation(LinearLayout.VERTICAL); + + // Toolbar layout + RelativeLayout toolbar = new RelativeLayout(cordova.getActivity()); + //Please, no more black! + toolbar.setBackgroundColor(android.graphics.Color.LTGRAY); + toolbar.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.MATCH_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.MATCH_PARENT); + backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT); + back.setLayoutParams(backLayoutParams); + back.setContentDescription("Back Button"); + back.setId(2); + Resources activityRes = cordova.getActivity().getResources(); + int backResId = activityRes.getIdentifier("ic_action_previous_item", "drawable", cordova.getActivity().getPackageName()); + Drawable backIcon = activityRes.getDrawable(backResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + back.setBackgroundDrawable(backIcon); + } + else + { + back.setBackground(backIcon); + } + + 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.MATCH_PARENT); + forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2); + forward.setLayoutParams(forwardLayoutParams); + forward.setContentDescription("Forward Button"); + forward.setId(3); + int fwdResId = activityRes.getIdentifier("ic_action_next_item", "drawable", cordova.getActivity().getPackageName()); + Drawable fwdIcon = activityRes.getDrawable(fwdResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + forward.setBackgroundDrawable(fwdIcon); + } + else + { + forward.setBackground(fwdIcon); + } + 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.MATCH_PARENT, LayoutParams.MATCH_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/Done button + Button close = new Button(cordova.getActivity()); + RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + close.setLayoutParams(closeLayoutParams); + forward.setContentDescription("Close Button"); + close.setId(5); + int closeResId = activityRes.getIdentifier("ic_action_remove", "drawable", cordova.getActivity().getPackageName()); + Drawable closeIcon = activityRes.getDrawable(closeResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + close.setBackgroundDrawable(closeIcon); + } + else + { + close.setBackground(closeIcon); + } + close.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + closeDialog(); + } + }); + + // WebView + inAppWebView = new AmazonWebView(cordova.getActivity()); + + CordovaActivity app = (CordovaActivity) cordova.getActivity(); + cordova.getFactory().initializeWebView(inAppWebView, 0x00FF00, false, null); + + inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView)); + AmazonWebViewClient client = new InAppBrowserClient(thatWebView, edittext); + inAppWebView.setWebViewClient(client); + AmazonWebSettings settings = inAppWebView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setBuiltInZoomControls(getShowZoomControls()); + settings.setPluginState(com.amazon.android.webkit.AmazonWebSettings.PluginState.ON); + + //Toggle whether this is enabled or not! + Bundle appSettings = cordova.getActivity().getIntent().getExtras(); + boolean enableDatabase = appSettings == null ? true : appSettings.getBoolean("InAppBrowserStorageEnabled", true); + if (enableDatabase) { + String databasePath = cordova.getActivity().getApplicationContext().getDir("inAppBrowserDB", Context.MODE_PRIVATE).getPath(); + settings.setDatabasePath(databasePath); + settings.setDatabaseEnabled(true); + } + settings.setDomStorageEnabled(true); + + if (clearAllCache) { + AmazonCookieManager.getInstance().removeAllCookie(); + } else if (clearSessionCache) { + AmazonCookieManager.getInstance().removeSessionCookie(); + } + + 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.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + + dialog.setContentView(main); + dialog.show(); + dialog.getWindow().setAttributes(lp); + // the goal of openhidden is to load the url and not display it + // Show() needs to be called to cause the URL to be loaded + if(openWindowHidden) { + dialog.hide(); + } + } + }; + this.cordova.getActivity().runOnUiThread(runnable); + return ""; + } + + /** + * Create a new plugin success result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + */ + private void sendUpdate(JSONObject obj, boolean keepCallback) { + sendUpdate(obj, keepCallback, PluginResult.Status.OK); + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + * @param status the status code to return to the JavaScript environment + */ + private void sendUpdate(JSONObject obj, boolean keepCallback, PluginResult.Status status) { + if (callbackContext != null) { + PluginResult result = new PluginResult(status, obj); + result.setKeepCallback(keepCallback); + callbackContext.sendPluginResult(result); + if (!keepCallback) { + callbackContext = null; + } + } + } + + /** + * The webview client receives notifications about appView + */ + public class InAppBrowserClient extends AmazonWebViewClient { + EditText edittext; + CordovaWebView webView; + + /** + * Constructor. + * + * @param mContext + * @param edittext + */ + public InAppBrowserClient(CordovaWebView webView, EditText mEditText) { + this.webView = webView; + 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(AmazonWebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + String newloc = ""; + if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) { + newloc = url; + } + // If dialing phone (tel:5551212) + else if (url.startsWith(AmazonWebView.SCHEME_TEL)) { + try { + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } + + else if (url.startsWith("geo:") || url.startsWith(AmazonWebView.SCHEME_MAILTO) || url.startsWith("market:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString()); + } + } + // If sms:5551212?body=This is the message + else if (url.startsWith("sms:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + + // Get address + String address = null; + int parmIndex = url.indexOf('?'); + if (parmIndex == -1) { + address = url.substring(4); + } + else { + address = url.substring(4, parmIndex); + + // If body, then set sms body + Uri uri = Uri.parse(url); + String query = uri.getQuery(); + if (query != null) { + if (query.startsWith("body=")) { + intent.putExtra("sms_body", query.substring(5)); + } + } + } + intent.setData(Uri.parse("sms:" + address)); + intent.putExtra("address", address); + intent.setType("vnd.android-dir/mms-sms"); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); + } + } + else { + newloc = "http://" + url; + } + + if (!newloc.equals(edittext.getText().toString())) { + edittext.setText(newloc); + } + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_START_EVENT); + obj.put("url", newloc); + + sendUpdate(obj, true); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + + public void onPageFinished(AmazonWebView view, String url) { + super.onPageFinished(view, url); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_STOP_EVENT); + obj.put("url", url); + + sendUpdate(obj, true); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + + public void onReceivedError(AmazonWebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", failingUrl); + obj.put("code", errorCode); + obj.put("message", description); + + sendUpdate(obj, true, PluginResult.Status.ERROR); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + } +} diff --git a/plugins/cordova-plugin-inappbrowser/src/amazon/InAppChromeClient.java b/plugins/cordova-plugin-inappbrowser/src/amazon/InAppChromeClient.java new file mode 100644 index 00000000..37cf101f --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/amazon/InAppChromeClient.java @@ -0,0 +1,146 @@ +/* + 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.inappbrowser; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +import com.amazon.android.webkit.AmazonWebChromeClient; +import com.amazon.android.webkit.AmazonGeolocationPermissions.Callback; +import com.amazon.android.webkit.AmazonJsPromptResult; +import com.amazon.android.webkit.AmazonWebStorage; +import com.amazon.android.webkit.AmazonWebView; +import com.amazon.android.webkit.AmazonWebViewClient; + +public class InAppChromeClient extends AmazonWebChromeClient { + + private CordovaWebView webView; + private String LOG_TAG = "InAppChromeClient"; + private long MAX_QUOTA = 100 * 1024 * 1024; + + public InAppChromeClient(CordovaWebView webView) { + super(); + this.webView = webView; + } + /** + * Handle database quota exceeded notification. + * + * @param url + * @param databaseIdentifier + * @param currentQuota + * @param estimatedSize + * @param totalUsedQuota + * @param quotaUpdater + */ + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, + long totalUsedQuota, AmazonWebStorage.QuotaUpdater quotaUpdater) + { + LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); + + if (estimatedSize < MAX_QUOTA) + { + //increase for 1Mb + long newQuota = estimatedSize; + LOG.d(LOG_TAG, "calling quotaUpdater.updateQuota newQuota: %d", newQuota); + quotaUpdater.updateQuota(newQuota); + } + else + { + // Set the quota to whatever it is and force an error + // TODO: get docs on how to handle this properly + quotaUpdater.updateQuota(currentQuota); + } + } + + /** + * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. + * + * @param origin + * @param callback + */ + @Override + public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { + super.onGeolocationPermissionsShowPrompt(origin, callback); + callback.invoke(origin, true, false); + } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * The prompt bridge provided for the InAppBrowser is capable of executing any + * oustanding callback belonging to the InAppBrowser plugin. Care has been + * taken that other callbacks cannot be triggered, and that no other code + * execution is possible. + * + * To trigger the bridge, the prompt default value should be of the form: + * + * gap-iab://<callbackId> + * + * where <callbackId> is the string id of the callback to trigger (something + * like "InAppBrowser0123456789") + * + * If present, the prompt message is expected to be a JSON-encoded value to + * pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid. + * + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + */ + @Override + public boolean onJsPrompt(AmazonWebView view, String url, String message, String defaultValue, AmazonJsPromptResult result) { + // See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute. + if (defaultValue != null && defaultValue.startsWith("gap")) { + if(defaultValue.startsWith("gap-iab://")) { + PluginResult scriptResult; + String scriptCallbackId = defaultValue.substring(10); + if (scriptCallbackId.startsWith("InAppBrowser")) { + if(message == null || message.length() == 0) { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray()); + } else { + try { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message)); + } catch(JSONException e) { + scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage()); + } + } + this.webView.sendPluginResult(scriptResult, scriptCallbackId); + result.confirm(""); + return true; + } + } + else + { + // Anything else with a gap: prefix should get this message + LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue); + result.cancel(); + return true; + } + } + return false; + } + +} diff --git a/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowser.java b/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowser.java new file mode 100644 index 00000000..217e48e1 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowser.java @@ -0,0 +1,879 @@ +/* + 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.inappbrowser; + +import android.annotation.SuppressLint; +import org.apache.cordova.inappbrowser.InAppBrowserDialog; +import android.content.Context; +import android.content.Intent; +import android.provider.Browser; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +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.CookieManager; +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; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.Config; +import org.apache.cordova.CordovaArgs; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginManager; +import org.apache.cordova.PluginResult; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.StringTokenizer; + +@SuppressLint("SetJavaScriptEnabled") +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 EXIT_EVENT = "exit"; + private static final String LOCATION = "location"; + private static final String ZOOM = "zoom"; + private static final String HIDDEN = "hidden"; + private static final String LOAD_START_EVENT = "loadstart"; + private static final String LOAD_STOP_EVENT = "loadstop"; + private static final String LOAD_ERROR_EVENT = "loaderror"; + private static final String CLEAR_ALL_CACHE = "clearcache"; + private static final String CLEAR_SESSION_CACHE = "clearsessioncache"; + private static final String HARDWARE_BACK_BUTTON = "hardwareback"; + + private InAppBrowserDialog dialog; + private WebView inAppWebView; + private EditText edittext; + private CallbackContext callbackContext; + private boolean showLocationBar = true; + private boolean showZoomControls = true; + private boolean openWindowHidden = false; + private boolean clearAllCache= false; + private boolean clearSessionCache=false; + private boolean hadwareBackButton=true; + + /** + * 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, CordovaArgs args, final CallbackContext callbackContext) throws JSONException { + if (action.equals("open")) { + this.callbackContext = callbackContext; + final String url = args.getString(0); + String t = args.optString(1); + if (t == null || t.equals("") || t.equals(NULL)) { + t = SELF; + } + final String target = t; + final HashMap<String, Boolean> features = parseFeature(args.optString(2)); + + Log.d(LOG_TAG, "target = " + target); + + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + String result = ""; + // SELF + if (SELF.equals(target)) { + Log.d(LOG_TAG, "in self"); + /* This code exists for compatibility between 3.x and 4.x versions of Cordova. + * Previously the Config class had a static method, isUrlWhitelisted(). That + * responsibility has been moved to the plugins, with an aggregating method in + * PluginManager. + */ + Boolean shouldAllowNavigation = null; + if (url.startsWith("javascript:")) { + shouldAllowNavigation = true; + } + if (shouldAllowNavigation == null) { + try { + Method iuw = Config.class.getMethod("isUrlWhiteListed", String.class); + shouldAllowNavigation = (Boolean)iuw.invoke(null, url); + } catch (NoSuchMethodException e) { + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + } + if (shouldAllowNavigation == null) { + try { + Method gpm = webView.getClass().getMethod("getPluginManager"); + PluginManager pm = (PluginManager)gpm.invoke(webView); + Method san = pm.getClass().getMethod("shouldAllowNavigation", String.class); + shouldAllowNavigation = (Boolean)san.invoke(pm, url); + } catch (NoSuchMethodException e) { + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + } + // load in webview + if (Boolean.TRUE.equals(shouldAllowNavigation)) { + Log.d(LOG_TAG, "loading in webview"); + webView.loadUrl(url); + } + //Load the dialer + else if (url.startsWith(WebView.SCHEME_TEL)) + { + try { + Log.d(LOG_TAG, "loading in dialer"); + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } + // load in InAppBrowser + else { + Log.d(LOG_TAG, "loading in InAppBrowser"); + result = showWebPage(url, features); + } + } + // SYSTEM + else if (SYSTEM.equals(target)) { + Log.d(LOG_TAG, "in system"); + result = openExternal(url); + } + // BLANK - or anything else + else { + Log.d(LOG_TAG, "in blank"); + result = showWebPage(url, features); + } + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } + }); + } + else if (action.equals("close")) { + closeDialog(); + } + else if (action.equals("injectScriptCode")) { + String jsWrapper = null; + if (args.getBoolean(1)) { + jsWrapper = String.format("prompt(JSON.stringify([eval(%%s)]), 'gap-iab://%s')", callbackContext.getCallbackId()); + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectScriptFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('script'); c.src = %%s; c.onload = function() { prompt('', 'gap-iab://%s'); }; d.body.appendChild(c); })(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleCode")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('style'); c.innerHTML = %%s; d.body.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('style'); c.innerHTML = %s; d.body.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("injectStyleFile")) { + String jsWrapper; + if (args.getBoolean(1)) { + jsWrapper = String.format("(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%s; d.head.appendChild(c); prompt('', 'gap-iab://%s');})(document)", callbackContext.getCallbackId()); + } else { + jsWrapper = "(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %s; d.head.appendChild(c); })(document)"; + } + injectDeferredObject(args.getString(0), jsWrapper); + } + else if (action.equals("show")) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + dialog.show(); + } + }); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK); + pluginResult.setKeepCallback(true); + this.callbackContext.sendPluginResult(pluginResult); + } + else { + return false; + } + return true; + } + + /** + * Called when the view navigates. + */ + @Override + public void onReset() { + closeDialog(); + } + + /** + * Called by AccelBroker when listener is to be shut down. + * Stop listener. + */ + public void onDestroy() { + closeDialog(); + } + + /** + * Inject an object (script or style) into the InAppBrowser WebView. + * + * This is a helper method for the inject{Script|Style}{Code|File} API calls, which + * provides a consistent method for injecting JavaScript code into the document. + * + * If a wrapper string is supplied, then the source string will be JSON-encoded (adding + * quotes) and wrapped using string formatting. (The wrapper string should have a single + * '%s' marker) + * + * @param source The source object (filename or script/style text) to inject into + * the document. + * @param jsWrapper A JavaScript string to wrap the source string in, so that the object + * is properly injected, or null if the source string is JavaScript text + * which should be executed directly. + */ + private void injectDeferredObject(String source, String jsWrapper) { + String scriptToInject; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length()-1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } else { + scriptToInject = source; + } + final String finalScriptToInject = scriptToInject; + this.cordova.getActivity().runOnUiThread(new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // This action will have the side-effect of blurring the currently focused element + inAppWebView.loadUrl("javascript:" + finalScriptToInject); + } else { + inAppWebView.evaluateJavascript(finalScriptToInject, null); + } + } + }); + } + + /** + * Put the list of features into a hash map + * + * @param optString + * @return + */ + private HashMap<String, Boolean> parseFeature(String optString) { + if (optString.equals(NULL)) { + return null; + } else { + HashMap<String, Boolean> map = new HashMap<String, Boolean>(); + 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; + } + } + + /** + * 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); + // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". + // Adding the MIME type to http: URLs causes them to not be handled by the downloader. + Uri uri = Uri.parse(url); + if ("file".equals(uri.getScheme())) { + intent.setDataAndType(uri, webView.getResourceApi().getMimeType(uri)); + } else { + intent.setData(uri); + } + intent.putExtra(Browser.EXTRA_APPLICATION_ID, cordova.getActivity().getPackageName()); + 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 + */ + public void closeDialog() { + final WebView childView = this.inAppWebView; + // The JS protects against multiple calls, so this should happen only when + // closeDialog() is called by other native code. + if (childView == null) { + return; + } + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + childView.setWebViewClient(new WebViewClient() { + // NB: wait for about:blank before dismissing + public void onPageFinished(WebView view, String url) { + if (dialog != null) { + dialog.dismiss(); + } + } + }); + // NB: From SDK 19: "If you call methods on WebView from any thread + // other than your app's UI thread, it can cause unexpected results." + // http://developer.android.com/guide/webapps/migrating.html#Threads + childView.loadUrl("about:blank"); + } + }); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", EXIT_EVENT); + sendUpdate(obj, false); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + + /** + * Checks to see if it is possible to go back one page in history, then does so. + */ + public void goBack() { + if (this.inAppWebView.canGoBack()) { + this.inAppWebView.goBack(); + } + } + + /** + * Can the web browser go back? + * @return boolean + */ + public boolean canGoBack() { + return this.inAppWebView.canGoBack(); + } + + /** + * Has the user set the hardware back button to go back + * @return boolean + */ + public boolean hardwareBack() { + return hadwareBackButton; + } + + /** + * 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; + } + + /** + * Should we show the zoom controls? + * + * @return boolean + */ + private boolean getShowZoomControls() { + return this.showZoomControls; + } + + private InAppBrowser getInAppBrowser(){ + return this; + } + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load. + * @param jsonObject + */ + public String showWebPage(final String url, HashMap<String, Boolean> features) { + // Determine if we should hide the location bar. + showLocationBar = true; + showZoomControls = true; + openWindowHidden = false; + if (features != null) { + Boolean show = features.get(LOCATION); + if (show != null) { + showLocationBar = show.booleanValue(); + } + Boolean zoom = features.get(ZOOM); + if (zoom != null) { + showZoomControls = zoom.booleanValue(); + } + Boolean hidden = features.get(HIDDEN); + if (hidden != null) { + openWindowHidden = hidden.booleanValue(); + } + Boolean hardwareBack = features.get(HARDWARE_BACK_BUTTON); + if (hardwareBack != null) { + hadwareBackButton = hardwareBack.booleanValue(); + } + Boolean cache = features.get(CLEAR_ALL_CACHE); + if (cache != null) { + clearAllCache = cache.booleanValue(); + } else { + cache = features.get(CLEAR_SESSION_CACHE); + if (cache != null) { + clearSessionCache = cache.booleanValue(); + } + } + } + + final CordovaWebView thatWebView = this.webView; + + // 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; + } + + @SuppressLint("NewApi") + public void run() { + // Let's create the main dialog + dialog = new InAppBrowserDialog(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.setInAppBroswer(getInAppBrowser()); + + // Main container layout + LinearLayout main = new LinearLayout(cordova.getActivity()); + main.setOrientation(LinearLayout.VERTICAL); + + // Toolbar layout + RelativeLayout toolbar = new RelativeLayout(cordova.getActivity()); + //Please, no more black! + toolbar.setBackgroundColor(android.graphics.Color.LTGRAY); + toolbar.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.MATCH_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.MATCH_PARENT); + backLayoutParams.addRule(RelativeLayout.ALIGN_LEFT); + back.setLayoutParams(backLayoutParams); + back.setContentDescription("Back Button"); + back.setId(2); + Resources activityRes = cordova.getActivity().getResources(); + int backResId = activityRes.getIdentifier("ic_action_previous_item", "drawable", cordova.getActivity().getPackageName()); + Drawable backIcon = activityRes.getDrawable(backResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + back.setBackgroundDrawable(backIcon); + } + else + { + back.setBackground(backIcon); + } + 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.MATCH_PARENT); + forwardLayoutParams.addRule(RelativeLayout.RIGHT_OF, 2); + forward.setLayoutParams(forwardLayoutParams); + forward.setContentDescription("Forward Button"); + forward.setId(3); + int fwdResId = activityRes.getIdentifier("ic_action_next_item", "drawable", cordova.getActivity().getPackageName()); + Drawable fwdIcon = activityRes.getDrawable(fwdResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + forward.setBackgroundDrawable(fwdIcon); + } + else + { + forward.setBackground(fwdIcon); + } + 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.MATCH_PARENT, LayoutParams.MATCH_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/Done button + Button close = new Button(cordova.getActivity()); + RelativeLayout.LayoutParams closeLayoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + closeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + close.setLayoutParams(closeLayoutParams); + forward.setContentDescription("Close Button"); + close.setId(5); + int closeResId = activityRes.getIdentifier("ic_action_remove", "drawable", cordova.getActivity().getPackageName()); + Drawable closeIcon = activityRes.getDrawable(closeResId); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + { + close.setBackgroundDrawable(closeIcon); + } + else + { + close.setBackground(closeIcon); + } + close.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + closeDialog(); + } + }); + + // WebView + inAppWebView = new WebView(cordova.getActivity()); + inAppWebView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + inAppWebView.setWebChromeClient(new InAppChromeClient(thatWebView)); + WebViewClient client = new InAppBrowserClient(thatWebView, edittext); + inAppWebView.setWebViewClient(client); + WebSettings settings = inAppWebView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setBuiltInZoomControls(getShowZoomControls()); + settings.setPluginState(android.webkit.WebSettings.PluginState.ON); + + //Toggle whether this is enabled or not! + Bundle appSettings = cordova.getActivity().getIntent().getExtras(); + boolean enableDatabase = appSettings == null ? true : appSettings.getBoolean("InAppBrowserStorageEnabled", true); + if (enableDatabase) { + String databasePath = cordova.getActivity().getApplicationContext().getDir("inAppBrowserDB", Context.MODE_PRIVATE).getPath(); + settings.setDatabasePath(databasePath); + settings.setDatabaseEnabled(true); + } + settings.setDomStorageEnabled(true); + + if (clearAllCache) { + CookieManager.getInstance().removeAllCookie(); + } else if (clearSessionCache) { + CookieManager.getInstance().removeSessionCookie(); + } + + 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.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + + dialog.setContentView(main); + dialog.show(); + dialog.getWindow().setAttributes(lp); + // the goal of openhidden is to load the url and not display it + // Show() needs to be called to cause the URL to be loaded + if(openWindowHidden) { + dialog.hide(); + } + } + }; + this.cordova.getActivity().runOnUiThread(runnable); + return ""; + } + + /** + * Create a new plugin success result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + */ + private void sendUpdate(JSONObject obj, boolean keepCallback) { + sendUpdate(obj, keepCallback, PluginResult.Status.OK); + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param obj a JSONObject contain event payload information + * @param status the status code to return to the JavaScript environment + */ + private void sendUpdate(JSONObject obj, boolean keepCallback, PluginResult.Status status) { + if (callbackContext != null) { + PluginResult result = new PluginResult(status, obj); + result.setKeepCallback(keepCallback); + callbackContext.sendPluginResult(result); + if (!keepCallback) { + callbackContext = null; + } + } + } + + /** + * The webview client receives notifications about appView + */ + public class InAppBrowserClient extends WebViewClient { + EditText edittext; + CordovaWebView webView; + + /** + * Constructor. + * + * @param mContext + * @param edittext + */ + public InAppBrowserClient(CordovaWebView webView, EditText mEditText) { + this.webView = webView; + 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; + } + // If dialing phone (tel:5551212) + else if (url.startsWith(WebView.SCHEME_TEL)) { + try { + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error dialing " + url + ": " + e.toString()); + } + } + + else if (url.startsWith("geo:") || url.startsWith(WebView.SCHEME_MAILTO) || url.startsWith("market:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error with " + url + ": " + e.toString()); + } + } + // If sms:5551212?body=This is the message + else if (url.startsWith("sms:")) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + + // Get address + String address = null; + int parmIndex = url.indexOf('?'); + if (parmIndex == -1) { + address = url.substring(4); + } + else { + address = url.substring(4, parmIndex); + + // If body, then set sms body + Uri uri = Uri.parse(url); + String query = uri.getQuery(); + if (query != null) { + if (query.startsWith("body=")) { + intent.putExtra("sms_body", query.substring(5)); + } + } + } + intent.setData(Uri.parse("sms:" + address)); + intent.putExtra("address", address); + intent.setType("vnd.android-dir/mms-sms"); + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(LOG_TAG, "Error sending sms " + url + ":" + e.toString()); + } + } + else { + newloc = "http://" + url; + } + + if (!newloc.equals(edittext.getText().toString())) { + edittext.setText(newloc); + } + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_START_EVENT); + obj.put("url", newloc); + + sendUpdate(obj, true); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_STOP_EVENT); + obj.put("url", url); + + sendUpdate(obj, true); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + + try { + JSONObject obj = new JSONObject(); + obj.put("type", LOAD_ERROR_EVENT); + obj.put("url", failingUrl); + obj.put("code", errorCode); + obj.put("message", description); + + sendUpdate(obj, true, PluginResult.Status.ERROR); + } catch (JSONException ex) { + Log.d(LOG_TAG, "Should never happen"); + } + } + } +} diff --git a/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowserDialog.java b/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowserDialog.java new file mode 100644 index 00000000..d7017202 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/InAppBrowserDialog.java @@ -0,0 +1,58 @@ +/* + 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.inappbrowser; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by Oliver on 22/11/2013. + */ +public class InAppBrowserDialog extends Dialog { + Context context; + InAppBrowser inAppBrowser = null; + + public InAppBrowserDialog(Context context, int theme) { + super(context, theme); + this.context = context; + } + + public void setInAppBroswer(InAppBrowser browser) { + this.inAppBrowser = browser; + } + + public void onBackPressed () { + if (this.inAppBrowser == null) { + this.dismiss(); + } else { + // better to go through the in inAppBrowser + // because it does a clean up + if (this.inAppBrowser.hardwareBack() && this.inAppBrowser.canGoBack()) { + this.inAppBrowser.goBack(); + } else { + this.inAppBrowser.closeDialog(); + } + } + } +} diff --git a/plugins/cordova-plugin-inappbrowser/src/android/InAppChromeClient.java b/plugins/cordova-plugin-inappbrowser/src/android/InAppChromeClient.java new file mode 100644 index 00000000..a2145e6a --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/InAppChromeClient.java @@ -0,0 +1,133 @@ +/* + 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.inappbrowser; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +import android.webkit.JsPromptResult; +import android.webkit.WebChromeClient; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.webkit.GeolocationPermissions.Callback; + +public class InAppChromeClient extends WebChromeClient { + + private CordovaWebView webView; + private String LOG_TAG = "InAppChromeClient"; + private long MAX_QUOTA = 100 * 1024 * 1024; + + public InAppChromeClient(CordovaWebView webView) { + super(); + this.webView = webView; + } + /** + * Handle database quota exceeded notification. + * + * @param url + * @param databaseIdentifier + * @param currentQuota + * @param estimatedSize + * @param totalUsedQuota + * @param quotaUpdater + */ + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) + { + LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); + quotaUpdater.updateQuota(MAX_QUOTA); + } + + /** + * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. + * + * @param origin + * @param callback + */ + @Override + public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { + super.onGeolocationPermissionsShowPrompt(origin, callback); + callback.invoke(origin, true, false); + } + + /** + * Tell the client to display a prompt dialog to the user. + * If the client returns true, WebView will assume that the client will + * handle the prompt dialog and call the appropriate JsPromptResult method. + * + * The prompt bridge provided for the InAppBrowser is capable of executing any + * oustanding callback belonging to the InAppBrowser plugin. Care has been + * taken that other callbacks cannot be triggered, and that no other code + * execution is possible. + * + * To trigger the bridge, the prompt default value should be of the form: + * + * gap-iab://<callbackId> + * + * where <callbackId> is the string id of the callback to trigger (something + * like "InAppBrowser0123456789") + * + * If present, the prompt message is expected to be a JSON-encoded value to + * pass to the callback. A JSON_EXCEPTION is returned if the JSON is invalid. + * + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + */ + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + // See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute. + if (defaultValue != null && defaultValue.startsWith("gap")) { + if(defaultValue.startsWith("gap-iab://")) { + PluginResult scriptResult; + String scriptCallbackId = defaultValue.substring(10); + if (scriptCallbackId.startsWith("InAppBrowser")) { + if(message == null || message.length() == 0) { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray()); + } else { + try { + scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message)); + } catch(JSONException e) { + scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage()); + } + } + this.webView.sendPluginResult(scriptResult, scriptCallbackId); + result.confirm(""); + return true; + } + } + else + { + // Anything else with a gap: prefix should get this message + LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue); + result.cancel(); + return true; + } + } + return false; + } + +} diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_next_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_next_item.png Binary files differnew file mode 100644 index 00000000..fa469d88 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_next_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_previous_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_previous_item.png Binary files differnew file mode 100644 index 00000000..e861ecce --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_previous_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_remove.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..f889617e --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-hdpi/ic_action_remove.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_next_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_next_item.png Binary files differnew file mode 100644 index 00000000..47365a30 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_next_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_previous_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_previous_item.png Binary files differnew file mode 100644 index 00000000..4ad2df42 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_previous_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_remove.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..e84853e4 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-mdpi/ic_action_remove.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_next_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_next_item.png Binary files differnew file mode 100644 index 00000000..5f304742 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_next_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_previous_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_previous_item.png Binary files differnew file mode 100644 index 00000000..ed8ac91d --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_previous_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_remove.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..4cd0458b --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xhdpi/ic_action_remove.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_next_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_next_item.png Binary files differnew file mode 100644 index 00000000..51479d8d --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_next_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_previous_item.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_previous_item.png Binary files differnew file mode 100644 index 00000000..bc8ff124 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_previous_item.png diff --git a/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_remove.png b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..331c545b --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/android/res/drawable-xxhdpi/ic_action_remove.png diff --git a/plugins/cordova-plugin-inappbrowser/src/blackberry10/README.md b/plugins/cordova-plugin-inappbrowser/src/blackberry10/README.md new file mode 100644 index 00000000..f0fa8607 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/blackberry10/README.md @@ -0,0 +1,43 @@ +<!--- + license: 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. +--> +# BlackBerry 10 In-App-Browser Plugin + +The in app browser functionality is entirely contained within common js. There is no native implementation required. +To install this plugin, follow the [Command-line Interface Guide](http://cordova.apache.org/docs/en/edge/guide_cli_index.md.html#The%20Command-line%20Interface). + +If you are not using the Cordova Command-line Interface, follow [Using Plugman to Manage Plugins](http://cordova.apache.org/docs/en/edge/guide_plugin_ref_plugman.md.html). +./cordova-plugin-battery-status/README.md +./cordova-plugin-camera/README.md +./cordova-plugin-console/README.md +./cordova-plugin-contacts/README.md +./cordova-plugin-device/README.md +./cordova-plugin-device-motion/README.md +./cordova-plugin-device-orientation/README.md +./cordova-plugin-device-orientation/src/blackberry10/README.md +./cordova-plugin-file/README.md +./cordova-plugin-file-transfer/README.md +./cordova-plugin-geolocation/README.md +./cordova-plugin-globalization/README.md +./cordova-plugin-inappbrowser/README.md +./cordova-plugin-inappbrowser/src/blackberry10/README.md +./cordova-plugin-media/README.md +./cordova-plugin-media-capture/README.md +./cordova-plugin-network-information/README.md +./cordova-plugin-splashscreen/README.md +./cordova-plugin-vibration/README.md diff --git a/plugins/cordova-plugin-inappbrowser/src/browser/InAppBrowserProxy.js b/plugins/cordova-plugin-inappbrowser/src/browser/InAppBrowserProxy.js new file mode 100644 index 00000000..33fbe476 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/browser/InAppBrowserProxy.js @@ -0,0 +1,221 @@ +/* + * + * 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. + * +*/ + +var cordova = require('cordova'), + channel = require('cordova/channel'), + urlutil = require('cordova/urlutil'); + +var browserWrap, + popup, + navigationButtonsDiv, + navigationButtonsDivInner, + backButton, + forwardButton, + closeButton; + +function attachNavigationEvents(element, callback) { + var onError = function () { + callback({ type: "loaderror", url: this.contentWindow.location}, {keepCallback: true}); + }; + + element.addEventListener("pageshow", function () { + callback({ type: "loadstart", url: this.contentWindow.location}, {keepCallback: true}); + }); + + element.addEventListener("load", function () { + callback({ type: "loadstop", url: this.contentWindow.location}, {keepCallback: true}); + }); + + element.addEventListener("error", onError); + element.addEventListener("abort", onError); +} + +var IAB = { + close: function (win, lose) { + if (browserWrap) { + if (win) win({ type: "exit" }); + + browserWrap.parentNode.removeChild(browserWrap); + browserWrap = null; + popup = null; + } + }, + + show: function (win, lose) { + if (browserWrap) { + browserWrap.style.display = "block"; + } + }, + + open: function (win, lose, args) { + var strUrl = args[0], + target = args[1], + features = args[2], + url; + + if (target === "_system" || target === "_self" || !target) { + window.location = strUrl; + } else { + // "_blank" or anything else + if (!browserWrap) { + browserWrap = document.createElement("div"); + browserWrap.style.position = "absolute"; + browserWrap.style.borderWidth = "40px"; + browserWrap.style.width = "calc(100% - 80px)"; + browserWrap.style.height = "calc(100% - 80px)"; + browserWrap.style.borderStyle = "solid"; + browserWrap.style.borderColor = "rgba(0,0,0,0.25)"; + + browserWrap.onclick = function () { + setTimeout(function () { + IAB.close(win); + }, 0); + }; + + document.body.appendChild(browserWrap); + } + + if (features.indexOf("hidden=yes") !== -1) { + browserWrap.style.display = "none"; + } + + popup = document.createElement("iframe"); + popup.style.borderWidth = "0px"; + popup.style.width = "100%"; + + browserWrap.appendChild(popup); + + if (features.indexOf("location=yes") !== -1 || features.indexOf("location") === -1) { + popup.style.height = "calc(100% - 60px)"; + + navigationButtonsDiv = document.createElement("div"); + navigationButtonsDiv.style.height = "60px"; + navigationButtonsDiv.style.backgroundColor = "#404040"; + navigationButtonsDiv.style.zIndex = "999"; + navigationButtonsDiv.onclick = function (e) { + e.cancelBubble = true; + }; + + navigationButtonsDivInner = document.createElement("div"); + navigationButtonsDivInner.style.paddingTop = "10px"; + navigationButtonsDivInner.style.height = "50px"; + navigationButtonsDivInner.style.width = "160px"; + navigationButtonsDivInner.style.margin = "0 auto"; + navigationButtonsDivInner.style.backgroundColor = "#404040"; + navigationButtonsDivInner.style.zIndex = "999"; + navigationButtonsDivInner.onclick = function (e) { + e.cancelBubble = true; + }; + + + backButton = document.createElement("button"); + backButton.style.width = "40px"; + backButton.style.height = "40px"; + backButton.style.borderRadius = "40px"; + + backButton.innerHTML = "←"; + backButton.addEventListener("click", function (e) { + if (popup.canGoBack) + popup.goBack(); + }); + + forwardButton = document.createElement("button"); + forwardButton.style.marginLeft = "20px"; + forwardButton.style.width = "40px"; + forwardButton.style.height = "40px"; + forwardButton.style.borderRadius = "40px"; + + forwardButton.innerHTML = "→"; + forwardButton.addEventListener("click", function (e) { + if (popup.canGoForward) + popup.goForward(); + }); + + closeButton = document.createElement("button"); + closeButton.style.marginLeft = "20px"; + closeButton.style.width = "40px"; + closeButton.style.height = "40px"; + closeButton.style.borderRadius = "40px"; + + closeButton.innerHTML = "✖"; + closeButton.addEventListener("click", function (e) { + setTimeout(function () { + IAB.close(win); + }, 0); + }); + + // iframe navigation is not yet supported + backButton.disabled = true; + forwardButton.disabled = true; + + navigationButtonsDivInner.appendChild(backButton); + navigationButtonsDivInner.appendChild(forwardButton); + navigationButtonsDivInner.appendChild(closeButton); + navigationButtonsDiv.appendChild(navigationButtonsDivInner); + + browserWrap.appendChild(navigationButtonsDiv); + } else { + popup.style.height = "100%"; + } + + // start listening for navigation events + attachNavigationEvents(popup, win); + + popup.src = strUrl; + } + }, + + injectScriptCode: function (win, fail, args) { + var code = args[0], + hasCallback = args[1]; + + if (browserWrap && popup) { + try { + popup.contentWindow.eval(code); + hasCallback && win([]); + } catch(e) { + console.error('Error occured while trying to injectScriptCode: ' + JSON.stringify(e)); + } + } + }, + + injectScriptFile: function (win, fail, args) { + var msg = 'Browser cordova-plugin-inappbrowser injectScriptFile is not yet implemented'; + console.warn(msg); + fail && fail(msg); + }, + + injectStyleCode: function (win, fail, args) { + var msg = 'Browser cordova-plugin-inappbrowser injectStyleCode is not yet implemented'; + console.warn(msg); + fail && fail(msg); + }, + + injectStyleFile: function (win, fail, args) { + var msg = 'Browser cordova-plugin-inappbrowser injectStyleFile is not yet implemented'; + console.warn(msg); + fail && fail(msg); + } +}; + +module.exports = IAB; + +require("cordova/exec/proxy").add("InAppBrowser", module.exports); diff --git a/plugins/cordova-plugin-inappbrowser/src/firefoxos/InAppBrowserProxy.js b/plugins/cordova-plugin-inappbrowser/src/firefoxos/InAppBrowserProxy.js new file mode 100644 index 00000000..f0d44c12 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/firefoxos/InAppBrowserProxy.js @@ -0,0 +1,191 @@ +/* + * + * 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. + * +*/ + +// https://developer.mozilla.org/en-US/docs/WebAPI/Browser + +var cordova = require('cordova'), + channel = require('cordova/channel'), + modulemapper = require('cordova/modulemapper'); + +var origOpenFunc = modulemapper.getOriginalSymbol(window, 'window.open'); +var browserWrap; + +var IABExecs = { + + close: function (win, lose) { + if (browserWrap) { + browserWrap.parentNode.removeChild(browserWrap); + browserWrap = null; + if (typeof(win) == "function") win({type:'exit'}); + } + }, + + /* + * Reveal browser if opened hidden + */ + show: function (win, lose) { + console.error('[FirefoxOS] show not implemented'); + }, + + open: function (win, lose, args) { + var strUrl = args[0], + target = args[1], + features_string = args[2] || "location=yes", //location=yes is default + features = {}, + url, + elem; + + var features_list = features_string.split(','); + features_list.forEach(function(feature) { + var tup = feature.split('='); + if (tup[1] == 'yes') { + tup[1] = true; + } else if (tup[1] == 'no') { + tup[1] = false; + } else { + var number = parseInt(tup[1]); + if (!isNaN(number)) { + tup[1] = number; + } + } + features[tup[0]] = tup[1]; + }); + + function updateIframeSizeNoLocation() { + browserWrap.style.width = window.innerWidth + 'px'; + browserWrap.style.height = window.innerHeight + 'px'; + browserWrap.style.zIndex = '999999999'; + browserWrap.browser.style.height = (window.innerHeight - 60) + 'px'; + browserWrap.browser.style.width = browserWrap.style.width; + } + + if (target === '_system') { + origOpenFunc.apply(window, [strUrl, '_blank']); + } else if (target === '_blank') { + var browserElem = document.createElement('iframe'); + browserElem.setAttribute('mozbrowser', true); + // make this loaded in its own child process + browserElem.setAttribute('remote', true); + browserElem.setAttribute('src', strUrl); + if (browserWrap) { + document.body.removeChild(browserWrap); + } + browserWrap = document.createElement('div'); + // assign browser element to browserWrap for future reference + browserWrap.browser = browserElem; + + browserWrap.classList.add('inAppBrowserWrap'); + // position fixed so that it works even when page is scrolled + browserWrap.style.position = 'fixed'; + browserElem.style.position = 'absolute'; + browserElem.style.border = 0; + browserElem.style.top = '60px'; + browserElem.style.left = '0px'; + updateIframeSizeNoLocation(); + + var menu = document.createElement('menu'); + menu.setAttribute('type', 'toolbar'); + var close = document.createElement('li'); + var back = document.createElement('li'); + var forward = document.createElement('li'); + + close.appendChild(document.createTextNode('×')); + back.appendChild(document.createTextNode('<')); + forward.appendChild(document.createTextNode('>')); + + close.classList.add('inAppBrowserClose'); + back.classList.add('inAppBrowserBack'); + forward.classList.add('inAppBrowserForward'); + + function checkForwardBackward() { + var backReq = browserElem.getCanGoBack(); + backReq.onsuccess = function() { + if (this.result) { + back.classList.remove('disabled'); + } else { + back.classList.add('disabled'); + } + } + var forwardReq = browserElem.getCanGoForward(); + forwardReq.onsuccess = function() { + if (this.result) { + forward.classList.remove('disabled'); + } else { + forward.classList.add('disabled'); + } + } + }; + + browserElem.addEventListener('mozbrowserloadend', checkForwardBackward); + + close.addEventListener('click', function () { + setTimeout(function () { + IABExecs.close(win, lose); + }, 0); + }, false); + + back.addEventListener('click', function () { + browserElem.goBack(); + }, false); + + forward.addEventListener('click', function () { + browserElem.goForward(); + }, false); + + menu.appendChild(back); + menu.appendChild(forward); + menu.appendChild(close); + + browserWrap.appendChild(menu); + browserWrap.appendChild(browserElem); + document.body.appendChild(browserWrap); + + //we use mozbrowserlocationchange instead of mozbrowserloadstart to get the url + browserElem.addEventListener('mozbrowserlocationchange', function(e){ + win({ + type:'loadstart', + url : e.detail + }) + }, false); + browserElem.addEventListener('mozbrowserloadend', function(e){ + win({type:'loadstop'}) + }, false); + browserElem.addEventListener('mozbrowsererror', function(e){ + win({type:'loaderror'}) + }, false); + browserElem.addEventListener('mozbrowserclose', function(e){ + win({type:'exit'}) + }, false); + } else { + window.location = strUrl; + } + }, + injectScriptCode: function (code, bCB) { + console.error('[FirefoxOS] injectScriptCode not implemented'); + }, + injectScriptFile: function (file, bCB) { + console.error('[FirefoxOS] injectScriptFile not implemented'); + } +}; + +module.exports = IABExecs; + +require('cordova/exec/proxy').add('InAppBrowser', module.exports); diff --git a/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.h b/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.h new file mode 100644 index 00000000..1ccc7b14 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.h @@ -0,0 +1,113 @@ +/* + 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. + */ + +#import <Cordova/CDVPlugin.h> +#import <Cordova/CDVInvokedUrlCommand.h> +#import <Cordova/CDVScreenOrientationDelegate.h> + +#ifdef __CORDOVA_4_0_0 + #import <Cordova/CDVUIWebViewDelegate.h> +#else + #import <Cordova/CDVWebViewDelegate.h> +#endif + +@class CDVInAppBrowserViewController; + +@interface CDVInAppBrowser : CDVPlugin { + BOOL _injectedIframeBridge; +} + +@property (nonatomic, retain) CDVInAppBrowserViewController* inAppBrowserViewController; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSRegularExpression *callbackIdPattern; + +- (void)open:(CDVInvokedUrlCommand*)command; +- (void)close:(CDVInvokedUrlCommand*)command; +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command; +- (void)show:(CDVInvokedUrlCommand*)command; + +@end + +@interface CDVInAppBrowserOptions : NSObject {} + +@property (nonatomic, assign) BOOL location; +@property (nonatomic, assign) BOOL toolbar; +@property (nonatomic, copy) NSString* closebuttoncaption; +@property (nonatomic, copy) NSString* toolbarposition; +@property (nonatomic, assign) BOOL clearcache; +@property (nonatomic, assign) BOOL clearsessioncache; + +@property (nonatomic, copy) NSString* presentationstyle; +@property (nonatomic, copy) NSString* transitionstyle; + +@property (nonatomic, assign) BOOL enableviewportscale; +@property (nonatomic, assign) BOOL mediaplaybackrequiresuseraction; +@property (nonatomic, assign) BOOL allowinlinemediaplayback; +@property (nonatomic, assign) BOOL keyboarddisplayrequiresuseraction; +@property (nonatomic, assign) BOOL suppressesincrementalrendering; +@property (nonatomic, assign) BOOL hidden; +@property (nonatomic, assign) BOOL disallowoverscroll; + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options; + +@end + +@interface CDVInAppBrowserViewController : UIViewController <UIWebViewDelegate, CDVScreenOrientationDelegate>{ + @private + NSString* _userAgent; + NSString* _prevUserAgent; + NSInteger _userAgentLockToken; + CDVInAppBrowserOptions *_browserOptions; + +#ifdef __CORDOVA_4_0_0 + CDVUIWebViewDelegate* _webViewDelegate; +#else + CDVWebViewDelegate* _webViewDelegate; +#endif + +} + +@property (nonatomic, strong) IBOutlet UIWebView* webView; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* closeButton; +@property (nonatomic, strong) IBOutlet UILabel* addressLabel; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* backButton; +@property (nonatomic, strong) IBOutlet UIBarButtonItem* forwardButton; +@property (nonatomic, strong) IBOutlet UIActivityIndicatorView* spinner; +@property (nonatomic, strong) IBOutlet UIToolbar* toolbar; + +@property (nonatomic, weak) id <CDVScreenOrientationDelegate> orientationDelegate; +@property (nonatomic, weak) CDVInAppBrowser* navigationDelegate; +@property (nonatomic) NSURL* currentURL; + +- (void)close; +- (void)navigateTo:(NSURL*)url; +- (void)showLocationBar:(BOOL)show; +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition; +- (void)setCloseButtonTitle:(NSString*)title; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions; + +@end + +@interface CDVInAppBrowserNavigationController : UINavigationController + +@property (nonatomic, weak) id <CDVScreenOrientationDelegate> orientationDelegate; + +@end + diff --git a/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.m b/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.m new file mode 100644 index 00000000..24f56c49 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ios/CDVInAppBrowser.m @@ -0,0 +1,1022 @@ +/* + 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. + */ + +#import "CDVInAppBrowser.h" +#import <Cordova/CDVPluginResult.h> +#import <Cordova/CDVUserAgentUtil.h> + +#define kInAppBrowserTargetSelf @"_self" +#define kInAppBrowserTargetSystem @"_system" +#define kInAppBrowserTargetBlank @"_blank" + +#define kInAppBrowserToolbarBarPositionBottom @"bottom" +#define kInAppBrowserToolbarBarPositionTop @"top" + +#define TOOLBAR_HEIGHT 44.0 +#define LOCATIONBAR_HEIGHT 21.0 +#define FOOTER_HEIGHT ((TOOLBAR_HEIGHT) + (LOCATIONBAR_HEIGHT)) + +#pragma mark CDVInAppBrowser + +@interface CDVInAppBrowser () { + NSInteger _previousStatusBarStyle; +} +@end + +@implementation CDVInAppBrowser + +- (void)pluginInitialize +{ + _previousStatusBarStyle = -1; + _callbackIdPattern = nil; +} + +- (void)onReset +{ + [self close:nil]; +} + +- (void)close:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"IAB.close() called but it was already closed."); + return; + } + // Things are cleaned up in browserExit. + [self.inAppBrowserViewController close]; +} + +- (BOOL) isSystemUrl:(NSURL*)url +{ + if ([[url host] isEqualToString:@"itunes.apple.com"]) { + return YES; + } + + return NO; +} + +- (void)open:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult* pluginResult; + + NSString* url = [command argumentAtIndex:0]; + NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf]; + NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]]; + + self.callbackId = command.callbackId; + + if (url != nil) { +#ifdef __CORDOVA_4_0_0 + NSURL* baseUrl = [self.webViewEngine URL]; +#else + NSURL* baseUrl = [self.webView.request URL]; +#endif + NSURL* absoluteUrl = [[NSURL URLWithString:url relativeToURL:baseUrl] absoluteURL]; + + if ([self isSystemUrl:absoluteUrl]) { + target = kInAppBrowserTargetSystem; + } + + if ([target isEqualToString:kInAppBrowserTargetSelf]) { + [self openInCordovaWebView:absoluteUrl withOptions:options]; + } else if ([target isEqualToString:kInAppBrowserTargetSystem]) { + [self openInSystem:absoluteUrl]; + } else { // _blank or anything else + [self openInInAppBrowser:absoluteUrl withOptions:options]; + } + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"incorrect number of arguments"]; + } + + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (void)openInInAppBrowser:(NSURL*)url withOptions:(NSString*)options +{ + CDVInAppBrowserOptions* browserOptions = [CDVInAppBrowserOptions parseOptions:options]; + + if (browserOptions.clearcache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"]) { + [storage deleteCookie:cookie]; + } + } + } + + if (browserOptions.clearsessioncache) { + NSHTTPCookie *cookie; + NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + for (cookie in [storage cookies]) + { + if (![cookie.domain isEqual: @".^filecookies^"] && cookie.isSessionOnly) { + [storage deleteCookie:cookie]; + } + } + } + + if (self.inAppBrowserViewController == nil) { + NSString* originalUA = [CDVUserAgentUtil originalUserAgent]; + self.inAppBrowserViewController = [[CDVInAppBrowserViewController alloc] initWithUserAgent:originalUA prevUserAgent:[self.commandDelegate userAgent] browserOptions: browserOptions]; + self.inAppBrowserViewController.navigationDelegate = self; + + if ([self.viewController conformsToProtocol:@protocol(CDVScreenOrientationDelegate)]) { + self.inAppBrowserViewController.orientationDelegate = (UIViewController <CDVScreenOrientationDelegate>*)self.viewController; + } + } + + [self.inAppBrowserViewController showLocationBar:browserOptions.location]; + [self.inAppBrowserViewController showToolBar:browserOptions.toolbar :browserOptions.toolbarposition]; + if (browserOptions.closebuttoncaption != nil) { + [self.inAppBrowserViewController setCloseButtonTitle:browserOptions.closebuttoncaption]; + } + // Set Presentation Style + UIModalPresentationStyle presentationStyle = UIModalPresentationFullScreen; // default + if (browserOptions.presentationstyle != nil) { + if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"pagesheet"]) { + presentationStyle = UIModalPresentationPageSheet; + } else if ([[browserOptions.presentationstyle lowercaseString] isEqualToString:@"formsheet"]) { + presentationStyle = UIModalPresentationFormSheet; + } + } + self.inAppBrowserViewController.modalPresentationStyle = presentationStyle; + + // Set Transition Style + UIModalTransitionStyle transitionStyle = UIModalTransitionStyleCoverVertical; // default + if (browserOptions.transitionstyle != nil) { + if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"fliphorizontal"]) { + transitionStyle = UIModalTransitionStyleFlipHorizontal; + } else if ([[browserOptions.transitionstyle lowercaseString] isEqualToString:@"crossdissolve"]) { + transitionStyle = UIModalTransitionStyleCrossDissolve; + } + } + self.inAppBrowserViewController.modalTransitionStyle = transitionStyle; + + // prevent webView from bouncing + if (browserOptions.disallowoverscroll) { + if ([self.inAppBrowserViewController.webView respondsToSelector:@selector(scrollView)]) { + ((UIScrollView*)[self.inAppBrowserViewController.webView scrollView]).bounces = NO; + } else { + for (id subview in self.inAppBrowserViewController.webView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + // UIWebView options + self.inAppBrowserViewController.webView.scalesPageToFit = browserOptions.enableviewportscale; + self.inAppBrowserViewController.webView.mediaPlaybackRequiresUserAction = browserOptions.mediaplaybackrequiresuseraction; + self.inAppBrowserViewController.webView.allowsInlineMediaPlayback = browserOptions.allowinlinemediaplayback; + if (IsAtLeastiOSVersion(@"6.0")) { + self.inAppBrowserViewController.webView.keyboardDisplayRequiresUserAction = browserOptions.keyboarddisplayrequiresuseraction; + self.inAppBrowserViewController.webView.suppressesIncrementalRendering = browserOptions.suppressesincrementalrendering; + } + + [self.inAppBrowserViewController navigateTo:url]; + if (!browserOptions.hidden) { + [self show:nil]; + } +} + +- (void)show:(CDVInvokedUrlCommand*)command +{ + if (self.inAppBrowserViewController == nil) { + NSLog(@"Tried to show IAB after it was closed."); + return; + } + if (_previousStatusBarStyle != -1) { + NSLog(@"Tried to show IAB while already shown"); + return; + } + + _previousStatusBarStyle = [UIApplication sharedApplication].statusBarStyle; + + CDVInAppBrowserNavigationController* nav = [[CDVInAppBrowserNavigationController alloc] + initWithRootViewController:self.inAppBrowserViewController]; + nav.orientationDelegate = self.inAppBrowserViewController; + nav.navigationBarHidden = YES; + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.inAppBrowserViewController != nil) { + [self.viewController presentViewController:nav animated:YES completion:nil]; + } + }); +} + +- (void)openInCordovaWebView:(NSURL*)url withOptions:(NSString*)options +{ + if ([self.commandDelegate URLIsWhitelisted:url]) { + NSURLRequest* request = [NSURLRequest requestWithURL:url]; +#ifdef __CORDOVA_4_0_0 + [self.webViewEngine loadRequest:request]; +#else + [self.webView loadRequest:request]; +#endif + } else { // this assumes the InAppBrowser can be excepted from the white-list + [self openInInAppBrowser:url withOptions:options]; + } +} + +- (void)openInSystem:(NSURL*)url +{ + if ([[UIApplication sharedApplication] canOpenURL:url]) { + [[UIApplication sharedApplication] openURL:url]; + } else { // handle any custom schemes to plugins + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + } +} + +// This is a helper method for the inject{Script|Style}{Code|File} API calls, which +// provides a consistent method for injecting JavaScript code into the document. +// +// If a wrapper string is supplied, then the source string will be JSON-encoded (adding +// quotes) and wrapped using string formatting. (The wrapper string should have a single +// '%@' marker). +// +// If no wrapper is supplied, then the source string is executed directly. + +- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper +{ + if (!_injectedIframeBridge) { + _injectedIframeBridge = YES; + // Create an iframe bridge in the new document to communicate with the CDVInAppBrowserViewController + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){var e = _cdvIframeBridge = d.createElement('iframe');e.style.display='none';d.body.appendChild(e);})(document)"]; + } + + if (jsWrapper != nil) { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@[source] options:0 error:nil]; + NSString* sourceArrayString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + if (sourceArrayString) { + NSString* sourceString = [sourceArrayString substringWithRange:NSMakeRange(1, [sourceArrayString length] - 2)]; + NSString* jsToInject = [NSString stringWithFormat:jsWrapper, sourceString]; + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:jsToInject]; + } + } else { + [self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:source]; + } +} + +- (void)injectScriptCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper = nil; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"_cdvIframeBridge.src='gap-iab://%@/'+encodeURIComponent(JSON.stringify([eval(%%@)]));", command.callbackId]; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectScriptFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('script'); c.src = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleCode:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('style'); c.innerHTML = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('style'); c.innerHTML = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (void)injectStyleFile:(CDVInvokedUrlCommand*)command +{ + NSString* jsWrapper; + + if ((command.callbackId != nil) && ![command.callbackId isEqualToString:@"INVALID"]) { + jsWrapper = [NSString stringWithFormat:@"(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %%@; c.onload = function() { _cdvIframeBridge.src='gap-iab://%@'; }; d.body.appendChild(c); })(document)", command.callbackId]; + } else { + jsWrapper = @"(function(d) { var c = d.createElement('link'); c.rel='stylesheet', c.type='text/css'; c.href = %@; d.body.appendChild(c); })(document)"; + } + [self injectDeferredObject:[command argumentAtIndex:0] withWrapper:jsWrapper]; +} + +- (BOOL)isValidCallbackId:(NSString *)callbackId +{ + NSError *err = nil; + // Initialize on first use + if (self.callbackIdPattern == nil) { + self.callbackIdPattern = [NSRegularExpression regularExpressionWithPattern:@"^InAppBrowser[0-9]{1,10}$" options:0 error:&err]; + if (err != nil) { + // Couldn't initialize Regex; No is safer than Yes. + return NO; + } + } + if ([self.callbackIdPattern firstMatchInString:callbackId options:0 range:NSMakeRange(0, [callbackId length])]) { + return YES; + } + return NO; +} + +/** + * The iframe bridge provided for the InAppBrowser is capable of executing any oustanding callback belonging + * to the InAppBrowser plugin. Care has been taken that other callbacks cannot be triggered, and that no + * other code execution is possible. + * + * To trigger the bridge, the iframe (or any other resource) should attempt to load a url of the form: + * + * gap-iab://<callbackId>/<arguments> + * + * where <callbackId> is the string id of the callback to trigger (something like "InAppBrowser0123456789") + * + * If present, the path component of the special gap-iab:// url is expected to be a URL-escaped JSON-encoded + * value to pass to the callback. [NSURL path] should take care of the URL-unescaping, and a JSON_EXCEPTION + * is returned if the JSON is invalid. + */ +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + NSURL* url = request.URL; + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + // See if the url uses the 'gap-iab' protocol. If so, the host should be the id of a callback to execute, + // and the path, if present, should be a JSON-encoded value to pass to the callback. + if ([[url scheme] isEqualToString:@"gap-iab"]) { + NSString* scriptCallbackId = [url host]; + CDVPluginResult* pluginResult = nil; + + if ([self isValidCallbackId:scriptCallbackId]) { + NSString* scriptResult = [url path]; + NSError* __autoreleasing error = nil; + + // The message should be a JSON-encoded array of the result of the script which executed. + if ((scriptResult != nil) && ([scriptResult length] > 1)) { + scriptResult = [scriptResult substringFromIndex:1]; + NSData* decodedResult = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; + if ((error == nil) && [decodedResult isKindOfClass:[NSArray class]]) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:(NSArray*)decodedResult]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_JSON_EXCEPTION]; + } + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:@[]]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:scriptCallbackId]; + return NO; + } + } else if ((self.callbackId != nil) && isTopLevelNavigation) { + // Send a loadstart event for each top-level navigation (includes redirects). + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstart", @"url":[url absoluteString]}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + _injectedIframeBridge = NO; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + if (self.callbackId != nil) { + // TODO: It would be more useful to return the URL the page is actually on (e.g. if it's been redirected). + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"loadstop", @"url":url}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + if (self.callbackId != nil) { + NSString* url = [self.inAppBrowserViewController.currentURL absoluteString]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsDictionary:@{@"type":@"loaderror", @"url":url, @"code": [NSNumber numberWithInteger:error.code], @"message": error.localizedDescription}]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + } +} + +- (void)browserExit +{ + if (self.callbackId != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK + messageAsDictionary:@{@"type":@"exit"}]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + self.callbackId = nil; + } + // Set navigationDelegate to nil to ensure no callbacks are received from it. + self.inAppBrowserViewController.navigationDelegate = nil; + // Don't recycle the ViewController since it may be consuming a lot of memory. + // Also - this is required for the PDF/User-Agent bug work-around. + self.inAppBrowserViewController = nil; + + if (IsAtLeastiOSVersion(@"7.0")) { + [[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle]; + } + + _previousStatusBarStyle = -1; // this value was reset before reapplying it. caused statusbar to stay black on ios7 +} + +@end + +#pragma mark CDVInAppBrowserViewController + +@implementation CDVInAppBrowserViewController + +@synthesize currentURL; + +- (id)initWithUserAgent:(NSString*)userAgent prevUserAgent:(NSString*)prevUserAgent browserOptions: (CDVInAppBrowserOptions*) browserOptions +{ + self = [super init]; + if (self != nil) { + _userAgent = userAgent; + _prevUserAgent = prevUserAgent; + _browserOptions = browserOptions; +#ifdef __CORDOVA_4_0_0 + _webViewDelegate = [[CDVUIWebViewDelegate alloc] initWithDelegate:self]; +#else + _webViewDelegate = [[CDVWebViewDelegate alloc] initWithDelegate:self]; +#endif + + [self createViews]; + } + + return self; +} + +- (void)createViews +{ + // We create the views in code for primarily for ease of upgrades and not requiring an external .xib to be included + + CGRect webViewBounds = self.view.bounds; + BOOL toolbarIsAtBottom = ![_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]; + webViewBounds.size.height -= _browserOptions.location ? FOOTER_HEIGHT : TOOLBAR_HEIGHT; + self.webView = [[UIWebView alloc] initWithFrame:webViewBounds]; + + self.webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + [self.view addSubview:self.webView]; + [self.view sendSubviewToBack:self.webView]; + + self.webView.delegate = _webViewDelegate; + self.webView.backgroundColor = [UIColor whiteColor]; + + self.webView.clearsContextBeforeDrawing = YES; + self.webView.clipsToBounds = YES; + self.webView.contentMode = UIViewContentModeScaleToFill; + self.webView.multipleTouchEnabled = YES; + self.webView.opaque = YES; + self.webView.scalesPageToFit = NO; + self.webView.userInteractionEnabled = YES; + + self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; + self.spinner.alpha = 1.000; + self.spinner.autoresizesSubviews = YES; + self.spinner.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; + self.spinner.clearsContextBeforeDrawing = NO; + self.spinner.clipsToBounds = NO; + self.spinner.contentMode = UIViewContentModeScaleToFill; + self.spinner.frame = CGRectMake(454.0, 231.0, 20.0, 20.0); + self.spinner.hidden = YES; + self.spinner.hidesWhenStopped = YES; + self.spinner.multipleTouchEnabled = NO; + self.spinner.opaque = NO; + self.spinner.userInteractionEnabled = NO; + [self.spinner stopAnimating]; + + self.closeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(close)]; + self.closeButton.enabled = YES; + + UIBarButtonItem* flexibleSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + + UIBarButtonItem* fixedSpaceButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpaceButton.width = 20; + + float toolbarY = toolbarIsAtBottom ? self.view.bounds.size.height - TOOLBAR_HEIGHT : 0.0; + CGRect toolbarFrame = CGRectMake(0.0, toolbarY, self.view.bounds.size.width, TOOLBAR_HEIGHT); + + self.toolbar = [[UIToolbar alloc] initWithFrame:toolbarFrame]; + self.toolbar.alpha = 1.000; + self.toolbar.autoresizesSubviews = YES; + self.toolbar.autoresizingMask = toolbarIsAtBottom ? (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin) : UIViewAutoresizingFlexibleWidth; + self.toolbar.barStyle = UIBarStyleBlackOpaque; + self.toolbar.clearsContextBeforeDrawing = NO; + self.toolbar.clipsToBounds = NO; + self.toolbar.contentMode = UIViewContentModeScaleToFill; + self.toolbar.hidden = NO; + self.toolbar.multipleTouchEnabled = NO; + self.toolbar.opaque = NO; + self.toolbar.userInteractionEnabled = YES; + + CGFloat labelInset = 5.0; + float locationBarY = toolbarIsAtBottom ? self.view.bounds.size.height - FOOTER_HEIGHT : self.view.bounds.size.height - LOCATIONBAR_HEIGHT; + + self.addressLabel = [[UILabel alloc] initWithFrame:CGRectMake(labelInset, locationBarY, self.view.bounds.size.width - labelInset, LOCATIONBAR_HEIGHT)]; + self.addressLabel.adjustsFontSizeToFitWidth = NO; + self.addressLabel.alpha = 1.000; + self.addressLabel.autoresizesSubviews = YES; + self.addressLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; + self.addressLabel.backgroundColor = [UIColor clearColor]; + self.addressLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + self.addressLabel.clearsContextBeforeDrawing = YES; + self.addressLabel.clipsToBounds = YES; + self.addressLabel.contentMode = UIViewContentModeScaleToFill; + self.addressLabel.enabled = YES; + self.addressLabel.hidden = NO; + self.addressLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumScaleFactor:")]) { + [self.addressLabel setValue:@(10.0/[UIFont labelFontSize]) forKey:@"minimumScaleFactor"]; + } else if ([self.addressLabel respondsToSelector:NSSelectorFromString(@"setMinimumFontSize:")]) { + [self.addressLabel setValue:@(10.0) forKey:@"minimumFontSize"]; + } + + self.addressLabel.multipleTouchEnabled = NO; + self.addressLabel.numberOfLines = 1; + self.addressLabel.opaque = NO; + self.addressLabel.shadowOffset = CGSizeMake(0.0, -1.0); + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.addressLabel.textAlignment = NSTextAlignmentLeft; + self.addressLabel.textColor = [UIColor colorWithWhite:1.000 alpha:1.000]; + self.addressLabel.userInteractionEnabled = NO; + + NSString* frontArrowString = NSLocalizedString(@"►", nil); // create arrow from Unicode char + self.forwardButton = [[UIBarButtonItem alloc] initWithTitle:frontArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goForward:)]; + self.forwardButton.enabled = YES; + self.forwardButton.imageInsets = UIEdgeInsetsZero; + + NSString* backArrowString = NSLocalizedString(@"◄", nil); // create arrow from Unicode char + self.backButton = [[UIBarButtonItem alloc] initWithTitle:backArrowString style:UIBarButtonItemStylePlain target:self action:@selector(goBack:)]; + self.backButton.enabled = YES; + self.backButton.imageInsets = UIEdgeInsetsZero; + + [self.toolbar setItems:@[self.closeButton, flexibleSpaceButton, self.backButton, fixedSpaceButton, self.forwardButton]]; + + self.view.backgroundColor = [UIColor grayColor]; + [self.view addSubview:self.toolbar]; + [self.view addSubview:self.addressLabel]; + [self.view addSubview:self.spinner]; +} + +- (void) setWebViewFrame : (CGRect) frame { + NSLog(@"Setting the WebView's frame to %@", NSStringFromCGRect(frame)); + [self.webView setFrame:frame]; +} + +- (void)setCloseButtonTitle:(NSString*)title +{ + // the advantage of using UIBarButtonSystemItemDone is the system will localize it for you automatically + // but, if you want to set this yourself, knock yourself out (we can't set the title for a system Done button, so we have to create a new one) + self.closeButton = nil; + self.closeButton = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:self action:@selector(close)]; + self.closeButton.enabled = YES; + self.closeButton.tintColor = [UIColor colorWithRed:60.0 / 255.0 green:136.0 / 255.0 blue:230.0 / 255.0 alpha:1]; + + NSMutableArray* items = [self.toolbar.items mutableCopy]; + [items replaceObjectAtIndex:0 withObject:self.closeButton]; + [self.toolbar setItems:items]; +} + +- (void)showLocationBar:(BOOL)show +{ + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL toolbarVisible = !self.toolbar.hidden; + + // prevent double show/hide + if (show == !(self.addressLabel.hidden)) { + return; + } + + if (show) { + self.addressLabel.hidden = NO; + + if (toolbarVisible) { + // toolBar at the bottom, leave as is + // put locationBar on top of the toolBar + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= FOOTER_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no toolBar, so put locationBar at the bottom + + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } + } else { + self.addressLabel.hidden = YES; + + if (toolbarVisible) { + // locationBar is on top of toolBar, hide locationBar + + // webView take up whole height less toolBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + } else { + // no toolBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)showToolBar:(BOOL)show : (NSString *) toolbarPosition +{ + CGRect toolbarFrame = self.toolbar.frame; + CGRect locationbarFrame = self.addressLabel.frame; + + BOOL locationbarVisible = !self.addressLabel.hidden; + + // prevent double show/hide + if (show == !(self.toolbar.hidden)) { + return; + } + + if (show) { + self.toolbar.hidden = NO; + CGRect webViewBounds = self.view.bounds; + + if (locationbarVisible) { + // locationBar at the bottom, move locationBar up + // put toolBar at the bottom + webViewBounds.size.height -= FOOTER_HEIGHT; + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + self.toolbar.frame = toolbarFrame; + } else { + // no locationBar, so put toolBar at the bottom + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= TOOLBAR_HEIGHT; + self.toolbar.frame = toolbarFrame; + } + + if ([toolbarPosition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + toolbarFrame.origin.y = 0; + webViewBounds.origin.y += toolbarFrame.size.height; + [self setWebViewFrame:webViewBounds]; + } else { + toolbarFrame.origin.y = (webViewBounds.size.height + LOCATIONBAR_HEIGHT); + } + [self setWebViewFrame:webViewBounds]; + + } else { + self.toolbar.hidden = YES; + + if (locationbarVisible) { + // locationBar is on top of toolBar, hide toolBar + // put locationBar at the bottom + + // webView take up whole height less locationBar height + CGRect webViewBounds = self.view.bounds; + webViewBounds.size.height -= LOCATIONBAR_HEIGHT; + [self setWebViewFrame:webViewBounds]; + + // move locationBar down + locationbarFrame.origin.y = webViewBounds.size.height; + self.addressLabel.frame = locationbarFrame; + } else { + // no locationBar, expand webView to screen dimensions + [self setWebViewFrame:self.view.bounds]; + } + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)viewDidUnload +{ + [self.webView loadHTMLString:nil baseURL:nil]; + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + [super viewDidUnload]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +- (void)close +{ + [CDVUserAgentUtil releaseLock:&_userAgentLockToken]; + self.currentURL = nil; + + if ((self.navigationDelegate != nil) && [self.navigationDelegate respondsToSelector:@selector(browserExit)]) { + [self.navigationDelegate browserExit]; + } + + // Run later to avoid the "took a long time" log message. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self respondsToSelector:@selector(presentingViewController)]) { + [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil]; + } else { + [[self parentViewController] dismissViewControllerAnimated:YES completion:nil]; + } + }); +} + +- (void)navigateTo:(NSURL*)url +{ + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + if (_userAgentLockToken != 0) { + [self.webView loadRequest:request]; + } else { + [CDVUserAgentUtil acquireLock:^(NSInteger lockToken) { + _userAgentLockToken = lockToken; + [CDVUserAgentUtil setUserAgent:_userAgent lockToken:lockToken]; + [self.webView loadRequest:request]; + }]; + } +} + +- (void)goBack:(id)sender +{ + [self.webView goBack]; +} + +- (void)goForward:(id)sender +{ + [self.webView goForward]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (IsAtLeastiOSVersion(@"7.0")) { + [[UIApplication sharedApplication] setStatusBarStyle:[self preferredStatusBarStyle]]; + } + [self rePositionViews]; + + [super viewWillAppear:animated]; +} + +// +// On iOS 7 the status bar is part of the view's dimensions, therefore it's height has to be taken into account. +// The height of it could be hardcoded as 20 pixels, but that would assume that the upcoming releases of iOS won't +// change that value. +// +- (float) getStatusBarOffset { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + float statusBarOffset = IsAtLeastiOSVersion(@"7.0") ? MIN(statusBarFrame.size.width, statusBarFrame.size.height) : 0.0; + return statusBarOffset; +} + +- (void) rePositionViews { + if ([_browserOptions.toolbarposition isEqualToString:kInAppBrowserToolbarBarPositionTop]) { + [self.webView setFrame:CGRectMake(self.webView.frame.origin.x, TOOLBAR_HEIGHT, self.webView.frame.size.width, self.webView.frame.size.height)]; + [self.toolbar setFrame:CGRectMake(self.toolbar.frame.origin.x, [self getStatusBarOffset], self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + } +} + +#pragma mark UIWebViewDelegate + +- (void)webViewDidStartLoad:(UIWebView*)theWebView +{ + // loading url, start spinner, update back/forward + + self.addressLabel.text = NSLocalizedString(@"Loading...", nil); + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner startAnimating]; + + return [self.navigationDelegate webViewDidStartLoad:theWebView]; +} + +- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType +{ + BOOL isTopLevelNavigation = [request.URL isEqual:[request mainDocumentURL]]; + + if (isTopLevelNavigation) { + self.currentURL = request.URL; + } + return [self.navigationDelegate webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType]; +} + +- (void)webViewDidFinishLoad:(UIWebView*)theWebView +{ + // update url, stop spinner, update back/forward + + self.addressLabel.text = [self.currentURL absoluteString]; + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + + [self.spinner stopAnimating]; + + // Work around a bug where the first time a PDF is opened, all UIWebViews + // reload their User-Agent from NSUserDefaults. + // This work-around makes the following assumptions: + // 1. The app has only a single Cordova Webview. If not, then the app should + // take it upon themselves to load a PDF in the background as a part of + // their start-up flow. + // 2. That the PDF does not require any additional network requests. We change + // the user-agent here back to that of the CDVViewController, so requests + // from it must pass through its white-list. This *does* break PDFs that + // contain links to other remote PDF/websites. + // More info at https://issues.apache.org/jira/browse/CB-2225 + BOOL isPDF = [@"true" isEqualToString :[theWebView stringByEvaluatingJavaScriptFromString:@"document.body==null"]]; + if (isPDF) { + [CDVUserAgentUtil setUserAgent:_prevUserAgent lockToken:_userAgentLockToken]; + } + + [self.navigationDelegate webViewDidFinishLoad:theWebView]; +} + +- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error +{ + // log fail message, stop spinner, update back/forward + NSLog(@"webView:didFailLoadWithError - %ld: %@", (long)error.code, [error localizedDescription]); + + self.backButton.enabled = theWebView.canGoBack; + self.forwardButton.enabled = theWebView.canGoForward; + [self.spinner stopAnimating]; + + self.addressLabel.text = NSLocalizedString(@"Load Error", nil); + + [self.navigationDelegate webView:theWebView didFailLoadWithError:error]; +} + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + +@end + +@implementation CDVInAppBrowserOptions + +- (id)init +{ + if (self = [super init]) { + // default values + self.location = YES; + self.toolbar = YES; + self.closebuttoncaption = nil; + self.toolbarposition = kInAppBrowserToolbarBarPositionBottom; + self.clearcache = NO; + self.clearsessioncache = NO; + + self.enableviewportscale = NO; + self.mediaplaybackrequiresuseraction = NO; + self.allowinlinemediaplayback = NO; + self.keyboarddisplayrequiresuseraction = YES; + self.suppressesincrementalrendering = NO; + self.hidden = NO; + self.disallowoverscroll = NO; + } + + return self; +} + ++ (CDVInAppBrowserOptions*)parseOptions:(NSString*)options +{ + CDVInAppBrowserOptions* obj = [[CDVInAppBrowserOptions alloc] init]; + + // NOTE: this parsing does not handle quotes within values + NSArray* pairs = [options componentsSeparatedByString:@","]; + + // parse keys and values, set the properties + for (NSString* pair in pairs) { + NSArray* keyvalue = [pair componentsSeparatedByString:@"="]; + + if ([keyvalue count] == 2) { + NSString* key = [[keyvalue objectAtIndex:0] lowercaseString]; + NSString* value = [keyvalue objectAtIndex:1]; + NSString* value_lc = [value lowercaseString]; + + BOOL isBoolean = [value_lc isEqualToString:@"yes"] || [value_lc isEqualToString:@"no"]; + NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setAllowsFloats:YES]; + BOOL isNumber = [numberFormatter numberFromString:value_lc] != nil; + + // set the property according to the key name + if ([obj respondsToSelector:NSSelectorFromString(key)]) { + if (isNumber) { + [obj setValue:[numberFormatter numberFromString:value_lc] forKey:key]; + } else if (isBoolean) { + [obj setValue:[NSNumber numberWithBool:[value_lc isEqualToString:@"yes"]] forKey:key]; + } else { + [obj setValue:value forKey:key]; + } + } + } + } + + return obj; +} + +@end + +@implementation CDVInAppBrowserNavigationController : UINavigationController + +- (void) viewDidLoad { + + CGRect frame = [UIApplication sharedApplication].statusBarFrame; + + // simplified from: http://stackoverflow.com/a/25669695/219684 + + UIToolbar* bgToolbar = [[UIToolbar alloc] initWithFrame:frame]; + bgToolbar.barStyle = UIBarStyleDefault; + [self.view addSubview:bgToolbar]; + + [super viewDidLoad]; +} + + +#pragma mark CDVScreenOrientationDelegate + +- (BOOL)shouldAutorotate +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) { + return [self.orientationDelegate shouldAutorotate]; + } + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [self.orientationDelegate supportedInterfaceOrientations]; + } + + return 1 << UIInterfaceOrientationPortrait; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) { + return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation]; + } + + return YES; +} + + +@end + diff --git a/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser.qml b/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser.qml new file mode 100644 index 00000000..781e8a6e --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser.qml @@ -0,0 +1,92 @@ +/* + * + * Copyright 2013 Canonical Ltd. + * + * 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. + * +*/ +import QtQuick 2.0 +import Ubuntu.Components.Popups 0.1 +import Ubuntu.Components 0.1 +import com.canonical.Oxide 1.0 + +Rectangle { + anchors.fill: parent + id: inappbrowser + property string url1 + Rectangle { + border.color: "black" + width: parent.width + height: urlEntry.height + color: "gray" + TextInput { + id: urlEntry + width: parent.width - closeButton.width + text: url1 + activeFocusOnPress: false + } + Image { + id: closeButton + width: height + x: parent.width - width + height: parent.height + source: "close.png" + MouseArea { + anchors.fill: parent + onClicked: { + root.exec("InAppBrowser", "close", [0, 0]) + } + } + } + } + + property string usContext: "oxide://main-world/2" + + function executeJS(scId, code) { + var req = _view.rootFrame.sendMessage(usContext, "EXECUTE", {code: code}); + + req.onreply = function(response) { + var code = 'cordova.callback(' + scId + ', JSON.parse(\'' + JSON.stringify(response.result) + '\'))'; + console.warn(code); + cordova.javaScriptExecNeeded(code); + console.warn("RESP:" + JSON.stringify(response)); + }; + } + + WebView { + width: parent.width + y: urlEntry.height + height: parent.height - y + url: url1 + id: _view + onLoadingStateChanged: { + root.exec("InAppBrowser", "loadFinished", [_view.loading]) + } + context: WebContext { + id: webcontext + + userScripts: [ + UserScript { + context: usContext + emulateGreasemonkey: true + url: "InAppBrowser_escapeScript.js" + } + ] + } + } +} diff --git a/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser_escapeScript.js b/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser_escapeScript.js new file mode 100644 index 00000000..07661bb6 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ubuntu/InAppBrowser_escapeScript.js @@ -0,0 +1,29 @@ +/* + * + * 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. + * +*/ + +oxide.addMessageHandler("EXECUTE", function(msg) { + var code = msg.args.code; + try { + msg.reply({result: eval(code)}); + } catch(e) { + msg.error("Code threw exception: \"" + e + "\""); + } +}); diff --git a/plugins/cordova-plugin-inappbrowser/src/ubuntu/close.png b/plugins/cordova-plugin-inappbrowser/src/ubuntu/close.png Binary files differnew file mode 100644 index 00000000..56373d1f --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ubuntu/close.png diff --git a/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.cpp b/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.cpp new file mode 100644 index 00000000..c5a9e64a --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.cpp @@ -0,0 +1,105 @@ +/* + * + * Copyright 2013 Canonical Ltd. + * + * 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. + * +*/ + +#include <QQuickView> +#include <QQuickItem> + +#include "inappbrowser.h" +#include <cordova.h> + +Inappbrowser::Inappbrowser(Cordova *cordova): CPlugin(cordova), _eventCb(0) { +} + +const char code[] = "\ +var component; \ +function createObject() { \ + component = Qt.createComponent(%1); \ + if (component.status == Component.Ready) \ + finishCreation(); \ + else \ + component.statusChanged.connect(finishCreation); \ +} \ +function finishCreation() { \ + CordovaWrapper.global.inappbrowser = component.createObject(root, \ + {root: root, cordova: cordova, url1: %2}); \ +} \ +createObject()"; + +const char EXIT_EVENT[] = "{type: 'exit'}"; +const char LOADSTART_EVENT[] = "{type: 'loadstart'}"; +const char LOADSTOP_EVENT[] = "{type: 'loadstop'}"; +const char LOADERROR_EVENT[] = "{type: 'loaderror'}"; + +void Inappbrowser::open(int cb, int, const QString &url, const QString &, const QString &) { + assert(_eventCb == 0); + + _eventCb = cb; + + QString path = m_cordova->get_app_dir() + "/../qml/InAppBrowser.qml"; + QString qml = QString(code) + .arg(CordovaInternal::format(path)).arg(CordovaInternal::format(url)); + m_cordova->execQML(qml); +} + +void Inappbrowser::show(int, int) { + m_cordova->execQML("CordovaWrapper.global.inappbrowser.visible = true"); +} + +void Inappbrowser::close(int, int) { + m_cordova->execQML("CordovaWrapper.global.inappbrowser.destroy()"); + this->callbackWithoutRemove(_eventCb, EXIT_EVENT); + _eventCb = 0; +} + +void Inappbrowser::injectStyleFile(int scId, int ecId, const QString& src, bool b) { + QString code("(function(d) { var c = d.createElement('link'); c.rel='stylesheet'; c.type='text/css'; c.href = %1; d.head.appendChild(c);})(document)"); + code = code.arg(CordovaInternal::format(src)); + + injectScriptCode(scId, ecId, code, b); +} + +void Inappbrowser::injectStyleCode(int scId, int ecId, const QString& src, bool b) { + QString code("(function(d) { var c = d.createElement('style'); c.innerHTML = %1; d.body.appendChild(c); })(document)"); + code = code.arg(CordovaInternal::format(src)); + + injectScriptCode(scId, ecId, code, b); +} + +void Inappbrowser::injectScriptFile(int scId, int ecId, const QString& src, bool b) { + QString code("(function(d) { var c = d.createElement('script'); c.src = %1; d.body.appendChild(c);})(document)"); + code = code.arg(CordovaInternal::format(src)); + + injectScriptCode(scId, ecId, code, b); +} + +void Inappbrowser::injectScriptCode(int scId, int, const QString& code, bool) { + m_cordova->execQML(QString("CordovaWrapper.global.inappbrowser.executeJS(%2, %1)").arg(CordovaInternal::format(code)).arg(scId)); +} + +void Inappbrowser::loadFinished(bool status) { + if (!status) { + this->callbackWithoutRemove(_eventCb, LOADSTOP_EVENT); + } else { + this->callbackWithoutRemove(_eventCb, LOADSTART_EVENT); + } +} diff --git a/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.h b/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.h new file mode 100644 index 00000000..1da4e033 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/ubuntu/inappbrowser.h @@ -0,0 +1,61 @@ +/* + * + * Copyright 2013 Canonical Ltd. + * + * 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. + * +*/ +#ifndef INAPPBROWSER_H +#define INAPPBROWSER_H + +#include <QtCore> +#include <cplugin.h> + +class Inappbrowser: public CPlugin { + Q_OBJECT +public: + Inappbrowser(Cordova *cordova); + + virtual const QString fullName() override { + return Inappbrowser::fullID(); + } + + virtual const QString shortName() override { + return "InAppBrowser"; + } + + static const QString fullID() { + return "InAppBrowser"; + } + +public slots: + void open(int cb, int, const QString &url, const QString &windowName, const QString &windowFeatures); + void show(int, int); + void close(int, int); + void injectStyleFile(int cb, int, const QString&, bool); + void injectStyleCode(int cb, int, const QString&, bool); + void injectScriptFile(int cb, int, const QString&, bool); + void injectScriptCode(int cb, int, const QString&, bool); + + void loadFinished(bool status); + +private: + int _eventCb; +}; + +#endif diff --git a/plugins/cordova-plugin-inappbrowser/src/windows/InAppBrowserProxy.js b/plugins/cordova-plugin-inappbrowser/src/windows/InAppBrowserProxy.js new file mode 100644 index 00000000..817516e5 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/windows/InAppBrowserProxy.js @@ -0,0 +1,326 @@ +/* + * + * 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. + * +*/ + +/*jslint sloppy:true */ +/*global Windows:true, require, document, setTimeout, window, module */ + + + +var cordova = require('cordova'), + channel = require('cordova/channel'), + urlutil = require('cordova/urlutil'); + +var browserWrap, + popup, + navigationButtonsDiv, + navigationButtonsDivInner, + backButton, + forwardButton, + closeButton, + bodyOverflowStyle; + +// x-ms-webview is available starting from Windows 8.1 (platformId is 'windows') +// http://msdn.microsoft.com/en-us/library/windows/apps/dn301831.aspx +var isWebViewAvailable = cordova.platformId == 'windows'; + +function attachNavigationEvents(element, callback) { + if (isWebViewAvailable) { + element.addEventListener("MSWebViewNavigationStarting", function (e) { + callback({ type: "loadstart", url: e.uri}, {keepCallback: true} ); + }); + + element.addEventListener("MSWebViewNavigationCompleted", function (e) { + callback({ type: e.isSuccess ? "loadstop" : "loaderror", url: e.uri}, {keepCallback: true}); + }); + + element.addEventListener("MSWebViewUnviewableContentIdentified", function (e) { + // WebView found the content to be not HTML. + // http://msdn.microsoft.com/en-us/library/windows/apps/dn609716.aspx + callback({ type: "loaderror", url: e.uri}, {keepCallback: true}); + }); + + element.addEventListener("MSWebViewContentLoading", function (e) { + if (navigationButtonsDiv) { + backButton.disabled = !popup.canGoBack; + forwardButton.disabled = !popup.canGoForward; + } + }); + } else { + var onError = function () { + callback({ type: "loaderror", url: this.contentWindow.location}, {keepCallback: true}); + }; + + element.addEventListener("unload", function () { + callback({ type: "loadstart", url: this.contentWindow.location}, {keepCallback: true}); + }); + + element.addEventListener("load", function () { + callback({ type: "loadstop", url: this.contentWindow.location}, {keepCallback: true}); + }); + + element.addEventListener("error", onError); + element.addEventListener("abort", onError); + } +} + +var IAB = { + close: function (win, lose) { + if (browserWrap) { + if (win) win({ type: "exit" }); + + browserWrap.parentNode.removeChild(browserWrap); + // Reset body overflow style to initial value + document.body.style.msOverflowStyle = bodyOverflowStyle; + browserWrap = null; + popup = null; + } + }, + show: function (win, lose) { + if (browserWrap) { + browserWrap.style.display = "block"; + } + }, + open: function (win, lose, args) { + var strUrl = args[0], + target = args[1], + features = args[2], + url; + + if (target === "_system") { + url = new Windows.Foundation.Uri(strUrl); + Windows.System.Launcher.launchUriAsync(url); + } else if (target === "_self" || !target) { + window.location = strUrl; + } else { + // "_blank" or anything else + if (!browserWrap) { + var browserWrapStyle = document.createElement('link'); + browserWrapStyle.rel = "stylesheet"; + browserWrapStyle.type = "text/css"; + browserWrapStyle.href = urlutil.makeAbsolute("/www/css/inappbrowser.css"); + + document.head.appendChild(browserWrapStyle); + + browserWrap = document.createElement("div"); + browserWrap.className = "inAppBrowserWrap"; + + if (features.indexOf("fullscreen=yes") > -1) { + browserWrap.classList.add("inAppBrowserWrapFullscreen"); + } + + // Save body overflow style to be able to reset it back later + bodyOverflowStyle = document.body.style.msOverflowStyle; + + browserWrap.onclick = function () { + setTimeout(function () { + IAB.close(win); + }, 0); + }; + + document.body.appendChild(browserWrap); + // Hide scrollbars for the whole body while inappbrowser's window is open + document.body.style.msOverflowStyle = "none"; + } + + if (features.indexOf("hidden=yes") !== -1) { + browserWrap.style.display = "none"; + } + + popup = document.createElement(isWebViewAvailable ? "x-ms-webview" : "iframe"); + if (popup instanceof HTMLIFrameElement) { + // For iframe we need to override bacground color of parent element here + // otherwise pages without background color set will have transparent background + popup.style.backgroundColor = "white"; + } + popup.style.borderWidth = "0px"; + popup.style.width = "100%"; + + browserWrap.appendChild(popup); + + if (features.indexOf("location=yes") !== -1 || features.indexOf("location") === -1) { + popup.style.height = "calc(100% - 60px)"; + + navigationButtonsDiv = document.createElement("div"); + navigationButtonsDiv.style.height = "60px"; + navigationButtonsDiv.style.backgroundColor = "#404040"; + navigationButtonsDiv.style.zIndex = "999"; + navigationButtonsDiv.onclick = function (e) { + e.cancelBubble = true; + }; + + navigationButtonsDivInner = document.createElement("div"); + navigationButtonsDivInner.style.paddingTop = "10px"; + navigationButtonsDivInner.style.height = "50px"; + navigationButtonsDivInner.style.width = "160px"; + navigationButtonsDivInner.style.margin = "0 auto"; + navigationButtonsDivInner.style.backgroundColor = "#404040"; + navigationButtonsDivInner.style.zIndex = "999"; + navigationButtonsDivInner.onclick = function (e) { + e.cancelBubble = true; + }; + + + backButton = document.createElement("button"); + backButton.style.width = "40px"; + backButton.style.height = "40px"; + backButton.style.borderRadius = "40px"; + + backButton.innerText = "<-"; + backButton.addEventListener("click", function (e) { + if (popup.canGoBack) + popup.goBack(); + }); + + forwardButton = document.createElement("button"); + forwardButton.style.marginLeft = "20px"; + forwardButton.style.width = "40px"; + forwardButton.style.height = "40px"; + forwardButton.style.borderRadius = "40px"; + + forwardButton.innerText = "->"; + forwardButton.addEventListener("click", function (e) { + if (popup.canGoForward) + popup.goForward(); + }); + + closeButton = document.createElement("button"); + closeButton.style.marginLeft = "20px"; + closeButton.style.width = "40px"; + closeButton.style.height = "40px"; + closeButton.style.borderRadius = "40px"; + + closeButton.innerText = "x"; + closeButton.addEventListener("click", function (e) { + setTimeout(function () { + IAB.close(win); + }, 0); + }); + + if (!isWebViewAvailable) { + // iframe navigation is not yet supported + backButton.disabled = true; + forwardButton.disabled = true; + } + + navigationButtonsDivInner.appendChild(backButton); + navigationButtonsDivInner.appendChild(forwardButton); + navigationButtonsDivInner.appendChild(closeButton); + navigationButtonsDiv.appendChild(navigationButtonsDivInner); + + browserWrap.appendChild(navigationButtonsDiv); + } else { + popup.style.height = "100%"; + } + + // start listening for navigation events + attachNavigationEvents(popup, win); + + if (isWebViewAvailable) { + strUrl = strUrl.replace("ms-appx://", "ms-appx-web://"); + } + popup.src = strUrl; + } + }, + + injectScriptCode: function (win, fail, args) { + var code = args[0], + hasCallback = args[1]; + + if (isWebViewAvailable && browserWrap && popup) { + var op = popup.invokeScriptAsync("eval", code); + op.oncomplete = function (e) { + var result = [e.target.result]; + hasCallback && win(result); + }; + op.onerror = function () { }; + op.start(); + } + }, + + injectScriptFile: function (win, fail, args) { + var filePath = args[0], + hasCallback = args[1]; + + if (!!filePath) { + filePath = urlutil.makeAbsolute(filePath); + } + + if (isWebViewAvailable && browserWrap && popup) { + var uri = new Windows.Foundation.Uri(filePath); + Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri).done(function (file) { + Windows.Storage.FileIO.readTextAsync(file).done(function (code) { + var op = popup.invokeScriptAsync("eval", code); + op.oncomplete = function(e) { + var result = [e.target.result]; + hasCallback && win(result); + }; + op.onerror = function () { }; + op.start(); + }); + }); + } + }, + + injectStyleCode: function (win, fail, args) { + var code = args[0], + hasCallback = args[1]; + + if (isWebViewAvailable && browserWrap && popup) { + injectCSS(popup, code, hasCallback && win); + } + }, + + injectStyleFile: function (win, fail, args) { + var filePath = args[0], + hasCallback = args[1]; + + filePath = filePath && urlutil.makeAbsolute(filePath); + + if (isWebViewAvailable && browserWrap && popup) { + var uri = new Windows.Foundation.Uri(filePath); + Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri).then(function (file) { + return Windows.Storage.FileIO.readTextAsync(file); + }).done(function (code) { + injectCSS(popup, code, hasCallback && win); + }, function () { + // no-op, just catch an error + }); + } + } +}; + +function injectCSS (webView, cssCode, callback) { + // This will automatically escape all thing that we need (quotes, slashes, etc.) + var escapedCode = JSON.stringify(cssCode); + var evalWrapper = "(function(d){var c=d.createElement('style');c.innerHTML=%s;d.head.appendChild(c);})(document)" + .replace('%s', escapedCode); + + var op = webView.invokeScriptAsync("eval", evalWrapper); + op.oncomplete = function() { + callback && callback([]); + }; + op.onerror = function () { }; + op.start(); +} + +module.exports = IAB; + +require("cordova/exec/proxy").add("InAppBrowser", module.exports); diff --git a/plugins/cordova-plugin-inappbrowser/src/wp/InAppBrowser.cs b/plugins/cordova-plugin-inappbrowser/src/wp/InAppBrowser.cs new file mode 100644 index 00000000..ddb51227 --- /dev/null +++ b/plugins/cordova-plugin-inappbrowser/src/wp/InAppBrowser.cs @@ -0,0 +1,515 @@ +/* + 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. +*/ + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.Phone.Controls; +using Microsoft.Phone.Shell; + +#if WP8 +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Windows.Storage; +using Windows.System; + +//Use alias in case Cordova File Plugin is enabled. Then the File class will be declared in both and error will occur. +using IOFile = System.IO.File; +#else +using Microsoft.Phone.Tasks; +#endif + +namespace WPCordovaClassLib.Cordova.Commands +{ + [DataContract] + public class BrowserOptions + { + [DataMember] + public string url; + + [DataMember] + public bool isGeolocationEnabled; + } + + public class InAppBrowser : BaseCommand + { + + private static WebBrowser browser; + private static ApplicationBarIconButton backButton; + private static ApplicationBarIconButton fwdButton; + + protected ApplicationBar AppBar; + + protected bool ShowLocation {get;set;} + protected bool StartHidden {get;set;} + + protected string NavigationCallbackId { get; set; } + + public void open(string options) + { + // reset defaults on ShowLocation + StartHidden features + ShowLocation = true; + StartHidden = false; + + string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + //BrowserOptions opts = JSON.JsonHelper.Deserialize<BrowserOptions>(options); + string urlLoc = args[0]; + string target = args[1]; + string featString = args[2]; + this.NavigationCallbackId = args[3]; + + if (!string.IsNullOrEmpty(featString)) + { + string[] features = featString.Split(','); + foreach (string str in features) + { + try + { + string[] split = str.Split('='); + switch (split[0]) + { + case "location": + ShowLocation = split[1].StartsWith("yes", StringComparison.OrdinalIgnoreCase); + break; + case "hidden": + StartHidden = split[1].StartsWith("yes", StringComparison.OrdinalIgnoreCase); + break; + } + } + catch (Exception) + { + // some sort of invalid param was passed, moving on ... + } + } + } + /* + _self - opens in the Cordova WebView if url is in the white-list, else it opens in the InAppBrowser + _blank - always open in the InAppBrowser + _system - always open in the system web browser + */ + switch (target) + { + case "_blank": + ShowInAppBrowser(urlLoc); + break; + case "_self": + ShowCordovaBrowser(urlLoc); + break; + case "_system": + ShowSystemBrowser(urlLoc); + break; + } + } + + public void show(string options) + { + string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + + + if (browser != null) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + browser.Visibility = Visibility.Visible; + AppBar.IsVisible = true; + }); + } + } + + public void injectScriptCode(string options) + { + string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + + bool bCallback = false; + if (bool.TryParse(args[1], out bCallback)) { }; + + string callbackId = args[2]; + + if (browser != null) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + var res = browser.InvokeScript("eval", new string[] { args[0] }); + + if (bCallback) + { + PluginResult result = new PluginResult(PluginResult.Status.OK, res.ToString()); + result.KeepCallback = false; + this.DispatchCommandResult(result); + } + + }); + } + } + + public void injectScriptFile(string options) + { + Debug.WriteLine("Error : Windows Phone cordova-plugin-inappbrowser does not currently support executeScript"); + string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + // throw new NotImplementedException("Windows Phone does not currently support 'executeScript'"); + } + + public void injectStyleCode(string options) + { + Debug.WriteLine("Error : Windows Phone cordova-plugin-inappbrowser does not currently support insertCSS"); + return; + + //string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + //bool bCallback = false; + //if (bool.TryParse(args[1], out bCallback)) { }; + + //string callbackId = args[2]; + + //if (browser != null) + //{ + //Deployment.Current.Dispatcher.BeginInvoke(() => + //{ + // if (bCallback) + // { + // string cssInsertString = "try{(function(doc){var c = '<style>body{background-color:#ffff00;}</style>'; doc.head.innerHTML += c;})(document);}catch(ex){alert('oops : ' + ex.message);}"; + // //cssInsertString = cssInsertString.Replace("_VALUE_", args[0]); + // Debug.WriteLine("cssInsertString = " + cssInsertString); + // var res = browser.InvokeScript("eval", new string[] { cssInsertString }); + // if (bCallback) + // { + // PluginResult result = new PluginResult(PluginResult.Status.OK, res.ToString()); + // result.KeepCallback = false; + // this.DispatchCommandResult(result); + // } + // } + + //}); + //} + } + + public void injectStyleFile(string options) + { + Debug.WriteLine("Error : Windows Phone cordova-plugin-inappbrowser does not currently support insertCSS"); + return; + + //string[] args = JSON.JsonHelper.Deserialize<string[]>(options); + //throw new NotImplementedException("Windows Phone does not currently support 'insertCSS'"); + } + + private void ShowCordovaBrowser(string url) + { + Uri loc = new Uri(url, UriKind.RelativeOrAbsolute); + Deployment.Current.Dispatcher.BeginInvoke(() => + { + PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; + if (frame != null) + { + PhoneApplicationPage page = frame.Content as PhoneApplicationPage; + if (page != null) + { + CordovaView cView = page.FindName("CordovaView") as CordovaView; + if (cView != null) + { + WebBrowser br = cView.Browser; + br.Navigate2(loc); + } + } + + } + }); + } + +#if WP8 + private async void ShowSystemBrowser(string url) + { + var pathUri = new Uri(url, UriKind.Absolute); + if (pathUri.Scheme == Uri.UriSchemeHttp || pathUri.Scheme == Uri.UriSchemeHttps) + { + await Launcher.LaunchUriAsync(pathUri); + return; + } + + var file = await GetFile(pathUri.AbsolutePath.Replace('/', Path.DirectorySeparatorChar)); + if (file != null) + { + await Launcher.LaunchFileAsync(file); + } + else + { + Debug.WriteLine("File not found."); + } + } + + private async Task<StorageFile> GetFile(string fileName) + { + //first try to get the file from the isolated storage + var localFolder = ApplicationData.Current.LocalFolder; + if (IOFile.Exists(Path.Combine(localFolder.Path, fileName))) + { + return await localFolder.GetFileAsync(fileName); + } + + //if file is not found try to get it from the xap + var filePath = Path.Combine(Package.Current.InstalledLocation.Path, fileName); + if (IOFile.Exists(filePath)) + { + return await StorageFile.GetFileFromPathAsync(filePath); + } + + return null; + } +#else + private void ShowSystemBrowser(string url) + { + WebBrowserTask webBrowserTask = new WebBrowserTask(); + webBrowserTask.Uri = new Uri(url, UriKind.Absolute); + webBrowserTask.Show(); + } +#endif + + private void ShowInAppBrowser(string url) + { + Uri loc = new Uri(url, UriKind.RelativeOrAbsolute); + + Deployment.Current.Dispatcher.BeginInvoke(() => + { + if (browser != null) + { + //browser.IsGeolocationEnabled = opts.isGeolocationEnabled; + browser.Navigate2(loc); + } + else + { + PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; + if (frame != null) + { + PhoneApplicationPage page = frame.Content as PhoneApplicationPage; + + string baseImageUrl = "Images/"; + + if (page != null) + { + Grid grid = page.FindName("LayoutRoot") as Grid; + if (grid != null) + { + browser = new WebBrowser(); + browser.IsScriptEnabled = true; + browser.LoadCompleted += new System.Windows.Navigation.LoadCompletedEventHandler(browser_LoadCompleted); + + browser.Navigating += new EventHandler<NavigatingEventArgs>(browser_Navigating); + browser.NavigationFailed += new System.Windows.Navigation.NavigationFailedEventHandler(browser_NavigationFailed); + browser.Navigated += new EventHandler<System.Windows.Navigation.NavigationEventArgs>(browser_Navigated); + browser.Navigate2(loc); + + if (StartHidden) + { + browser.Visibility = Visibility.Collapsed; + } + + //browser.IsGeolocationEnabled = opts.isGeolocationEnabled; + grid.Children.Add(browser); + } + + ApplicationBar bar = new ApplicationBar(); + bar.BackgroundColor = Colors.Gray; + bar.IsMenuEnabled = false; + + backButton = new ApplicationBarIconButton(); + backButton.Text = "Back"; + + backButton.IconUri = new Uri(baseImageUrl + "appbar.back.rest.png", UriKind.Relative); + backButton.Click += new EventHandler(backButton_Click); + bar.Buttons.Add(backButton); + + + fwdButton = new ApplicationBarIconButton(); + fwdButton.Text = "Forward"; + fwdButton.IconUri = new Uri(baseImageUrl + "appbar.next.rest.png", UriKind.Relative); + fwdButton.Click += new EventHandler(fwdButton_Click); + bar.Buttons.Add(fwdButton); + + ApplicationBarIconButton closeBtn = new ApplicationBarIconButton(); + closeBtn.Text = "Close"; + closeBtn.IconUri = new Uri(baseImageUrl + "appbar.close.rest.png", UriKind.Relative); + closeBtn.Click += new EventHandler(closeBtn_Click); + bar.Buttons.Add(closeBtn); + + page.ApplicationBar = bar; + bar.IsVisible = !StartHidden; + AppBar = bar; + + page.BackKeyPress += page_BackKeyPress; + + } + + } + } + }); + } + + void page_BackKeyPress(object sender, System.ComponentModel.CancelEventArgs e) + { +#if WP8 + if (browser.CanGoBack) + { + browser.GoBack(); + } + else + { + close(); + } + e.Cancel = true; +#else + browser.InvokeScript("execScript", "history.back();"); +#endif + } + + void browser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e) + { + + } + + void fwdButton_Click(object sender, EventArgs e) + { + if (browser != null) + { + try + { +#if WP8 + browser.GoForward(); +#else + browser.InvokeScript("execScript", "history.forward();"); +#endif + } + catch (Exception) + { + + } + } + } + + void backButton_Click(object sender, EventArgs e) + { + if (browser != null) + { + try + { +#if WP8 + browser.GoBack(); +#else + browser.InvokeScript("execScript", "history.back();"); +#endif + } + catch (Exception) + { + + } + } + } + + void closeBtn_Click(object sender, EventArgs e) + { + this.close(); + } + + + public void close(string options = "") + { + if (browser != null) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; + if (frame != null) + { + PhoneApplicationPage page = frame.Content as PhoneApplicationPage; + if (page != null) + { + Grid grid = page.FindName("LayoutRoot") as Grid; + if (grid != null) + { + grid.Children.Remove(browser); + } + page.ApplicationBar = null; + page.BackKeyPress -= page_BackKeyPress; + } + } + + browser = null; + string message = "{\"type\":\"exit\"}"; + PluginResult result = new PluginResult(PluginResult.Status.OK, message); + result.KeepCallback = false; + this.DispatchCommandResult(result, NavigationCallbackId); + }); + } + } + + void browser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) + { +#if WP8 + if (browser != null) + { + backButton.IsEnabled = browser.CanGoBack; + fwdButton.IsEnabled = browser.CanGoForward; + + } +#endif + string message = "{\"type\":\"loadstop\", \"url\":\"" + e.Uri.OriginalString + "\"}"; + PluginResult result = new PluginResult(PluginResult.Status.OK, message); + result.KeepCallback = true; + this.DispatchCommandResult(result, NavigationCallbackId); + } + + void browser_NavigationFailed(object sender, System.Windows.Navigation.NavigationFailedEventArgs e) + { + string message = "{\"type\":\"error\",\"url\":\"" + e.Uri.OriginalString + "\"}"; + PluginResult result = new PluginResult(PluginResult.Status.ERROR, message); + result.KeepCallback = true; + this.DispatchCommandResult(result, NavigationCallbackId); + } + + void browser_Navigating(object sender, NavigatingEventArgs e) + { + string message = "{\"type\":\"loadstart\",\"url\":\"" + e.Uri.OriginalString + "\"}"; + PluginResult result = new PluginResult(PluginResult.Status.OK, message); + result.KeepCallback = true; + this.DispatchCommandResult(result, NavigationCallbackId); + } + + } + + internal static class WebBrowserExtensions + { + /// <summary> + /// Improved method to initiate request to the provided URI. Supports 'data:text/html' urls. + /// </summary> + /// <param name="browser">The browser instance</param> + /// <param name="uri">The requested uri</param> + internal static void Navigate2(this WebBrowser browser, Uri uri) + { + // IE10 does not support data uri so we use NavigateToString method instead + if (uri.Scheme == "data") + { + // we should remove the scheme identifier and unescape the uri + string uriString = Uri.UnescapeDataString(uri.AbsoluteUri); + // format is 'data:text/html, ...' + string html = new System.Text.RegularExpressions.Regex("^data:text/html,").Replace(uriString, ""); + browser.NavigateToString(html); + } + else + { + browser.Navigate(uri); + } + } + } +} |
