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