Java tutorial
/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> === * * Copyright 2015, Enno Gottschalk <mrmaffen@googlemail.com> * * Tomahawk is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Tomahawk is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>. */ package org.runbuddy.libtomahawk.resolver; import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.webkit.CookieManager; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.ImageView; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import com.squareup.okhttp.Response; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.runbuddy.libtomahawk.database.CollectionDb; import org.runbuddy.libtomahawk.database.CollectionDbManager; import org.runbuddy.libtomahawk.resolver.models.ScriptInterfaceRequestOptions; import org.runbuddy.libtomahawk.resolver.models.ScriptResolverMetaData; import org.runbuddy.libtomahawk.resolver.models.ScriptResolverTrack; import org.runbuddy.libtomahawk.resolver.plugins.ScriptChartProviderPluginFactory; import org.runbuddy.libtomahawk.resolver.plugins.ScriptCollectionPluginFactory; import org.runbuddy.libtomahawk.resolver.plugins.ScriptInfoPluginFactory; import org.runbuddy.libtomahawk.resolver.plugins.ScriptPlaylistGeneratorFactory; import org.runbuddy.libtomahawk.resolver.plugins.ScriptResolverPluginFactory; import org.runbuddy.libtomahawk.utils.GsonHelper; import org.runbuddy.libtomahawk.utils.ImageUtils; import org.runbuddy.libtomahawk.utils.NetworkUtils; import org.runbuddy.tomahawk.R; import org.runbuddy.tomahawk.activities.TomahawkMainActivity; import org.runbuddy.tomahawk.app.TomahawkApp; import org.runbuddy.tomahawk.utils.IdGenerator; import org.runbuddy.tomahawk.utils.PreferenceUtils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import de.greenrobot.event.EventBus; public class ScriptAccount implements ScriptWebViewClient.WebViewClientReadyListener { private final static String TAG = ScriptAccount.class.getSimpleName(); public final static String SCRIPT_INTERFACE_NAME = "Tomahawk"; public final static String CONFIG = "config"; public final static String ENABLED_KEY = "_enabled_"; private String mPath; private boolean mManuallyInstalled; private String mName; private WebView mWebView; private HashMap<String, ScriptJob> mJobs = new HashMap<>(); private HashMap<String, ScriptObject> mObjects = new HashMap<>(); private ScriptResolverPluginFactory mResolverPluginFactory = new ScriptResolverPluginFactory(); private ScriptCollectionPluginFactory mCollectionPluginFactory = new ScriptCollectionPluginFactory(); private ScriptInfoPluginFactory mInfoPluginFactory = new ScriptInfoPluginFactory(); private ScriptChartProviderPluginFactory mChartsProviderPluginFactory = new ScriptChartProviderPluginFactory(); private ScriptPlaylistGeneratorFactory mPlaylistGeneratorFactory = new ScriptPlaylistGeneratorFactory(); private ScriptResolver mScriptResolver; private ScriptResolverMetaData mMetaData; @SuppressLint({ "AddJavascriptInterface", "SetJavaScriptEnabled" }) public ScriptAccount(String path, boolean manuallyInstalled) { String prefix = manuallyInstalled ? "file://" : "file:///android_asset"; mPath = prefix + path; mManuallyInstalled = manuallyInstalled; String[] parts = mPath.split("/"); mName = parts[parts.length - 1]; InputStream inputStream = null; try { if (mManuallyInstalled) { File metadataFile = new File(path + File.separator + "content" + File.separator + "metadata.json"); inputStream = new FileInputStream(metadataFile); } else { inputStream = TomahawkApp.getContext().getAssets() .open(path.substring(1) + "/content/metadata.json"); } String metadataString = IOUtils.toString(inputStream, Charsets.UTF_8); mMetaData = GsonHelper.get().fromJson(metadataString, ScriptResolverMetaData.class); if (mMetaData == null) { Log.e(TAG, "Couldn't read metadata.json. Cannot instantiate ScriptAccount."); return; } } catch (IOException e) { Log.e(TAG, "ScriptAccount: " + e.getClass() + ": " + e.getLocalizedMessage()); Log.e(TAG, "Couldn't read metadata.json. Cannot instantiate ScriptAccount."); return; } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { Log.e(TAG, "ScriptAccount: " + e.getClass() + ": " + e.getLocalizedMessage()); } } } CookieManager.setAcceptFileSchemeCookies(true); mWebView = new WebView(TomahawkApp.getContext()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true); } WebSettings settings = mWebView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDatabaseEnabled(true); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { //noinspection deprecation settings.setDatabasePath(TomahawkApp.getContext().getDir("databases", Context.MODE_PRIVATE).getPath()); } settings.setDomStorageEnabled(true); mWebView.setWebChromeClient(new TomahawkWebChromeClient()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mWebView.getSettings().setAllowUniversalAccessFromFileURLs(true); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { //initalize WebView String data = "<!DOCTYPE html>" + "<html>" + "<head><title>" + mName + "</title></head>" + "<body>" + "<script src=\"file:///android_asset/js/rsvp-latest.min.js" + "\" type=\"text/javascript\"></script>" + "<script src=\"file:///android_asset/js/cryptojs-core.js" + "\" type=\"text/javascript\"></script>"; if (mMetaData.manifest.scripts != null) { for (String scriptPath : mMetaData.manifest.scripts) { data += "<script src=\"" + mPath + "/content/" + scriptPath + "\" type=\"text/javascript\"></script>"; } } try { String[] cryptoJsScripts = TomahawkApp.getContext().getAssets().list("js/cryptojs"); for (String scriptPath : cryptoJsScripts) { data += "<script src=\"file:///android_asset/js/cryptojs/" + scriptPath + "\" type=\"text/javascript\"></script>"; } } catch (IOException e) { Log.e(TAG, "ScriptResolver: " + e.getClass() + ": " + e.getLocalizedMessage()); } data += "<script src=\"file:///android_asset/js/tomahawk_android_pre.js" + "\" type=\"text/javascript\"></script>" + "<script src=\"file:///android_asset/js/tomahawk.js" + "\" type=\"text/javascript\"></script>" + "<script src=\"file:///android_asset/js/tomahawk-infosystem.js" + "\" type=\"text/javascript\"></script>" + "<script src=\"file:///android_asset/js/tomahawk_android_post.js" + "\" type=\"text/javascript\"></script>" + "<script src=\"" + mPath + "/content/" + mMetaData.manifest.main + "\" type=\"text/javascript\"></script>" + "</body></html>"; mWebView.setWebViewClient(new ScriptWebViewClient(ScriptAccount.this)); mWebView.addJavascriptInterface(new ScriptInterface(ScriptAccount.this), SCRIPT_INTERFACE_NAME); mWebView.loadDataWithBaseURL("file:///android_asset/test.html", data, "text/html", null, null); } }); } /** * This method is being called, when the {@link ScriptWebViewClient} has completely loaded the * given .js script. */ @Override public void onWebViewClientReady() { //TODO: Remove this hack once we can get rid of Tomahawk.resolver.instance completely evaluateJavaScript("Tomahawk.resolver.instance = Tomahawk.resolver.instance " + "|| Tomahawk.extend(Tomahawk.Resolver, {});" + "Tomahawk.PluginManager.registerPlugin('" + ScriptObject.TYPE_RESOLVER + "', Tomahawk.resolver.instance);"); } public ScriptResolver getScriptResolver() { return mScriptResolver; } public void setScriptResolver(ScriptResolver scriptResolver) { mScriptResolver = scriptResolver; } public ScriptResolverMetaData getMetaData() { return mMetaData; } public String getPath() { return mPath; } public String getName() { return mName; } public void setConfig(Map<String, Object> config) { String rawJsonString = GsonHelper.get().toJson(config); PreferenceUtils.edit().putString(buildPreferenceKey(), rawJsonString).commit(); mScriptResolver.saveUserConfig(); } /** * @return the Map<String, String> containing the Config information of this resolver */ public Map<String, Object> getConfig() { String rawJsonString = PreferenceUtils.getString(buildPreferenceKey()); Map<String, Object> result = null; if (rawJsonString != null) { result = GsonHelper.get().fromJson(rawJsonString, Map.class); } if (result == null) { result = new HashMap<>(); } return result; } public void loadIcon(ImageView imageView, boolean grayOut) { ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, mPath + "/content/" + mMetaData.manifest.icon, grayOut ? R.color.disabled_resolver : 0); } public void loadIconWhite(ImageView imageView, int tintColorResId) { ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, mPath + "/content/" + mMetaData.manifest.iconWhite, tintColorResId); } public String getIconBackgroundPath() { return mPath + "/content/" + mMetaData.manifest.iconBackground; } public void loadIconBackground(ImageView imageView, boolean grayOut) { ImageUtils.loadDrawableIntoImageView(TomahawkApp.getContext(), imageView, mPath + "/content/" + mMetaData.manifest.iconBackground, grayOut ? R.color.disabled_resolver : 0); } public boolean isManuallyInstalled() { return mManuallyInstalled; } private String buildPreferenceKey() { return mName + "_" + CONFIG; } public void unregisterAllPlugins() { //TODO: Uncomment this once we can get rid of Tomahawk.resolver.instance completely /* for (String objectId : mResolverPluginFactory.getScriptPlugins().keySet()) { String json = mObjects.get(objectId).toJson(); evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + ScriptObject.TYPE_RESOLVER + "', " + json + ");"); } */ for (String objectId : mCollectionPluginFactory.getScriptPlugins().keySet()) { String json = mObjects.get(objectId).toJson(); evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + ScriptObject.TYPE_COLLECTION + "', " + json + ");"); } for (String objectId : mInfoPluginFactory.getScriptPlugins().keySet()) { String json = mObjects.get(objectId).toJson(); evaluateJavaScript("Tomahawk.PluginManager.unregisterPlugin('" + ScriptObject.TYPE_INFOPLUGIN + "', " + json + ");"); } } public void startJob(final ScriptJob job) { final String requestId = IdGenerator.getSessionUniqueStringId(); mJobs.put(requestId, job); new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { evaluateJavaScript("Tomahawk.PluginManager.invoke(" + "'" + requestId + "'," + "'" + job.getScriptObject().getId() + "'," + "'" + job.getMethodName() + "'," + GsonHelper.get().toJson(job.getArguments()) + ")"); } }); } private void evaluateJavaScript(final String code) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { mWebView.loadUrl("javascript: " + code); } }); } public void reportScriptJobResult(JsonObject result) { JsonElement requestIdNode = result.get("requestId"); String requestId = null; if (requestIdNode != null && requestIdNode.isJsonPrimitive()) { requestId = result.get("requestId").getAsString(); } if (requestId != null && !requestId.isEmpty()) { ScriptJob job = mJobs.get(requestId); if (job != null) { JsonElement errorNode = result.get("error"); if (errorNode == null) { job.reportResults(result.get("data")); } else if (errorNode.isJsonPrimitive()) { job.reportFailure(result.get("error").getAsString()); } else { job.reportFailure("no error message provided"); } } else { Log.e(TAG, "reportScriptJobResult - ScriptAccount:" + mName + ", couldn't find ScriptJob with given requestId"); } } else { Log.e(TAG, "reportScriptJobResult - ScriptAccount:" + mName + ", requestId is null or empty"); } } public void registerScriptPlugin(String type, String objectId) { ScriptObject object = mObjects.get(objectId); if (object == null) { object = new ScriptObject(objectId, this); mObjects.put(objectId, object); } switch (type) { case ScriptObject.TYPE_RESOLVER: mResolverPluginFactory.registerPlugin(object, this); PipeLine.get().onPluginLoaded(this); break; case ScriptObject.TYPE_COLLECTION: mCollectionPluginFactory.registerPlugin(object, this); break; case ScriptObject.TYPE_INFOPLUGIN: mInfoPluginFactory.registerPlugin(object, this); break; case ScriptObject.TYPE_CHARTSPROVIDER: mChartsProviderPluginFactory.registerPlugin(object, this); break; case ScriptObject.TYPE_PLAYLISTGENERATOR: mPlaylistGeneratorFactory.registerPlugin(object, this); break; default: Log.e(TAG, "registerScriptPlugin - ScriptAccount:" + mName + ", ScriptPlugin type not supported!"); } } public void unregisterScriptPlugin(String type, String objectId) { ScriptObject object = mObjects.get(objectId); if (object == null) { Log.e(TAG, "unregisterScriptPlugin - ScriptAccount:" + mName + ", tried to unregister a plugin that was not registered!"); } else { switch (type) { case ScriptObject.TYPE_RESOLVER: mResolverPluginFactory.unregisterPlugin(object); break; case ScriptObject.TYPE_COLLECTION: mCollectionPluginFactory.unregisterPlugin(object); break; case ScriptObject.TYPE_INFOPLUGIN: mInfoPluginFactory.unregisterPlugin(object); break; case ScriptObject.TYPE_CHARTSPROVIDER: mChartsProviderPluginFactory.unregisterPlugin(object); break; case ScriptObject.TYPE_PLAYLISTGENERATOR: mPlaylistGeneratorFactory.unregisterPlugin(object); break; default: Log.e(TAG, "unregisterScriptPlugin - ScriptAccount:" + mName + ", ScriptPlugin type not supported!"); } } } public void invokeNativeScriptJob(int requestId, String methodName, String paramsString) { JsonObject params = GsonHelper.get().fromJson(paramsString, JsonObject.class); if (methodName.equals("collectionAddTracks")) { String id = params.get("id").getAsString(); List<ScriptResolverTrack> tracks = GsonHelper.get().fromJson(params.getAsJsonArray("tracks"), new TypeToken<List<ScriptResolverTrack>>() { }.getType()); CollectionDb collectionDb = CollectionDbManager.get().getCollectionDb(id); collectionDb.addTracks(tracks); reportNativeScriptJobResult(requestId, "'" + collectionDb.getRevision() + "'"); } else if (methodName.equals("collectionWipe")) { String id = params.get("id").getAsString(); CollectionDbManager.get().getCollectionDb(id).wipe(); reportNativeScriptJobResult(requestId, null); } else if (methodName.equals("collectionRevision")) { String id = params.get("id").getAsString(); CollectionDb collectionDb = CollectionDbManager.get().getCollectionDb(id); reportNativeScriptJobResult(requestId, "'" + collectionDb.getRevision() + "'"); } else if (methodName.equals("collectionInitialized")) { String id = params.get("id").getAsString(); CollectionDbManager.get().getCollectionDb(id).wipe(); reportNativeScriptJobResult(requestId, null); } else if (methodName.equals("httpRequest")) { ScriptInterfaceRequestOptions options = GsonHelper.get().fromJson(paramsString, ScriptInterfaceRequestOptions.class); reportNativeScriptJobResult(requestId, GsonHelper.get().toJson(jsHttpRequest(options))); } else if (methodName.equals("showWebView")) { String url = params.get("url").getAsString(); // This will open up a WebViewActivity, which will call onShowWebViewFinished when // finished TomahawkMainActivity.ShowWebViewEvent event = new TomahawkMainActivity.ShowWebViewEvent(); event.mRequestid = requestId; event.mUrl = url; EventBus.getDefault().post(event); } } private void reportNativeScriptJobResult(int requestId, String result) { if (result == null) { evaluateJavaScript("Tomahawk.NativeScriptJobManager.reportNativeScriptJobResult( " + requestId + " );"); } else { evaluateJavaScript("Tomahawk.NativeScriptJobManager.reportNativeScriptJobResult( " + requestId + ", " + result + " );"); } } public void onShowWebViewFinished(int requestId, String url) { if (url != null) { HashMap<String, Object> args = new HashMap<>(); args.put("url", url); reportNativeScriptJobResult(requestId, GsonHelper.get().toJson(args)); } } private JsonObject jsHttpRequest(ScriptInterfaceRequestOptions options) { Response response = null; try { String url = null; Map<String, String> headers = null; String method = null; String username = null; String password = null; String data = null; boolean isTestingConfig = false; if (options != null) { url = options.url; headers = options.headers; method = options.method; username = options.username; password = options.password; data = options.data; isTestingConfig = options.isTestingConfig; } java.net.CookieManager cookieManager = getCookieManager(isTestingConfig); response = NetworkUtils.httpRequest(method, url, headers, username, password, data, true, cookieManager); // We have to encode the %-chars because the Android WebView automatically decodes // percentage-escaped chars ... for whatever reason. Seems likely that this is a bug. String responseText = response.body().string().replace("%", "%25"); JsonObject responseHeaders = new JsonObject(); for (String headerName : response.headers().names()) { String concatenatedValues = ""; for (int i = 0; i < response.headers(headerName).size(); i++) { if (i > 0) { concatenatedValues += "\n"; } concatenatedValues += response.headers(headerName).get(i); } String escapedKey = headerName.toLowerCase().replace("%", "%25"); String escapedValue = concatenatedValues.replace("%", "%25"); responseHeaders.addProperty(escapedKey, escapedValue); } int status = response.code(); String statusText = response.message().replace("%", "%25"); JsonObject result = new JsonObject(); result.addProperty("responseText", responseText); result.add("responseHeaders", responseHeaders); result.addProperty("status", status); result.addProperty("statusText", statusText); return result; } catch (IOException e) { Log.e(TAG, "jsHttpRequest: " + e.getClass() + ": " + e.getLocalizedMessage()); return null; } finally { if (response != null) { try { response.body().close(); } catch (IOException e) { Log.e(TAG, "jsHttpRequest: " + e.getClass() + ": " + e.getLocalizedMessage()); } } } } public java.net.CookieManager getCookieManager(boolean isTestingConfig) { String cookieContextId; if (isTestingConfig) { cookieContextId = mName + "_testConfig"; } else { cookieContextId = mName; } return NetworkUtils.getCookieManager(cookieContextId); } }