org.ciasaboark.tacere.service.EventSilencerService.java Source code

Java tutorial

Introduction

Here is the source code for org.ciasaboark.tacere.service.EventSilencerService.java

Source

/*
 * Copyright (c) 2015 Jonathan Nelson
 * Released under the BSD license.  For details see the COPYING file.
 */

package org.ciasaboark.tacere.service;

import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Vibrator;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import org.ciasaboark.tacere.database.DataSetManager;
import org.ciasaboark.tacere.database.DatabaseInterface;
import org.ciasaboark.tacere.event.EventInstance;
import org.ciasaboark.tacere.event.EventManager;
import org.ciasaboark.tacere.event.ringer.RingerType;
import org.ciasaboark.tacere.manager.AlarmManagerWrapper;
import org.ciasaboark.tacere.manager.NotificationManagerWrapper;
import org.ciasaboark.tacere.manager.RingerStateManager;
import org.ciasaboark.tacere.manager.ServiceStateManager;
import org.ciasaboark.tacere.manager.VolumesManager;
import org.ciasaboark.tacere.notifier.WidgetNotifier;
import org.ciasaboark.tacere.prefs.BetaPrefs;
import org.ciasaboark.tacere.prefs.Prefs;

import java.util.Deque;

public class EventSilencerService extends IntentService {
    public static final String QUICKSILENCE_DURATION = "duration";
    public static final String WAKE_REASON = "wake_reason";
    public static final String QUICKSILENCE_BROADCAST_KEY = "quicksilence";
    private static final String TAG = "EventSilencerService";
    private static final long TEN_SECONDS = 10000;
    private static Prefs prefs;
    private static AlarmManagerWrapper alarmManager;
    private static NotificationManagerWrapper notificationManager;
    private static RingerStateManager ringerStateManager;
    private static VolumesManager volumesManager;
    private static ServiceStateManager stateManager;
    private static DatabaseInterface databaseInterface;

    public EventSilencerService() {
        super(TAG);
    }

    /**
     * Looks for four different types of intents, one to start a
     * quick silence request, one to stop an ongoing quick silence request,
     * one to start the service (if not already started), and a normal request
     * to check for active events.  The type of intent is determined by mapping
     * the extra info "type" to one of the enumerated types in RequestTypes.
     * Normal request either have no extra info, or equate to RequestTypes.NORMAL
     */
    @Override
    public void onHandleIntent(Intent intent) {
        prefs = new Prefs(this);
        alarmManager = new AlarmManagerWrapper(this);
        notificationManager = new NotificationManagerWrapper(this);
        ringerStateManager = new RingerStateManager(this);
        volumesManager = new VolumesManager(this);
        stateManager = ServiceStateManager.getInstance(this);
        databaseInterface = DatabaseInterface.getInstance(this);

        // pull extra info (if any) from the incoming intent
        RequestTypes requestType = RequestTypes.NORMAL;
        try {
            int requestTypeValue = intent.getIntExtra(WAKE_REASON, -1);
            requestType = RequestTypes.getTypeForInt(requestTypeValue);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "unknown request type, using " + RequestTypes.NORMAL + " instead");
        }

        Log.d(TAG, "waking for request type: " + requestType.toString());

