/** @author [email protected] */ public class TimelineCursorLoader1 extends Loader<Cursor> implements MyServiceEventsListener { private final TimelineListParameters mParams; private Cursor mCursor = null; private long instanceId = InstanceId.next(); private MyServiceEventsReceiver serviceConnector; private final Object asyncLoaderLock = new Object(); @GuardedBy("asyncLoaderLock") private AsyncLoader asyncLoader = null; public TimelineCursorLoader1(TimelineListParameters params) { super(MyContextHolder.get().context()); this.mParams = params; serviceConnector = new MyServiceEventsReceiver(this); } @Override protected void onStartLoading() { final String method = "onStartLoading"; logV(method, getParams()); serviceConnector.registerReceiver(getContext()); if (mayReuseResult()) { logV(method, "reusing result"); deliverResultsAndClean(mCursor); } else if (getParams().mReQuery || taskIsNotRunning()) { restartLoader(); } } private void logV(String method, Object obj) { if (MyLog.isVerboseEnabled()) { String message = (obj != null) ? obj.toString() : ""; MyLog.v(this, String.valueOf(instanceId) + " " + method + "; " + message); } } private boolean taskIsNotRunning() { boolean isNotRunning = true; synchronized (asyncLoaderLock) { if (asyncLoader != null) { isNotRunning = (asyncLoader.getStatus() != Status.RUNNING); } } return isNotRunning; } private boolean mayReuseResult() { boolean ok = false; if (!getParams().mReQuery && !takeContentChanged() && mCursor != null && !mCursor.isClosed()) { synchronized (asyncLoaderLock) { if (asyncLoader == null) { ok = true; } } } return ok; } private void restartLoader() { final String method = "restartLoader"; boolean ended = false; synchronized (asyncLoaderLock) { if (MyLog.isVerboseEnabled() && asyncLoader != null) { logV(method, "status:" + getAsyncLoaderStatus()); } if (cancelAsyncTask(method)) { try { asyncLoader = new AsyncLoader(); asyncLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } catch (Exception e) { logD(method, "", e); ended = true; asyncLoader = null; } } } if (ended) { logV(method, "deliver null as result"); deliverResultsAndClean(null); } } private void logD(String method, String message, Throwable tr) { MyLog.d(this, String.valueOf(instanceId) + " " + method + "; " + message, tr); } private void deliverResultsAndClean(Cursor cursor) { Cursor cursorPrev = null; try { if (this.mCursor != cursor) { cursorPrev = this.mCursor; this.mCursor = cursor; } if (getParams().cancelled || cursor == null) { deliverCancellation(); } else { deliverResult(cursor); } } finally { DbUtils.closeSilently(cursorPrev, "asyncLoaderEnded"); synchronized (asyncLoaderLock) { asyncLoader = null; } } } private boolean cancelAsyncTask(String callerMethod) { boolean cancelled = false; synchronized (asyncLoaderLock) { if (MyLog.isVerboseEnabled() && asyncLoader != null) { logV(callerMethod + "-cancelAsyncTask", "status:" + getAsyncLoaderStatus()); } if (asyncLoader != null && asyncLoader.getStatus() == Status.RUNNING) { if (asyncLoader.cancel(true)) { logV(callerMethod, "task cancelled"); } else { logV(callerMethod, "couldn't cancel task"); } } asyncLoader = null; cancelled = true; } return cancelled; } private String getAsyncLoaderStatus() { String status = "null"; synchronized (asyncLoaderLock) { if (asyncLoader != null) { status = asyncLoader.getStatus().name(); } } return status; } @Override protected void onStopLoading() { cancelAsyncTask("onStopLoading"); } @Override protected boolean onCancelLoad() { cancelAsyncTask("onCancelLoad"); return true; } private static final long MIN_LIST_REQUERY_MILLISECONDS = 3000; private long previousRequeryTime = 0; @Override protected void onForceLoad() { if (isStarted() && System.currentTimeMillis() - previousRequeryTime > MIN_LIST_REQUERY_MILLISECONDS) { previousRequeryTime = System.currentTimeMillis(); getParams().mReQuery = true; onStartLoading(); } } @Override protected void onReset() { serviceConnector.unregisterReceiver(getContext()); disposeResult(); cancelAsyncTask("onReset"); } private void disposeResult() { DbUtils.closeSilently(mCursor, "disposeResult"); mCursor = null; } /** @author [email protected] */ private class AsyncLoader extends AsyncTask<Void, Void, Cursor> { @Override protected Cursor doInBackground(Void... voidParams) { markStart(); prepareQueryInBackground(); Cursor cursor = queryDatabase(); checkIfReloadIsNeeded(cursor); return cursor; } private void markStart() { getParams().startTime = System.nanoTime(); getParams().cancelled = false; getParams().timelineToReload = TimelineType.UNKNOWN; if (MyLog.isVerboseEnabled()) { logV( "markStart", (TextUtils.isEmpty(getParams().mSearchQuery) ? "" : "queryString=\"" + getParams().mSearchQuery + "\"; ") + getParams().mTimelineType + "; isCombined=" + (getParams().mTimelineCombined ? "yes" : "no")); } } private void prepareQueryInBackground() { if (getParams().mLastItemSentDate > 0) { getParams() .mSa .addSelection( ProjectionMap.MSG_TABLE_ALIAS + "." + MyDatabase.Msg.SENT_DATE + " >= ?", new String[] {String.valueOf(getParams().mLastItemSentDate)}); } } private Cursor queryDatabase() { final String method = "queryDatabase"; Cursor cursor = null; for (int attempt = 0; attempt < 3 && !isCancelled(); attempt++) { try { cursor = MyContextHolder.get() .context() .getContentResolver() .query( getParams().mContentUri, getParams().mProjection, getParams().mSa.selection, getParams().mSa.selectionArgs, getParams().mSortOrder); break; } catch (IllegalStateException e) { logD(method, "Attempt " + attempt + " to prepare cursor", e); DbUtils.closeSilently(cursor); try { Thread.sleep(500); } catch (InterruptedException e2) { logD(method, "Attempt " + attempt + " to prepare cursor was interrupted", e2); break; } } } return cursor; } private void checkIfReloadIsNeeded(Cursor cursor) { if (!getParams().mLoadOneMorePage && TextUtils.isEmpty(getParams().mSearchQuery) && cursor != null && !cursor.isClosed() && cursor.getCount() == 0) { switch (getParams().mTimelineType) { case USER: // This timeline doesn't update automatically so let's do it now if necessary LatestTimelineItem latestTimelineItem = new LatestTimelineItem(getParams().mTimelineType, getParams().mSelectedUserId); if (latestTimelineItem.isTimeToAutoUpdate()) { getParams().timelineToReload = getParams().mTimelineType; } break; case FOLLOWING_USER: // This timeline doesn't update automatically so let's do it now if necessary latestTimelineItem = new LatestTimelineItem(getParams().mTimelineType, getParams().myAccountUserId); if (latestTimelineItem.isTimeToAutoUpdate()) { getParams().timelineToReload = getParams().mTimelineType; } break; default: if (MyQuery.userIdToLongColumnValue( User.HOME_TIMELINE_DATE, getParams().myAccountUserId) == 0) { // This is supposed to be a one time task. getParams().timelineToReload = TimelineType.ALL; } break; } } } @Override protected void onPostExecute(Cursor result) { singleEnd(result); } @Override protected void onCancelled(Cursor result) { getParams().cancelled = true; singleEnd(null); } private void singleEnd(Cursor result) { logExecutionStats(result); TimelineCursorLoader1.this.deliverResultsAndClean(result); } private void logExecutionStats(Cursor cursor) { if (MyLog.isVerboseEnabled()) { StringBuilder text = new StringBuilder(getParams().cancelled ? "cancelled" : "ended"); if (!getParams().cancelled) { String cursorInfo; if (cursor == null) { cursorInfo = "cursor is null"; } else if (cursor.isClosed()) { cursorInfo = "cursor is Closed"; } else { cursorInfo = cursor.getCount() + " rows"; } text.append(", " + cursorInfo); } text.append( ", " + java.util.concurrent.TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - getParams().startTime) + " ms"); logV("stats", text.toString()); } } } @Override public void onReceive(CommandData commandData, MyServiceEvent event) { if (event != MyServiceEvent.AFTER_EXECUTING_COMMAND) { return; } final String method = "onReceive"; switch (commandData.getCommand()) { case AUTOMATIC_UPDATE: case FETCH_TIMELINE: if (mParams.mTimelineType != commandData.getTimelineType()) { break; } case GET_STATUS: case SEARCH_MESSAGE: if (commandData.getResult().getDownloadedCount() > 0) { if (MyLog.isVerboseEnabled()) { logV(method, "Content changed, " + commandData.toString()); } onContentChanged(); } break; case CREATE_FAVORITE: case DESTROY_FAVORITE: case DESTROY_REBLOG: case DESTROY_STATUS: case FETCH_ATTACHMENT: case FETCH_AVATAR: case REBLOG: case UPDATE_STATUS: if (!commandData.getResult().hasError()) { if (MyLog.isVerboseEnabled()) { logV(method, "Content changed, " + commandData.toString()); } onContentChanged(); } break; default: break; } } @Override public void onContentChanged() { if (taskIsNotRunning()) { super.onContentChanged(); } else { logV("onContentChanged", "ignoring because task is running"); } } @Override public String toString() { StringBuilder sb = new StringBuilder(64); sb.append("instance:" + instanceId + ","); sb.append("id:" + getId() + ","); return MyLog.formatKeyValue(this, sb.toString()); } public TimelineListParameters getParams() { return mParams; } }
/** * This receiver starts and stops {@link MyService} and also queries its state. Android system * creates new instance of this type on each Intent received. This is why we're keeping a state in * static fields. */ public class MyServiceManager extends BroadcastReceiver { private static final String TAG = MyServiceManager.class.getSimpleName(); private final long instanceId = InstanceId.next(); public MyServiceManager() { MyLog.v(this, "Created, instanceId=" + instanceId); } /** * Is the service started. @See <a * href="http://groups.google.com/group/android-developers/browse_thread/thread/8c4bd731681b8331/bf3ae8ef79cad75d">here</a> */ private static volatile MyServiceState mServiceState = MyServiceState.UNKNOWN; /** {@link System#nanoTime()} when the state was queued or received last time ( 0 - never ) */ private static volatile long stateQueuedTime = 0; /** If true, we sent state request and are waiting for reply from {@link MyService} */ private static volatile boolean waitingForServiceState = false; /** * How long are we waiting for {@link MyService} response before deciding that the service is * stopped */ private static final int STATE_QUERY_TIMEOUT_SECONDS = 3; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(MyAction.SERVICE_STATE.getAction())) { MyContextHolder.initialize(context, this); synchronized (mServiceState) { stateQueuedTime = System.nanoTime(); waitingForServiceState = false; mServiceState = MyServiceState.load(intent.getStringExtra(IntentExtra.SERVICE_STATE.key)); } MyLog.d(this, "Notification received: Service state=" + mServiceState); } else if ("android.intent.action.BOOT_COMPLETED".equals(action)) { MyLog.d(this, "Trying to start service on boot"); sendCommand(CommandData.getEmpty()); } else if ("android.intent.action.ACTION_SHUTDOWN".equals(action)) { // We need this to persist unsaved data in the service MyLog.d(this, "Stopping service on Shutdown"); setServiceUnavailable(); stopService(); } } /** * Starts MyService asynchronously if it is not already started and send command to it. * * @param commandData to the service or null */ public static void sendCommand(CommandData commandData) { if (!isServiceAvailable()) { // Imitate a soft service error commandData.getResult().incrementNumIoExceptions(); commandData.getResult().setMessage("Service is not available"); MyServiceEventsBroadcaster.newInstance(MyContextHolder.get(), MyServiceState.STOPPED) .setCommandData(commandData) .setEvent(MyServiceEvent.AFTER_EXECUTING_COMMAND) .broadcast(); return; } sendCommandEvenForUnavailable(commandData); } public static void sendManualForegroundCommand(CommandData commandData) { sendForegroundCommand(commandData.setManuallyLaunched(true)); } public static void sendForegroundCommand(CommandData commandData) { sendCommand(commandData.setInForeground(true)); } static void sendCommandEvenForUnavailable(CommandData commandData) { // Using explicit Service intent, // see // http://stackoverflow.com/questions/18924640/starting-android-service-using-explicit-vs-implicit-intent Intent serviceIntent = new Intent(MyContextHolder.get().context(), MyService.class); if (commandData != null) { serviceIntent = commandData.toIntent(serviceIntent); } MyContextHolder.get().context().startService(serviceIntent); } /** Stop {@link MyService} asynchronously */ public static synchronized void stopService() { if (!MyContextHolder.get().isReady()) { return; } // Don't do "context.stopService", because we may lose some information and (or) get Force Close // This is "mild" stopping CommandData element = new CommandData(CommandEnum.STOP_SERVICE, ""); MyContextHolder.get() .context() .sendBroadcast(element.toIntent(MyAction.EXECUTE_COMMAND.getIntent())); } /** * Returns previous service state and queries service for its current state asynchronously. * Doesn't start the service, so absence of the reply will mean that service is stopped @See <a * href="http://groups.google.com/group/android-developers/browse_thread/thread/8c4bd731681b8331/bf3ae8ef79cad75d">here</a> */ public static MyServiceState getServiceState() { synchronized (mServiceState) { long time = System.nanoTime(); if (waitingForServiceState && (time - stateQueuedTime) > java.util.concurrent.TimeUnit.SECONDS.toMillis(STATE_QUERY_TIMEOUT_SECONDS)) { // Timeout expired waitingForServiceState = false; mServiceState = MyServiceState.STOPPED; } else if (!waitingForServiceState && mServiceState == MyServiceState.UNKNOWN) { // State is unknown, we need to query the Service again waitingForServiceState = true; stateQueuedTime = time; mServiceState = MyServiceState.UNKNOWN; CommandData element = new CommandData(CommandEnum.BROADCAST_SERVICE_STATE, ""); MyContextHolder.get() .context() .sendBroadcast(element.toIntent(MyAction.EXECUTE_COMMAND.getIntent())); } } return mServiceState; } private static Object serviceAvailableLock = new Object(); @GuardedBy("serviceAvailableLock") private static Boolean mServiceAvailable = true; @GuardedBy("serviceAvailableLock") private static long timeWhenTheServiceWillBeAvailable = 0; public static boolean isServiceAvailable() { boolean isAvailable = MyContextHolder.get().isReady(); if (!isAvailable) { boolean tryToInitialize = false; synchronized (serviceAvailableLock) { tryToInitialize = mServiceAvailable; } if (tryToInitialize && !MyContextHolder.get().initialized()) { MyContextHolder.initialize(null, TAG); isAvailable = MyContextHolder.get().isReady(); } } if (isAvailable) { long availableInMillis = 0; synchronized (serviceAvailableLock) { availableInMillis = timeWhenTheServiceWillBeAvailable - System.currentTimeMillis(); if (!mServiceAvailable && availableInMillis <= 0) { setServiceAvailable(); } isAvailable = mServiceAvailable; } if (!isAvailable) { MyLog.v( TAG, "Service will be available in " + java.util.concurrent.TimeUnit.MILLISECONDS.toSeconds(availableInMillis) + " seconds"); } } else { MyLog.v(TAG, "Service is unavailable: Context is not Ready"); } return isAvailable; } public static void setServiceAvailable() { synchronized (serviceAvailableLock) { mServiceAvailable = true; timeWhenTheServiceWillBeAvailable = 0; } } public static void setServiceUnavailable() { synchronized (serviceAvailableLock) { mServiceAvailable = false; timeWhenTheServiceWillBeAvailable = System.currentTimeMillis() + java.util.concurrent.TimeUnit.SECONDS.toMillis(15 * 60); } } }