• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2   * Copyright (C) 2015 The Android Open Source Project
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   *      http://www.apache.org/licenses/LICENSE-2.0
9   *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.android.messaging.datamodel.action;
18  
19  import android.content.ContentValues;
20  import android.content.Context;
21  import android.content.Intent;
22  import android.database.Cursor;
23  import android.net.ConnectivityManager;
24  import android.os.Parcel;
25  import android.os.Parcelable;
26  import android.telephony.ServiceState;
27  
28  import com.android.messaging.Factory;
29  import com.android.messaging.datamodel.BugleDatabaseOperations;
30  import com.android.messaging.datamodel.DataModel;
31  import com.android.messaging.datamodel.DatabaseHelper;
32  import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
33  import com.android.messaging.datamodel.DatabaseWrapper;
34  import com.android.messaging.datamodel.MessagingContentProvider;
35  import com.android.messaging.datamodel.data.MessageData;
36  import com.android.messaging.datamodel.data.ParticipantData;
37  import com.android.messaging.sms.MmsUtils;
38  import com.android.messaging.util.BugleGservices;
39  import com.android.messaging.util.BugleGservicesKeys;
40  import com.android.messaging.util.BuglePrefs;
41  import com.android.messaging.util.BuglePrefsKeys;
42  import com.android.messaging.util.ConnectivityUtil.ConnectivityListener;
43  import com.android.messaging.util.LogUtil;
44  import com.android.messaging.util.OsUtil;
45  import com.android.messaging.util.PhoneUtils;
46  
47  import java.util.HashSet;
48  import java.util.Set;
49  
50  /**
51   * Action used to lookup any messages in the pending send/download state and either fail them or
52   * retry their action. This action only initiates one retry at a time - further retries should be
53   * triggered by successful sending of a message, network status change or exponential backoff timer.
54   */
55  public class ProcessPendingMessagesAction extends Action implements Parcelable {
56      private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
57      private static final int PENDING_INTENT_REQUEST_CODE = 101;
58  
processFirstPendingMessage()59      public static void processFirstPendingMessage() {
60          // Clear any pending alarms or connectivity events
61          unregister();
62          // Clear retry count
63          setRetry(0);
64  
65          // Start action
66          final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
67          action.start();
68      }
69  
scheduleProcessPendingMessagesAction(final boolean failed, final Action processingAction)70      public static void scheduleProcessPendingMessagesAction(final boolean failed,
71              final Action processingAction) {
72          LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages"
73                  + (failed ? "(message failed)" : ""));
74          // Can safely clear any pending alarms or connectivity events as either an action
75          // is currently running or we will run now or register if pending actions possible.
76          unregister();
77  
78          final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
79          boolean scheduleAlarm = false;
80          // If message succeeded and if Bugle is default SMS app just carry on with next message
81          if (!failed && isDefaultSmsApp) {
82              // Clear retry attempt count as something just succeeded
83              setRetry(0);
84  
85              // Lookup and queue next message for immediate processing by background worker
86              //  iff there are no pending messages this will do nothing and return true.
87              final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
88              if (action.queueActions(processingAction)) {
89                  if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
90                      if (processingAction.hasBackgroundActions()) {
91                          LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued");
92                      } else {
93                          LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue");
94                      }
95                  }
96                  // Have queued next action if needed, nothing more to do
97                  return;
98              }
99              // In case of error queuing schedule a retry
100              scheduleAlarm = true;
101              LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying");
102          }
103          if (getHavePendingMessages() || scheduleAlarm) {
104              // Still have a pending message that needs to be queued for processing
105              final ConnectivityListener listener = new ConnectivityListener() {
106                  @Override
107                  public void onConnectivityStateChanged(final Context context, final Intent intent) {
108                      final int networkType =
109                              MmsUtils.getConnectivityEventNetworkType(context, intent);
110                      if (networkType != ConnectivityManager.TYPE_MOBILE) {
111                          return;
112                      }
113                      final boolean isConnected = !intent.getBooleanExtra(
114                              ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
115                      // TODO: Should we check in more detail?
116                      if (isConnected) {
117                          onConnected();
118                      }
119                  }
120  
121                  @Override
122                  public void onPhoneStateChanged(final Context context, final int serviceState) {
123                      if (serviceState == ServiceState.STATE_IN_SERVICE) {
124                          onConnected();
125                      }
126                  }
127  
128                  private void onConnected() {
129                      LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected; starting action");
130  
131                      // Clear any pending alarms or connectivity events but leave attempt count alone
132                      unregister();
133  
134                      // Start action
135                      final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
136                      action.start();
137                  }
138              };
139              // Read and increment attempt number from shared prefs
140              final int retryAttempt = getNextRetry();
141              register(listener, retryAttempt);
142          } else {
143              // No more pending messages (presumably the message that failed has expired) or it
144              // may be possible that a send and a download are already in process.
145              // Clear retry attempt count.
146              // TODO Might be premature if send and download in process...
147              //  but worst case means we try to send a bit more often.
148              setRetry(0);
149  
150              if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
151                  LogUtil.v(TAG, "ProcessPendingMessagesAction: No more pending messages");
152              }
153          }
154      }
155  
register(final ConnectivityListener listener, final int retryAttempt)156      private static void register(final ConnectivityListener listener, final int retryAttempt) {
157          int retryNumber = retryAttempt;
158  
159          // Register to be notified about connectivity changes
160          DataModel.get().getConnectivityUtil().register(listener);
161  
162          final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
163          final long initialBackoffMs = BugleGservices.get().getLong(
164                  BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS,
165                  BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT);
166          final long maxDelayMs = BugleGservices.get().getLong(
167                  BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS,
168                  BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT);
169          long delayMs;
170          long nextDelayMs = initialBackoffMs;
171          do {
172              delayMs = nextDelayMs;
173              retryNumber--;
174              nextDelayMs = delayMs * 2;
175          }
176          while (retryNumber > 0 && nextDelayMs < maxDelayMs);
177  
178          LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt
179                  + " in " + delayMs + " ms");
180  
181          action.schedule(PENDING_INTENT_REQUEST_CODE, delayMs);
182      }
183  
unregister()184      private static void unregister() {
185          // Clear any pending alarms or connectivity events
186          DataModel.get().getConnectivityUtil().unregister();
187  
188          final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
189          action.schedule(PENDING_INTENT_REQUEST_CODE, Long.MAX_VALUE);
190  
191          if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
192              LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed "
193                      + "events and clearing scheduled alarm");
194          }
195      }
196  
setRetry(final int retryAttempt)197      private static void setRetry(final int retryAttempt) {
198          final BuglePrefs prefs = Factory.get().getApplicationPrefs();
199          prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
200      }
201  
getNextRetry()202      private static int getNextRetry() {
203          final BuglePrefs prefs = Factory.get().getApplicationPrefs();
204          final int retryAttempt =
205                  prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1;
206          prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
207          return retryAttempt;
208      }
209  
ProcessPendingMessagesAction()210      private ProcessPendingMessagesAction() {
211      }
212  
213      /**
214       * Read from the DB and determine if there are any messages we should process
215       * @return true if we have pending messages
216       */
getHavePendingMessages()217      private static boolean getHavePendingMessages() {
218          final DatabaseWrapper db = DataModel.get().getDatabase();
219          final long now = System.currentTimeMillis();
220  
221          final String toSendMessageId = findNextMessageToSend(db, now);
222          if (toSendMessageId != null) {
223              return true;
224          } else {
225              final String toDownloadMessageId = findNextMessageToDownload(db, now);
226              if (toDownloadMessageId != null) {
227                  return true;
228              }
229          }
230          // Messages may be in the process of sending/downloading even when there are no pending
231          // messages...
232          return false;
233      }
234  
235      /**
236       * Queue any pending actions
237       * @param actionState
238       * @return true if action queued (or no actions to queue) else false
239       */
queueActions(final Action processingAction)240      private boolean queueActions(final Action processingAction) {
241          final DatabaseWrapper db = DataModel.get().getDatabase();
242          final long now = System.currentTimeMillis();
243          boolean succeeded = true;
244  
245          // Will queue no more than one message to send plus one message to download
246          // This keeps outgoing messages "in order" but allow downloads to happen even if sending
247          //  gets blocked until messages time out.  Manual resend bumps messages to head of queue.
248          final String toSendMessageId = findNextMessageToSend(db, now);
249          final String toDownloadMessageId = findNextMessageToDownload(db, now);
250          if (toSendMessageId != null) {
251              LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId
252                      + " for sending");
253              // This could queue nothing
254              if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) {
255                  LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
256                          + toSendMessageId + " for sending");
257                  succeeded = false;
258              }
259          }
260          if (toDownloadMessageId != null) {
261              LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId
262                      + " for download");
263              // This could queue nothing
264              if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId,
265                      processingAction)) {
266                  LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
267                          + toDownloadMessageId + " for download");
268                  succeeded = false;
269              }
270          }
271          if (toSendMessageId == null && toDownloadMessageId == null) {
272              if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
273                  LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download");
274              }
275          }
276          return succeeded;
277      }
278  
279      @Override
executeAction()280      protected Object executeAction() {
281          // If triggered by alarm will not have unregistered yet
282          unregister();
283  
284          if (PhoneUtils.getDefault().isDefaultSmsApp()) {
285              queueActions(this);
286          } else {
287              if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
288                  LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling");
289              }
290              scheduleProcessPendingMessagesAction(true, this);
291          }
292  
293          return null;
294      }
295  
findNextMessageToSend(final DatabaseWrapper db, final long now)296      private static String findNextMessageToSend(final DatabaseWrapper db, final long now) {
297          String toSendMessageId = null;
298          db.beginTransaction();
299          Cursor sending = null;
300          Cursor cursor = null;
301          int sendingCnt = 0;
302          int pendingCnt = 0;
303          int failedCnt = 0;
304          try {
305              // First check to see if we have any messages already sending
306              sending = db.query(DatabaseHelper.MESSAGES_TABLE,
307                      MessageData.getProjection(),
308                      DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
309                      new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
310                             Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)},
311                      null,
312                      null,
313                      DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
314              final boolean messageCurrentlySending = sending.moveToNext();
315              sendingCnt = sending.getCount();
316              // Look for messages we could send
317              final ContentValues values = new ContentValues();
318              values.put(DatabaseHelper.MessageColumns.STATUS,
319                      MessageData.BUGLE_STATUS_OUTGOING_FAILED);
320              cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
321                      MessageData.getProjection(),
322                      DatabaseHelper.MessageColumns.STATUS + " IN ("
323                              + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ","
324                              + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")",
325                      null,
326                      null,
327                      null,
328                      DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
329              pendingCnt = cursor.getCount();
330  
331              while (cursor.moveToNext()) {
332                  final MessageData message = new MessageData();
333                  message.bind(cursor);
334                  if (message.getInResendWindow(now)) {
335                      // If no messages currently sending
336                      if (!messageCurrentlySending) {
337                          // Resend this message
338                          toSendMessageId = message.getMessageId();
339                          // Before queuing the message for resending, check if the message's self is
340                          // active. If not, switch back to the system's default subscription.
341                          if (OsUtil.isAtLeastL_MR1()) {
342                              final ParticipantData messageSelf = BugleDatabaseOperations
343                                      .getExistingParticipant(db, message.getSelfId());
344                              if (messageSelf == null || !messageSelf.isActiveSubscription()) {
345                                  final ParticipantData defaultSelf = BugleDatabaseOperations
346                                          .getOrCreateSelf(db, PhoneUtils.getDefault()
347                                                  .getDefaultSmsSubscriptionId());
348                                  if (defaultSelf != null) {
349                                      message.bindSelfId(defaultSelf.getId());
350                                      final ContentValues selfValues = new ContentValues();
351                                      selfValues.put(MessageColumns.SELF_PARTICIPANT_ID,
352                                              defaultSelf.getId());
353                                      BugleDatabaseOperations.updateMessageRow(db,
354                                              message.getMessageId(), selfValues);
355                                      MessagingContentProvider.notifyMessagesChanged(
356                                              message.getConversationId());
357                                  }
358                              }
359                          }
360                      }
361                      break;
362                  } else {
363                      failedCnt++;
364  
365                      // Mark message as failed
366                      BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
367                      MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
368                  }
369              }
370              db.setTransactionSuccessful();
371          } finally {
372              db.endTransaction();
373              if (cursor != null) {
374                  cursor.close();
375              }
376              if (sending != null) {
377                  sending.close();
378              }
379          }
380  
381          if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
382              LogUtil.d(TAG, "ProcessPendingMessagesAction: "
383                      + sendingCnt + " messages already sending, "
384                      + pendingCnt + " messages to send, "
385                      + failedCnt + " failed messages");
386          }
387  
388          return toSendMessageId;
389      }
390  
findNextMessageToDownload(final DatabaseWrapper db, final long now)391      private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) {
392          String toDownloadMessageId = null;
393          db.beginTransaction();
394          Cursor cursor = null;
395          int downloadingCnt = 0;
396          int pendingCnt = 0;
397          try {
398              // First check if we have any messages already downloading
399              downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
400                      DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
401                      new String[] {
402                          Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
403                          Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
404                      });
405  
406              // TODO: This query is not actually needed if downloadingCnt == 0.
407              cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
408                      MessageData.getProjection(),
409                      DatabaseHelper.MessageColumns.STATUS + " =? OR "
410                              + DatabaseHelper.MessageColumns.STATUS + " =?",
411                      new String[]{
412                              Integer.toString(
413                                      MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD),
414                              Integer.toString(
415                                      MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD)
416                      },
417                      null,
418                      null,
419                      DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
420  
421              pendingCnt = cursor.getCount();
422  
423              // If no messages are currently downloading and there is a download pending,
424              // queue the download of the oldest pending message.
425              if (downloadingCnt == 0 && cursor.moveToNext()) {
426                  // Always start the next pending message. We will check if a download has
427                  // expired in DownloadMmsAction and mark message failed there.
428                  final MessageData message = new MessageData();
429                  message.bind(cursor);
430                  toDownloadMessageId = message.getMessageId();
431              }
432              db.setTransactionSuccessful();
433          } finally {
434              db.endTransaction();
435              if (cursor != null) {
436                  cursor.close();
437              }
438          }
439  
440          if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
441              LogUtil.d(TAG, "ProcessPendingMessagesAction: "
442                      + downloadingCnt + " messages already downloading, "
443                      + pendingCnt + " messages to download");
444          }
445  
446          return toDownloadMessageId;
447      }
448  
ProcessPendingMessagesAction(final Parcel in)449      private ProcessPendingMessagesAction(final Parcel in) {
450          super(in);
451      }
452  
453      public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR
454              = new Parcelable.Creator<ProcessPendingMessagesAction>() {
455          @Override
456          public ProcessPendingMessagesAction createFromParcel(final Parcel in) {
457              return new ProcessPendingMessagesAction(in);
458          }
459  
460          @Override
461          public ProcessPendingMessagesAction[] newArray(final int size) {
462              return new ProcessPendingMessagesAction[size];
463          }
464      };
465  
466      @Override
writeToParcel(final Parcel parcel, final int flags)467      public void writeToParcel(final Parcel parcel, final int flags) {
468          writeActionToParcel(parcel, flags);
469      }
470  }
471