        switch (requestType) {
        case FIRST_WAKE:
            //reset some settings and schedule an immediate restart
            firstWake();
            break;
        case PROVIDER_CHANGED:
            //sync the calendar database and schedule an immediate restart
            databaseInterface.syncCalendarDb();
            scheduleServiceRestart();
            notifyCursorAdapterDataChanged();
            break;
        case SETTINGS_CHANGED:
            //the user might have selected different calendars or changed some other settings
            //update the database and restart
            databaseInterface.syncCalendarDb();
            scheduleServiceRestart();
            notifyCursorAdapterDataChanged();
            break;
        case QUICKSILENCE:
            // we should never get a quicksilence request with a zero or negative duration
            int duration = intent.getIntExtra(QUICKSILENCE_DURATION, -1);
            if (duration <= 0) {
                Log.e(TAG, "got a quicksilence duration <= 0: " + duration + " this should not " + "have happened");
            } else {
                quickSilence(duration);
            }
            break;
        case CANCEL_QUICKSILENCE:
            cancelQuickSilence();
            break;
        case NORMAL:
            if (!prefs.shouldAllCalendarsBeSynced() && prefs.getSelectedCalendarsIds().isEmpty()) {
                Log.d(TAG, "no calendars have been selected, skipping sync");
            } else {
                syncDatabaseIfEmpty();
            }
            if (prefs.isServiceActivated()) {
                if (stateManager.isQuicksilenceActive()) {
                    Log.d(TAG, "normal wake, but quicksilence is active, will wait for next wake");
                } else {
                    checkForActiveEventsAndSilence();
                }
            } else { // normal wake requests (but service is marked to be inactive)
                Log.d(TAG, "normal wake, but service is not enabled, shutting down.");
                shutdownService();
            }
            updateWidgets();
            break;
        default:
            Log.e(TAG, "got unknown request type, restarting normally");
            scheduleServiceRestart();
        }
    }

    /**
     * Sync the calendar database, set the service state to be inactive, then schedule an immediate
     * restart to look for any active events
     */
    private void firstWake() {
        ringerStateManager.clearStoredRingerState();
        volumesManager.clearStoredVolumes();
        stateManager.resetServiceState();
        databaseInterface.syncCalendarDb();
        notifyCursorAdapterDataChanged();
        // since the state is saved in a SharedPreferences file it can become out of sync
        // if the device restarts. To solve this set the service as not active, then schedule an
        // immediate restart to look for active events
        alarmManager.scheduleImmediateAlarm(RequestTypes.NORMAL);
    }

    private void scheduleServiceRestart() {
        // schedule an immediate wakeup only if we aren't in a quick silence period
        if (stateManager.isQuicksilenceNotActive()) {
            alarmManager.scheduleImmediateAlarm(RequestTypes.NORMAL);
        }
    }

    private void notifyCursorAdapterDataChanged() {
        DataSetManager dsm = new DataSetManager(this, getApplicationContext());
        dsm.broadcastDataSetChangedMessage();
    }

    private void quickSilence(int durationMinutes) {
        // quick silence requests can occur during three states, either no event is active, an event
        // is active, or a previous quick silence request is still ongoing. If this request occurred
        // while no event was active, then save the current ringer state so it can be restored later
        if (stateManager.isQuicksilenceNotActive()) {
            //only vibrate if we are going from nothing active -> quicksilence or from
            //event active -> quicksilence.
            vibrate();
        }
        ringerStateManager.storeRingerStateIfNeeded();
        stateManager.resetServiceState();
        long endTimeStamp = System.currentTimeMillis() + durationMinutes * EventInstance.MILLISECONDS_IN_MINUTE;
        stateManager.setQuickSilenceActive(endTimeStamp);

        long wakeAt = System.currentTimeMillis() + (EventInstance.MILLISECONDS_IN_MINUTE * durationMinutes);
        alarmManager.scheduleCancelQuickSilenceAlarmAt(wakeAt);

        //quick silence requests are always explicitly request to silence the ringer
        ringerStateManager.setPhoneRinger(RingerType.SILENT);

        notificationManager.displayQuickSilenceNotification(durationMinutes);
        sendQuicksilenceBroadcast();
        updateWidgets();
    }

    /**
     * Cancels any ongoing quick silence request then schedules an immediate restart to look for
     * active events
     */
    private void cancelQuickSilence() {
        stateManager.resetServiceState();
        volumesManager.restoreVolumes();
        ringerStateManager.restorePhoneRinger();
        notificationManager.cancelAllNotifications();
        vibrate();
        // schedule an immediate normal wakeup
        alarmManager.scheduleNormalWakeAt(System.currentTimeMillis());
        sendQuicksilenceBroadcast();
        updateWidgets();
    }

    private void syncDatabaseIfEmpty() {
        if (databaseInterface.isDatabaseEmpty()) {
            databaseInterface.syncCalendarDb();
            notifyCursorAdapterDataChanged();
        }
    }

    private void checkForActiveEventsAndSilence() {
        Log.d(TAG, "checking for events to silence");
        Deque<EventInstance> events = databaseInterface.getAllActiveEvents();
        boolean foundEvent = false;
        for (EventInstance event : events) {
            if (shouldEventSilence(event)) {
                Log.d(TAG, "found event to silence for: " + event.toString());
                foundEvent = true;
                silenceForEventAndShowNotification(event);
                break;
            }
        }

        if (!foundEvent) {
            Log.d(TAG, "did not find an event to silence for");
            //if we couldn't find any events, then do some cleanup and schedule the service to be
            //restarted when the last active checked event ends. If there were no active events,
            //then we need to sleep until the next event in the database starts.  If there are no
            //events in the database either, then just sleep for 24 hours.
            if (stateManager.isEventActive()) {
                Log.d(TAG, "event previously active, but non active now, restoring volumes");
                vibrate();
                stateManager.resetServiceState();
                notificationManager.cancelAllNotifications();
                volumesManager.restoreVolumes();
                ringerStateManager.restorePhoneRinger();
                notifyCursorAdapterDataChanged();
            }

            long wakeAt;
            if (!events.isEmpty()) {
                EventInstance lastActiveEvent = events.getLast();
                wakeAt = lastActiveEvent.getOriginalEnd();
                Log.d(TAG, "sleeping until last known event ends at " + wakeAt);
            } else {
                EventInstance nextInactiveEvent = databaseInterface.nextEvent();
                if (nextInactiveEvent != null) {
                    wakeAt = nextInactiveEvent.getBegin()
                            - (EventInstance.MILLISECONDS_IN_MINUTE * prefs.getBufferMinutes());
                    Log.d(TAG, "sleeping until next known event starts at " + wakeAt);
                } else {
                    Log.d(TAG, "sleeping for 24 hrs");
                    wakeAt = System.currentTimeMillis() + EventInstance.MILLISECONDS_IN_DAY;
                }
            }

            alarmManager.scheduleNormalWakeAt(wakeAt);
        }
    }

    /**
     * Shut down the background service, remove all ongoing notifications, restore volumes and
     * ringer and cancel any set alarms
     */
    private void shutdownService() {
        Log.d(TAG, "shutting down service");
        if (stateManager.isEventActive()) {
            Log.d(TAG, "event currently active, restoring volumes");
            vibrate();
            stateManager.resetServiceState();
            notificationManager.cancelAllNotifications();
            volumesManager.restoreVolumes();
            ringerStateManager.setPhoneRinger(ringerStateManager.getStoredRingerState());
        }
        alarmManager.cancelAllAlarms();
        shutdown();
    }

    private void updateWidgets() {
        Log.d(TAG, "updating widgets");
        WidgetNotifier widgetNotifier = new WidgetNotifier(this);
        widgetNotifier.updateAllWidgets();
    }

    private void vibrate() {
        BetaPrefs betaPrefs = new BetaPrefs(this);
        if (betaPrefs.getDisableVibration()) {
            Log.d(TAG, "vibrations disabled");
        } else {
            Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            long[] pattern = { 0, 500, 200, 500 };
            vibrator.vibrate(pattern, -1);
        }
    }

    private void sendQuicksilenceBroadcast() {
        Intent intent = new Intent(QUICKSILENCE_BROADCAST_KEY);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }

    private boolean shouldEventSilence(EventInstance event) {
        boolean eventShouldSilence = false;
        EventManager eventManger = new EventManager(this, event);
        if (eventManger.getBestRinger() != RingerType.IGNORE) {
            eventShouldSilence = true;
        }

        return eventShouldSilence;
    }

    private void silenceForEventAndShowNotification(EventInstance event) {
        //only vibrate if we are transitioning from no event active to an active event
        long oldEventId = stateManager.getActiveEventId();
        if (!stateManager.isEventActive()) {
            vibrate();
        } else {
            DataSetManager dataSetManager = new DataSetManager(this, this);
            long id = stateManager.getActiveEventId();
            dataSetManager.broadcastDataSetChangedForId(id);
        }

        stateManager.resetServiceState();
        stateManager.setActiveEvent(event);
        ringerStateManager.storeRingerStateIfNeeded();
        volumesManager.storeVolumesIfNeeded();

        EventManager eventManager = new EventManager(this, event);
        RingerType bestRinger = eventManager.getBestRinger();
        ringerStateManager.setPhoneRinger(bestRinger);
        volumesManager.silenceMediaAndAlarmVolumesIfNeeded();

        //nofity the listview that both the old event and the new event rows should be updated
        DataSetManager dataSetManager = new DataSetManager(this, this);
        dataSetManager.broadcastDataSetChangedForId(oldEventId);
        dataSetManager.broadcastDataSetChangedForId(event.getEventId());

        notificationManager.displayEventNotification(event);
        // the extra ten seconds is to make sure that the event ending is pushed into the
        // correct minute
        long wakeAt = event.getEffectiveEnd() + (EventInstance.MILLISECONDS_IN_MINUTE * prefs.getBufferMinutes())
                + TEN_SECONDS;
        alarmManager.scheduleNormalWakeAt(wakeAt);
    }

    private void shutdown() {
        stopSelf();
    }
}