All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.java.com.cloudant.sync.replication.PeriodicReplicationService Maven / Gradle / Ivy

There is a newer version: 2.4.1
Show newest version
/*
 * Copyright © 2016 IBM Corp. All rights reserved.
 *
 * 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.cloudant.sync.replication;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

/**
 * This {@link android.app.Service} is an abstract class that is the basis for creating a service
 * that performs periodic replications (i.e. replications that occur at regular intervals). The
 * period between replications may be varied depending on whether other application components
 * are bound to the service or not, so as to allow for more frequent replications when an app
 * is in active use and less frequent replications the rest of the time.
 *
 * @param  The {@link PeriodicReplicationReceiver} associated with this Service that is
 *           responsible for handling the alarms triggered by the {@link AlarmManager} at
 *           the intervals when replication is required and handles resetting of alarms after
 *           reboot of the device.
 */
public abstract class PeriodicReplicationService
    extends ReplicationService {

    /* Name of the SharedPreferences file used to store alarm times. We store the alarm
     * times in preferences so we can reset the alarms as accurately as possible after reboot
     * and so we can adjust alarm times when components bind to or unbind from this Service.
     * So that multiple PeriodicReplicationService instances can be used in one application without
     * interfering with each other, the preferences in this file are stored using keys prefixed with
     * the name of the concrete class implementing the PeriodicReplicationService. */
    static final String PREFERENCES_FILE_NAME = "com.cloudant.preferences";

    /* We store the elapsed time since booting at which the last alarm occurred in SharedPreferences
     * using a key with this suffix. This is used to set the initial alarm time when periodic
     * replications are started. */
    private static final String LAST_ALARM_ELAPSED_TIME_SUFFIX = ".lastAlarmElapsed";

    /* We store the wall-clock time at which the last alarm occurred in SharedPreferences
     * using a key with this suffix. This is used to set the initial alarm after a reboot. */
    private static final String LAST_ALARM_CLOCK_TIME_SUFFIX = ".lastAlarmClock";

    /* We store a flag indicating whether periodic replications are enabled in SharedPreferences
     * using a key with this suffix. We have to store the flag persistently as the service may be
     * stopped and started by the operating system. */
    private static final String PERIODIC_REPLICATION_ENABLED_SUFFIX
        = ".periodicReplicationsActive";

    /* We store a flag indicating whether periodic replications were explicitly stopped in
     * SharedPreferences using a key with this suffix. We have to store the flag persistently as
     * the service may be stopped and started by the operating system. */
    private static final String EXPLICITLY_STOPPED_SUFFIX = ".explicitlyStopped";

    /* We store a flag indicating whether there are replications pending in
     * SharedPreferences using a key with this suffix. Replications may be pending because they
     * are currently in progress and have not yet completed, or becasue a previous scheduled
     * replication didn't take place because the conditions for replication were not met. We have
     * to store the flag persistently as the service may be stopped and started by the operating
     * system. */
    private static final String REPLICATIONS_PENDING_SUFFIX = ".replicationsPending";

    private static final long MILLISECONDS_IN_SECOND = 1000L;

    /**
     * To start periodic replications, this value should be passed to this service in an Intent
     * using an Extra with the {@link #EXTRA_COMMAND} key.
     * @see
     * 
     *     Replication Policy User Guide
     */
    public static final int COMMAND_START_PERIODIC_REPLICATION = 2;

    /**
     * To stop periodic replications, this value should be passed to this service in an Intent
     * using an Extra with the {@link #EXTRA_COMMAND} key.
     * @see
     * 
     *     Replication Policy User Guide
     */
    public static final int COMMAND_STOP_PERIODIC_REPLICATION = 3;

    /**
     * When the device is rebooted, this value should be passed to this service in an Intent
     * using an Extra with the {@link #EXTRA_COMMAND} key so that replications can be setup again
     * following a reboot.
     * @see
     * 
     *     Replication Policy User Guide
     */
    public static final int COMMAND_DEVICE_REBOOTED = 4;

    /**
     * When the period between replications has been altered, this value should be passed to this
     * service in an Intent using an Extra with the {@link #EXTRA_COMMAND} key so that the interval
     * between replications can be re-evaluated.
     * @see
     * 
     *     Replication Policy User Guide
     */
    public static final int COMMAND_RESET_REPLICATION_TIMERS = 5;

    private static final String TAG = "PRS";

    private SharedPreferences mPrefs;
    Class clazz;
    protected boolean mBound;

    protected PeriodicReplicationService(Class clazz) {
        this.clazz = clazz;
    }

    /**
     * If the stored preferences are in the old format, upgrade them to the new format so that
     * the app continues to work after upgrade to this version.
     */
    protected void upgradePreferences() {
        String alarmDueElapsed = "com.cloudant.sync.replication.PeriodicReplicationService.alarmDueElapsed";
        if (mPrefs.contains(alarmDueElapsed)) {
            // These are old style preferences. We need to rewrite them in the new form that allows
            // multiple replication policies.
            String alarmDueClock = "com.cloudant.sync.replication.PeriodicReplicationService.alarmDueClock";
            String replicationsActive = "com.cloudant.sync.replication.PeriodicReplicationService.periodicReplicationsActive";
            long elapsed = mPrefs.getLong(alarmDueElapsed, 0);
            long clock = mPrefs.getLong(alarmDueClock, 0);
            boolean enabled = mPrefs.getBoolean(replicationsActive, false);

            SharedPreferences.Editor editor = mPrefs.edit();
            editor.putLong(constructKey(LAST_ALARM_ELAPSED_TIME_SUFFIX),
                elapsed - (getIntervalInSeconds() * MILLISECONDS_IN_SECOND));
            editor.putLong(constructKey(LAST_ALARM_CLOCK_TIME_SUFFIX),
                clock - (getIntervalInSeconds() * MILLISECONDS_IN_SECOND));
            editor.putBoolean(constructKey(PERIODIC_REPLICATION_ENABLED_SUFFIX), enabled);

            editor.remove(alarmDueElapsed);
            editor.remove(alarmDueClock);
            editor.remove(replicationsActive);

            editor.apply();
        }
    }

    protected class ServiceHandler extends ReplicationService.ServiceHandler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            Intent intent = msg.getData().getParcelable(EXTRA_INTENT);

            switch (msg.arg2) {
                case COMMAND_START_PERIODIC_REPLICATION:
                    startPeriodicReplication();
                    releaseWakeLock(intent);
                    stopSelf(msg.arg1);
                    break;
                case COMMAND_STOP_PERIODIC_REPLICATION:
                    stopPeriodicReplication();
                    setExplicitlyStopped(true);
                    releaseWakeLock(intent);
                    stopSelf(msg.arg1);
                    break;
                case COMMAND_DEVICE_REBOOTED:
                    resetAlarmDueTimesOnReboot();
                    releaseWakeLock(intent);
                    stopSelf(msg.arg1);
                    break;
                case COMMAND_RESET_REPLICATION_TIMERS:
                    restartPeriodicReplications();
                    releaseWakeLock(intent);
                    stopSelf(msg.arg1);
                    break;
                default:
                    // Do nothing
                    break;
            }

            super.handleMessage(msg);
        }
    }

    @Override
    protected Handler getHandler(Looper looper) {
        return new ServiceHandler(looper);
    }

    @Override
    public void onCreate() {
        mPrefs = getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
        upgradePreferences();
        super.onCreate();
    }

    @Override
    public synchronized IBinder onBind(Intent intent) {
        mBound = true;
        if (isPeriodicReplicationEnabled()) {
            restartPeriodicReplications();
        } else if (startReplicationOnBind()) {
            startPeriodicReplication();
        }
        return super.onBind(intent);
    }

    @Override
    public synchronized boolean onUnbind(Intent intent) {
        super.onUnbind(intent);
        mBound = false;
        if (isPeriodicReplicationEnabled()) {
            restartPeriodicReplications();
        }
        // Ensure onRebind is called when new clients bind to the service.
        return true;
    }

    @Override
    public synchronized void onRebind(Intent intent) {
        super.onRebind(intent);
        mBound = true;
        if (isPeriodicReplicationEnabled()) {
            restartPeriodicReplications();
        } else if (startReplicationOnBind()) {
            startPeriodicReplication();
        }
    }

    @Override
    protected void startReplications() {
        super.startReplications();
        setLastAlarmTime(0);
        setReplicationsPending(this, getClass(), true);
    }

    /** Start periodic replications. */
    public synchronized void startPeriodicReplication() {
        if (!isPeriodicReplicationEnabled()) {
            setPeriodicReplicationEnabled(true);
            AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
            Intent alarmIntent = new Intent(this, clazz);
            alarmIntent.setAction(PeriodicReplicationReceiver.ALARM_ACTION);
            // We need to use a BroadcastReceiver rather than sending the Intent directly to the
            // Service to ensure the device wakes up if it's asleep. Sending the Intent directly
            // to the Service would be unreliable.
            PendingIntent pendingAlarmIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, 0);

            long initialTriggerTime;
            if (explicitlyStopped()) {
                // Replications were explicitly stopped, so we want the first replication to
                // happen immediately.
                initialTriggerTime = SystemClock.elapsedRealtime();
                setExplicitlyStopped(false);
            } else {
                // Replications were implicitly stopped (e.g. by rebooting the device), so we
                // want to resume the previous schedule.
                initialTriggerTime = getNextAlarmDueElapsedTime();
            }

            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                initialTriggerTime,
                getIntervalInSeconds() * MILLISECONDS_IN_SECOND,
                pendingAlarmIntent);
        } else {
            Log.i(TAG, "Attempted to start an already running alarm manager");
        }
    }

    /** Stop replications currently in progress and cancel future scheduled replications. */
    public synchronized void stopPeriodicReplication() {
        if (isPeriodicReplicationEnabled()) {
            setPeriodicReplicationEnabled(false);
            AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
            Intent alarmIntent = new Intent(this, clazz);
            alarmIntent.setAction(PeriodicReplicationReceiver.ALARM_ACTION);
            PendingIntent pendingAlarmIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, 0);

            alarmManager.cancel(pendingAlarmIntent);

            stopReplications();
        } else {
            Log.i(TAG, "Attempted to stop an already stopped alarm manager");
        }
    }

    /** Stop and restart periodic replication. */
    final protected void restartPeriodicReplications() {
        stopPeriodicReplication();
        startPeriodicReplication();
    }

    /**
     * Store the time the alarm last fired, both as elapsed time since boot (using
     * {@link SystemClock#elapsedRealtime()}) and as standard "wall" clock time (using
     * {@link System#currentTimeMillis()}. We generally want to set our alarms based on the elapsed
     * time since booting as that is not affected by the system clock being reset. However, we use
     * the clock time to set the alarm after a reboot as clearly in this case the time since boot is
     * useless.
     * @param millisBeforeNow The number of milliseconds before the current time at which to record
     *                        that the alarm should have last fired.
     */
    private void setLastAlarmTime(long millisBeforeNow) {
        SharedPreferences.Editor editor = mPrefs.edit();
        editor.putLong(constructKey(LAST_ALARM_ELAPSED_TIME_SUFFIX),
            SystemClock.elapsedRealtime() - millisBeforeNow);
        editor.putLong(constructKey(LAST_ALARM_CLOCK_TIME_SUFFIX),
            System.currentTimeMillis() - millisBeforeNow);
        editor.apply();
    }

    /**
     * @return The time since the device was booted at which the next periodic replication should
     * begin, calculated by adding the replication interval to the SharedPreferences value
     * storing the time since device boot at which the last periodic replication began.
     */
    private long getNextAlarmDueElapsedTime() {
        return mPrefs.getLong(constructKey(LAST_ALARM_ELAPSED_TIME_SUFFIX), 0)
            + (getIntervalInSeconds() * MILLISECONDS_IN_SECOND);
    }

    /**
     * @return The wall clock time at which the next periodic replication should begin, calculated
     * by adding the replication interval to the SharedPreferences value storing the wall clock time
     * at which the last periodic replication began.
     */
    private long getNextAlarmDueClockTime() {
        return mPrefs.getLong(constructKey(LAST_ALARM_CLOCK_TIME_SUFFIX), 0)
            + (getIntervalInSeconds() * MILLISECONDS_IN_SECOND);
    }

    /**
     * Set a flag in SharedPreferences to indicate whether periodic replications are enabled.
     * @param running true to indicate that periodic replications are enabled, otherwise false.
     */
    private void setPeriodicReplicationEnabled(boolean running) {
        SharedPreferences.Editor editor = mPrefs.edit();
        editor.putBoolean(constructKey(PERIODIC_REPLICATION_ENABLED_SUFFIX), running);
        editor.apply();
    }

    /**
     * @return The value of the flag stored in SharedPreferences indicating whether periodic
     * replications are currently enabled.
     */
    private boolean isPeriodicReplicationEnabled() {
        return mPrefs.getBoolean(constructKey(PERIODIC_REPLICATION_ENABLED_SUFFIX),
            false);
    }

    public static boolean isPeriodicReplicationEnabled(Context context,
                                                        Class  prsClass) {
        SharedPreferences prefs = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context
            .MODE_PRIVATE);
        return prefs.getBoolean(constructKey(prsClass, PERIODIC_REPLICATION_ENABLED_SUFFIX), false);
    }

    /**
     * Set a flag in SharedPreferences to indicate whether periodic replications were explicitly
     * stopped.
     * @param explicitlyStopped true to indicate that periodic replications were stopped
     *                          explicitly by the sending of COMMAND_STOP_PERIODIC_REPLICATION,
     *                          otherwise false.
     */
    private void setExplicitlyStopped(boolean explicitlyStopped) {
        SharedPreferences.Editor editor = mPrefs.edit();
        editor.putBoolean(constructKey(EXPLICITLY_STOPPED_SUFFIX), explicitlyStopped);
        editor.apply();
    }

    /**
     * @return The value of the flag stored in SharedPreferences indicating whether periodic
     * replications were explicitly stopped.
     */
    private boolean explicitlyStopped() {
        return mPrefs.getBoolean(constructKey(EXPLICITLY_STOPPED_SUFFIX), true);
    }

    /**
     * Reset the alarm times stored in SharedPreferences following a reboot of the device.
     * After a reboot, the AlarmManager must be setup again so that periodic replications will
     * occur following reboot.
     */
    private void resetAlarmDueTimesOnReboot() {
        // As the device has been rebooted, we use clock time rather than elapsed time since
        // booting to set check whether we missed any alarms while the device was off and to
        // make sure the next alarm time isn't too far in the future (indicating the system clock
        // has been reset).
        //
        // We subtract the current time from the next expected alarm time and then do the
        // following checks:
        // * If it's less than zero, that means we missed an alarm when the device was off, so
        //   we schedule a replication immediately by setting the last alarm time to a time
        //   getIntervalInSeconds() ago.
        // * If it's more than getIntervalInSeconds() in the future, that means the system clock
        //   has been reset since the last alarm was fired.  Therefore, we check that the initial
        //   interval for the alarm is no later than getIntervalInSeconds() after the
        //   current time so we minimise the impact of the system clock being reset and will at most
        //   have to wait for the normal interval time.
        // * Otherwise, the clock time for the alarm seems reasonable, but we still need to update
        //   the SharedPreference for the elapsed time since boot at which the last alarm would have
        //   fired as that currently refers to the time since boot from the previous boot of the
        //   device.
        //
        // We don't actually setup the AlarmManager here as it is up to the subclass to determine
        // if all other conditions for the replication policy are met and determine whether to
        // restart replications after a reboot.
        setPeriodicReplicationEnabled(false);
        long initialInterval = getNextAlarmDueClockTime() - System.currentTimeMillis();
        if (initialInterval < 0) {
            setLastAlarmTime(getIntervalInSeconds() * MILLISECONDS_IN_SECOND);
        } else if (initialInterval > getIntervalInSeconds() * MILLISECONDS_IN_SECOND) {
            setLastAlarmTime(0);
        } else {
            setLastAlarmTime((getIntervalInSeconds() * MILLISECONDS_IN_SECOND) - initialInterval);
        }
    }

    /**
     * @return The interval (in seconds) between replications depending on whether a component is
     * bound to the service or not.
     */
    private int getIntervalInSeconds() {
        if (mBound) {
            return getBoundIntervalInSeconds();
        } else {
            return getUnboundIntervalInSeconds();
        }
    }

    String constructKey(String suffix) {
        return constructKey(getClass(), suffix);
    }

    static String constructKey(Class prsClass,
                                       String suffix) {
        return prsClass.getName() + suffix;
    }

    @Override
    public void allReplicationsCompleted() {
        super.allReplicationsCompleted();
        setReplicationsPending(this, getClass(), false);
    }

    /**
     * Sets whether there are replications pending. This may be because replications are
     * currently in progress and have not yet completed, or because a previous scheduled
     * replication didn't take place because the conditions for replication were not met.
     * @param context
     * @param prsClass
     * @param pending true if there is a replication pending, or false otherwise.
     */
    public static void setReplicationsPending(Context context, Class prsClass, boolean pending) {
        SharedPreferences prefs = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context
            .MODE_PRIVATE);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean(constructKey(prsClass, REPLICATIONS_PENDING_SUFFIX), pending);
        editor.apply();
    }

    /**
     * Gets whether there are replications pending. Replications may be pending because they are
     * currently in progress and have not yet completed, or because a previous scheduled
     * replication didn't take place because the conditions for replication were not met.
     * @param context
     * @param prsClass
     * @return
     */
    public static boolean replicationsPending(Context context, Class prsClass) {
        SharedPreferences prefs = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context
            .MODE_PRIVATE);
        return prefs.getBoolean(constructKey(prsClass, REPLICATIONS_PENDING_SUFFIX), true);
    }

    /**
     * @return The interval between replications to be used when a client is bound to the service.
     * To prolong battery life, this should be made as long as possible. Note that on Android 5.1
     * and above (API Level 22), if the interval is less than 60 seconds, Android expands the
     * interval to 60 seconds.
     */
    protected abstract int getBoundIntervalInSeconds();

    /**
     * @return The interval between replications to be used when no client is bound to the service.
     * To prolong battery life, this should be made as long as possible. Note that on Android 5.1
     * and above (API Level 22), if the interval is less than 60 seconds, Android expands the
     * interval to 60 seconds.
     */
    protected abstract int getUnboundIntervalInSeconds();

    /**
     * @return True if you want periodic replication to start when a client binds to the service and
     * false otherwise. This is called each when a client binds to the service, so should contain
     * any logic to determine whether replications should begin. E.g. if you want replications to
     * only happen when on WiFi, this should only return true if you are on WiFi when it is called.
     */
    protected abstract boolean startReplicationOnBind();
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy