/** @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);
    }
  }
}