diff --git a/code/smartphone.js b/code/smartphone.js index 8078cbd4..d01d2545 100644 --- a/code/smartphone.js +++ b/code/smartphone.js @@ -176,3 +176,18 @@ window.useAndroidPanes = function() { return (typeof android !== 'undefined' && android && android.addPane && window.isSmartphone()); } +if(typeof android !== 'undefined' && android && android.getFileRequestUrlPrefix) { + window.requestFile = function(callback) { + do { + var funcName = "onFileSelected" + parseInt(Math.random()*0xFFFF).toString(16); + } while(window[funcName] !== undefined) + + window[funcName] = function(filename, content) { + callback(decodeURIComponent(filename), atob(content)); + }; + var script = document.createElement('script'); + script.src = android.getFileRequestUrlPrefix() + funcName; + (document.body || document.head || document.documentElement).appendChild(script); + }; +} + diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java b/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java index 90215244..13cd2dc7 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java @@ -18,10 +18,12 @@ import android.widget.BaseAdapter; import android.widget.TextView; import android.widget.Toast; +import com.cradle.iitc_mobile.IITC_Mobile.ResponseHandler; + /** * this class manages automatic login using the Google account stored on the device */ -public class IITC_DeviceAccountLogin implements AccountManagerCallback { +public class IITC_DeviceAccountLogin implements AccountManagerCallback, ResponseHandler { /** * Adapter to show available accounts in a ListView. Accounts are read from mAccounts */ @@ -126,6 +128,7 @@ public class IITC_DeviceAccountLogin implements AccountManagerCallback { /** * called by IITC_Mobile when the authentication activity has finished. */ + @Override public void onActivityResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) // authentication activity succeeded, request token again @@ -149,7 +152,7 @@ public class IITC_DeviceAccountLogin implements AccountManagerCallback { // There is a reason we need to start the given activity if we want an // authentication token. (Could be user confirmation or something else. Whatever, // we have to start it) IITC_Mobile will call it using startActivityForResult - mActivity.startLoginActivity(launch); + mActivity.startActivityForResult(launch, this); return; } diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java b/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java index 9bc8d73b..285ff6f2 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java @@ -1,11 +1,19 @@ package com.cradle.iitc_mobile; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; import android.content.SharedPreferences; import android.content.res.AssetManager; import android.net.Uri; import android.os.Environment; import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Base64OutputStream; import android.webkit.WebResourceResponse; +import android.widget.Toast; + +import com.cradle.iitc_mobile.IITC_Mobile.ResponseHandler; import org.json.JSONObject; @@ -16,7 +24,10 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.net.URL; +import java.net.URLEncoder; import java.util.HashMap; public class IITC_FileManager { @@ -112,6 +123,10 @@ public class IITC_FileManager { return mAssetManager.open(filename); } + private WebResourceResponse getFileRequest(Uri uri) { + return new FileRequest(uri); + } + private WebResourceResponse getScript(Uri uri) { InputStream stream; try { @@ -179,6 +194,10 @@ public class IITC_FileManager { return os.toString(); } + public String getFileRequestPrefix() { + return "//file-request" + DOMAIN + "/"; + } + public String getIITCVersion() throws IOException { InputStream stream = getAssetFile("total-conversion-build.user.js"); @@ -196,8 +215,90 @@ public class IITC_FileManager { return getScript(uri); if ("user-plugin".equals(host)) return getUserPlugin(uri); + if ("file-request".equals(host)) + return getFileRequest(uri); Log.e("could not generate response for url: " + uri); return EMPTY; } + + private class FileRequest extends WebResourceResponse implements ResponseHandler, Runnable { + private Intent mData; + private String mFunctionName; + private int mResultCode; + private PipedOutputStream mStreamOut; + + private FileRequest(Uri uri) { + // create two connected streams we can write to after the file has been read + super("application/x-javascript", "UTF-8", new PipedInputStream()); + + try { + mStreamOut = new PipedOutputStream((PipedInputStream) getData()); + } catch (IOException e) { + Log.w(e); + } + + // the function to call + mFunctionName = uri.getPathSegments().get(0); + + // create the chooser Intent + final Intent target = new Intent(Intent.ACTION_GET_CONTENT); + target.setType("file/*"); + target.addCategory(Intent.CATEGORY_OPENABLE); + + Intent intent = Intent.createChooser(target, "Choose file"); + try { + mIitc.startActivityForResult(intent, this); + } catch (ActivityNotFoundException e) { + Toast.makeText(mIitc, "No activity to select a file found." + + "Please install a file browser of your choice!", Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onActivityResult(int resultCode, Intent data) { + mIitc.deleteResponseHandler(this); // to enable garbage collection + + mResultCode = resultCode; + mData = data; + + new Thread(this, "FileRequestReader").start(); + } + + @Override + public void run() { + try { + if (mResultCode == Activity.RESULT_OK && mData != null) { + Uri uri = mData.getData(); + File file = new File(uri.getPath()); + + mStreamOut.write( + (mFunctionName + "('" + URLEncoder.encode(file.getName(), "UTF-8") + "', '").getBytes()); + + Base64OutputStream encoder = + new Base64OutputStream(mStreamOut, Base64.NO_CLOSE | Base64.NO_WRAP | Base64.DEFAULT); + + FileInputStream fileinput = new FileInputStream(file); + int c; + while ((c = fileinput.read()) != -1) + { + encoder.write(c); + } + + encoder.close(); + mStreamOut.write("');".getBytes()); + } + + mStreamOut.close(); + } catch (IOException e) { + Log.w(e); + + // try to close stream, but ignore errors + try { + mStreamOut.close(); + } catch (IOException e1) { + } + } + } + } } diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java b/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java index 939599bf..cd7aee85 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java @@ -224,4 +224,9 @@ public class IITC_JSInterface { } }); } + + @JavascriptInterface + public String getFileRequestUrlPrefix() { + return mIitc.getFileManager().getFileRequestPrefix(); + } } diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java index 561bd420..218efb73 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java @@ -42,10 +42,9 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.Stack; +import java.util.Vector; public class IITC_Mobile extends Activity implements OnSharedPreferenceChangeListener { - - private static final int REQUEST_LOGIN = 1; private static final String mIntelUrl = "https://www.ingress.com/intel"; private SharedPreferences mSharedPrefs; @@ -55,6 +54,7 @@ public class IITC_Mobile extends Activity implements OnSharedPreferenceChangeLis private IITC_NavigationHelper mNavigationHelper; private IITC_MapSettings mMapSettings; private IITC_DeviceAccountLogin mLogin; + private Vector mResponseHandlers = new Vector(); private boolean mDesktopMode = false; private boolean mAdvancedMenu = false; private MenuItem mSearchMenuItem; @@ -572,23 +572,33 @@ public class IITC_Mobile extends Activity implements OnSharedPreferenceChangeLis return this.mIitcWebView; } - /** - * It can occur that in order to authenticate, an external activity has to be launched. - * (This could for example be a confirmation dialog.) - */ - public void startLoginActivity(Intent launch) { - startActivityForResult(launch, REQUEST_LOGIN); // REQUEST_LOGIN is to recognize the result + public void startActivityForResult(Intent launch, ResponseHandler handler) { + int index = mResponseHandlers.indexOf(handler); + if (index == -1) { + mResponseHandlers.add(handler); + index = mResponseHandlers.indexOf(handler); + } + + startActivityForResult(launch, RESULT_FIRST_USER + index); + } + + public void deleteResponseHandler(ResponseHandler handler) { + int index = mResponseHandlers.indexOf(handler); + if (index != -1) { + // set value to null to enable garbage collection, but don't remove it to keep indexes + mResponseHandlers.set(index, null); + } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_LOGIN: - // authentication activity has returned. mLogin will continue authentication - mLogin.onActivityResult(resultCode, data); - break; - default: - super.onActivityResult(requestCode, resultCode, data); + int index = requestCode - RESULT_FIRST_USER; + + try { + ResponseHandler handler = mResponseHandlers.get(index); + handler.onActivityResult(resultCode, data); + } catch (ArrayIndexOutOfBoundsException e) { + super.onActivityResult(requestCode, resultCode, data); } } @@ -764,4 +774,8 @@ public class IITC_Mobile extends Activity implements OnSharedPreferenceChangeLis public IITC_UserLocation getUserLocation() { return mUserLocation; } + + public interface ResponseHandler { + void onActivityResult(int resultCode, Intent data); + } } diff --git a/plugins/add-kml.user.js b/plugins/add-kml.user.js index 1ba8191e..8d46cf4a 100644 --- a/plugins/add-kml.user.js +++ b/plugins/add-kml.user.js @@ -26,6 +26,59 @@ window.plugin.overlayKML.loadExternals = function() { @@INCLUDERAW:external/leaflet.filelayer.js@@ try { console.log('done loading leaflet.filelayer JS'); } catch(e) {} + if (window.requestFile !== undefined) { + try { console.log('Loading android webview extensions for leaflet.filelayer JS now'); } catch(e) {} + var FileLoaderMixin = { + parse: function (fileContent, fileName) { + // Check file extension + var ext = fileName.split('.').pop(), + parser = this._parsers[ext]; + if (!parser) { + window.alert("Unsupported file type " + file.type + '(' + ext + ')'); + return; + } + this.fire('data:loading', {filename: fileName, format: ext}); + var layer = parser.call(this, fileContent, ext); + this.fire('data:loaded', {layer: layer, filename: fileName, format: ext}); + } + }; + FileLoader.include(FileLoaderMixin); + + var FileLayerLoadMixin = { + getLoader: function () { + return this.loader; + }, + _initContainer: function () { + // Create a button, and bind click on hidden file input + var zoomName = 'leaflet-control-filelayer leaflet-control-zoom', + barName = 'leaflet-bar', + partName = barName + '-part', + container = L.DomUtil.create('div', zoomName + ' ' + barName); + var link = L.DomUtil.create('a', zoomName + '-in ' + partName, container); + link.innerHTML = L.Control.FileLayerLoad.LABEL; + link.href = '#'; + link.title = L.Control.FileLayerLoad.TITLE; + + var stop = L.DomEvent.stopPropagation; + L.DomEvent + .on(link, 'click', stop) + .on(link, 'mousedown', stop) + .on(link, 'dblclick', stop) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', function (e) { + window.requestFile(function(filename, content) { + _fileLayerLoad.getLoader().parse(content, filename); + }); + e.preventDefault(); + }); + return container; + } + }; + L.Control.FileLayerLoad.include(FileLayerLoadMixin); + + try { console.log('done loading android webview extensions for leaflet.filelayer JS'); } catch(e) {} + } + try { console.log('Loading KML JS now'); } catch(e) {} @@INCLUDERAW:external/KML.js@@ try { console.log('done loading KML JS'); } catch(e) {} @@ -37,6 +90,8 @@ window.plugin.overlayKML.loadExternals = function() { window.plugin.overlayKML.load(); } +var _fileLayerLoad = null; + window.plugin.overlayKML.load = function() { // Provide popup window allow user to select KML to overlay @@ -50,13 +105,14 @@ window.plugin.overlayKML.load = function() { }); L.Control.FileLayerLoad.LABEL = 'Open'; - L.Control.fileLayerLoad({ + _fileLayerLoad = L.Control.fileLayerLoad({ fitBounds: true, layerOptions: { pointToLayer: function (data, latlng) { return L.marker(latlng, {icon: KMLIcon}); }}, - }).addTo(map); + }); + _fileLayerLoad.addTo(map); } var setup = function() { @@ -66,3 +122,4 @@ var setup = function() { // PLUGIN END ////////////////////////////////////////////////////////// @@PLUGINEND@@ +