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