Home | History | Annotate | Download | only in action
      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 
     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 
     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 
    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 
    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 
    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 
    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 
    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      */
    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      */
    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
    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 
    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 
    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 
    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
    467     public void writeToParcel(final Parcel parcel, final int flags) {
    468         writeActionToParcel(parcel, flags);
    469     }
    470 }
    471