com.danielme.muspyforandroid.services.MuspyClient.java Source code

Java tutorial

Introduction

Here is the source code for com.danielme.muspyforandroid.services.MuspyClient.java

Source

/*
 * Copyright (C) 2012-2013 Daniel Medina <http://danielme.com>
 * 
 * This file is part of "Muspy for Android".
 * 
 * "Muspy for Android" 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, version 3.
 *
 * "Muspy for Android" 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 version 3
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-3.0.html/>
 */
package com.danielme.muspyforandroid.services;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Base64;
import android.util.Log;

import com.danielme.muspyforandroid.Constants;
import com.danielme.muspyforandroid.exc.AuthorizationException;
import com.danielme.muspyforandroid.model.Artist;
import com.danielme.muspyforandroid.model.Release;
import com.danielme.muspyforandroid.model.UserSettings;
import com.danielme.muspyforandroid.utils.ArtistComparator;
import com.danielme.muspyforandroid.utils.Utils;

/**
 * WS client.
 * 
 * @author Daniel Medina (danielme.com)
 * @since 1.0
 * 
 */
public final class MuspyClient {

    /* ************************************************************ */
    /* FIELDS AND CONSTANTS */
    /* ************************************************************ */

    private static final String AUTHORIZATION = "Authorization";

    private static final String APPLICATION_JSON = "application/json";

    private static final String CONTENT_TYPE = "Content-Type";

    private BasicHttpContext localContext;

    private DefaultHttpClient defaultHttpClient;

    private List<Artist> artists_cache;

    private boolean cache = true;

    private static final HttpHost TARGETHOST = new HttpHost(Constants.HOST, 443, "https");

    private static MuspyClient muspyClient = null;

    private ArtistComparator artistComparator = new ArtistComparator();

    // prevents the caller from creating objects
    private MuspyClient() {
        // not implemented
    }

    /**
     * Singleton
     */
    public static MuspyClient getInstance() {
        if (muspyClient == null) {
            muspyClient = new MuspyClient();
        }
        return muspyClient;
    }

    /* ************************************************************ */
    /* PUBLIC */
    /* ************************************************************ */

    /**
     * Returns artists.
     */
    public List<Artist> getArtists(Context context) throws Exception {
        if (artists_cache == null || !cache) {
            HttpGet httpGet = new HttpGet(Constants.API.ARTISTS + Utils.getUserId(context));
            httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON);
            setAuthHeader(httpGet, context);

            HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext());
            String responseString = EntityUtils.toString(response.getEntity());

            if (!checkAuth(response)) {
                throw new AuthorizationException();
            }

            JSONArray jsonArray = new JSONArray(responseString);

            artists_cache = new LinkedList<Artist>();

            for (int i = 0; i < jsonArray.length(); i++) {
                artists_cache.add(populateArtist(jsonArray.getJSONObject(i)));
            }

