Java tutorial
/* * Copyright 2012 Mike Niyonkuru * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.niyonkuru.koodroid.service; import android.app.IntentService; import android.app.backup.BackupManager; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.os.Bundle; import android.os.ResultReceiver; import android.text.format.DateUtils; import android.util.Log; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebView; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import net.niyonkuru.koodroid.App; import net.niyonkuru.koodroid.R; import net.niyonkuru.koodroid.html.ErrorHandler; import net.niyonkuru.koodroid.html.HtmlHandler.HandlerException; import net.niyonkuru.koodroid.html.LinksHandler; import net.niyonkuru.koodroid.html.SubscribersHandler; import net.niyonkuru.koodroid.provider.AccountContract.Subscribers; import net.niyonkuru.koodroid.provider.SettingsContract.Settings; import net.niyonkuru.koodroid.security.EasySSLSocketFactory; import net.niyonkuru.koodroid.util.Config; import net.niyonkuru.koodroid.util.IntentUtils; import net.niyonkuru.koodroid.util.NetworkUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import com.crittercism.app.Crittercism; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.NameValuePair; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.CookieStore; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ConnectTimeoutException; 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.cookie.Cookie; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.util.EntityUtils; import static net.niyonkuru.koodroid.BuildConfig.DEBUG; /** * Authenticates a username and password with the Koodo web server and sends back a result to the caller. */ public class SessionService extends IntentService { private static final String TAG = "SessionService"; public static final String LOGIN_FINISHED = "net.niyonkuru.koodroid.action.LOGIN_FINISHED"; public static final String EXTRA_REQUEST = "request"; public static final String EXTRA_EMAIL = "email"; public static final String EXTRA_PASSWORD = "password"; public static final String EXTRA_STATUS_RECEIVER = "statusReceiver"; public static final String EXTRA_BROADCAST = "broadcast"; public static final int REQUEST_LOGIN = 0x1; public static final int REQUEST_LOGOUT = 0x2; public static final int STATUS_RUNNING = 0x1; public static final int STATUS_ERROR = 0x2; public static final int STATUS_FINISHED = 0x3; private Settings mSettings; private DefaultHttpClient mHttpClient; private HttpPost mPostRequest; private final Thread mDisconnect; public SessionService() { super(TAG); /* do not make network requests on the UI Thread */ mDisconnect = new Thread(new Runnable() { @Override public void run() { if (mPostRequest != null) { mPostRequest.abort(); } if (mHttpClient != null) { mHttpClient.getConnectionManager().shutdown(); } } }); } @Override public void onCreate() { super.onCreate(); mSettings = App.getSettings(); mHttpClient = getHttpClient(this); setProvinceCookie(mHttpClient); } /** * Adds a province cookie to the {@link CookieStore} of the {@link DefaultHttpClient} object. This will stop the * Koodo Servers from prompting us with region selection on login. */ private static void setProvinceCookie(DefaultHttpClient httpClient) { CookieStore cookieStore = httpClient.getCookieStore(); BasicClientCookie cookie = new BasicClientCookie("prov", "on"); cookie.setDomain("." + Config.DOMAIN); cookie.setPath("/"); cookieStore.addCookie(cookie); } @Override public void onHandleIntent(Intent intent) { if (DEBUG) Log.d(TAG, "onHandleIntent(intent=" + intent.toString() + ")"); int request = intent.getIntExtra(EXTRA_REQUEST, REQUEST_LOGIN); if (request == REQUEST_LOGOUT) { logout(); return; } final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_STATUS_RECEIVER); final String email = intent.getStringExtra(EXTRA_EMAIL); final String password = intent.getStringExtra(EXTRA_PASSWORD); boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); /* totally ignore this request until full credentials are provided */ if (email == null || password == null) return; final long startLogin = System.currentTimeMillis(); final long lastLogin = mSettings.lastLogin(); /* if the last successful login is within the last 15 minutes */ if (startLogin - lastLogin <= DateUtils.MINUTE_IN_MILLIS * 15) { /* do a credentials check again the local data store */ if (email.equals(mSettings.email()) && password.equals(mSettings.password())) { if (broadcast) IntentUtils.callWidget(this, LOGIN_FINISHED); announce(receiver, STATUS_FINISHED); return; } } try { if (NetworkUtils.isConnected(this)) { /* announce to the caller that we are now running */ announce(receiver, STATUS_RUNNING); } else throw new ServiceException(getString(R.string.error_network_down)); if (mSettings.email() == null) { /* this is likely a new user */ Crittercism.leaveBreadcrumb(TAG + ": first_time_login"); } login(email, password); saveCookies(); if (DEBUG) Log.d(TAG, "login took " + (System.currentTimeMillis() - startLogin) + "ms"); } catch (IOException e) { if (DEBUG) Log.e(TAG, "Problem while logging in", e); /* if the exception was simply from an abort */ if (mPostRequest != null && mPostRequest.isAborted()) return; if (receiver != null) { // Pass back error to surface listener final Bundle bundle = new Bundle(); bundle.putString(Intent.EXTRA_TEXT, e.toString()); receiver.send(STATUS_ERROR, bundle); } return; /* do not announce success below */ } if (broadcast) IntentUtils.callWidget(this, LOGIN_FINISHED); announce(receiver, STATUS_FINISHED); } /** * Announce success to the available receiver */ private void announce(ResultReceiver receiver, int status) { if (receiver != null) { receiver.send(status, Bundle.EMPTY); } } @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy()"); super.onDestroy(); mDisconnect.start(); } private void login(String email, String password) throws IOException { try { mPostRequest = new HttpPost(new URI(Config.LOGIN_URL)); } catch (URISyntaxException e) { throw new ServiceException(e.getMessage()); } List<NameValuePair> formData = buildFormData(email, password, getString(R.string.locale)); try { /* fill the login request with form values */ mPostRequest.setEntity(new UrlEncodedFormEntity(formData, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new ServiceException(e.getMessage()); } Document doc; try { final HttpResponse response = mHttpClient.execute(mPostRequest); final Integer status = response.getStatusLine().getStatusCode(); /* scumbag server did not return a 200 code */ if (status != HttpStatus.SC_OK) throw new ServiceException(status.toString()); HttpEntity entity = response.getEntity(); doc = Jsoup.parse(EntityUtils.toString(response.getEntity())); if (entity != null) { entity.consumeContent(); } } catch (UnknownHostException e) { throw new ServiceException(e.getMessage()); } catch (ConnectTimeoutException e) { throw new ServiceException(getString(R.string.error_connection_timeout)); } catch (ClientProtocolException e) { throw new ServiceException(e.getMessage()); } catch (ParseException e) { throw new ServiceException(e.getMessage()); } catch (SocketTimeoutException e) { throw new ServiceException(getString(R.string.error_response_timeout)); } catch (IOException e) { // This could be caused by closing the httpclient connection manager throw new ServiceException(e.getMessage()); } final Resources resources = getResources(); final ContentResolver resolver = getContentResolver(); /* this is a new user logging in */ if (!email.equalsIgnoreCase(mSettings.email())) { /* clear old preferences */ resolver.delete(Settings.CONTENT_URI, null, null); } try { new SubscribersHandler(resources, email).parseAndApply(doc, resolver); new LinksHandler(resources).parseAndApply(doc, resolver); ContentValues values = new ContentValues(3); values.put(Settings.EMAIL, email); values.put(Settings.PASSWORD, password); values.put(Settings.LAST_LOGIN, System.currentTimeMillis()); resolver.insert(Settings.CONTENT_URI, values); new BackupManager(this).dataChanged(); } catch (HandlerException e) { /* check if these errors could be caused by invalid pages */ new ErrorHandler(resources).parseAndThrow(doc); throw e; } } private List<NameValuePair> buildFormData(String username, String password, String language) { List<NameValuePair> formData = new ArrayList<NameValuePair>(); formData.add(new BasicNameValuePair("IDToken1", username)); formData.add(new BasicNameValuePair("IDToken2", password)); formData.add(new BasicNameValuePair("service", "koodo")); formData.add(new BasicNameValuePair("realm", "koodo")); formData.add(new BasicNameValuePair("portal", "koodo")); formData.add(new BasicNameValuePair("locale", language)); formData.add(new BasicNameValuePair("encoded", "false")); formData.add(new BasicNameValuePair("goto", null)); return formData; } private void logout() { final ContentResolver cr = getContentResolver(); final String email = mSettings.email(); /* clear application settings */ cr.delete(Settings.CONTENT_URI, null, null); try { /* allow a 5 second window for batch operations to finish */ Thread.sleep(5 * DateUtils.SECOND_IN_MILLIS); } catch (InterruptedException ignored) { } /* clear database data */ cr.delete(Subscribers.CONTENT_URI, Subscribers.SUBSCRIBER_EMAIL + "='" + email + "'", null); CookieManager cookieManager = CookieManager.getInstance(); if (cookieManager != null) { cookieManager.removeAllCookie(); } stopSelf(); } /** * Save cookies for later use by a {@link WebView} */ private void saveCookies() { List<Cookie> cookies = mHttpClient.getCookieStore().getCookies(); if (cookies.isEmpty()) return; CookieSyncManager.createInstance(this); CookieManager cookieManager = CookieManager.getInstance(); for (Cookie cookie : cookies) { String url = (cookie.isSecure() ? "https://" : "http://") + Config.DOMAIN + cookie.getPath(); String value = cookie.getName() + "=" + cookie.getValue(); if (cookie.getDomain().startsWith(".")) { value += "; domain=" + cookie.getDomain(); } cookieManager.setCookie(url, value); } } /** * Create a DefaultHttpClient Object with several parameters set * * @return a new DefaultHttpClient Object */ private static DefaultHttpClient getHttpClient(Context context) { HttpParams params = new BasicHttpParams(); // Use generous timeouts for slow mobile networks int timeout = (int) (25 * DateUtils.SECOND_IN_MILLIS); HttpConnectionParams.setConnectionTimeout(params, timeout); HttpConnectionParams.setSoTimeout(params, timeout); /* Inexplicably speeds up POST requests? */ HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setUserAgent(params, buildUserAgent(context)); HttpConnectionParams.setSocketBufferSize(params, 8192); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("https", new EasySSLSocketFactory(), 443)); ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); return new DefaultHttpClient(cm, params); } /** * Build and return a user-agent string that can identify this application to remote servers. Contains the package * name and version code. */ private static String buildUserAgent(Context context) { try { final PackageManager manager = context.getPackageManager(); final PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); return info.packageName + "/" + info.versionName + " (" + info.versionCode + ")"; } catch (NameNotFoundException e) { return null; } } }