info.graffy.android.websms.connector.meinbmw.ConnectorMeinBMW.java Source code

Java tutorial

Introduction

Here is the source code for info.graffy.android.websms.connector.meinbmw.ConnectorMeinBMW.java

Source

/*
 * Copyright (C) 2010 Silas Graffy
 *
 * This file is part of MeinBMW Connector for WebSMS by Felix Bechstein.
 *
 * This program 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.
 *
 * This program 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
 * this program; If not, see <http://www.gnu.org/licenses/>.
 */

package info.graffy.android.websms.connector.meinbmw;

import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import de.ub0r.android.websms.connector.common.Connector;
import de.ub0r.android.websms.connector.common.ConnectorCommand;
import de.ub0r.android.websms.connector.common.ConnectorSpec;
import de.ub0r.android.websms.connector.common.ConnectorSpec.SubConnectorSpec;
import de.ub0r.android.websms.connector.common.Utils;
import de.ub0r.android.websms.connector.common.WebSMSException;

public class ConnectorMeinBMW extends Connector {

    private final static String ROOT_PAGE_URL = "https://www.meinbmw.de";
    private final static String SMS_PAGE_URL = "https://www.meinbmw.de/tabid/80/Default.aspx";
    private final static String REFERER_URL = "https://www.meinbmw.de/Home/tabid/36/ctl/Login/Default.aspx";
    private final static String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.52 Safari/536.5";
    private final static String ENCODING = "UTF-8";
    private final static String[] SSL_FINGERPRINTS = {
            "b4:2b:76:39:ec:53:ee:83:39:02:a3:70:c1:fc:a9:a5:f7:24:d5:2a",
            "0f:9e:67:16:e7:e0:98:02:22:49:d6:a0:74:9b:76:11:7e:c6:e1:e9",
            "df:27:77:83:2d:7b:41:02:61:0d:52:25:f4:c3:a0:0c:6c:3c:3e:1c",
            "bd:54:a8:bc:61:9d:ec:70:dd:a9:05:39:ea:b6:cc:18:3d:0c:8a:8d" };
    private final static String SESSION_INPUT_VALUE_REGEXP = ".*<input type=\"hidden\" name=\"__VIEWSTATE_CACHEKEY\" id=\"__VIEWSTATE_CACHEKEY\" value=\"(VS_[a-z\\d]+_\\d+)\" />.*";
    private final static String LOGIN_POST_DESTINATION_REGEXP = ".*<form name=\"Form\" method=\"post\" action=\"(/ServiceCenter/.+/SMSService/tabid/80/ctl/Login/Default.aspx\\?returnurl=.+\\.aspx)\".*id=\"Form\" enctype=\"multipart/form-data\" .*";
    private final static String SEND_SUCCESS_SUBSTRING = "verschickt.";
    private final static String SMS_PAGE_LOAD_SUCCESS_SUBSTRING = "MeinBMW.de - Service-Center - Mein BMW Services - SMS Service";
    private final static String WRONG_LOGIN_MESSAGE_SUBSTRING = "Ihre E-Mail-Adresse oder Ihr Passwort ist leider nicht korrekt. Bitte berprfen Sie Ihre Eingaben."; // TODO changed
    private final static String TODAYS_SMS_EXPIRED_SUBSTRING = "heute ist erreicht. Bitte nutzen Sie diesen Service wieder ab morgen";

    private static String currentHtmlResultPage;
    private static Context currentContext;
    private final static Pattern sessionInputValueExtractPattern = Pattern.compile(SESSION_INPUT_VALUE_REGEXP,
            Pattern.DOTALL);
    private final static Pattern postUrlExtractPattern = Pattern.compile(LOGIN_POST_DESTINATION_REGEXP,
            Pattern.DOTALL);

    @Override
    public final ConnectorSpec initSpec(final Context context) {
        setCurrentContext(context);
        return createConnectorSpec();
    }

    @Override
    public final ConnectorSpec updateSpec(final Context context, final ConnectorSpec connectorSpec) {
        setCurrentContext(context);
        return setConnectorStatus(connectorSpec);
    }

    @Override
    protected final void doSend(final Context context, final Intent intent) {
        setCurrentContext(context);

        tryToLoadSmsPage();

        if (!isSmsPageLoaded()) {
            final String user = getUserName();
            final String pass = getPassword();
            doLogin(user, pass);
        }

        final String phonenumber = getPhonenumber(intent);
        final String text = getMessageText(intent);

        sendSms(phonenumber, text);
    }

