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