Android Open Source - android_sms_backup Sms Sync Service






From Project

Back to project page android_sms_backup.

License

The source code is released under:

Apache License

If you think the Android project android_sms_backup listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/* Copyright (c) 2009 Christoph Studer <chstuder@gmail.com>
 */*w  w w  .j a v  a  2  s. c  om*/
 * 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 tv.studer.smssync;

import java.net.URLEncoder;
import java.util.List;

import tv.studer.smssync.CursorToMessage.ConversionResult;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.os.PowerManager.WakeLock;
import android.util.Log;

import com.android.email.mail.Folder;
import com.android.email.mail.Message;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Folder.FolderType;
import com.android.email.mail.Folder.OpenMode;
import com.android.email.mail.store.ImapStore;

public class SmsSyncService extends Service {

    /** Number of messages sent per sync request. */
    private static final int MAX_MSG_PER_REQUEST = 1;
    
    /** Flag indicating whether this service is already running. */
    // Should this be split into sIsRunning and sIsWorking? One for the
    // service, the other for the actual backing up?
    private static boolean sIsRunning = false;

    // State information
    /** Current state. See {@link #getState()}. */
    private static SmsSyncState sState = SmsSyncState.IDLE;

    /**
     * Number of messages that currently need a sync. Only valid when sState ==
     * SYNC.
     */
    private static int sItemsToSync;

    /**
     * Number of messages already synced during this cycle. Only valid when
     * sState == SYNC.
     */
    private static int sCurrentSyncedItems;

    /**
     * Field containing a description of the last error. See
     * {@link #getErrorDescription()}.
     */
    private static String sLastError;

    /**
     * This {@link StateChangeListener} is notified whenever {@link #sState} is
     * updated.
     */
    private static StateChangeListener sStateChangeListener;

    /**
     * A wakelock held while this service is working.
     */
    private static WakeLock sWakeLock;
    
    /**
     * A wifilock held while this service is working.
     */
    private static WifiLock sWifiLock;
    
    /**
     * Indicates that the user canceled the current backup and that this service
     * should finish working ASAP.
     */
    private static boolean sCanceled;
    
    public enum SmsSyncState {
        IDLE, CALC, LOGIN, SYNC, AUTH_FAILED, GENERAL_ERROR, CANCELED;
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }
    
