/* * Copyright (C) 2009 University of Washington * * 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 com.radicaldynamic.groupinform.services; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.http.NameValuePair; import org.apache.http.client.CookieStore; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.message.BasicNameValuePair; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Binder; import android.os.ConditionVariable; import android.os.IBinder; import android.util.Log; import com.radicaldynamic.groupinform.activities.AccountDeviceList; import com.radicaldynamic.groupinform.activities.AccountFolderList; import com.radicaldynamic.groupinform.application.Collect; import com.radicaldynamic.groupinform.logic.AccountDevice; import com.radicaldynamic.groupinform.logic.InformOnlineSession; import com.radicaldynamic.groupinform.logic.InformOnlineState; import com.radicaldynamic.groupinform.utilities.HttpUtils; import com.radicaldynamic.groupinform.utilities.FileUtilsExtended; /** * */ public class InformOnlineService extends Service { private static final String t = "InformOnlineService: "; // Whether or not we are currently attempting to connect to the service private boolean mConnecting = false; // Assume that we are not initialized when this service starts private boolean mInitialized = false; // Assume that the "online service" is not answering when we start private boolean mServicePingSuccessful = false; // Assume that we are not signed in when this service starts private boolean mSignedIn = false; // This is the object that receives interactions from clients. // See RemoteService for a more complete example. private final IBinder mBinder = new LocalBinder(); private ConditionVariable mCondition; private Runnable mTask = new Runnable() { public void run() { for (int i = 1; i > 0; ++i) { if (mConnecting == false) connect(false); // Retry connection to Inform Online service every 10 minutes if (mCondition.block(600 * 1000)) break; } } }; @Override public void onCreate() { // Do some basic initialization for this service Collect.getInstance().setInformOnlineState(new InformOnlineState(getApplicationContext())); restoreSession(); // connect() may not be run because of offline mode but metadata should still be loaded if available if (Collect.getInstance().getInformOnlineState().isOfflineModeEnabled()) { AccountDeviceList.loadDeviceList(); AccountFolderList.loadFolderList(); } Thread persistentConnectionThread = new Thread(null, mTask, "InformOnlineService"); mCondition = new ConditionVariable(false); persistentConnectionThread.start(); } /* * (non-Javadoc) * @see android.app.Service#onDestroy() * * Perform cleanup here. Note that we need to effectively reset the state of this service here * because there is no guarantee that the object will be destroyed before the app resumes (and * then assumes that the state being reported is accurate). */ @Override public void onDestroy() { if (isInitialized()) { serializeSession(); reinitializeService(); } mCondition.open(); } /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ public class LocalBinder extends Binder { public InformOnlineService getService() { return InformOnlineService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "received start ID " + startId + ": " + intent); return START_STICKY; } // Triggered by UI button when the user wants to manually switch to OFFLINE mode public boolean goOffline() { if (checkout()) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "went offline at users request"); Collect.getInstance().getInformOnlineState().setOfflineModeEnabled(true); return true; } else { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "unable to go offline at users request"); return false; } } // Triggered by UI button when the user wants to force online mode public boolean goOnline() { // Force online (but only if a connection attempt is not already underway) if (!mConnecting) connect(true); if (isSignedIn()) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "went online at users request"); Collect.getInstance().getInformOnlineState().setOfflineModeEnabled(false); return true; } else { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "unable to go online at users request"); return false; } } // connect() has been run at least once public boolean isInitialized() { return mInitialized; } /* * Application is ready for regular operation & user interaction. * * This does not mean that we were able to ping the service or * that we are signed in. */ public boolean isReady() { return isInitialized() && isRegistered(); } // Registration info for this device is stored in the app's shared preferences public boolean isRegistered() { return Collect.getInstance().getInformOnlineState().hasRegistration(); } // Is Inform Online available? public boolean isRespondingToPings() { return mServicePingSuccessful; } // Registered (implied) connected & signed in public boolean isSignedIn() { return mSignedIn; } // Bring the service back to the defaults it would have had when originally started public void reinitializeService() { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "service reinitialized"); mInitialized = mServicePingSuccessful = mSignedIn = false; } /* * Sign in to the Inform Online service * Only called by connect() */ private boolean checkin() { // Assume we are registered unless told otherwise boolean registered = true; List<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("deviceId", Collect.getInstance().getInformOnlineState().getDeviceId())); params.add(new BasicNameValuePair("deviceKey", Collect.getInstance().getInformOnlineState().getDeviceKey())); params.add(new BasicNameValuePair("fingerprint", Collect.getInstance().getInformOnlineState().getDeviceFingerprint())); try { params.add(new BasicNameValuePair("lastCheckinWith", this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName)); } catch (NameNotFoundException e1) { params.add(new BasicNameValuePair("lastCheckinWith", "unknown")); } String checkinUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/checkin"; String postResult = HttpUtils.postUrlData(checkinUrl, params); JSONObject checkin; try { checkin = (JSONObject) new JSONTokener(postResult).nextValue(); String result = checkin.optString(InformOnlineState.RESULT, InformOnlineState.FAILURE); if (result.equals(InformOnlineState.OK)) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "successful checkin"); Collect.getInstance().getInformOnlineState().setExpired(false); // Update device role -- it might have changed Collect.getInstance().getInformOnlineState().setDeviceRole(checkin.optString("role", AccountDevice.ROLE_UNASSIGNED)); } else if (result.equals(InformOnlineState.EXPIRED)) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "associated order is expired; marking device as expired"); Collect.getInstance().getInformOnlineState().setExpired(true); } else if (result.equals(InformOnlineState.FAILURE)) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "checkin unsuccessful"); registered = false; } else { // Something bad happened if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "system error while processing postResult"); } } catch (NullPointerException e) { // Communication error if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "no postResult to parse. Communication error with node.js server?"); e.printStackTrace(); } catch (JSONException e) { // Parse error (malformed result) if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "failed to parse postResult " + postResult); e.printStackTrace(); } // Clear the session for subsequent requests and reset stored state if (registered == false) Collect.getInstance().getInformOnlineState().resetDevice(); return registered; } /* * Try and say "goodbye" to Inform Online so that we know that this client's session is no longer needed. * * Checkouts are the result of a manual process and as such they will knock us * offline until the user manually puts us back into the online state. */ private boolean checkout() { boolean saidGoodbye = false; String checkoutUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/checkout"; String getResult = HttpUtils.getUrlData(checkoutUrl); JSONObject checkout; try { checkout = (JSONObject) new JSONTokener(getResult).nextValue(); String result = checkout.optString(InformOnlineState.RESULT, InformOnlineState.ERROR); if (result.equals(InformOnlineState.OK)) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "said goodbye to Group Complete"); } else { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "device checkout unnecessary"); } saidGoodbye = true; } catch (NullPointerException e) { // Communication error if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "no getResult to parse. Communication error with node.js server?"); e.printStackTrace(); } catch (JSONException e) { // Parse error (malformed result) if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "failed to parse getResult " + getResult); e.printStackTrace(); } finally { // Running a checkout ALWAYS "signs us out" mSignedIn = false; } Collect.getInstance().getInformOnlineState().setSession(null); return saidGoodbye; } /* * Connect to the Inform Online service and if registered, attempt to sign in */ private void connect(boolean forceOnline) { mConnecting = true; // Make sure that the user has not specifically requested that we be offline if (Collect.getInstance().getInformOnlineState().isOfflineModeEnabled() && forceOnline == false) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "offline mode enabled; not auto-connecting"); /* * This is not a complete initialization (in the sense that we attempted connection) but we need * to pretend that it is so that the UI can move forward to whatever state is most suitable. */ mInitialized = true; mConnecting = false; return; } if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "pinging " + Collect.getInstance().getInformOnlineState().getServerUrl()); // Try to ping the service to see if it is "up" (and determine whether we are registered) String pingUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/ping"; String getResult = HttpUtils.getUrlData(pingUrl); JSONObject ping; try { ping = (JSONObject) new JSONTokener(getResult).nextValue(); String result = ping.optString(InformOnlineState.RESULT, InformOnlineState.ERROR); // Online and registered (checked in) if (result.equals(InformOnlineState.OK)) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "ping successful (we are connected and checked in)"); mServicePingSuccessful = mSignedIn = true; } else if (result.equals(InformOnlineState.FAILURE)) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "ping successful but not signed in (will attempt checkin)"); mServicePingSuccessful = true; if (Collect.getInstance().getInformOnlineState().hasRegistration() && checkin()) { if (Collect.Log.INFO) Log.i(Collect.LOGTAG, t + "checkin successful (we are connected)"); // Fetch regardless of the fact that we're not yet marked as being signed in AccountDeviceList.fetchDeviceList(true); AccountFolderList.fetchFolderList(true); mSignedIn = true; } else { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "checkin failed (registration invalid)"); mSignedIn = false; } } else { // Assume offline if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "ping failed (we are offline)"); mSignedIn = false; } } catch (NullPointerException e) { // This usually indicates a communication error and will send us into an offline state if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "ping error while communicating with service (we are offline)"); e.printStackTrace(); mServicePingSuccessful = mSignedIn = false; } catch (JSONException e) { // Parse errors (malformed result) send us into an offline state if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "ping error while parsing getResult " + getResult + " (we are offline)"); e.printStackTrace(); mServicePingSuccessful = mSignedIn = false; } finally { // Load regardless of whether we are signed in AccountDeviceList.loadDeviceList(); AccountFolderList.loadFolderList(); // Unblock mInitialized = true; mConnecting = false; } } /* * Restore a serialized session from disk */ private void restoreSession() { // Restore any serialized session information File sessionCache = new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE); if (sessionCache.exists()) { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "restoring cached session"); try { InformOnlineSession session = new InformOnlineSession(); FileInputStream fis = new FileInputStream(sessionCache); ObjectInputStream ois = new ObjectInputStream(fis); session = (InformOnlineSession) ois.readObject(); ois.close(); fis.close(); Collect.getInstance().getInformOnlineState().setSession(new BasicCookieStore()); Iterator<InformOnlineSession> cookies = session.getCookies().iterator(); if (cookies.hasNext()) { InformOnlineSession ios = cookies.next(); BasicClientCookie bcc = new BasicClientCookie(ios.getName(), ios.getValue()); bcc.setDomain(ios.getDomain()); bcc.setExpiryDate(ios.getExpiryDate()); bcc.setPath(ios.getPath()); bcc.setVersion(ios.getVersion()); Collect.getInstance().getInformOnlineState().getSession().addCookie(bcc); } } catch (Exception e) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "problem restoring cached session " + e.toString()); e.printStackTrace(); // Don't leave a broken file hanging new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE).delete(); // Clear the session Collect.getInstance().getInformOnlineState().setSession(null); } } else { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "no session to restore"); } } /* * Serialize the current session to disk */ private void serializeSession() { // Attempt to serialize the session for later use if (Collect.getInstance().getInformOnlineState().getSession() instanceof CookieStore) { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "serializing session"); try { InformOnlineSession session = new InformOnlineSession(); Iterator<Cookie> cookies = Collect.getInstance().getInformOnlineState().getSession().getCookies().iterator(); while (cookies.hasNext()) { Cookie c = cookies.next(); session.getCookies().add(new InformOnlineSession( c.getDomain(), c.getExpiryDate(), c.getName(), c.getPath(), c.getValue(), c.getVersion() )); } FileOutputStream fos = new FileOutputStream(new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE)); ObjectOutputStream out = new ObjectOutputStream(fos); out.writeObject(session); out.close(); fos.close(); } catch (Exception e) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "problem serializing session " + e.toString()); e.printStackTrace(); // Make sure that we don't leave a broken file hanging new File(getCacheDir(), FileUtilsExtended.SESSION_CACHE_FILE).delete(); } } else { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "no session to serialize"); } } }