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