    private final void setCurrentContext(final Context context) {
        currentContext = context;
    }

    private final ConnectorSpec setConnectorStatus(final ConnectorSpec connectorSpec) {
        final SharedPreferences p = getDefaultSharedPreferences();
        if (p.getBoolean(Preferences.PREFS_ENABLED, false))
            if (getUserName().length() > 0 && getPassword().length() > 0)
                connectorSpec.setReady();
            else
                connectorSpec.setStatus(ConnectorSpec.STATUS_ENABLED);
        else
            connectorSpec.setStatus(ConnectorSpec.STATUS_INACTIVE);
        return connectorSpec;
    }

    private final SharedPreferences getDefaultSharedPreferences() {
        return PreferenceManager.getDefaultSharedPreferences(currentContext);
    }

    private final String getUserName() {
        final SharedPreferences p = getDefaultSharedPreferences();
        return p.getString(Preferences.PREFS_USER, "");
    }

    private final String getPassword() {
        final SharedPreferences p = getDefaultSharedPreferences();
        return p.getString(Preferences.PREFS_PASSWORD, "");
    }

    private final ConnectorSpec createConnectorSpec() {
        final String name = getStringResource(R.string.connector_meinbmw_name);
        final ConnectorSpec c = new ConnectorSpec(name);
        c.setAuthor(getStringResource(R.string.connector_meinbmw_author));
        c.setBalance(null);
        c.setCapabilities(ConnectorSpec.CAPABILITIES_SEND | ConnectorSpec.CAPABILITIES_PREFS);
        c.addSubConnector("0", "", SubConnectorSpec.FEATURE_NONE);
        return c;
    }

    private final String getStringResource(final int resourceStringID) {
        return currentContext.getString(resourceStringID);
    }

    private final String getPhonenumber(final Intent intent) {
        final ConnectorCommand cc = new ConnectorCommand(intent);
        return Utils.getRecipientsNumber(cc.getRecipients()[0]);
    }

    private final String getMessageText(final Intent intent) {
        final ConnectorCommand cc = new ConnectorCommand(intent);
        return cc.getText();
    }

    private final void tryToLoadSmsPage() {
        StatusLine httpResult;
        httpResult = performHttpRequestForStatusLine(SMS_PAGE_URL);

        if (httpResult.getStatusCode() != HttpStatus.SC_OK)
            throw new WebSMSException(getStringResource(R.string.error_sms_page_result) + httpResult.toString());
    }

    private final boolean isSmsPageLoaded() {
        return currentHtmlResultPage.indexOf(SMS_PAGE_LOAD_SUCCESS_SUBSTRING) > -1;
    }

    private final void doLogin(final String user, final String password) {
        StatusLine httpResult;

        final String sessionInputValueLogin = extractSessionInputValue();
        final String postDestination = ROOT_PAGE_URL + extractRelativePostUrl();
        httpResult = performRawLogin(user, password, postDestination, sessionInputValueLogin);

        if (httpResult.getStatusCode() != HttpStatus.SC_OK)
            throw new WebSMSException(getStringResource(R.string.error_login_form_result) + httpResult.toString());

        if (wasLoginOrPasswordWrong())
            throw new WebSMSException(getStringResource(R.string.error_mail));

        if (!isSmsPageLoaded())
            throw new WebSMSException(getStringResource(R.string.error_login_sms_unknown));
    }

    private final void sendSms(String phonenumber, final String message) {
        phonenumber = Utils.international2oldformat(phonenumber);

        if (message.length() > 160)
            throw new WebSMSException(getStringResource(R.string.error_length));

        if (areSmsExiredForToday())
            throw new WebSMSException(getStringResource(R.string.error_sms_expired));

        final String sessionInputValueSms = extractSessionInputValue();
        StatusLine httpResult;
        httpResult = performRawSendSms(phonenumber, message, sessionInputValueSms);

        if (httpResult.getStatusCode() != HttpStatus.SC_OK)
            throw new WebSMSException(getStringResource(R.string.error_sms_form_result) + httpResult.toString());

        if (sendingFailed())
            throw new WebSMSException(getStringResource(R.string.error_sending_sms_unknown));
    }

