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 could 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             // Prior to L_MR1, isActiveSubscription is true always
358             boolean isActiveSubscription = true;
359             if (OsUtil.isAtLeastL_MR1()) {
360                 final ParticipantData messageSelf =
361                         BugleDatabaseOperations.getExistingParticipant(db, selfId);
362                 if (messageSelf == null || !messageSelf.isActiveSubscription()) {
363                     isActiveSubscription = false;
364                 }
365             }
366             while (cursor.moveToNext()) {
367                 final MessageData message = new MessageData();
368                 message.bind(cursor);
369 
370                 // Mark this message as failed if the message's self is inactive or the message is
371                 // outside of resend window
372                 if (!isActiveSubscription || !message.getInResendWindow(now)) {
373                     failedCnt++;
374 
375                     // Mark message as failed
376                     BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
377                     MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
378                 } else {
379                     // If no messages currently sending
380                     if (sendingCnt == 0) {
381                         // Send this message
382                         toSendMessageId = message.getMessageId();
383                     }
384                     break;
385                 }
386             }
387             db.setTransactionSuccessful();
388         } finally {
389             db.endTransaction();
390             if (cursor != null) {
391                 cursor.close();
392             }
393         }
394 
395         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
396             LogUtil.d(TAG, "ProcessPendingMessagesAction: "
397                     + sendingCnt + " messages already sending, "
398                     + pendingCnt + " messages to send, "
399                     + failedCnt + " failed messages");
400         }
401 
402         return toSendMessageId;
403     }
404 
findNextMessageToDownload(final DatabaseWrapper db, final long now, final String selfId)405     private static String findNextMessageToDownload(final DatabaseWrapper db, final long now,
406             final String selfId) {
407         String toDownloadMessageId = null;
408         Cursor cursor = null;
409         int downloadingCnt = 0;
410         int pendingCnt = 0;
411         db.beginTransaction();
412         try {
413             // First check if we have any messages already downloading
414             downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
415                     DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
416                     + DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =?",
417                     new String[] {
418                         Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
419                         Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING),
420                         selfId
421                     });
422 
423             // TODO: This query is not actually needed if downloadingCnt == 0.
424             cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
425                     MessageData.getProjection(),
426                     DatabaseHelper.MessageColumns.STATUS + " IN (?, ?) AND "
427                     + DatabaseHelper.MessageColumns.SELF_PARTICIPANT_ID + " =? ",
428                     new String[]{
429                         Integer.toString(MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD),
430                         Integer.toString(
431                                 MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD),
432                         selfId
433                     },
434                     null,
435                     null,
436                     DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
437 
438             pendingCnt = cursor.getCount();
439 
440             // If no messages are currently downloading and there is a download pending,
441             // queue the download of the oldest pending message.
442             if (downloadingCnt == 0 && cursor.moveToNext()) {
443                 // Always start the next pending message. We will check if a download has
444                 // expired in DownloadMmsAction and mark message failed there.
445                 final MessageData message = new MessageData();
446                 message.bind(cursor);
447                 toDownloadMessageId = message.getMessageId();
448             }
449             db.setTransactionSuccessful();
450         } finally {
451             db.endTransaction();
452             if (cursor != null) {
453                 cursor.close();
454             }
455         }
456 
457         if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
458             LogUtil.d(TAG, "ProcessPendingMessagesAction: "
459                     + downloadingCnt + " messages already downloading, "
460                     + pendingCnt + " messages to download");
461         }
462 
463         return toDownloadMessageId;
464     }
465 
ProcessPendingMessagesAction(final Parcel in)466     private ProcessPendingMessagesAction(final Parcel in) {
467         super(in);
468     }
469 
470     public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR
471             = new Parcelable.Creator<ProcessPendingMessagesAction>() {
472         @Override
473         public ProcessPendingMessagesAction createFromParcel(final Parcel in) {
474             return new ProcessPendingMessagesAction(in);
475         }
476 
477         @Override
478         public ProcessPendingMessagesAction[] newArray(final int size) {
479             return new ProcessPendingMessagesAction[size];
480         }
481     };
482 
483     @Override
writeToParcel(final Parcel parcel, final int flags)484     public void writeToParcel(final Parcel parcel, final int flags) {
485         writeActionToParcel(parcel, flags);
486     }
487 }
488