diff options
Diffstat (limited to 'src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java')
-rw-r--r-- | src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java b/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java new file mode 100644 index 0000000..8a41f4a --- /dev/null +++ b/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.android.messaging.datamodel.action; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.ServiceState; + +import com.android.messaging.Factory; +import com.android.messaging.datamodel.BugleDatabaseOperations; +import com.android.messaging.datamodel.DataModel; +import com.android.messaging.datamodel.DatabaseHelper; +import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; +import com.android.messaging.datamodel.DatabaseWrapper; +import com.android.messaging.datamodel.MessagingContentProvider; +import com.android.messaging.datamodel.data.MessageData; +import com.android.messaging.datamodel.data.ParticipantData; +import com.android.messaging.sms.MmsUtils; +import com.android.messaging.util.BugleGservices; +import com.android.messaging.util.BugleGservicesKeys; +import com.android.messaging.util.BuglePrefs; +import com.android.messaging.util.BuglePrefsKeys; +import com.android.messaging.util.ConnectivityUtil.ConnectivityListener; +import com.android.messaging.util.LogUtil; +import com.android.messaging.util.OsUtil; +import com.android.messaging.util.PhoneUtils; + +import java.util.HashSet; +import java.util.Set; + +/** + * Action used to lookup any messages in the pending send/download state and either fail them or + * retry their action. This action only initiates one retry at a time - further retries should be + * triggered by successful sending of a message, network status change or exponential backoff timer. + */ +public class ProcessPendingMessagesAction extends Action implements Parcelable { + private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; + private static final int PENDING_INTENT_REQUEST_CODE = 101; + + public static void processFirstPendingMessage() { + // Clear any pending alarms or connectivity events + unregister(); + // Clear retry count + setRetry(0); + + // Start action + final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); + action.start(); + } + + public static void scheduleProcessPendingMessagesAction(final boolean failed, + final Action processingAction) { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages" + + (failed ? "(message failed)" : "")); + // Can safely clear any pending alarms or connectivity events as either an action + // is currently running or we will run now or register if pending actions possible. + unregister(); + + final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp(); + boolean scheduleAlarm = false; + // If message succeeded and if Bugle is default SMS app just carry on with next message + if (!failed && isDefaultSmsApp) { + // Clear retry attempt count as something just succeeded + setRetry(0); + + // Lookup and queue next message for immediate processing by background worker + // iff there are no pending messages this will do nothing and return true. + final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); + if (action.queueActions(processingAction)) { + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + if (processingAction.hasBackgroundActions()) { + LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued"); + } else { + LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue"); + } + } + // Have queued next action if needed, nothing more to do + return; + } + // In case of error queuing schedule a retry + scheduleAlarm = true; + LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying"); + } + if (getHavePendingMessages() || scheduleAlarm) { + // Still have a pending message that needs to be queued for processing + final ConnectivityListener listener = new ConnectivityListener() { + @Override + public void onConnectivityStateChanged(final Context context, final Intent intent) { + final int networkType = + MmsUtils.getConnectivityEventNetworkType(context, intent); + if (networkType != ConnectivityManager.TYPE_MOBILE) { + return; + } + final boolean isConnected = !intent.getBooleanExtra( + ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + // TODO: Should we check in more detail? + if (isConnected) { + onConnected(); + } + } + + @Override + public void onPhoneStateChanged(final Context context, final int serviceState) { + if (serviceState == ServiceState.STATE_IN_SERVICE) { + onConnected(); + } + } + + private void onConnected() { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected; starting action"); + + // Clear any pending alarms or connectivity events but leave attempt count alone + unregister(); + + // Start action + final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); + action.start(); + } + }; + // Read and increment attempt number from shared prefs + final int retryAttempt = getNextRetry(); + register(listener, retryAttempt); + } else { + // No more pending messages (presumably the message that failed has expired) or it + // may be possible that a send and a download are already in process. + // Clear retry attempt count. + // TODO Might be premature if send and download in process... + // but worst case means we try to send a bit more often. + setRetry(0); + + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "ProcessPendingMessagesAction: No more pending messages"); + } + } + } + + private static void register(final ConnectivityListener listener, final int retryAttempt) { + int retryNumber = retryAttempt; + + // Register to be notified about connectivity changes + DataModel.get().getConnectivityUtil().register(listener); + + final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); + final long initialBackoffMs = BugleGservices.get().getLong( + BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS, + BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT); + final long maxDelayMs = BugleGservices.get().getLong( + BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS, + BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT); + long delayMs; + long nextDelayMs = initialBackoffMs; + do { + delayMs = nextDelayMs; + retryNumber--; + nextDelayMs = delayMs * 2; + } + while (retryNumber > 0 && nextDelayMs < maxDelayMs); + + LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt + + " in " + delayMs + " ms"); + + action.schedule(PENDING_INTENT_REQUEST_CODE, delayMs); + } + + private static void unregister() { + // Clear any pending alarms or connectivity events + DataModel.get().getConnectivityUtil().unregister(); + + final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); + action.schedule(PENDING_INTENT_REQUEST_CODE, Long.MAX_VALUE); + + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed " + + "events and clearing scheduled alarm"); + } + } + + private static void setRetry(final int retryAttempt) { + final BuglePrefs prefs = Factory.get().getApplicationPrefs(); + prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt); + } + + private static int getNextRetry() { + final BuglePrefs prefs = Factory.get().getApplicationPrefs(); + final int retryAttempt = + prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1; + prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt); + return retryAttempt; + } + + private ProcessPendingMessagesAction() { + } + + /** + * Read from the DB and determine if there are any messages we should process + * @return true if we have pending messages + */ + private static boolean getHavePendingMessages() { + final DatabaseWrapper db = DataModel.get().getDatabase(); + final long now = System.currentTimeMillis(); + + final String toSendMessageId = findNextMessageToSend(db, now); + if (toSendMessageId != null) { + return true; + } else { + final String toDownloadMessageId = findNextMessageToDownload(db, now); + if (toDownloadMessageId != null) { + return true; + } + } + // Messages may be in the process of sending/downloading even when there are no pending + // messages... + return false; + } + + /** + * Queue any pending actions + * @param actionState + * @return true if action queued (or no actions to queue) else false + */ + private boolean queueActions(final Action processingAction) { + final DatabaseWrapper db = DataModel.get().getDatabase(); + final long now = System.currentTimeMillis(); + boolean succeeded = true; + + // Will queue no more than one message to send plus one message to download + // This keeps outgoing messages "in order" but allow downloads to happen even if sending + // gets blocked until messages time out. Manual resend bumps messages to head of queue. + final String toSendMessageId = findNextMessageToSend(db, now); + final String toDownloadMessageId = findNextMessageToDownload(db, now); + if (toSendMessageId != null) { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId + + " for sending"); + // This could queue nothing + if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) { + LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " + + toSendMessageId + " for sending"); + succeeded = false; + } + } + if (toDownloadMessageId != null) { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId + + " for download"); + // This could queue nothing + if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId, + processingAction)) { + LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " + + toDownloadMessageId + " for download"); + succeeded = false; + } + } + if (toSendMessageId == null && toDownloadMessageId == null) { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download"); + } + } + return succeeded; + } + + @Override + protected Object executeAction() { + // If triggered by alarm will not have unregistered yet + unregister(); + + if (PhoneUtils.getDefault().isDefaultSmsApp()) { + queueActions(this); + } else { + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling"); + } + scheduleProcessPendingMessagesAction(true, this); + } + + return null; + } + + private static String findNextMessageToSend(final DatabaseWrapper db, final long now) { + String toSendMessageId = null; + db.beginTransaction(); + Cursor sending = null; + Cursor cursor = null; + int sendingCnt = 0; + int pendingCnt = 0; + int failedCnt = 0; + try { + // First check to see if we have any messages already sending + sending = db.query(DatabaseHelper.MESSAGES_TABLE, + MessageData.getProjection(), + DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", + new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING), + Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)}, + null, + null, + DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); + final boolean messageCurrentlySending = sending.moveToNext(); + sendingCnt = sending.getCount(); + // Look for messages we could send + final ContentValues values = new ContentValues(); + values.put(DatabaseHelper.MessageColumns.STATUS, + MessageData.BUGLE_STATUS_OUTGOING_FAILED); + cursor = db.query(DatabaseHelper.MESSAGES_TABLE, + MessageData.getProjection(), + DatabaseHelper.MessageColumns.STATUS + " IN (" + + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + "," + + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")", + null, + null, + null, + DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); + pendingCnt = cursor.getCount(); + + while (cursor.moveToNext()) { + final MessageData message = new MessageData(); + message.bind(cursor); + if (message.getInResendWindow(now)) { + // If no messages currently sending + if (!messageCurrentlySending) { + // Resend this message + toSendMessageId = message.getMessageId(); + // Before queuing the message for resending, check if the message's self is + // active. If not, switch back to the system's default subscription. + if (OsUtil.isAtLeastL_MR1()) { + final ParticipantData messageSelf = BugleDatabaseOperations + .getExistingParticipant(db, message.getSelfId()); + if (messageSelf == null || !messageSelf.isActiveSubscription()) { + final ParticipantData defaultSelf = BugleDatabaseOperations + .getOrCreateSelf(db, PhoneUtils.getDefault() + .getDefaultSmsSubscriptionId()); + if (defaultSelf != null) { + message.bindSelfId(defaultSelf.getId()); + final ContentValues selfValues = new ContentValues(); + selfValues.put(MessageColumns.SELF_PARTICIPANT_ID, + defaultSelf.getId()); + BugleDatabaseOperations.updateMessageRow(db, + message.getMessageId(), selfValues); + MessagingContentProvider.notifyMessagesChanged( + message.getConversationId()); + } + } + } + } + break; + } else { + failedCnt++; + + // Mark message as failed + BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values); + MessagingContentProvider.notifyMessagesChanged(message.getConversationId()); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + if (cursor != null) { + cursor.close(); + } + if (sending != null) { + sending.close(); + } + } + + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "ProcessPendingMessagesAction: " + + sendingCnt + " messages already sending, " + + pendingCnt + " messages to send, " + + failedCnt + " failed messages"); + } + + return toSendMessageId; + } + + private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) { + String toDownloadMessageId = null; + db.beginTransaction(); + Cursor cursor = null; + int downloadingCnt = 0; + int pendingCnt = 0; + try { + // First check if we have any messages already downloading + downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE, + DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", + new String[] { + Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING), + Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) + }); + + // TODO: This query is not actually needed if downloadingCnt == 0. + cursor = db.query(DatabaseHelper.MESSAGES_TABLE, + MessageData.getProjection(), + DatabaseHelper.MessageColumns.STATUS + " =? OR " + + DatabaseHelper.MessageColumns.STATUS + " =?", + new String[]{ + Integer.toString( + MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD), + Integer.toString( + MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD) + }, + null, + null, + DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); + + pendingCnt = cursor.getCount(); + + // If no messages are currently downloading and there is a download pending, + // queue the download of the oldest pending message. + if (downloadingCnt == 0 && cursor.moveToNext()) { + // Always start the next pending message. We will check if a download has + // expired in DownloadMmsAction and mark message failed there. + final MessageData message = new MessageData(); + message.bind(cursor); + toDownloadMessageId = message.getMessageId(); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + if (cursor != null) { + cursor.close(); + } + } + + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "ProcessPendingMessagesAction: " + + downloadingCnt + " messages already downloading, " + + pendingCnt + " messages to download"); + } + + return toDownloadMessageId; + } + + private ProcessPendingMessagesAction(final Parcel in) { + super(in); + } + + public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR + = new Parcelable.Creator<ProcessPendingMessagesAction>() { + @Override + public ProcessPendingMessagesAction createFromParcel(final Parcel in) { + return new ProcessPendingMessagesAction(in); + } + + @Override + public ProcessPendingMessagesAction[] newArray(final int size) { + return new ProcessPendingMessagesAction[size]; + } + }; + + @Override + public void writeToParcel(final Parcel parcel, final int flags) { + writeActionToParcel(parcel, flags); + } +} |