at.ac.uniklu.mobile.sportal.service.MutingService.java Source code

Java tutorial

Introduction

Here is the source code for at.ac.uniklu.mobile.sportal.service.MutingService.java

Source

/*
 * Copyright (c) 2014 Mario Guggenberger <mario.guggenberger@aau.at>
 *
 * This file is part of AAU Studentenportal.
 *
 * AAU Studentenportal is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * AAU Studentenportal is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with AAU Studentenportal.  If not, see <http://www.gnu.org/licenses/>.
 */

package at.ac.uniklu.mobile.sportal.service;

import java.util.Date;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.location.Location;
import android.location.LocationManager;
import android.media.AudioManager;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import at.ac.uniklu.mobile.sportal.Studentportal;
import at.ac.uniklu.mobile.sportal.LoginActivity;
import at.ac.uniklu.mobile.sportal.R;
import at.ac.uniklu.mobile.sportal.persistence.MutingPeriod;
import at.ac.uniklu.mobile.sportal.persistence.StudentPortalDB;
import at.ac.uniklu.mobile.sportal.service.MutingUtils.MutingRegion;
import at.ac.uniklu.mobile.sportal.util.Analytics;
import at.ac.uniklu.mobile.sportal.util.Preferences;

/**
 * This service manages the automute feature that mutes the phone while a course is ongoing.
 * TODO The code is a total mess and needs some serious refactoring.
 * 
 * cases:
 *  - SERVICE TURN ON: 
 *     - take a currently running or the next upcoming event and schedule it
 *     - if there are more events at the same time, take any one
 *  - MUTE:
 *     - mute the phone if prerequisites are satisfied (location, preferences, etc...)
 *       and schedule unmute at the end of the event
 *     - if prerequisites aren't satisfied, schedule next upcoming event
 *  - UNMUTE:
 *     - look at other currently running but not yet finished events and schedule mute
 *     - if no other running events found, unmute phone and schedule next upcoming event
 *  - SERVICE TURN OFF:
 *     - if phone is muted by the service, unmute it
 *     - if an upcoming alarm is scheduled, cancel it
 */
public class MutingService extends IntentService {

    public static final String ACTION = "mutingservice.action";
    public static final int ACTION_NONE = 0;
    public static final int ACTION_TURN_ON = 1;
    public static final int ACTION_TURN_OFF = 2;
    public static final int ACTION_MUTE = 3;
    public static final int ACTION_UNMUTE = 4;
    public static final int ACTION_SCHEDULE = 5;
    public static final int ACTION_REQUEST_ISRUNNING = 20;

    public static final String ACTION_RESPONSE_ISRUNNING = "response_running";

    public static final String ACTION_RESPONSE_SHUTDOWN = "response_shutdown";
    public static final int ACTION_RESPONSE_SHUTDOWN_NO_UPCOMING_DATES = 1;

    public static final String ACTION_RESPONSE_MESSAGE = "response_message";

    private static final String TAG = "MutingService";
    private static final boolean DEBUG_WITH_FAKE_ALARMS = false;
    public static final String EXTRA_ALARM_ID = "mutingservice.extraalarmid";

    public MutingService() {
        super("MutingService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            Analytics.onServiceStart(this);
            switch (intent.getExtras().getInt(ACTION, ACTION_NONE)) {
            case ACTION_TURN_ON:
                turnOn();
                break;
            case ACTION_TURN_OFF:
                turnOff();
                break;
            case ACTION_MUTE:
                mute(intent.getExtras().getInt(EXTRA_ALARM_ID));
                break;
            case ACTION_UNMUTE:
                unmute();
                break;
            case ACTION_SCHEDULE:
                if (isRunning())
                    scheduleMute();
                break;
            case ACTION_REQUEST_ISRUNNING:
                ServiceToActivityBroadcastReceiver.sendBroadcast(this,
                        new Intent().putExtra(ACTION_RESPONSE_ISRUNNING, isRunning()));
                break;
            }
            Analytics.onServiceStop(this);
        } finally {
            MutingServiceWakeLock.releaseWakeLock();
        }
    }