    private final StatusLine performRawLogin(final String user, final String password,
            final String postDestinationUrl, final String sessionInputValueLogin) {
        final ArrayList<BasicNameValuePair> nameValuePairs = new ArrayList<BasicNameValuePair>();
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr$Login$Login_DNN$txtUsername", user));
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr$Login$Login_DNN$txtPassword", password));
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr$Login$Login_DNN$cmdLogin", "Login"));
        nameValuePairs.add(new BasicNameValuePair("__EVENTTARGET", ""));
        nameValuePairs.add(new BasicNameValuePair("__EVENTARGUMENT", ""));
        nameValuePairs.add(new BasicNameValuePair("__VIEWSTATE_CACHEKEY", sessionInputValueLogin));
        nameValuePairs.add(new BasicNameValuePair("__VIEWSTATE", ""));

        return performHttpRequestForStatusLine(postDestinationUrl, nameValuePairs);
    }

    private final StatusLine performRawSendSms(final String phonenumber, final String message,
            final String sessionInputValueSms) {
        final ArrayList<BasicNameValuePair> nameValuePairs = new ArrayList<BasicNameValuePair>();
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr493$SfiSms_View$phone", phonenumber));
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr493$SfiSms_View$subject", message));
        nameValuePairs.add(new BasicNameValuePair("dnn$ctr493$SfiSms_View$sendData", "Senden"));
        nameValuePairs.add(new BasicNameValuePair("__EVENTTARGET", ""));
        nameValuePairs.add(new BasicNameValuePair("__EVENTARGUMENT", ""));
        nameValuePairs.add(new BasicNameValuePair("__VIEWSTATE_CACHEKEY", sessionInputValueSms));
        nameValuePairs.add(new BasicNameValuePair("__VIEWSTATE", ""));

        return performHttpRequestForStatusLine(SMS_PAGE_URL, nameValuePairs);
    }

    private final StatusLine performHttpRequestForStatusLine(final String url) {
        return performHttpRequestForStatusLine(url, null);
    }

    private final StatusLine performHttpRequestForStatusLine(final String url,
            final ArrayList<BasicNameValuePair> postData) {
        try {
            final HttpResponse response = performHttpRequestForStatusLineForResponse(url, postData);
            final HttpEntity entity = response.getEntity();
            if (entity != null) {
                currentHtmlResultPage = EntityUtils.toString(entity, ENCODING);
                entity.consumeContent();
            }
            return response.getStatusLine();
        } catch (IOException e) {
            throw new WebSMSException(e.toString());
        }
    }

    private final HttpResponse performHttpRequestForStatusLineForResponse(final String url,
            final ArrayList<BasicNameValuePair> postData) throws IOException {
        try {
            return performHttpRequestForStatusLineUtils(url, postData);
        } catch (IOException e) {
            //HACK: This fails regularly with "SSL shutdown failed: I/O error during system call, Broken pipe",
            //      see https://issues.apache.org/jira/browse/HTTPCLIENT-951?focusedCommentId=12901563&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#action_12901563
            //      But in this case the second try usually works ...
            return performHttpRequestForStatusLineUtils(url, postData);
        }
    }

    private final HttpResponse performHttpRequestForStatusLineUtils(final String url,
            final ArrayList<BasicNameValuePair> postData) throws IOException {
        Utils.HttpOptions o = new Utils.HttpOptions(ENCODING);
        o.url = url;
        o.userAgent = USER_AGENT;
        o.referer = REFERER_URL;
        o.knownFingerprints = SSL_FINGERPRINTS;
        o.addFormParameter(postData);
        return Utils.getHttpClient(o);
    }

    private final String extractRelativePostUrl() {
        final Matcher matcher = postUrlExtractPattern.matcher(currentHtmlResultPage);
        if (matcher.matches())
            return matcher.group(1);
        throw new WebSMSException(getStringResource(R.string.error_no_post_url_found));
    }

    private final String extractSessionInputValue() {
        final Matcher matcher = sessionInputValueExtractPattern.matcher(currentHtmlResultPage);
        if (matcher.matches())
            return matcher.group(1);
        throw new WebSMSException(getStringResource(R.string.error_no_session_input_value_found));
    }

    private final boolean wasLoginOrPasswordWrong() {
        return currentHtmlResultPage.indexOf(WRONG_LOGIN_MESSAGE_SUBSTRING) > -1;
    }

    private final boolean areSmsExiredForToday() {
        return currentHtmlResultPage.indexOf(TODAYS_SMS_EXPIRED_SUBSTRING) > -1;
    }

    private final boolean sendingFailed() {
        return currentHtmlResultPage.indexOf(SEND_SUCCESS_SUBSTRING) == -1;
    }
}