org.runbuddy.libtomahawk.resolver.ScriptAccount.java Source code

Java tutorial

Introduction

Here is the source code for org.runbuddy.libtomahawk.resolver.ScriptAccount.java

Source

/* == 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);
    }

}