/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.datamodel.action; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.provider.Telephony.Mms; import androidx.collection.LongSparseArray; import com.android.messaging.Factory; import com.android.messaging.datamodel.DataModel; import com.android.messaging.datamodel.DatabaseWrapper; import com.android.messaging.datamodel.MessagingContentProvider; import com.android.messaging.datamodel.SyncManager; import com.android.messaging.datamodel.SyncManager.ThreadInfoCache; import com.android.messaging.datamodel.data.ParticipantData; import com.android.messaging.mmslib.SqliteWrapper; import com.android.messaging.sms.DatabaseMessages; import com.android.messaging.sms.DatabaseMessages.LocalDatabaseMessage; import com.android.messaging.sms.DatabaseMessages.MmsMessage; import com.android.messaging.sms.DatabaseMessages.SmsMessage; import com.android.messaging.sms.MmsUtils; import com.android.messaging.util.Assert; import com.android.messaging.util.BugleGservices; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.BuglePrefs; import com.android.messaging.util.BuglePrefsKeys; import com.android.messaging.util.ContentType; import com.android.messaging.util.LogUtil; import com.android.messaging.util.OsUtil; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Action used to sync messages from smsmms db to local database */ public class SyncMessagesAction extends Action implements Parcelable { static final long SYNC_FAILED = Long.MIN_VALUE; private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; private static final String KEY_START_TIMESTAMP = "start_timestamp"; private static final String KEY_MAX_UPDATE = "max_update"; private static final String KEY_LOWER_BOUND = "lower_bound"; private static final String KEY_UPPER_BOUND = "upper_bound"; private static final String BUNDLE_KEY_LAST_TIMESTAMP = "last_timestamp"; private static final String BUNDLE_KEY_SMS_MESSAGES = "sms_to_add"; private static final String BUNDLE_KEY_MMS_MESSAGES = "mms_to_add"; private static final String BUNDLE_KEY_MESSAGES_TO_DELETE = "messages_to_delete"; /** * Start a full sync (backed off a few seconds to avoid pulling sending/receiving messages). */ public static void fullSync() { final BugleGservices bugleGservices = BugleGservices.get(); final long smsSyncBackoffTimeMillis = bugleGservices.getLong( BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS, BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT); final long now = System.currentTimeMillis(); // TODO: Could base this off most recent message in db but now should be okay... final long startTimestamp = now - smsSyncBackoffTimeMillis; final SyncMessagesAction action = new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp); action.start(); } /** * Start an incremental sync to pull messages since last sync (backed off a few seconds).. */ public static void sync() { final BugleGservices bugleGservices = BugleGservices.get(); final long smsSyncBackoffTimeMillis = bugleGservices.getLong( BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS, BugleGservicesKeys.SMS_SYNC_BACKOFF_TIME_MILLIS_DEFAULT); final long now = System.currentTimeMillis(); // TODO: Could base this off most recent message in db but now should be okay... final long startTimestamp = now - smsSyncBackoffTimeMillis; sync(startTimestamp); } /** * Start an incremental sync when the application starts up (no back off as not yet * sending/receiving). */ public static void immediateSync() { final long now = System.currentTimeMillis(); // TODO: Could base this off most recent message in db but now should be okay... final long startTimestamp = now; sync(startTimestamp); } private static void sync(final long startTimestamp) { if (!OsUtil.hasSmsPermission()) { // Sync requires READ_SMS permission return; } final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); // Lower bound is end of previous sync final long syncLowerBoundTimeMillis = prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME, BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT); final SyncMessagesAction action = new SyncMessagesAction(syncLowerBoundTimeMillis, startTimestamp, 0, startTimestamp); action.start(); } private SyncMessagesAction(final long lowerBound, final long upperBound, final int maxMessagesToUpdate, final long startTimestamp) { actionParameters.putLong(KEY_LOWER_BOUND, lowerBound); actionParameters.putLong(KEY_UPPER_BOUND, upperBound); actionParameters.putInt(KEY_MAX_UPDATE, maxMessagesToUpdate); actionParameters.putLong(KEY_START_TIMESTAMP, startTimestamp); } @Override protected Object executeAction() { final DatabaseWrapper db = DataModel.get().getDatabase(); long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND); final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND); final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE); final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP); if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Request to sync messages from " + lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (start timestamp = " + startTimestamp + ", message update limit = " + initialMaxMessagesToUpdate + ")"); } final SyncManager syncManager = DataModel.get().getSyncManager(); if (lowerBoundTimeMillis >= 0) { // Cursors final SyncCursorPair cursors = new SyncCursorPair(-1L, lowerBoundTimeMillis); final boolean inSync = cursors.isSynchronized(db); if (!inSync) { if (syncManager.delayUntilFullSync(startTimestamp) == 0) { lowerBoundTimeMillis = -1; actionParameters.putLong(KEY_LOWER_BOUND, lowerBoundTimeMillis); if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis + " not in sync; promoting to full sync"); } } else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis + " not in sync; will do incremental sync"); } } else { if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Messages before " + lowerBoundTimeMillis + " are in sync"); } } } // Check if sync allowed (can be too soon after last or one is already running) if (syncManager.shouldSync(lowerBoundTimeMillis < 0, startTimestamp)) { syncManager.startSyncBatch(upperBoundTimeMillis); requestBackgroundWork(); } return null; } @Override protected Bundle doBackgroundWork() { final BugleGservices bugleGservices = BugleGservices.get(); final DatabaseWrapper db = DataModel.get().getDatabase(); final int maxMessagesToScan = bugleGservices.getInt( BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN, BugleGservicesKeys.SMS_SYNC_BATCH_MAX_MESSAGES_TO_SCAN_DEFAULT); final int initialMaxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE); final int smsSyncSubsequentBatchSizeMin = bugleGservices.getInt( BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN, BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MIN_DEFAULT); final int smsSyncSubsequentBatchSizeMax = bugleGservices.getInt( BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX, BugleGservicesKeys.SMS_SYNC_BATCH_SIZE_MAX_DEFAULT); // Cap sync size to GServices limits final int maxMessagesToUpdate = Math.max(smsSyncSubsequentBatchSizeMin, Math.min(initialMaxMessagesToUpdate, smsSyncSubsequentBatchSizeMax)); final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND); final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND); LogUtil.i(TAG, "SyncMessagesAction: Starting batch for messages from " + lowerBoundTimeMillis + " to " + upperBoundTimeMillis + " (message update limit = " + maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan + ")"); // Clear last change time so that we can work out if this batch is dirty when it completes final SyncManager syncManager = DataModel.get().getSyncManager(); // Clear the singleton cache that maps threads to recipients and to conversations. final SyncManager.ThreadInfoCache cache = syncManager.getThreadInfoCache(); cache.clear(); // Sms messages to store final ArrayList smsToAdd = new ArrayList(); // Mms messages to store final LongSparseArray mmsToAdd = new LongSparseArray(); // List of local SMS/MMS to remove final ArrayList messagesToDelete = new ArrayList(); long lastTimestampMillis = SYNC_FAILED; if (syncManager.isSyncing(upperBoundTimeMillis)) { // Cursors final SyncCursorPair cursors = new SyncCursorPair(lowerBoundTimeMillis, upperBoundTimeMillis); // Actually compare the messages using cursor pair lastTimestampMillis = syncCursorPair(db, cursors, smsToAdd, mmsToAdd, messagesToDelete, maxMessagesToScan, maxMessagesToUpdate, cache); } final Bundle response = new Bundle(); // If comparison succeeds bundle up the changes for processing in ActionService if (lastTimestampMillis > SYNC_FAILED) { final ArrayList mmsToAddList = new ArrayList(); for (int i = 0; i < mmsToAdd.size(); i++) { final MmsMessage mms = mmsToAdd.valueAt(i); mmsToAddList.add(mms); } response.putParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES, smsToAdd); response.putParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES, mmsToAddList); response.putParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE, messagesToDelete); } response.putLong(BUNDLE_KEY_LAST_TIMESTAMP, lastTimestampMillis); return response; } /** * Compare messages based on timestamp and uri * @param db local database wrapper * @param cursors cursor pair holding references to local and remote messages * @param smsToAdd newly found sms messages to add * @param mmsToAdd newly found mms messages to add * @param messagesToDelete messages not found needing deletion * @param maxMessagesToScan max messages to scan for changes * @param maxMessagesToUpdate max messages to return for updates * @param cache cache for conversation id / thread id / recipient set mapping * @return timestamp of the oldest message seen during the sync scan */ private long syncCursorPair(final DatabaseWrapper db, final SyncCursorPair cursors, final ArrayList smsToAdd, final LongSparseArray mmsToAdd, final ArrayList messagesToDelete, final int maxMessagesToScan, final int maxMessagesToUpdate, final ThreadInfoCache cache) { long lastTimestampMillis; final long startTimeMillis = SystemClock.elapsedRealtime(); // Number of messages scanned local and remote int localPos = 0; int remotePos = 0; int localTotal = 0; int remoteTotal = 0; // Scan through the messages on both sides and prepare messages for local message table // changes (including adding and deleting) try { cursors.query(db); localTotal = cursors.getLocalCount(); remoteTotal = cursors.getRemoteCount(); if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Scanning cursors (local count = " + localTotal + ", remote count = " + remoteTotal + ", message update limit = " + maxMessagesToUpdate + ", message scan limit = " + maxMessagesToScan + ")"); } lastTimestampMillis = cursors.scan(maxMessagesToScan, maxMessagesToUpdate, smsToAdd, mmsToAdd, messagesToDelete, cache); localPos = cursors.getLocalPosition(); remotePos = cursors.getRemotePosition(); if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Scanned cursors (local position = " + localPos + " of " + localTotal + ", remote position = " + remotePos + " of " + remoteTotal + ")"); } // Batch loading the parts of the MMS messages in this batch loadMmsParts(mmsToAdd); // Lookup senders for incoming mms messages setMmsSenders(mmsToAdd, cache); } catch (final SQLiteException e) { LogUtil.e(TAG, "SyncMessagesAction: Database exception", e); // Let's abort lastTimestampMillis = SYNC_FAILED; } catch (final Exception e) { // We want to catch anything unexpected since this is running in a separate thread // and any unexpected exception will just fail this thread silently. // Let's crash for dogfooders! LogUtil.wtf(TAG, "SyncMessagesAction: unexpected failure in scan", e); lastTimestampMillis = SYNC_FAILED; } finally { if (cursors != null) { cursors.close(); } } final long endTimeMillis = SystemClock.elapsedRealtime(); if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: Scan complete (took " + (endTimeMillis - startTimeMillis) + " ms). " + smsToAdd.size() + " remote SMS to add, " + mmsToAdd.size() + " MMS to add, " + messagesToDelete.size() + " local messages to delete. " + "Oldest timestamp seen = " + lastTimestampMillis); } return lastTimestampMillis; } /** * Perform local database updates and schedule follow on sync actions */ @Override protected Object processBackgroundResponse(final Bundle response) { final long lastTimestampMillis = response.getLong(BUNDLE_KEY_LAST_TIMESTAMP); final long lowerBoundTimeMillis = actionParameters.getLong(KEY_LOWER_BOUND); final long upperBoundTimeMillis = actionParameters.getLong(KEY_UPPER_BOUND); final int maxMessagesToUpdate = actionParameters.getInt(KEY_MAX_UPDATE); final long startTimestamp = actionParameters.getLong(KEY_START_TIMESTAMP); // Check with the sync manager if any conflicting updates have been made to databases final SyncManager syncManager = DataModel.get().getSyncManager(); final boolean orphan = !syncManager.isSyncing(upperBoundTimeMillis); // lastTimestampMillis used to indicate failure if (orphan) { // This batch does not match current in progress timestamp. LogUtil.w(TAG, "SyncMessagesAction: Ignoring orphan sync batch for messages from " + lowerBoundTimeMillis + " to " + upperBoundTimeMillis); } else { final boolean dirty = syncManager.isBatchDirty(lastTimestampMillis); if (lastTimestampMillis == SYNC_FAILED) { LogUtil.e(TAG, "SyncMessagesAction: Sync failed - terminating"); // Failed - update last sync times to throttle our failure rate final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); // Save sync completion time so next sync will start from here prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp); // Remember last full sync so that don't start background full sync right away prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp); syncManager.complete(); } else if (dirty) { LogUtil.w(TAG, "SyncMessagesAction: Redoing dirty sync batch of messages from " + lowerBoundTimeMillis + " to " + upperBoundTimeMillis); // Redo this batch final SyncMessagesAction nextBatch = new SyncMessagesAction(lowerBoundTimeMillis, upperBoundTimeMillis, maxMessagesToUpdate, startTimestamp); syncManager.startSyncBatch(upperBoundTimeMillis); requestBackgroundWork(nextBatch); } else { // Succeeded final ArrayList smsToAdd = response.getParcelableArrayList(BUNDLE_KEY_SMS_MESSAGES); final ArrayList mmsToAdd = response.getParcelableArrayList(BUNDLE_KEY_MMS_MESSAGES); final ArrayList messagesToDelete = response.getParcelableArrayList(BUNDLE_KEY_MESSAGES_TO_DELETE); final int messagesUpdated = smsToAdd.size() + mmsToAdd.size() + messagesToDelete.size(); // Perform local database changes in one transaction long txnTimeMillis = 0; if (messagesUpdated > 0) { final long startTimeMillis = SystemClock.elapsedRealtime(); final SyncMessageBatch batch = new SyncMessageBatch(smsToAdd, mmsToAdd, messagesToDelete, syncManager.getThreadInfoCache()); batch.updateLocalDatabase(); final long endTimeMillis = SystemClock.elapsedRealtime(); txnTimeMillis = endTimeMillis - startTimeMillis; LogUtil.i(TAG, "SyncMessagesAction: Updated local database " + "(took " + txnTimeMillis + " ms). Added " + smsToAdd.size() + " SMS, added " + mmsToAdd.size() + " MMS, deleted " + messagesToDelete.size() + " messages."); // TODO: Investigate whether we can make this more fine-grained. MessagingContentProvider.notifyEverythingChanged(); } else { if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: No local database updates to make"); } if (!syncManager.getHasFirstSyncCompleted()) { // If we have never completed a sync before (fresh install) and there are // no messages, still inform the UI of a change so it can update syncing // messages shown to the user MessagingContentProvider.notifyConversationListChanged(); MessagingContentProvider.notifyPartsChanged(); } } // Determine if there are more messages that need to be scanned if (lastTimestampMillis >= 0 && lastTimestampMillis >= lowerBoundTimeMillis) { if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { LogUtil.d(TAG, "SyncMessagesAction: More messages to sync; scheduling next " + "sync batch now."); } // Include final millisecond of last sync in next sync final long newUpperBoundTimeMillis = lastTimestampMillis + 1; final int newMaxMessagesToUpdate = nextBatchSize(messagesUpdated, txnTimeMillis); final SyncMessagesAction nextBatch = new SyncMessagesAction(lowerBoundTimeMillis, newUpperBoundTimeMillis, newMaxMessagesToUpdate, startTimestamp); // Proceed with next batch syncManager.startSyncBatch(newUpperBoundTimeMillis); requestBackgroundWork(nextBatch); } else { final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); // Save sync completion time so next sync will start from here prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, startTimestamp); if (lowerBoundTimeMillis < 0) { // Remember last full sync so that don't start another full sync right away prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, startTimestamp); } final long now = System.currentTimeMillis(); // After any sync check if new messages have arrived final SyncCursorPair recents = new SyncCursorPair(startTimestamp, now); final SyncCursorPair olders = new SyncCursorPair(-1L, startTimestamp); final DatabaseWrapper db = DataModel.get().getDatabase(); if (!recents.isSynchronized(db)) { LogUtil.i(TAG, "SyncMessagesAction: Changed messages after sync; " + "scheduling an incremental sync now."); // Just add a new batch for recent messages final SyncMessagesAction nextBatch = new SyncMessagesAction(startTimestamp, now, 0, startTimestamp); syncManager.startSyncBatch(now); requestBackgroundWork(nextBatch); // After partial sync verify sync state } else if (lowerBoundTimeMillis >= 0 && !olders.isSynchronized(db)) { // Add a batch going back to start of time LogUtil.w(TAG, "SyncMessagesAction: Changed messages before sync batch; " + "scheduling a full sync now."); final SyncMessagesAction nextBatch = new SyncMessagesAction(-1L, startTimestamp, 0, startTimestamp); syncManager.startSyncBatch(startTimestamp); requestBackgroundWork(nextBatch); } else { LogUtil.i(TAG, "SyncMessagesAction: All messages now in sync"); // All done, in sync syncManager.complete(); } } // Either sync should be complete or we should have a follow up request Assert.isTrue(hasBackgroundActions() || !syncManager.isSyncing()); } } return null; } /** * Decide the next batch size based on the stats we collected with past batch * @param messagesUpdated number of messages updated in this batch * @param txnTimeMillis time the transaction took in ms * @return Target number of messages to sync for next batch */ private static int nextBatchSize(final int messagesUpdated, final long txnTimeMillis) { final BugleGservices bugleGservices = BugleGservices.get(); final long smsSyncSubsequentBatchTimeLimitMillis = bugleGservices.getLong( BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS, BugleGservicesKeys.SMS_SYNC_BATCH_TIME_LIMIT_MILLIS_DEFAULT); if (txnTimeMillis <= 0) { return 0; } // Number of messages we can sync within the batch time limit using // the average sync time calculated based on the stats we collected // in previous batch return (int) ((double) (messagesUpdated) / (double) txnTimeMillis * smsSyncSubsequentBatchTimeLimitMillis); } /** * Batch loading MMS parts for the messages in current batch */ private void loadMmsParts(final LongSparseArray mmses) { final Context context = Factory.get().getApplicationContext(); final int totalIds = mmses.size(); for (int start = 0; start < totalIds; start += MmsUtils.MAX_IDS_PER_QUERY) { final int end = Math.min(start + MmsUtils.MAX_IDS_PER_QUERY, totalIds); //excluding final int count = end - start; final String batchSelection = String.format( Locale.US, "%s != '%s' AND %s IN %s", Mms.Part.CONTENT_TYPE, ContentType.APP_SMIL, Mms.Part.MSG_ID, MmsUtils.getSqlInOperand(count)); final String[] batchSelectionArgs = new String[count]; for (int i = 0; i < count; i++) { batchSelectionArgs[i] = Long.toString(mmses.valueAt(start + i).getId()); } final Cursor cursor = SqliteWrapper.query( context, context.getContentResolver(), MmsUtils.MMS_PART_CONTENT_URI, DatabaseMessages.MmsPart.PROJECTION, batchSelection, batchSelectionArgs, null/*sortOrder*/); if (cursor != null) { try { while (cursor.moveToNext()) { // Delay loading the media content for parsing for efficiency // TODO: load the media and fill in the dimensions when // we actually display it final DatabaseMessages.MmsPart part = DatabaseMessages.MmsPart.get(cursor, false/*loadMedia*/); final DatabaseMessages.MmsMessage mms = mmses.get(part.mMessageId); if (mms != null) { mms.addPart(part); } } } finally { cursor.close(); } } } } /** * Batch loading MMS sender for the messages in current batch */ private void setMmsSenders(final LongSparseArray mmses, final ThreadInfoCache cache) { // Store all the MMS messages for (int i = 0; i < mmses.size(); i++) { final MmsMessage mms = mmses.valueAt(i); final boolean isOutgoing = mms.mType != Mms.MESSAGE_BOX_INBOX; String senderId = null; if (!isOutgoing) { // We only need to find out sender phone number for received message senderId = getMmsSender(mms, cache); if (senderId == null) { LogUtil.w(TAG, "SyncMessagesAction: Could not find sender of incoming MMS " + "message " + mms.getUri() + "; using 'unknown sender' instead"); senderId = ParticipantData.getUnknownSenderDestination(); } } mms.setSender(senderId); } } /** * Find out the sender of an MMS message */ private String getMmsSender(final MmsMessage mms, final ThreadInfoCache cache) { final List recipients = cache.getThreadRecipients(mms.mThreadId); Assert.notNull(recipients); Assert.isTrue(recipients.size() > 0); if (recipients.size() == 1 && recipients.get(0).equals(ParticipantData.getUnknownSenderDestination())) { LogUtil.w(TAG, "SyncMessagesAction: MMS message " + mms.mUri + " has unknown sender " + "(thread id = " + mms.mThreadId + ")"); } return MmsUtils.getMmsSender(recipients, mms.mUri); } private SyncMessagesAction(final Parcel in) { super(in); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SyncMessagesAction createFromParcel(final Parcel in) { return new SyncMessagesAction(in); } @Override public SyncMessagesAction[] newArray(final int size) { return new SyncMessagesAction[size]; } }; @Override public void writeToParcel(final Parcel parcel, final int flags) { writeActionToParcel(parcel, flags); } }