org.tomahawk.libtomahawk.resolver.ScriptResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.tomahawk.libtomahawk.resolver.ScriptResolver.java

Source

/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
 *
 *   Copyright 2013, 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.tomahawk.libtomahawk.resolver;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.json.JSONArray;
import org.json.JSONObject;
import org.tomahawk.libtomahawk.authentication.AuthenticatorManager;
import org.tomahawk.libtomahawk.authentication.AuthenticatorUtils;
import org.tomahawk.libtomahawk.collection.Album;
import org.tomahawk.libtomahawk.collection.Artist;
import org.tomahawk.libtomahawk.collection.CollectionManager;
import org.tomahawk.libtomahawk.collection.ScriptResolverCollection;
import org.tomahawk.libtomahawk.collection.Track;
import org.tomahawk.libtomahawk.infosystem.InfoSystemUtils;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverAlbumResult;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverAlbumTrackResult;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverArtistResult;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverCollectionMetaData;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverConfigUi;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverMetaData;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverResult;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverResultEntry;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverSettings;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult;
import org.tomahawk.libtomahawk.utils.StringEscapeUtils;
import org.tomahawk.tomahawk_android.R;
import org.tomahawk.tomahawk_android.TomahawkApp;
import org.tomahawk.tomahawk_android.activities.TomahawkMainActivity;
import org.tomahawk.tomahawk_android.utils.ThreadManager;
import org.tomahawk.tomahawk_android.utils.TomahawkRunnable;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebSettings;
import android.webkit.WebView;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * This class represents a javascript resolver.
 */
public class ScriptResolver extends Resolver {

    private final static String TAG = ScriptResolver.class.getSimpleName();

    private final static String SCRIPT_INTERFACE_NAME = "Tomahawk";

    public final static String CONFIG = "config";

    public final static String ENABLED_KEY = "_enabled_";

    // We have to map the original cache keys to an id string, because a string containing "\t\t"
    // delimiters does come out without the delimiters, after it has been processed in the js
    // resolver script
    private ConcurrentHashMap<String, String> mQueryKeys = new ConcurrentHashMap<String, String>();

    private String mId;

    private WebView mWebView;

    private String mPath;

    private ScriptResolverMetaData mMetaData;

    private ScriptResolverCollectionMetaData mCollectionMetaData;

    private String mIconPath;

    private int mWeight;

    private int mTimeout;

    private ScriptResolverConfigUi mConfigUi;

    private boolean mEnabled;

    private boolean mReady;

    private boolean mStopped;

    private ObjectMapper mObjectMapper;

    private SharedPreferences mSharedPreferences;

    private boolean mBrowsable;

    private boolean mPlaylistSync;

    private boolean mAccountFactory;

    private boolean mUrlLookup;

    private boolean mConfigTestable;

    private FuzzyIndex mFuzzyIndex;

    private String mFuzzyIndexPath;

    private static final int TIMEOUT_HANDLER_MSG = 1337;