            Collections.sort(artists_cache, artistComparator);
        }

        return artists_cache;
    }

    public List<Artist> refreshArtists(Context context) throws Exception {
        artists_cache = null;
        return getArtists(context);
    }

    public void clearArtists() {
        artists_cache = null;
    }

    public UserSettings getUserSettings(Context context) throws Exception {
        UserSettings userSettings = getUserSettings(Utils.getEmail(context), Utils.getPass(context));
        if (userSettings == null) {
            throw new AuthorizationException();
        }
        return userSettings;
    }

    public boolean updateUserSettings(Context context, String email, String password, boolean notifications,
            boolean filterAlbum, boolean filterSingle, boolean filterEP, boolean filterCompilation,
            boolean filterLive, boolean filterRemix, boolean filterOther) throws Exception {
        HttpPut httpPut = new HttpPut(Constants.API.USER + Utils.getUserId(context));

        setAuthHeader(httpPut, context);

        List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>();

        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY, getIntString(notifications)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_ALBUM, getIntString(filterAlbum)));
        nameValuepairs.add(
                new BasicNameValuePair(Constants.SETTINGS.NOTIFY_COMPILATION, getIntString(filterCompilation)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_EP, getIntString(filterEP)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_LIVE, getIntString(filterLive)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_OTHER, getIntString(filterOther)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_REMIX, getIntString(filterRemix)));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_SINGLE, getIntString(filterSingle)));

        // TODO password
        if (Utils.hasText(email) && !email.equals(Utils.getUserId(context))) {
            nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.EMAIL, email));
        }

        httpPut.setEntity(new UrlEncodedFormEntity(nameValuepairs));

        Log.d(MuspyClient.class.toString(), "updateUserSettings");
        HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut);
        String responseString = EntityUtils.toString(httpResponse.getEntity());

        if (responseString.toUpperCase().contains(Constants.EMAIL_NON_UNIQUE.toUpperCase())) {
            return false;
        }

        return true;
    }

    public UserSettings getUserSettings(String email, String pass) throws Exception {
        UserSettings userSettings = null;
        HttpGet httpGet = new HttpGet(Constants.API.USER);
        httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON);
        httpGet.setHeader(AUTHORIZATION,
                "Basic " + Base64.encodeToString((email + ":" + pass).getBytes(), Base64.NO_WRAP));

        HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext());

        String responseString = EntityUtils.toString(response.getEntity());

        if (checkAuth(response)) {
            userSettings = populateSettings(responseString);
        }

        return userSettings;
    }

    public void addArtist(Context context, String mbid) throws Exception {
        HttpPut httpPut = new HttpPut(Constants.API.ARTISTS + Utils.getUserId(context) + "/" + mbid);
        httpPut.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON);

        setAuthHeader(httpPut, context);

        JSONObject request = new JSONObject();
        httpPut.setEntity(new StringEntity(request.toString()));
        Log.d(MuspyClient.class.toString(), "addArtist");
        HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut);

        if (!checkAuth(httpResponse)) {
            throw new AuthorizationException();
        }
        artists_cache = null;

    }

    public void importLastFm(Context context, String username, String period, String top) throws Exception {
        HttpPut httpPut = new HttpPut(Constants.API.ARTISTS + Utils.getUserId(context) + "/");
        setAuthHeader(httpPut, context);

        List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>();
        nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.IMPORT, Constants.LASTFM.LASTFM));
        nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.COUNT, top));
        nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.PERIOD, period));
        nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.USERNAME, username));

        httpPut.setEntity(new UrlEncodedFormEntity(nameValuepairs));
        Log.d(MuspyClient.class.toString(), "importLastFm");
        HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut);

        if (!checkAuth(httpResponse)) {
            throw new AuthorizationException();
        }
        artists_cache = null;
        cache = false;

    }

    public List<Release> getReleases(Context context, int offset, String mbid, int size) throws Exception {
        StringBuilder getReleases = new StringBuilder(Constants.API.RELEASES);
        if (!Utils.hasText(mbid)) {
            getReleases.append(Utils.getUserId(context));
        }
        getReleases.append("?" + Constants.API.PARAM_OFFSET + "=" + offset);
        getReleases.append("&" + Constants.API.PARAM_LIMIT + "=" + size);
        if (Utils.hasText(mbid)) {
            getReleases.append("&" + Constants.API.PARAM_MBID + "=" + mbid);
        }

        HttpGet httpGet = new HttpGet(getReleases.toString());
        httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON);

        HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext());

        String respString = EntityUtils.toString(response.getEntity());
        JSONArray jsonArray = new JSONArray(respString);

        List<Release> releases = new ArrayList<Release>(jsonArray.length());

        for (int i = 0; i < jsonArray.length(); i++) {
            releases.add(populateRelease(jsonArray.getJSONObject(i)));
        }

        return releases;
    }

    public Release getRelease(String mbid) throws Exception {
        HttpGet httpGet = new HttpGet(Constants.API.RELEASE + mbid);
        httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON);

        HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext());

        String respString = EntityUtils.toString(response.getEntity());
        JSONObject jsonRelease = new JSONObject(respString);

        return populateRelease(jsonRelease);
    }

    public Drawable getCover(String mbid) {
        Drawable drawable = null;
        HttpURLConnection urlConnection = null;
        try {
            URL url = new URL(Constants.URL_COVER + mbid);
            urlConnection = (HttpURLConnection) url.openConnection();

            drawable = Drawable.createFromStream(urlConnection.getInputStream(), mbid);
        } catch (IOException ex1) {
            //cover not found
        } catch (Exception ex2) {
            Log.e(MuspyClient.class.toString(), ex2.getMessage(), ex2);
        } finally {
            urlConnection.disconnect();
        }

        return drawable;
    }

    public void deleteArtists(Context context, List<String> mbids) throws Exception {
        HttpDelete httpDelete = null;
        artists_cache = null;
        Log.d(MuspyClient.class.toString(), "deleting " + mbids.size() + " artists");
        for (String mbid : mbids) {
            httpDelete = new HttpDelete(Constants.API.ARTISTS + Utils.getUserId(context) + "/" + mbid);
            httpDelete.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON);
            setAuthHeader(httpDelete, context);
            getDefaultHttpClient().execute(httpDelete);
        }

    }

    public UserSettings signUp(String email, String password) throws Exception {
        List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>();
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.EMAIL, email));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.PASSWORD, password));
        nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.ACTIVATE, "1"));

        HttpPost httpPost = new HttpPost(Constants.API.USER);
        httpPost.setEntity(new UrlEncodedFormEntity(nameValuepairs));

        Log.d(MuspyClient.class.toString(), "signUp");
        HttpResponse httpResponse = getDefaultHttpClient().execute(httpPost);
        String responseString = EntityUtils.toString(httpResponse.getEntity());

        if (responseString.toUpperCase().contains(Constants.USER_CREATED.toUpperCase())) {
            return getUserSettings(email, password);
        } else if (responseString.toUpperCase().contains(Constants.EMAIL_IN_USE_ERROR.toUpperCase())) {
            return null;
        } else {
            throw new Exception("unknown response in sign up");
        }

    }

    /**
     * Resets password with jsoup.
     */
    // TODO use api when available - probably never :(
    public boolean resetPassword(String email) throws Exception {
        boolean valid = true;

        HttpPost httpPost = new HttpPost(Constants.URL_RESET);

        List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>();
        nameValuepairs.add(new BasicNameValuePair(Constants.EMAIL, email));

        nameValuepairs.add(new BasicNameValuePair("csrfmiddlewaretoken", getCSRF()));
        httpPost.setHeader("Referer", Constants.URL_RESET);
        httpPost.setEntity(new UrlEncodedFormEntity(nameValuepairs));

        Log.d(MuspyClient.class.toString(), "resetPassword");

        HttpResponse httpResponse = getDefaultHttpClient().execute(httpPost);
        String responseString = EntityUtils.toString(httpResponse.getEntity());

        int start = responseString.indexOf("<title>");
        int end = responseString.indexOf("</title>");

        if (responseString.substring(start, end).toLowerCase().contains("reset")) {
            valid = false;
        }

        return valid;
    }

    public void deleteAccount(Context context) throws Exception {
        HttpDelete httpDelete = null;

        Log.d(MuspyClient.class.toString(), "deleting account");

        httpDelete = new HttpDelete(Constants.API.USER + Utils.getUserId(context));
        // httpDelete.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON);
        setAuthHeader(httpDelete, context);
        HttpResponse httpResponse = getDefaultHttpClient().execute(httpDelete);

        if (!checkAuth(httpResponse)) {
            throw new AuthorizationException();
        }
        artists_cache = null;
    }

    /* ************************************************************ */
    /* RESPONSE MAPPING */
    /* ************************************************************ */

    private UserSettings populateSettings(String responseString) throws Exception {
        UserSettings userSettings = new UserSettings();

        JSONObject responseJSON = new JSONObject(responseString);
        userSettings = new UserSettings();
        userSettings.setId(responseJSON.getString(Constants.SETTINGS.USERID));
        userSettings.setEmail(responseJSON.getString(Constants.SETTINGS.EMAIL));
        userSettings.setNotifications(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY));
        userSettings.setFilterAlbum(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_ALBUM));
        userSettings.setFilterCompilation(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_COMPILATION));
        userSettings.setFilterEP(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_EP));
        userSettings.setFilterLive(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_LIVE));
        userSettings.setFilterOther(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_OTHER));
        userSettings.setFilterSingle(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_SINGLE));
        userSettings.setFilterRemix(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_REMIX));

        return userSettings;
    }

    private Release populateRelease(JSONObject jsonRelease) throws JSONException {
        Release release = new Release();

        release.setMbid(jsonRelease.getString(Constants.RELEASE.MBID));
        release.setName(jsonRelease.getString(Constants.RELEASE.NAME));

        String type = jsonRelease.getString(Constants.RELEASE.TYPE);
        if (Locale.getDefault().getLanguage().equals(Constants.LOCALE_ES)) {
            type = Constants.TYPE.TYPES_ES.get(type.toUpperCase());
            if (type == null) {
                type = "";
            }
        }
        release.setType(type);

        String jsonDate = jsonRelease.getString(Constants.RELEASE.DATE);
        release.setYear(jsonDate.substring(0, 4));
        if (jsonDate.length() > 4) {
            release.setMonth(jsonDate.substring(5, 7));
            if (jsonDate.length() > 8) {
                release.setDay(jsonDate.substring(8));
            }
        }
        JSONObject jsonArtist = jsonRelease.getJSONObject(Constants.RELEASE.ARTIST_OBJ);

        release.setArtist(populateArtist(jsonArtist));
        return release;
    }

    private Artist populateArtist(JSONObject jsonArtist) throws JSONException {
        Artist artist = new Artist();
        artist.setMbid(jsonArtist.getString(Constants.ARTIST.MBID));
        artist.setDisambiguation(jsonArtist.getString(Constants.ARTIST.DISAMBIGUATION));
        artist.setName(jsonArtist.getString(Constants.ARTIST.NAME));
        artist.setSortname(jsonArtist.getString(Constants.ARTIST.SORTNAME));
        return artist;
    }

    /* ************************************************************ */
    /* PRIVATE */
    /* ************************************************************ */

    private void setAuthHeader(AbstractHttpMessage abstractHttpMessage, Context context)
            throws AuthorizationException {
        abstractHttpMessage.setHeader(AUTHORIZATION, "Basic " + Base64.encodeToString(
                (Utils.getEmail(context) + ":" + Utils.getPass(context)).getBytes(), Base64.NO_WRAP));
    }

    // TODO This implementation accepts all certificates. This should be
    // improved in future versions in order to validate muspy.com
    // certificate.
    // http://stackoverflow.com/questions/2703161/how-to-ignore-ssl-certificate-errors-in-apache-httpclient-4-0
    private class MySSLSocketFactory extends SSLSocketFactory {
        private SSLContext sslContext = SSLContext.getInstance("TLS");

        public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException,
                KeyStoreException, UnrecoverableKeyException {
            super(truststore);

            TrustManager trustManager = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException {
                    // not implemented
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException {
                    // not implemented
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };

            sslContext.init(null, new TrustManager[] { trustManager }, null);
        }

        @Override
        public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
            return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
        }

        @Override
        public Socket createSocket() throws IOException {
            return sslContext.getSocketFactory().createSocket();
        }
    }

    private DefaultHttpClient getDefaultHttpClient() throws Exception {
        if (defaultHttpClient == null) {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(null, null);

            SSLSocketFactory sslSocketFactory = new MySSLSocketFactory(trustStore);
            sslSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", sslSocketFactory, 443));

            defaultHttpClient = new DefaultHttpClient(
                    new ThreadSafeClientConnManager(new BasicHttpParams(), registry), new BasicHttpParams());
        }

        return defaultHttpClient;
    }

    private BasicHttpContext getBasicHttpContext() {
        if (localContext == null) {
            localContext = new BasicHttpContext();
            BasicScheme basicAuth = new BasicScheme();
            localContext.setAttribute("preemptive-auth", basicAuth);
        }
        return localContext;
    }

    private String getCSRF() throws Exception {
        HttpGet httpGet = new HttpGet(Constants.URL_RESET);
        HttpResponse httpResponse = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext());
        String responseString = EntityUtils.toString(httpResponse.getEntity());
        Document doc = Jsoup.parse(responseString);
        Elements elementsByTag = doc.getElementsByTag("input");
        for (Element element : elementsByTag) {
            if ("csrfmiddlewaretoken".equals(element.attr("name"))) {
                return element.attr("value");
            }
        }
        throw new Exception("csrf not found");
    }

    private String getIntString(boolean booleanValue) {
        return booleanValue ? "1" : "0";
    }

    private boolean checkAuth(HttpResponse response) {
        boolean auth = true;
        if (response.getStatusLine() != null && response.getStatusLine().getStatusCode() == 401) {
            auth = false;
        }
        return auth;
    }

}