    private static void acquireWakeLock(Context ctx) {
        if (sWakeLock == null) {
            PowerManager pMgr = (PowerManager) ctx.getSystemService(POWER_SERVICE);
            sWakeLock = pMgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    "SmsSyncService.sync() wakelock.");
            
            WifiManager wMgr = (WifiManager) ctx.getSystemService(WIFI_SERVICE);
            sWifiLock = wMgr.createWifiLock("SMS Backup");
        }
        sWakeLock.acquire();
        sWifiLock.acquire();
    }
    
    private static void releaseWakeLock(Context ctx) {
        sWakeLock.release();
        sWifiLock.release();
    }
    
    @Override
    //TODO(chstuder): Clean this flow up a bit and split it into multiple
    // methods. Make clean distinction between onStart(...) and backup(...).
    public void onStart(final Intent intent, int startId) {
        super.onStart(intent, startId);
        
        synchronized (this.getClass()) {
            // Only start a sync if there's no other sync going on at this time.
            if (!sIsRunning) {
                acquireWakeLock(this);
                sIsRunning = true;
                // Start sync in new thread.
                new Thread() {
                    public void run() {
                        // Lower thread priority a little. We're not the UI.
                        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                        try {
                            // On first sync we need to know whether to skip or
                            // sync current messages.
                            if (PrefStore.isFirstSync(SmsSyncService.this)
                                    && !intent.hasExtra(Consts.KEY_SKIP_MESSAGES)) {
                                throw new GeneralErrorException(SmsSyncService.this,
                                        R.string.err_first_sync_needs_skip_flag, null);
                            }
     /**
 <h2>Sync or skip?</h2>
     * <p>
     * <code>skipMessages</code>: If this parameter is <code>true</code>, all
     * current messages stored on the device are skipped and marked as "synced".
     * Future backups will ignore these messages and only messages arrived
     * afterwards will be sent to the server.
     * </p>
     * 
     * @param skipMessages whether to skip all messages on this device.
*/
                            if (intent.getBooleanExtra(Consts.KEY_SKIP_MESSAGES, false) {
               // Only update the max synced ID, do not really sync.
                    updateMaxSyncedDate(getMaxItemDate());
                    PrefStore.setLastSync(this);
                       sItemsToSync = 0;
                   sCurrentSyncedItems = 0;
                   updateState(SmsSyncState.IDLE);
                   Log.i(Consts.TAG, "All messages skipped.");
          } else {
                            int numRetries = intent.getIntExtra(Consts.KEY_NUM_RETRIES, 0);
                            GeneralErrorException lastException = null;
                            
                            // Try sync numRetries + 1 times.
                            while (numRetries >= 0) {
                                try {
                                    backup();
                                    break;
                                } catch (GeneralErrorException e) {
                                    Log.w(Consts.TAG, e.getMessage());
                                    Log.i(Consts.TAG, "Retrying sync in 2 seconds. (" + (numRetries - 1) +  ")");
                                    lastException = e;
                                    if (numRetries > 1) {
                                        try {
                                            Thread.sleep(2000);
                                        } catch (InterruptedException e1) { /* ignore */ }
                                    }
                                }
                                numRetries--;
                            }
                            if (lastException != null) {
                                throw lastException;
                            }
        } // else { } for skipMessages
                        } catch (GeneralErrorException e) {
                            Log.i(Consts.TAG, "", e);
                            sLastError = e.getLocalizedMessage();
                            updateState(SmsSyncState.GENERAL_ERROR);
                        } catch (AuthenticationErrorException e) {
                            Log.i(Consts.TAG, "", e);
                            sLastError = e.getLocalizedMessage();
                            updateState(SmsSyncState.AUTH_FAILED);
                        } finally {
                            stopSelf();
                            Alarms.scheduleRegularSync(SmsSyncService.this);
                            sIsRunning = false;
                            releaseWakeLock(SmsSyncService.this);
                        }
                    }
                }.start();
            } else {
                Log.d(Consts.TAG, "SmsSyncService.onStart(): Already running.");
            }
        }
    }

    /**
     * <p>
     * This is the main method that defines the general flow for a
     * synchronization.
     * </p>
     * <h2>Typical flow</h2>
     * <p>
     * This is a typical sync flow:
     * </p>
     * <ol>
     * <li>{@link SmsSyncState#CALC}: The list of messages requiring a sync is
     * determined. This is done by querying the SMS content provider for
     * messages with
     * <code>ID > {@link #getMaxSyncedDate()} AND type != {@link #MESSAGE_TYPE_DRAFT}</code>
     * .</li>
     * <li>{@link SmsSyncState#LOGIN}: An SSL connection is opened to the Gmail IMAP
     * server using the user provided credentials.</li>
     * <li>{@link SmsSyncState#SYNC}: The messages determined in step #1 are
     * sent to the server, possibly in chunks of a maximum of
     * {@link #MAX_MSG_PER_REQUEST} per request. After each successful sync
     * request, the maximum ID of synced messages is updated such that future
     * syncs will skip.</li>
     * <li>{@link SmsSyncState#CANCELED}: If {@link #cancel()} was called during
     * backup, the backup will stop at the next possible occasion.</li>
     * </ol>
     * 
     * <h2>Preconditions</h2>
     * <p>
     * This method requires the login information to be set. If either username
     * or password are unset, a {@link GeneralErrorException} is thrown.
     * </p>
     * 
     * @throws GeneralErrorException Thrown when there there was an error during
     *             sync.
     */
    private void backup() throws GeneralErrorException,
            AuthenticationErrorException {
        Log.i(Consts.TAG, "Starting backup...");
        sCanceled = false;

        if (!PrefStore.isLoginInformationSet(this)) {
            throw new GeneralErrorException(this, R.string.err_sync_requires_login_info, null);
        }

        String username = PrefStore.getLoginUsername(this);
        String password = PrefStore.getLoginPassword(this);

        updateState(SmsSyncState.CALC);

        sItemsToSync = 0;
        sCurrentSyncedItems = 0;
        
        Cursor items = getItemsToSync();
        int maxItemsPerSync = PrefStore.getMaxItemsPerSync(this);
        sItemsToSync = Math.min(items.getCount(), maxItemsPerSync);
        Log.d(Consts.TAG, "Total messages to backup: " + sItemsToSync);
        if (sItemsToSync == 0) {
            PrefStore.setLastSync(this);
            if (PrefStore.isFirstSync(this)) {
                // If this is the first backup we need to write something to PREF_MAX_SYNCED_DATE
                // such that we know that we've performed a backup before.
                PrefStore.setMaxSyncedDate(this, PrefStore.DEFAULT_MAX_SYNCED_DATE);
            }
            updateState(SmsSyncState.IDLE);
            Log.d(Consts.TAG, "Nothing to do.");
            return;
        }

        updateState(SmsSyncState.LOGIN);

        ImapStore imapStore;
        Folder folder;
        boolean folderExists;
        String label = PrefStore.getImapFolder(this);
        try {
            imapStore = new ImapStore(String.format(Consts.IMAP_URI, URLEncoder.encode(username),
                    URLEncoder.encode(password).replace("+", "%20")));
            folder = imapStore.getFolder(label);
            folderExists = folder.exists();
            if (!folderExists) {
                Log.i(Consts.TAG, "Label '" + label + "' does not exist yet. Creating.");
                folder.create(FolderType.HOLDS_MESSAGES);
            }
            folder.open(OpenMode.READ_WRITE);
        } catch (MessagingException e) {
            throw new AuthenticationErrorException(e);
        }
        
        CursorToMessage converter = new CursorToMessage(this, username);
        try {
            while (true) {
                // Cancel sync if requested by the user.
                if (sCanceled) {
                    Log.i(Consts.TAG, "Backup canceled by user.");
                    updateState(SmsSyncState.CANCELED);
                    break;
                }
                updateState(SmsSyncState.SYNC);
                ConversionResult result = converter.cursorToMessageArray(items,
                        MAX_MSG_PER_REQUEST);
                List<Message> messages = result.messageList;
                // Stop the sync if all items where uploaded or if the maximum number
                // of messages per sync was uploaded.
                if (messages.size() == 0
                        || sCurrentSyncedItems >= maxItemsPerSync) {
                    Log.i(Consts.TAG, "Sync done: " + sCurrentSyncedItems + " items uploaded.");
                    PrefStore.setLastSync(SmsSyncService.this);
                    updateState(SmsSyncState.IDLE);
                    folder.close(true);
                    break;
                }

                Log.d(Consts.TAG, "Sending " + messages.size() + " messages to server.");
                folder.appendMessages(messages.toArray(new Message[messages.size()]));
                sCurrentSyncedItems += messages.size();
                updateState(SmsSyncState.SYNC);
                updateMaxSyncedDate(result.maxDate);
                result = null;
                messages = null;
            }
        } catch (MessagingException e) {
            throw new GeneralErrorException(this, R.string.err_communication_error, e);
        } finally {
            items.close();
        }
    }

    /**
     * Returns a cursor of SMS messages that have not yet been synced with the
     * server. This includes all messages with
     * <code>date &lt; {@link #getMaxSyncedDate()}</code> which are no drafs.
     */
    private Cursor getItemsToSync() {
        ContentResolver r = getContentResolver();
        String selection = String.format("%s > ? AND %s <> ?",
                SmsConsts.DATE, SmsConsts.TYPE);
        String[] selectionArgs = new String[] {
                String.valueOf(getMaxSyncedDate()), String.valueOf(SmsConsts.MESSAGE_TYPE_DRAFT)
        };
        String sortOrder = SmsConsts.DATE + " LIMIT " + PrefStore.getMaxItemsPerSync(this);
        return r.query(Uri.parse("content://sms"), null, selection, selectionArgs, sortOrder);
    }

    /**
     * Returns the maximum date of all SMS messages (except for drafts).
     */
    private long getMaxItemDate() {
        ContentResolver r = getContentResolver();
        String selection = SmsConsts.TYPE + " <> ?";
        String[] selectionArgs = new String[] {
            String.valueOf(SmsConsts.MESSAGE_TYPE_DRAFT)
        };
        String[] projection = new String[] {
            SmsConsts.DATE
        };
        Cursor result = r.query(Uri.parse("content://sms"), projection, selection, selectionArgs,
                SmsConsts.DATE + " DESC LIMIT 1");

        try
        {
            if (result.moveToFirst()) {
                return result.getLong(0);
            } else {
                return PrefStore.DEFAULT_MAX_SYNCED_DATE;
            }
        }
        catch (RuntimeException e)
        {
            result.close();
            throw e;
        }
    }

    /**
     * Returns the largest date of all messages that have successfully been synced
     * with the server.
     */
    private long getMaxSyncedDate() {
        return PrefStore.getMaxSyncedDate(this);
    }

    /**
     * Persists the provided ID so it can later on be retrieved using
     * {@link #getMaxSyncedDate()}. This should be called when after each
     * successful sync request to a server.
     * 
     * @param maxSyncedId
     */
    private void updateMaxSyncedDate(long maxSyncedDate) {
        PrefStore.setMaxSyncedDate(this, maxSyncedDate);
        Log.d(Consts.TAG, "Max synced date set to: " + maxSyncedDate);
    }

    // Actions available from other classes.
    
    /**
     * Cancels the current ongoing backup.
     * 
     * TODO(chstuder): Clean up this interface a bit. It's strange the backup is
     * started by an intent but canceling is done through a static method.
     * 
     * But all other alternatives seem strange too. An intent just to cancel a backup?
     */
    static void cancel() {
        if (SmsSyncService.sIsRunning) {
            SmsSyncService.sCanceled = true;
        }
    }
    
    // Statistics accessible from other classes.

    /**
     * Returns whether there is currently a backup going on or not.
     * 
     */
    static boolean isWorking() {
        return sIsRunning;
    }
    
    /**
     * Returns the current state of the service. Also see
     * {@link #setStateChangeListener(StateChangeListener)} to get notified when
     * the state changes.
     */
    static SmsSyncState getState() {
        return sState;
    }

    /**
     * Returns a description of the last error. Only valid if
     * <code>{@link #getState()} == {@link SmsSyncState#GENERAL_ERROR}</code>.
     */
    static String getErrorDescription() {
        return (sState == SmsSyncState.GENERAL_ERROR) ? sLastError : null;
    }

    /**
     * Returns the number of messages that require sync during the current
     * cycle.
     */
    static int getItemsToSyncCount() {
        return sItemsToSync;
    }

    /**
     * Returns the number of already synced messages during the current cycle.
     */
    static int getCurrentSyncedItems() {
        return sCurrentSyncedItems;
    }

    /**
     * Registers a {@link StateChangeListener} that is notified whenever the
     * state of the service changes. Note that at most one listener can be
     * registered and you need to call {@link #unsetStateChangeListener()} in
     * between calls to this method.
     * 
     * @see #getState()
     * @see #unsetStateChangeListener()
     */
    static void setStateChangeListener(StateChangeListener listener) {
        if (sStateChangeListener != null) {
            throw new IllegalStateException("setStateChangeListener(...) called when there"
                    + " was still some other listener "
                    + "registered. Use unsetStateChangeListener() first.");
        }
        sStateChangeListener = listener;
    }

    /**
     * Unregisters the currently registered {@link StateChangeListener}.
     * 
     * @see #setStateChangeListener(StateChangeListener)
     */
    static void unsetStateChangeListener() {
        sStateChangeListener = null;
    }

    /**
     * Internal method that needs to be called whenever the state of the service
     * changes.
     */
    private static void updateState(SmsSyncState newState) {
        SmsSyncState old = sState;
        sState = newState;
        if (sStateChangeListener != null) {
            sStateChangeListener.stateChanged(old, newState);
        }
    }

    /**
     * A state change listener interface that provides a callback that is called
     * whenever the state of the {@link SmsSyncService} changes.
     * 
     * @see SmsSyncService#setStateChangeListener(StateChangeListener)
     */
    public interface StateChangeListener {
        /**
         * Called whenever the sync state of the service changed.
         */
        public void stateChanged(SmsSyncState oldState, SmsSyncState newState);
    }

    /**
     * Exception indicating an error while synchronizing.
     */
    public static class GeneralErrorException extends Exception {
        private static final long serialVersionUID = 1L;

        public GeneralErrorException(String msg, Throwable t) {
            super(msg, t);
        }
        
        public GeneralErrorException(Context ctx, int msgId, Throwable t) {
            super(ctx.getString(msgId), t);
        }
    }
    
    public static class AuthenticationErrorException extends Exception {
        private static final long serialVersionUID = 1L;

        public AuthenticationErrorException(Throwable t) {
            super(t.getLocalizedMessage(), t);
        }
    }

}




Java Source Code List

tv.studer.smssync.Alarms.java
tv.studer.smssync.Consts.java
tv.studer.smssync.CursorToMessage.java
tv.studer.smssync.PrefStore.java
tv.studer.smssync.SmsBroadcastReceiver.java
tv.studer.smssync.SmsConsts.java
tv.studer.smssync.SmsSyncService.java
tv.studer.smssync.SmsSync.java