    // Handler which sets the mStopped bool to true after the timeout has occured.
    // Meaning this resolver is no longer being shown as resolving.
    private final Handler mTimeOutHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            removeMessages(msg.what);
            mStopped = true;
        }
    };

    /**
     * Construct a new {@link ScriptResolver}
     *
     * @param metaData this resolver's metadata (parsed from metadata.json)
     * @param path     {@link String} containing the path to this js resolver's "content"-folder
     */
    public ScriptResolver(ScriptResolverMetaData metaData, String path,
            OnResolverReadyListener onResolverReadyListener) {
        super(metaData.name, onResolverReadyListener);

        mObjectMapper = InfoSystemUtils.getObjectMapper();
        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(TomahawkApp.getContext());
        mMetaData = metaData;
        if (mMetaData.staticCapabilities != null) {
            for (String capability : mMetaData.staticCapabilities) {
                if (capability.equals("configTestable")) {
                    mConfigTestable = true;
                }
            }
        }
        mPath = path;
        mReady = false;
        mStopped = true;
        mId = mMetaData.pluginName;
        mIconPath = "file:///android_asset/" + path + "/" + mMetaData.manifest.icon;
        if (getConfig().get(ENABLED_KEY) != null) {
            mEnabled = (Boolean) getConfig().get(ENABLED_KEY);
        } else {
            if (TomahawkApp.PLUGINNAME_JAMENDO.equals(mId) || TomahawkApp.PLUGINNAME_OFFICIALFM.equals(mId)
                    || TomahawkApp.PLUGINNAME_SOUNDCLOUD.equals(mId)) {
                setEnabled(true);
            } else {
                setEnabled(false);
            }
        }
        mFuzzyIndexPath = TomahawkApp.getContext().getFilesDir().getAbsolutePath() + File.separator + getId()
                + ".lucene";
        FuzzyIndex fuzzyIndex = new FuzzyIndex();
        if (fuzzyIndex.create(mFuzzyIndexPath, false)) {
            Log.d(TAG, "Found a fuzzy index at: " + mFuzzyIndexPath);
            mFuzzyIndex = fuzzyIndex;
        } else {
            Log.d(TAG, "Didn't find a fuzzy index");
        }

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                //pre-initalize WebView
                getWebView();
            }
        });
    }

    /**
     * @return whether or not this {@link Resolver} is ready
     */
    @Override
    public boolean isReady() {
        return mReady;
    }

    /**
     * @return whether or not this {@link ScriptResolver} is currently resolving
     */
    @Override
    public boolean isResolving() {
        return mReady && !mStopped;
    }

    @Override
    public String getIconPath() {
        return mIconPath;
    }

    @Override
    public int getIconResId() {
        return 0;
    }

    /**
     * Initialize the WebView. Loads the .js script from the given path and sets the appropriate
     * base URL.
     *
     * @return the initialized WebView
     */
    private synchronized WebView getWebView() {
        if (mWebView == null) {
            mWebView = new WebView(TomahawkApp.getContext());
            WebSettings settings = mWebView.getSettings();
            settings.setJavaScriptEnabled(true);
            settings.setDatabaseEnabled(true);
            settings.setDatabasePath(TomahawkApp.getContext().getDir("databases", Context.MODE_PRIVATE).getPath());
            settings.setDomStorageEnabled(true);
            mWebView.setWebChromeClient(new TomahawkWebChromeClient());
            mWebView.setWebViewClient(new ScriptWebViewClient(ScriptResolver.this));
            final ScriptInterface scriptInterface = new ScriptInterface(ScriptResolver.this);
            mWebView.addJavascriptInterface(scriptInterface, SCRIPT_INTERFACE_NAME);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                mWebView.getSettings().setAllowUniversalAccessFromFileURLs(true);
            }

            final String baseurl = "file:///android_asset/test.html";
            String data = "<!DOCTYPE html>" + "<html><body>"
                    + "<script src=\"file:///android_asset/js/cryptojs-core.js"
                    + "\" type=\"text/javascript\"></script>";
            for (String scriptPath : mMetaData.manifest.scripts) {
                data += "<script src=\"file:///android_asset/" + mPath + "/" + 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_android_post.js"
                    + "\" type=\"text/javascript\"></script>" + "<script src=\"file:///android_asset/" + mPath + "/"
                    + mMetaData.manifest.main + "\" type=\"text/javascript\"></script>" + "</body></html>";
            final String finalData = data;
            mWebView.loadDataWithBaseURL(baseurl, finalData, "text/html", null, null);
        }
        return mWebView;
    }

    /**
     * This method is being called, when the {@link ScriptWebViewClient} has completely loaded the
     * given .js script.
     */
    public void onWebViewClientReady() {
        resolverInit();
        mReady = true;
        onResolverReady();
    }

    /**
     * This method calls the js function resolver.init().
     */
    private void resolverInit() {
        loadUrl("javascript:" + makeJSFunctionCallbackJava(R.id.scriptresolver_resolver_init,
                "Tomahawk.resolver.instance.init()", false));
    }

    /**
     * This method tries to get the {@link Resolver}'s settings.
     */
    private void resolverSettings() {
        loadUrl("javascript:" + makeJSFunctionCallbackJava(R.id.scriptresolver_resolver_settings,
                "Tomahawk.resolver.instance.settings", true));
    }

    /**
     * This method tries to save the {@link Resolver}'s UserConfig.
     */
    private void resolverSaveUserConfig() {
        loadUrl("javascript: Tomahawk.resolver.instance.saveUserConfig()");
    }

    /**
     * This method tries to get the {@link Resolver}'s UserConfig.
     */
    private void resolverGetConfigUi() {
        loadUrl("javascript:" + makeJSFunctionCallbackJava(R.id.scriptresolver_resolver_get_config_ui,
                "Tomahawk.resolver.instance.getConfigUi()", true));
    }

    public void callback(final int callbackId, final String responseText,
            final Map<String, List<String>> responseHeaders, final int status, final String statusText) {
        final Map<String, String> headers = new HashMap<String, String>();
        for (String key : responseHeaders.keySet()) {
            if (key != null) {
                String concatenatedValues = "";
                for (int i = 0; i < responseHeaders.get(key).size(); i++) {
                    if (i > 0) {
                        concatenatedValues += "\n";
                    }
                    concatenatedValues += responseHeaders.get(key).get(i);
                }
                headers.put(key, concatenatedValues);
            }
        }
        try {
            String headersString = mObjectMapper.writeValueAsString(headers);
            loadUrl("javascript: Tomahawk.callback(" + callbackId + "," + "'"
                    + StringEscapeUtils.escapeJavaScript(responseText) + "'," + "'"
                    + StringEscapeUtils.escapeJavaScript(headersString) + "'," + status + "," + "'"
                    + StringEscapeUtils.escapeJavaScript(statusText) + "');");
        } catch (IOException e) {
            Log.e(TAG, "callback: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
    }

    /**
     * Sometimes we need the String returned by a certain javascript function. Therefore we wrap the
     * function call in a call to "callbackToJava", which is being received by the ScriptInterface.
     * The ScriptInterface redirects the call to this method, where we can access the returned
     * String. Throughout this whole process we are passing an id along, which enables us to
     * identify which call wants to return its result.
     *
     * @param id         used to identify which function did the callback
     * @param jsonString the json-string which is the result of the called function. Can be null.
     */
    public void handleCallbackToJava(final int id, final String... jsonString) {
        try {
            if (id == R.id.scriptresolver_resolver_settings && jsonString != null && jsonString.length == 1) {
                ScriptResolverSettings settings = mObjectMapper.readValue(jsonString[0],
                        ScriptResolverSettings.class);
                mWeight = settings.weight;
                mTimeout = settings.timeout * 1000;
                resolverGetConfigUi();
            } else if (id == R.id.scriptresolver_resolver_get_config_ui && jsonString != null
                    && jsonString.length == 1) {
                mConfigUi = mObjectMapper.readValue(jsonString[0], ScriptResolverConfigUi.class);
            } else if (id == R.id.scriptresolver_resolver_init) {
                resolverSettings();
            } else if (id == R.id.scriptresolver_resolver_collection && jsonString != null
                    && jsonString.length == 1) {
                mCollectionMetaData = mObjectMapper.readValue(jsonString[0],
                        ScriptResolverCollectionMetaData.class);
                CollectionManager.getInstance().addCollection(new ScriptResolverCollection(this));
            }
        } catch (IOException e) {
            Log.e(TAG, "handleCallbackToJava: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
    }

    public void addTrackResultsString(final String results) {
        ThreadManager.getInstance().execute(new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_REPORTING) {
            @Override
            public void run() {
                ScriptResolverResult result = null;
                try {
                    result = mObjectMapper.readValue(results, ScriptResolverResult.class);
                } catch (IOException e) {
                    Log.e(TAG, "addTrackResultsString: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
                if (result != null) {
                    ArrayList<Result> parsedResults = parseResultList(result.results, result.qid);
                    PipeLine.getInstance().reportResults(mQueryKeys.get(result.qid), parsedResults, mId);
                }
                mTimeOutHandler.removeCallbacksAndMessages(null);
                mStopped = true;
            }
        });
    }

    public void addAlbumResultsString(final String results) {
        ThreadManager.getInstance().execute(new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_REPORTING) {
            @Override
            public void run() {
                ScriptResolverAlbumResult result = null;
                try {
                    result = mObjectMapper.readValue(results, ScriptResolverAlbumResult.class);
                } catch (IOException e) {
                    Log.e(TAG, "addAlbumResultsString: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
                if (result != null) {
                    ScriptResolverCollection collection = (ScriptResolverCollection) CollectionManager.getInstance()
                            .getCollection(result.qid);
                    if (collection != null) {
                        Artist artist = Artist.get(result.artist);
                        ArrayList<Album> albums = new ArrayList<Album>();
                        for (String albumName : result.albums) {
                            albums.add(Album.get(albumName, artist));
                        }
                        collection.addAlbumResults(albums);
                    }
                }
                mTimeOutHandler.removeCallbacksAndMessages(null);
                mStopped = true;
            }
        });
    }

    public void addArtistResultsString(final String results) {
        ThreadManager.getInstance().execute(new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_REPORTING) {
            @Override
            public void run() {
                ScriptResolverArtistResult result = null;
                try {
                    result = mObjectMapper.readValue(results, ScriptResolverArtistResult.class);
                } catch (IOException e) {
                    Log.e(TAG, "addArtistResultsString: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
                if (result != null) {
                    ScriptResolverCollection collection = (ScriptResolverCollection) CollectionManager.getInstance()
                            .getCollection(result.qid);
                    if (collection != null) {
                        ArrayList<Artist> artists = new ArrayList<Artist>();
                        for (String artistName : result.artists) {
                            artists.add(Artist.get(artistName));
                        }
                        collection.addArtistResults(artists);
                    }
                }
                mTimeOutHandler.removeCallbacksAndMessages(null);
                mStopped = true;
            }
        });
    }

    public void addAlbumTrackResultsString(final String results) {
        ThreadManager.getInstance().execute(new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_REPORTING) {
            @Override
            public void run() {
                ScriptResolverAlbumTrackResult result = null;
                try {
                    result = mObjectMapper.readValue(results, ScriptResolverAlbumTrackResult.class);
                } catch (IOException e) {
                    Log.e(TAG, "addAlbumTrackResultsString: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
                if (result != null) {
                    ScriptResolverCollection collection = (ScriptResolverCollection) CollectionManager.getInstance()
                            .getCollection(result.qid);
                    ArrayList<Result> parsedResults = parseResultList(result.results, result.qid);
                    Artist artist = Artist.get(result.artist);
                    Album album = Album.get(result.album, artist);
                    collection.addAlbumTrackResults(album, parsedResults);
                }
                mTimeOutHandler.removeCallbacksAndMessages(null);
                mStopped = true;
            }
        });
    }

    public void lookupUrl(String url) {
        loadUrl("javascript: Tomahawk.resolver.instance.lookupUrl('" + url + "')");
    }

    public void addUrlResultString(final String url, final String resultString) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ScriptResolverUrlResult result = null;
                try {
                    result = mObjectMapper.readValue(resultString, ScriptResolverUrlResult.class);
                } catch (IOException e) {
                    Log.e(TAG, "addUrlResultString: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
                if (result != null) {
                    PipeLine.getInstance().reportUrlResult(url, ScriptResolver.this, result);
                }
                mStopped = true;
            }
        }).start();
    }

    public void reportStreamUrl(String qid, String url, String stringifiedHeaders) {
        try {
            Map<String, String> headers = null;
            if (stringifiedHeaders != null) {
                headers = mObjectMapper.readValue(stringifiedHeaders, Map.class);
            }
            String resultKey = mQueryKeys.get(qid);
            PipeLine.getInstance().sendStreamUrlReportBroadcast(resultKey, url, headers);
        } catch (IOException e) {
            Log.e(TAG, "reportStreamUrl: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
    }

    public void collection() {
        loadUrl("javascript:" + makeJSFunctionCallbackJava(R.id.scriptresolver_resolver_collection,
                "Tomahawk.resolver.instance.collection()", true));
    }

    public void tracks(String qid, String artistName, String albumName) {
        String escapedQid = StringEscapeUtils.escapeJavaScript(qid);
        String escapedArtistName = StringEscapeUtils.escapeJavaScript(artistName);
        String escapedAlbumName = StringEscapeUtils.escapeJavaScript(albumName);
        loadUrl("javascript: Tomahawk.resolver.instance.tracks( '" + escapedQid + "', '" + escapedArtistName
                + "', '" + escapedAlbumName + "' )");
    }

    public void artists(String qid) {
        String escapedQid = StringEscapeUtils.escapeJavaScript(qid);
        loadUrl("javascript: Tomahawk.resolver.instance.artists( '" + escapedQid + "' )");
    }

    public void albums(String qid, String artistName) {
        String escapedQid = StringEscapeUtils.escapeJavaScript(qid);
        String escapedArtistName = StringEscapeUtils.escapeJavaScript(artistName);
        loadUrl("javascript: Tomahawk.resolver.instance.albums( '" + escapedQid + "', '" + escapedArtistName
                + "' )");
    }

    public void loadUrl(final String url) {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                getWebView().loadUrl(url);
            }
        });
    }

    /**
     * Invoke the javascript to resolve the given {@link Query}.
     *
     * @param query the {@link Query} which should be resolved
     * @return whether or not the Resolver is ready to resolve
     */
    @Override
    public boolean resolve(final Query query) {
        if (mReady) {
            mStopped = false;
            mTimeOutHandler.removeCallbacksAndMessages(null);
            mTimeOutHandler.sendEmptyMessageDelayed(TIMEOUT_HANDLER_MSG, mTimeout);
            String qid = TomahawkMainActivity.getSessionUniqueStringId();
            mQueryKeys.put(qid, query.getCacheKey());

            // construct javascript call url
            final String url;
            String escapedQid = StringEscapeUtils.escapeJavaScript(qid);
            if (query.isFullTextQuery()) {
                String fullTextQuery = StringEscapeUtils.escapeJavaScript(query.getFullTextQuery());
                url = "javascript: Tomahawk.resolver.instance.search( '" + escapedQid + "', '" + fullTextQuery
                        + "' )";
            } else {
                String artistName = StringEscapeUtils.escapeJavaScript(query.getArtist().getName());
                String albumName = StringEscapeUtils.escapeJavaScript(query.getAlbum().getName());
                String trackName = StringEscapeUtils.escapeJavaScript(query.getName());
                url = "javascript: Tomahawk.resolver.instance.resolve( '" + escapedQid + "', '" + artistName
                        + "', '" + albumName + "', '" + trackName + "' )";
            }

            // call it
            loadUrl(url);
        }
        return mReady;
    }

    public void getStreamUrl(final Result result, String callbackFuncName) {
        if (result != null) {
            String resultId = TomahawkMainActivity.getSessionUniqueStringId();
            // we are using the same map as we do when resolving queries
            mQueryKeys.put(resultId, result.getCacheKey());
            loadUrl("javascript: Tomahawk.resolver.instance." + callbackFuncName + "( '"
                    + StringEscapeUtils.escapeJavaScript(resultId) + "', '"
                    + StringEscapeUtils.escapeJavaScript(result.getPath()) + "' )");
        }
    }

    /**
     * Parses the given {@link JSONArray} into a {@link ArrayList} of {@link Result}s.
     *
     * @param resultEntries ArrayList of ScriptResolverResultEntries containing the raw result
     *                      information
     * @return a {@link ArrayList} of {@link Result}s containing the parsed data
     */
    private ArrayList<Result> parseResultList(ArrayList<ScriptResolverResultEntry> resultEntries, String queryKey) {
        ArrayList<Result> resultList = new ArrayList<Result>();
        for (ScriptResolverResultEntry resultEntry : resultEntries) {
            if (resultEntry != null && !TextUtils.isEmpty(resultEntry.url)
                    && !TextUtils.isEmpty(resultEntry.track)) {
                Artist artist;
                if (resultEntry.artist != null) {
                    artist = Artist.get(resultEntry.artist);
                } else {
                    artist = Artist.get("");
                }

                Album album;
                if (resultEntry.album != null) {
                    album = Album.get(resultEntry.album, artist);
                } else {
                    album = Album.get("", artist);
                }

                Track track = Track.get(resultEntry.track, album, artist);
                track.setAlbumPos(resultEntry.albumpos);
                track.setDiscNumber(resultEntry.discnumber);
                if (resultEntry.year != null && resultEntry.year.matches("-?\\d+")) {
                    track.setYear(Integer.valueOf(resultEntry.year));
                }
                track.setDuration(resultEntry.duration * 1000);

                Result result = Result.get(resultEntry.url, track, this, queryKey);
                result.setBitrate(resultEntry.bitrate);
                result.setSize(resultEntry.size);
                result.setPurchaseUrl(resultEntry.purchaseUrl);
                result.setLinkUrl(resultEntry.linkUrl);
                result.setArtist(artist);
                result.setAlbum(album);
                result.setTrack(track);

                resultList.add(result);
            }
        }
        return resultList;
    }

    /**
     * Wraps the given js call into the necessary functions to make sure, that the javascript
     * function will callback the exposed java method callbackToJava in the {@link ScriptInterface}
     *
     * @param id                 used to later identify the callback
     * @param string             the {@link String} which should be surrounded. Usually a simple js
     *                           function call.
     * @param shouldReturnResult whether or not this js function call will return with a {@link
     *                           JSONObject} as a result
     * @return the computed {@link String}
     */
    private String makeJSFunctionCallbackJava(int id, String string, boolean shouldReturnResult) {
        return SCRIPT_INTERFACE_NAME + ".callbackToJava(" + id + ",JSON.stringify(" + string + "),"
                + shouldReturnResult + ");";
    }

    /**
     * @return this {@link ScriptResolver}'s id
     */
    @Override
    public String getId() {
        return mId;
    }

    public String getName() {
        return mMetaData.name;
    }

    @Override
    public String getCollectionName() {
        return mCollectionMetaData.prettyname;
    }

    /**
     * @return the absolute filepath (without file://android_asset) of the corresponding script
     */
    public String getScriptFilePath() {
        return mPath + "/" + mMetaData.manifest.main;
    }

    public void setConfig(Map<String, Object> config) {
        try {
            String rawJsonString = mObjectMapper.writeValueAsString(config);
            mSharedPreferences.edit().putString(buildPreferenceKey(), rawJsonString).commit();
            resolverSaveUserConfig();
        } catch (IOException e) {
            Log.e(TAG, "setConfig: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
    }

    /**
     * @return the Map<String, String> containing the Config information of this resolver
     */
    public Map<String, Object> getConfig() {
        String rawJsonString = mSharedPreferences.getString(buildPreferenceKey(), "");
        try {
            return mObjectMapper.readValue(rawJsonString, Map.class);
        } catch (IOException e) {
            Log.e(TAG, "getConfig: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
        return new HashMap<String, Object>();
    }

    /**
     * @return this {@link ScriptResolver}'s weight
     */
    @Override
    public int getWeight() {
        return mWeight;
    }

    public String getDescription() {
        return mMetaData.description;
    }

    private String buildPreferenceKey() {
        return mMetaData.pluginName + "_" + CONFIG;
    }

    public ScriptResolverConfigUi getConfigUi() {
        return mConfigUi;
    }

    public boolean isEnabled() {
        AuthenticatorUtils utils = AuthenticatorManager.getInstance().getAuthenticatorUtils(mId);
        if (utils != null) {
            return utils.isLoggedIn();
        }
        return mEnabled;
    }

    public void setEnabled(boolean enabled) {
        Log.d(TAG, this.mId + " has been " + (enabled ? "enabled" : "disabled"));
        mEnabled = enabled;
        Map<String, Object> config = getConfig();
        config.put(ENABLED_KEY, enabled);
        setConfig(config);
    }

    public void reportCapabilities(int in) {
        BigInteger bigInt = BigInteger.valueOf(in);
        if (bigInt.testBit(0)) {
            mBrowsable = true;
            collection();
        }
        if (bigInt.testBit(1)) {
            mPlaylistSync = true;
        }
        if (bigInt.testBit(2)) {
            mAccountFactory = true;
        }
        if (bigInt.testBit(3)) {
            mUrlLookup = true;
        }
        if (bigInt.testBit(4)) {
            mConfigTestable = true;
        }
    }

    public boolean isBrowsable() {
        return mBrowsable;
    }

    public boolean isPlaylistSync() {
        return mPlaylistSync;
    }

    public boolean isAccountFactory() {
        return mAccountFactory;
    }

    public boolean hasUrlLookup() {
        return mUrlLookup;
    }

    public boolean isConfigTestable() {
        return mConfigTestable;
    }

    public boolean hasFuzzyIndex() {
        return mFuzzyIndex != null;
    }

    public FuzzyIndex getFuzzyIndex() {
        return mFuzzyIndex;
    }

    public void createFuzzyIndex() {
        if (mFuzzyIndex != null) {
            mFuzzyIndex.close();
        }
        FuzzyIndex fuzzyIndex = new FuzzyIndex();
        if (fuzzyIndex.create(mFuzzyIndexPath, true)) {
            mFuzzyIndex = fuzzyIndex;
        }
    }

    public void configTest() {
        loadUrl("javascript: Tomahawk.resolver.instance.configTest()");
    }

    public void onConfigTestResult(final int type, final String message) {
        Log.d(TAG, getName() + ": Config test result received. type: " + type + ", message:" + message);
        AuthenticatorManager.broadcastConfigTestResult(getId(),
                AuthenticatorManager.CONFIG_TEST_RESULT_PLUGINTYPE_RESOLVER, type, message, null);
    }
}