/* * Copyright 2012 The Stanford MobiSocial Laboratory * * 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 mobisocial.musubi.service; import mobisocial.musubi.App; import mobisocial.musubi.R; import mobisocial.musubi.identity.AphidIdentityProvider; import mobisocial.musubi.identity.IdentityProvider; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.TestSettingsProvider.Settings; import mobisocial.musubi.service.MessageEncodeProcessor.ProcessorThread; import mobisocial.musubi.syncadapter.SyncService; import org.mobisocial.corral.ContentCorral; import android.app.AlarmManager; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; import android.provider.ContactsContract; import android.widget.Toast; /** * <p>A persistent service for managing Musubi's object processing subsystem. * * <p>The MusubiService manages a set of services that manage objects as * they are sent/received from the network. These services are implemented * as Handlers running on their own HandlerThreads. * * <p>We refer to the 'outer' data model as messages and the 'inner' data model * as objects. An object is made available to applications only after it has * been processed, and processing may only occur once an object and its encoding * are both available. * * <p>The flow of data is as follows: * <ul> * <li>Send: insert object => encode message => process object & send message * <li>Receive: insert message => decode message => process object * </ul> * * <p>The service architecture in more details: * <pre> * Frontend sends obj: * -- ensure feed exists * -- insert into objects with processed=0 * notify PLAIN_OBJ_READY * * Frontend discovers new identity (eg, address book updated) * -- ensure idHash exists in identities * notify IDENTITY_AVAILABLE * if (owned) notify IDENTITY_OWNED * * Frontend talks to network: * listen on IDENTITY_OWNED * loop over network available * -- insert into encoded * notify ENCODED_RECEIVED * * MessageEncodeHandler: * listen on PLAIN_OBJ_READY * loop over objects.encoded_id is null * -- ensure feed exists * -- insert into encoded * -- set object.encoded_id, object.universal_hash * notify PREPARED_ENCODED * notify APP_OBJ_READY * * MessageDecodeHandler: * listen on ENCODED_RECEIVED * loop over encoded.processed=0 and encoded.outbound=0 * -- look for identity profile updates * -- verify grey/whitelist * -- verify feed exists, add user * -- insert into objects * -- set encoded.processed=1 * -- set object.encoded_id, object.universal_hash * notify APP_OBJ_READY * * ObjPipelineHandler: * listen on APP_OBJ_READY, IDENTITY_AVAILABLE * loop on objects.processed=0 (and objects.encoded != null???) TODO * -- assert (short(univ_hash) = short_univ_hash) * -- triggers ObjHandlers (render caches, user notifications, * -- sets objects.processed=1 * notify itemUri(objects, oid), itemUri(feeds, fid) * * AMQPService: * listen on PREPARED_ENCODED * loop over encoded.processed=0 AND encoded.outbound=1 * -- delivers to network (w/ ack) * -- sets encoded.processed=1 * </pre> */ public class MusubiService extends Service { static final boolean DBG = true; public static final String TAG = "MusubiService"; private NotificationManager mNotificationManager; private SQLiteOpenHelper mDatabaseSource; private ContentCorral mContentCorral; MessageEncodeProcessor mMessageEncodeProcessor; MessageDecodeProcessor mMessageDecodeProcessor; ObjPipelineProcessor mPipelineProcessor; ProfilePushProcessor mProfilePushProcessor; CorralUploadProcessor mCorralUploadProcessor; WizardStepHandler mWizardStepHandler; KeyUpdateHandler mKeyUpdateHandler; FacebookUpdateHandler mFacebookUpdateHandler; AddressBookUpdateHandler mAddressBookUpdateHandler; IdentityProvider mIdentityProvider; /** * Called when a new object has been added by an application on this device. */ public static final Uri PLAIN_OBJ_READY = MusubiContentProvider.createUri("send-obj"); /** * Called when a a previously unencoded object has been encoded. * @See MessageEncodeProcessor */ public static final Uri PREPARED_ENCODED = MusubiContentProvider.createUri("send-encoded-obj"); /** * Called when an encoded object has been received from a remote device. * @See {@link MessageDecodeProcessor} */ public static final Uri ENCODED_RECEIVED = MusubiContentProvider.createUri("rec-encoded-obj"); /** * Called when an encoded object received from another application has been decoded. */ public static final Uri APP_OBJ_READY = MusubiContentProvider.createUri("obj-available"); /** * Called when an owned identity has been added to the database. */ public static final Uri OWNED_IDENTITY_AVAILABLE = MusubiContentProvider.createUri("owned-id-available"); /** * Called when one of this device's account's profile has been updated. * @see ProfilePushProcessor */ public static final Uri MY_PROFILE_UPDATED = MusubiContentProvider.createUri("my-profile-updated"); /** * Called when a feed has been updated. */ public static final Uri FEED_UPDATED = MusubiContentProvider.createUri("feed-updated"); /** * Called when a contact has been added to the local address book. * @See ProfilePushProcessor */ public static final Uri WHITELIST_APPENDED = MusubiContentProvider.createUri("whitelist-appended"); /** * Called when a contact has some renderable metadata changed. * @See ProfilePushProcessor */ public static final Uri PRIMARY_CONTENT_CHANGED = MusubiContentProvider.createUri("profile-updated"); /** * Called when a whitelist/blacklist/graylist changes other than a whitelist append * @See ProfilePushProcessor */ public static final Uri COLORLIST_CHANGED = MusubiContentProvider.createUri("colorlist-change"); /** * Triggered when a user has explicitly requested a profile sync. */ public static final Uri PROFILE_SYNC_REQUESTED = MusubiContentProvider.createUri("profile-sync-requested"); /** * Called when the user has completed a step in the wizard. * @see WizardStepHandler */ public static final Uri WIZARD_STEP_TAKEN = MusubiContentProvider.createUri("wizard-step-taken"); /** * Called when a fresh token has been associated with one of this device's accounts. */ public static final Uri AUTH_TOKEN_REFRESH = MusubiContentProvider.createUri("token-refresh"); /** * Called when a new facebook is linked and periodically updated */ public static final Uri FACEBOOK_FRIEND_REFRESH = MusubiContentProvider.createUri("facebook-friend-refresh"); /** * Called when network status changed. Should reset any exponential backoffs. */ public static final Uri NETWORK_CHANGED = MusubiContentProvider.createUri("network-changed"); /** * Called when network status changed. Should reset any exponential backoffs. */ public static final Uri USER_ACTIVITY_RESUME = MusubiContentProvider.createUri("user-activity-resume"); /** * Called a new identity has been inserted by a user action. This means we should ignore any batching we normally attempt to do. */ public static final Uri FORCE_RESCAN_CONTACTS = MusubiContentProvider.createUri("rescan-contacts"); /** * Called when we need to force a profile object to be sent to a new friend we discovered. */ public static final Uri FORCE_PROFILE_PUSH = MusubiContentProvider.createUri("force-profile-push"); /** * Called when the address book scanner has completed a pass. */ public static final Uri ADDRESS_BOOK_SCANNED = MusubiContentProvider.createUri("address-book-scanned"); public static final Uri REQUEST_ADDRESS_BOOK_SCAN = MusubiContentProvider.createUri("scan-address-book"); /** * Called once the wizard is known to have sent its first message. */ public static final Uri WIZARD_READY = MusubiContentProvider.createUri("wizard-ready"); /** * Called when we need to trigger the app manifests to update (for now once on start) */ public static final Uri UPDATE_APP_MANIFESTS = MusubiContentProvider.createUri("update-app-manifests"); /** * Notifies the uploader service that content is available. */ public static final Uri UPLOAD_AVAILABLE = MusubiContentProvider.createUri("upload-available"); public static final Uri DOWNLOAD_REQUESTED = MusubiContentProvider.createUri("download-request"); public static final String EXTRA_OBSERVER = "mobisocial.musubi.service.OBSERVER"; @Override public void onCreate() { mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); mDatabaseSource = App.getDatabaseSource(this); new InitializeServiceTask().execute(); } @Override public void onDestroy() { //happens in testing only AFAIK. if(mNotificationManager != null) mNotificationManager.cancel(R.string.active); Toast.makeText(this, R.string.stopping, Toast.LENGTH_SHORT).show(); } public class MusubiServiceBinder extends Binder { public MusubiService getService() { return MusubiService.this; } } private final IBinder mBinder = new MusubiServiceBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } class InitializeServiceTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { Context context = MusubiService.this; ContentResolver resolver = getContentResolver(); Settings test_settings = App.getTestSettings(context); // TODO: content corral should manage it's own ip ups and downs. // XXX: Pre-ICS, BluetoothAdapter.getDefaultAdapter() must be // called on a thread with a looper. // Starting corral deferred until postExecute. mContentCorral = new ContentCorral(context); //we share the identity provider because the initialization of the ib signature and //encryption scheme take a quite some time if (test_settings != null && test_settings.mAlternateIdentityProvider != null) { mIdentityProvider = test_settings.mAlternateIdentityProvider; } else { mIdentityProvider = new AphidIdentityProvider(context); } /* * Looping observer services. Each looper runs on its own thread. */ mKeyUpdateHandler = KeyUpdateHandler.newInstance(context, mDatabaseSource, mIdentityProvider); resolver.registerContentObserver(AUTH_TOKEN_REFRESH, true, mKeyUpdateHandler); mFacebookUpdateHandler = FacebookUpdateHandler.newInstance(context, mDatabaseSource); resolver.registerContentObserver(FACEBOOK_FRIEND_REFRESH, true, mFacebookUpdateHandler); mMessageEncodeProcessor = MessageEncodeProcessor.newInstance(context, mDatabaseSource, mKeyUpdateHandler, mIdentityProvider); resolver.registerContentObserver(PLAIN_OBJ_READY, false, mMessageEncodeProcessor); mMessageDecodeProcessor = MessageDecodeProcessor.newInstance(context, mDatabaseSource, mKeyUpdateHandler, mIdentityProvider); resolver.registerContentObserver(ENCODED_RECEIVED, false, mMessageDecodeProcessor); mPipelineProcessor = ObjPipelineProcessor.newInstance(context); resolver.registerContentObserver(APP_OBJ_READY, false, mPipelineProcessor); resolver.registerContentObserver(OWNED_IDENTITY_AVAILABLE, false, mPipelineProcessor); mProfilePushProcessor = ProfilePushProcessor.newInstance(context, mDatabaseSource); resolver.registerContentObserver(OWNED_IDENTITY_AVAILABLE, false, mProfilePushProcessor); resolver.registerContentObserver(WHITELIST_APPENDED, false, mProfilePushProcessor); resolver.registerContentObserver(PROFILE_SYNC_REQUESTED, false, mProfilePushProcessor); resolver.registerContentObserver(MY_PROFILE_UPDATED, false, mProfilePushProcessor.getProfileUpdateObserver()); //run it when we start in case we crashed have pending work to do mProfilePushProcessor.dispatchChange(false); mCorralUploadProcessor = CorralUploadProcessor.newInstance(context, mDatabaseSource); resolver.registerContentObserver(UPLOAD_AVAILABLE, false, mCorralUploadProcessor); mWizardStepHandler = WizardStepHandler.newInstance(context, mDatabaseSource); resolver.registerContentObserver(WIZARD_STEP_TAKEN, true, mWizardStepHandler); if (test_settings == null || test_settings.mShouldDisableAddressBookSync == false) { mAddressBookUpdateHandler = AddressBookUpdateHandler.newInstance(context, mDatabaseSource, resolver); resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, mAddressBookUpdateHandler); resolver.registerContentObserver(REQUEST_ADDRESS_BOOK_SCAN, true, mAddressBookUpdateHandler); } /* * The tokens of each identity should be refreshed periodically. * The user's list of Facebook friends should also be refreshed periodically */ AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); long currentTime = System.currentTimeMillis(); final long TWENTY_MINUTES = 1000*60*20; // Key refresh alarm (daily) final Intent refreshAuthTokenIntent = new Intent(MusubiIntentService.ACTION_AUTH_TOKEN_REFRESH); refreshAuthTokenIntent.setClass(context, MusubiIntentService.class); PendingIntent keyAlarmSender = PendingIntent.getService(context, 0, refreshAuthTokenIntent, 0); am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentTime + AlarmManager.INTERVAL_DAY, AlarmManager.INTERVAL_DAY, keyAlarmSender); // Facebook friend refresh alarm (daily) final Intent fbRefreshIntent = new Intent(MusubiIntentService.ACTION_FACEBOOK_REFRESH); fbRefreshIntent.setClass(context, MusubiIntentService.class); PendingIntent fbAlarmSender = PendingIntent.getService(context, 0, fbRefreshIntent, 0); am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentTime + AlarmManager.INTERVAL_DAY + TWENTY_MINUTES, AlarmManager.INTERVAL_DAY, fbAlarmSender); final Intent rdIntent = new Intent(MusubiIntentService.ACTION_ROLLING_DELETE); rdIntent.setClass(context, MusubiIntentService.class); PendingIntent rdAlarmSender = PendingIntent .getService(context, 0, rdIntent, 0); am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentTime + AlarmManager.INTERVAL_DAY + (TWENTY_MINUTES*2), AlarmManager.INTERVAL_DAY, rdAlarmSender); getContentResolver().notifyChange(MusubiService.WIZARD_STEP_TAKEN, null); Handler process_starter = new Handler(getMainLooper()); final int ms = 1000; int ticks = 0; process_starter.postDelayed(new Runnable() { @Override public void run() { startService(refreshAuthTokenIntent); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mMessageDecodeProcessor.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mMessageEncodeProcessor.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mPipelineProcessor.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mAddressBookUpdateHandler.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mProfilePushProcessor.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { startService(fbRefreshIntent); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { mCorralUploadProcessor.dispatchChange(false); } }, ++ticks * ms); process_starter.postDelayed(new Runnable() { @Override public void run() { startService(rdIntent); } }, ++ticks * ms); Intent amqp_intent = new Intent(context, AMQPService.class); startService(amqp_intent); Intent app_update_intent = new Intent(context, AppUpdaterService.class); startService(app_update_intent); Intent sync_intent = new Intent(context, SyncService.class); startService(sync_intent); WebRenderService.bindAndSaveService(context); return null; } @Override protected void onPostExecute(Void result) { if (mContentCorral != null) { mContentCorral.start(); } } } public void shutdownThreads() { if(mMessageEncodeProcessor != null && mMessageEncodeProcessor.mThread != null) { mMessageEncodeProcessor.mThread.getLooper().quit(); try { mMessageEncodeProcessor.mThread.join(); } catch (InterruptedException e) {} for (ProcessorThread t : mMessageEncodeProcessor.mProcessorThreads) { t.mLooper.quit(); try { t.join(); } catch (InterruptedException e) {} } } if(mMessageDecodeProcessor != null && mMessageDecodeProcessor.mThread != null) { mMessageDecodeProcessor.mThread.getLooper().quit(); try { mMessageDecodeProcessor.mThread.join(); } catch (InterruptedException e) {} } if(mPipelineProcessor != null && mPipelineProcessor.mThread != null) { mPipelineProcessor.mThread.getLooper().quit(); try { mPipelineProcessor.mThread.join(); } catch (InterruptedException e) {} } if(mProfilePushProcessor != null && mProfilePushProcessor.mThread != null) { mProfilePushProcessor.mThread.getLooper().quit(); try { mProfilePushProcessor.mThread.join(); } catch (InterruptedException e) {} } if (mCorralUploadProcessor != null && mCorralUploadProcessor.mThread != null) { mCorralUploadProcessor.mThread.getLooper().quit(); try { mCorralUploadProcessor.mThread.join(); } catch (InterruptedException e) {} } if(mWizardStepHandler != null && mWizardStepHandler.mThread != null) { mWizardStepHandler.mThread.getLooper().quit(); try { mWizardStepHandler.mThread.join(); } catch (InterruptedException e) {} } if(mKeyUpdateHandler != null && mKeyUpdateHandler.mThread != null) { mKeyUpdateHandler.mThread.getLooper().quit(); try { mKeyUpdateHandler.mThread.join(); } catch (InterruptedException e) {} } if(mAddressBookUpdateHandler != null && mAddressBookUpdateHandler.mThread != null) { mAddressBookUpdateHandler.mThread.getLooper().quit(); try { mAddressBookUpdateHandler.mThread.join(); } catch (InterruptedException e) {} } } }