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