    /**
     * Turns ON automatic ringtone muting during courses.
     */
    private void turnOn() {
        Log.d(TAG, "turnOn()");

        if (isRunning()) {
            Log.d(TAG, "service is already running, cancelling TURN ON");
            return;
        }

        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_ON);

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        if (!Preferences.isAutomuteEnabled(this, preferences)) {
            Log.d(TAG, "Cancelling automute turn on. User wants it to be off.");
            return;
        }

        // reset the preference for the case of a crash where unmute() or turnOff() has never been called
        if (Preferences.isMutingPeriod(preferences)) {
            Preferences.setMutingPeriod(preferences, false);
        }

        scheduleMute();
    }

    /**
     * Determines if the muting service is running (ON) or stopped (OFF).
     * The muting service is defined as running if there is an alarm scheduled that will either mute or unmute 
     * the phone at a specific time. It is stopped if no alarm is scheduled.
     * http://code.google.com/p/android/issues/detail?id=3776
     * http://stackoverflow.com/questions/2110620/how-to-handle-an-alarm-triggered-each-day-in-android
     * @return true if the muting service is running or false if it is stopped
     */
    private boolean isRunning() {
        Intent alarmIntent = new Intent(this, OnAlarmReceiver.class).putExtra(ACTION, ACTION_NONE);
        boolean isRunning = PendingIntent.getBroadcast(this, 0, alarmIntent, PendingIntent.FLAG_NO_CREATE) != null;
        Log.d(TAG, "isRunning(): " + isRunning);
        return isRunning;
    }

    /**
     * Turns OFF automatic ringtone muting during courses.
     */
    private void turnOff() {
        Log.d(TAG, "turnOff()");
        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_OFF);

        // if the phone is currently in a muting period, turn the ringtone back on before turning off the service
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        if (Preferences.isMutingPeriod(preferences)) {
            MutingUtils.ringtoneTurnOn(this);
            Preferences.setMutingPeriod(preferences, false);
        }

        // remove eventually existing user notification
        removeNotification(Studentportal.NOTIFICATION_MS_INFO);

        // cancel an eventually existing pending alarm and the beloging intent as well (otherwise isRunning would always return true)
        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        Intent alarmIntent = new Intent(this, OnAlarmReceiver.class).putExtra(ACTION, ACTION_NONE);
        PendingIntent pendingAlarmIntent = PendingIntent.getBroadcast(this, 0, alarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        alarmManager.cancel(pendingAlarmIntent);
        pendingAlarmIntent.cancel();

        // cancel an eventually exisiting location broadcast receiver
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        Intent locationIntent = new Intent("at.ac.uniklu.mobile.sportal.LOCATION_UPDATE");
        PendingIntent pendingLocationIntent = PendingIntent.getBroadcast(this, 0, locationIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        locationManager.removeUpdates(pendingLocationIntent);
        pendingLocationIntent.cancel();

        if (isRunning()) {
            Log.e(TAG, "COULD NOT TURN OFF");
        }
    }

    /**
     * Schedules an alarm through the AlarmManager. Alarms are typically scheduled at time when courses begin or end.
     * @param time the time at which the alarm will go off
     * @param action the action that will be called when the alarm goes off
     */
    private void scheduleAlarm(long time, int action, int alarmId) {
        Log.d(TAG, "scheduling alarm action " + action + " @ " + new Date(time).toLocaleString() + " (aID:"
                + alarmId + ")");

        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        Intent alarmIntent = new Intent(this, OnAlarmReceiver.class).putExtra(ACTION, action)
                .putExtra(EXTRA_ALARM_ID, alarmId);
        PendingIntent pendingAlarmIntent = PendingIntent.getBroadcast(this, 0, alarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        time = SystemClock.elapsedRealtime() + (time - System.currentTimeMillis()); //  convert unixtime to system runtime
        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pendingAlarmIntent);
    }

    private void scheduleMute() {
        scheduleMute(false);
    }

    private void scheduleMute(boolean next) {
        Log.d(TAG, "scheduleMute(next:" + next + ")");
        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_MUTE_SCHEDULE);

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        // determine next muting time
        MutingPeriod mutingPeriod;
        if (DEBUG_WITH_FAKE_ALARMS) {
            mutingPeriod = new MutingPeriod();
            mutingPeriod.setId(-1);
            mutingPeriod.setBegin(System.currentTimeMillis() + 10000);
            mutingPeriod.setEnd(mutingPeriod.getBegin() + 10000);
            mutingPeriod.setName("Fakeevent");
        } else {
            StudentPortalDB db = Studentportal.getStudentPortalDB();

            if (Preferences.isMutingPeriod(preferences) || next) {
                // we are currently in a muting period, so schedule the next period
                mutingPeriod = db.mutingPeriods_getNextPeriod();
            } else {
                mutingPeriod = db.mutingPeriods_getCurrentPeriod();
                if (mutingPeriod == null)
                    mutingPeriod = db.mutingPeriods_getNextPeriod();
            }

            if (mutingPeriod == null) {
                // no upcoming alarm stored in the DB, try to fetch new ones from the server
                Log.d(TAG, "muting DB empty, fetching new ones from the server...");
                try {
                    if (!Studentportal.getSportalClient().login().isLoggedIn()) {
                        Studentportal.getSportalClient().login(Preferences.getUsername(preferences),
                                Preferences.getPassword(preferences), null);
                    }
                    db.mutingPeriods_insertNew(Studentportal.getSportalClient().getTermine(null, null, 10, null));
                    db.mutingPeriods_cleanup();
                } catch (Exception e) {
                    Log.e(TAG, e.getMessage(), e);
                    Analytics.onError(Analytics.ERROR_MUTINGSERVICE_ONLINEUPDATE, e);
                    // notify user that there's a server connection problem
                    notifyUser(Studentportal.NOTIFICATION_MS_ERROR, false, getString(R.string.app_name),
                            getString(R.string.app_name),
                            getString(R.string.automute_notification_onlineupdate_failed),
                            android.R.drawable.stat_notify_error, new Intent(this, LoginActivity.class));
                }
                if (!(Preferences.isMutingPeriod(preferences) || next)) {
                    mutingPeriod = db.mutingPeriods_getCurrentPeriod();
                }
                if (mutingPeriod == null) {
                    mutingPeriod = db.mutingPeriods_getNextPeriod();
                }
            }
        }

        boolean isUpcomingMutingDateExisting = (mutingPeriod != null);

        if (isUpcomingMutingDateExisting) {
            scheduleAlarm(mutingPeriod.getBegin(), ACTION_MUTE, mutingPeriod.getId());
            Log.d(TAG, "scheduled: " + mutingPeriod.getName() + " @ "
                    + new Date(mutingPeriod.getBegin()).toLocaleString() + " (until "
                    + new Date(mutingPeriod.getEnd()).toLocaleString() + " / id: " + mutingPeriod.getId() + ")");
            // TODO notify user about next scheduled muting
        } else {
            // inform user via a notification that no upcoming dates were found to be muted and turn OFF
            //         notifyUser(NOTIFICATION_ERROR, Notification.FLAG_AUTO_CANCEL, getString(R.string.app_name), getString(R.string.app_name), 
            //               getString(R.string.automute_notification_disabled_no_courses));

            // set the automute preference to false so that the automute indicator corresponds with the preferences screen
            Preferences.setAutomuteEnabled(this, preferences, false);

            // notify listening activities about the shutdown
            ServiceToActivityBroadcastReceiver.sendBroadcast(this,
                    new Intent().putExtra(ACTION_RESPONSE_SHUTDOWN, ACTION_RESPONSE_SHUTDOWN_NO_UPCOMING_DATES)
                            .putExtra(ACTION_RESPONSE_MESSAGE,
                                    getString(R.string.automute_notification_disabled_no_courses)));
        }
    }

    private void scheduleUnmute(long time) {
        Log.d(TAG, "scheduleUnmute()");
        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_UNMUTE_SCHEDULE);

        if (DEBUG_WITH_FAKE_ALARMS) {
            scheduleAlarm(time, ACTION_UNMUTE, -1);
        } else {
            // determine next unmuting time
            // if there's another course starting before the current one is finished, schedule another mute instead
            StudentPortalDB db = Studentportal.getStudentPortalDB();
            MutingPeriod nextMutingPeriod = db.mutingPeriods_getNextPeriod();
            if (!DEBUG_WITH_FAKE_ALARMS && nextMutingPeriod != null && time > nextMutingPeriod.getBegin()) {
                Log.d(TAG, "overlap detected, turning unmute into another mute (" + new Date(time).toLocaleString()
                        + " overlaps with begin time " + new Date(nextMutingPeriod.getBegin()).toLocaleString()
                        + " / id: " + nextMutingPeriod.getId() + ")");
                scheduleMute();
            } else {
                scheduleAlarm(time, ACTION_UNMUTE, -1);
            }
        }
    }

    private void mute(int alarmId) {
        Log.d(TAG, "mute()");

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        if (Preferences.getLocationStatus(preferences) == Preferences.LOCATION_STATUS_WAITING) {
            Log.d(TAG, "mute() blocked - waiting for a location update");
            return;
        }

        // check if phone is already muted by the user
        AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
        boolean isPhoneAlreadyMuted = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL
                && !Preferences.isMutingPeriod(preferences);
        if (isPhoneAlreadyMuted) {
            Log.d(TAG, "phone is already muted, scheduling next mute");
            scheduleMute(true);
            return;
        }

        // load the current period from the db
        MutingPeriod mutingPeriod = null;
        Log.d(TAG, "muting period id: " + alarmId);
        if (DEBUG_WITH_FAKE_ALARMS) {
            mutingPeriod = new MutingPeriod();
            mutingPeriod.setId(-1);
            mutingPeriod.setBegin(System.currentTimeMillis());
            mutingPeriod.setEnd(mutingPeriod.getBegin() + 10000);
            mutingPeriod.setName("Fakeevent");
        } else {
            mutingPeriod = Studentportal.getStudentPortalDB().mutingPeriods_getPeriod(alarmId);
        }

        // check if phone is located at university
        notifyUser(Studentportal.NOTIFICATION_MS_INFO, true, mutingPeriod.getName(), mutingPeriod.getName(),
                getString(R.string.automute_notification_course_started_locating));
        boolean isPhoneLocationKnown = false;
        boolean isPhoneLocatedAtUniversity = false;
        String locationSource = null;

        WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
        if (wifiManager.isWifiEnabled()) {
            ScanResult scanResult = MutingUtils.findMutingWifiNetwork(wifiManager.getScanResults());
            if (scanResult != null) {
                Log.d(TAG, "phone located by wifi: " + scanResult.SSID);
                isPhoneLocationKnown = true;
                isPhoneLocatedAtUniversity = true;
                locationSource = "wifi (" + scanResult.SSID + ")";
            }
        }

        if (!isPhoneLocationKnown) {
            // phone location could not be determined by wifi, trying network location instead...
            LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
            if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                Intent locationIntent = new Intent("at.ac.uniklu.mobile.sportal.LOCATION_UPDATE")
                        .putExtra(EXTRA_ALARM_ID, alarmId);
                PendingIntent pendingLocationIntent = PendingIntent.getBroadcast(this, 0, locationIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);
                // remove the location receiver (so it doesn't get registered multiple times [could also happen on overlapping mute() calls)
                locationManager.removeUpdates(pendingLocationIntent);

                if (Preferences.getLocationStatus(preferences) == Preferences.LOCATION_STATUS_RECEIVED) {
                    isPhoneLocationKnown = true;
                    pendingLocationIntent.cancel();
                    Location location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
                    if (location == null) {
                        Log.d(TAG, "location received but still null");
                    } else {
                        MutingRegion mutingRegion = MutingUtils.findOverlappingMutingRegion(location);
                        if (mutingRegion != null) {
                            Log.d(TAG, "phone located by network @ " + mutingRegion.getName());
                            isPhoneLocatedAtUniversity = true;
                            locationSource = "location (" + mutingRegion.getName() + ")";
                        }
                    }
                } else {
                    Log.d(TAG, "trying to locate the phone by network...");
                    // wait for a location update
                    Preferences.setLocationStatus(preferences, Preferences.LOCATION_STATUS_WAITING);
                    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0,
                            pendingLocationIntent);
                    return; // exit method - it will be re-called from the location broadcast receiver on a location update
                }
            }
        }

        boolean isAlwaysMuteEnabled = Preferences.isAutomuteWithoutLocation(this, preferences);

        if (isPhoneLocationKnown) {
            if (!isPhoneLocatedAtUniversity) {
                Log.d(TAG, "phone is not located at university, scheduling next mute");
                scheduleMute(true);
                removeNotification(Studentportal.NOTIFICATION_MS_INFO);
                return;
            }
        } else {
            Log.d(TAG, "phone cannot be located");
            if (!isAlwaysMuteEnabled) {
                Log.d(TAG, "alwaysmute is disabled, scheduling next mute");
                Preferences.setLocationStatus(preferences, Preferences.LOCATION_STATUS_NONE);
                scheduleMute(true);
                removeNotification(Studentportal.NOTIFICATION_MS_INFO);
                return;
            }
        }

        // only turn the ringtone off if we aren't currently in a muting period.
        // if we are in a muting period the ringtone is already muted and the request should be ignored,
        // else rintoneTurnOn() won't turn the ringtone back on because ringtone override will be set to true
        if (!Preferences.isMutingPeriod(preferences)) {
            MutingUtils.ringtoneTurnOff(this);
        }

        // persist that from now on the phone is in a muting period
        Preferences.setMutingPeriod(preferences, true);

        // inform user via a notification that a course has started and the phone has been muted
        notifyUser(Studentportal.NOTIFICATION_MS_INFO, true, mutingPeriod.getName(), mutingPeriod.getName(),
                getString(R.string.automute_notification_course_muted));

        final boolean isPhoneLocationKnownAnalytics = isPhoneLocationKnown;
        final String locationSourceAnalytics = locationSource;
        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_MUTE, "isPhoneLocationKnown",
                isPhoneLocationKnownAnalytics + "", "locationSource", locationSourceAnalytics);

        scheduleUnmute(mutingPeriod.getEnd());
    }

    private void unmute() {
        Log.d(TAG, "unmute()");
        Analytics.onEvent(Analytics.EVENT_MUTINGSERVICE_UNMUTE);

        MutingUtils.ringtoneTurnOn(this);

        // persist that from now on the phone is NOT in a muting period
        Editor preferenceEditor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        Preferences.setMutingPeriod(preferenceEditor, false);
        Preferences.setLocationStatus(preferenceEditor, Preferences.LOCATION_STATUS_NONE);
        preferenceEditor.commit();

        // remove eventually existing user notification
        removeNotification(Studentportal.NOTIFICATION_MS_INFO);

        scheduleMute();
    }

    private void notifyUser(int id, boolean ongoing, String tickerText, String contentTitle, String contentText,
            int icon, Intent intent) {
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        Notification notification = new NotificationCompat.Builder(this).setSmallIcon(icon).setTicker(tickerText)
                .setContentText(contentText).setContentTitle(contentTitle).setContentIntent(contentIntent)
                .setWhen(System.currentTimeMillis()).setOngoing(ongoing).build();

        notificationManager.notify(id, notification);
    }

    private void notifyUser(int id, boolean ongoing, String tickerText, String contentTitle, String contentText) {
        notifyUser(id, ongoing, tickerText, contentTitle, contentText, R.drawable.notify_course, new Intent());
    }

    private void removeNotification(int id) {
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.cancel(id);
    }
}