uk.co.spookypeanut.wake_me_at.WakeMeAtService.java Source code

Java tutorial

Introduction

Here is the source code for uk.co.spookypeanut.wake_me_at.WakeMeAtService.java

Source

package uk.co.spookypeanut.wake_me_at;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.support.v4.app.NotificationCompat;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;

import android.text.format.Time;
import android.util.Log;
import android.widget.Toast;

/*
This file is part of Wake Me At. Wake Me At is the legal property
of its developer, Henry Bush (spookypeanut).
    
Wake Me At 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.
    
Wake Me At 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 Wake Me At, in the file "COPYING".  If not, see
<http://www.gnu.org/licenses/>.
*/

/**
 * The service that watches the current location, and triggers the alarm if
 * required
 * @author spookypeanut
 */
public class WakeMeAtService extends Service implements LocationListener {
    static final String ACTION_FOREGROUND = "uk.co.spookypeanut.wake_me_at.service";
    private String LOG_NAME;
    private String BROADCAST_UPDATE;
    public static boolean serviceRunning = false;

    // The minimum time (in milliseconds) before reporting the location again
    static final long SECONDS = 1000;
    private long mMinTime;
    private long mNoLocationWarningTime;
    private long mWarningRepeat;

    static final int ALARM_INTENT_REF = 0;
    static final int CANCEL_INTENT_REF = 1;

    // The minimum distance (in metres) before reporting the location again
    static final float minDistance = 0;

    private Handler mHandler = new Handler();
    Time lastLocation = new Time();

    private static final int ALARMNOTIFY_ID = 1;

    private static final Class<?>[] mStartForegroundSignature = new Class[] { int.class, Notification.class };
    private static final Class<?>[] mStopForegroundSignature = new Class[] { boolean.class };

    private DatabaseManager db;
    private UnitConverter uc;

    private long mRowId;
    private String mNick;
    private Location mCurrLocation = new Location("");
    private Location mFinalDestination = new Location("");
    private double mMetresAway = -1.0;
    private int mPreset;
    private float mRadius;
    private int mProvider;
    private String mUnit;
    private boolean mWarnSound;
    private boolean mWarnVibrate;
    private boolean mWarnToast;
    private boolean mWarningOn;
    PowerManager.WakeLock wl = null;

    private boolean mAlarm = false;

    private Intent mAlarmIntent;

    private LocationManager mLocationManager;
    private NotificationManager mNM;
    private NotificationCompat.Builder mBuilder;
    private PendingIntent mIntentOnSelect;
    private PendingIntent mCancelIntent;
    private Method mStartForeground;
    private Method mStopForeground;
    private Object[] mStartForegroundArgs = new Object[2];
    private Object[] mStopForegroundArgs = new Object[1];

    // Old api
    private Method mSetForeground;
    private Object[] mSetForegroundArgs = new Object[1];

    /**
     * Copied from one of the API example tools. One day I'll have to
     * actually go through and figure out what this does (or rather,
     * why it does it)
     * @param id
     * @param notification
     */
    void startForegroundCompat(int id) {
        // If we have the new startForeground API, then use it.
        Notification notification = mBuilder.build();
        if (mStartForeground != null) {
            mStartForegroundArgs[0] = Integer.valueOf(id);
            mStartForegroundArgs[1] = notification;
            try {
                mStartForeground.invoke(this, mStartForegroundArgs);
            } catch (InvocationTargetException e) {
                // Should not happen.
                Log.w(LOG_NAME, "Unable to invoke startForeground", e);
            } catch (IllegalAccessException e) {
                // Should not happen.
                Log.w(LOG_NAME, "Unable to invoke startForeground", e);
            }
            return;
        }

        // Fall back on the old API.
        mSetForegroundArgs[0] = Boolean.TRUE;
        invokeMethod(mSetForeground, mSetForegroundArgs);
        mNM.notify(id, notification);
    }

    void invokeMethod(Method method, Object[] args) {
        try {
            method.invoke(this, args);
        } catch (InvocationTargetException e) {
            // Should not happen.
            Log.w(LOG_NAME, "Unable to invoke method", e);
        } catch (IllegalAccessException e) {
            // Should not happen.
            Log.w(LOG_NAME, "Unable to invoke method", e);
        }
    }

    void stopForegroundCompat(int id) {
        // If we have the new stopForeground API, then use it.
        if (mStopForeground != null) {
            mStopForegroundArgs[0] = Boolean.TRUE;
            try {
                mStopForeground.invoke(this, mStopForegroundArgs);
            } catch (InvocationTargetException e) {
                // Should not happen.
                Log.w(LOG_NAME, "Unable to invoke stopForeground", e);
            } catch (IllegalAccessException e) {
                // Should not happen.
                Log.w(LOG_NAME, "Unable to invoke stopForeground", e);
            }
            return;
        }

        // Fall back on the old API.  Note to cancel BEFORE changing the
        // foreground state, since we could be killed at that point.
        mNM.cancel(id);
        mSetForegroundArgs[0] = Boolean.FALSE;
        invokeMethod(mSetForeground, mSetForegroundArgs);
    }

    @Override
    public void onCreate() {
        LOG_NAME = (String) getText(R.string.app_name_nospaces);
        BROADCAST_UPDATE = (String) getText(R.string.serviceBroadcastName);
        mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        db = new DatabaseManager(this);

        try {
            mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
            mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
        } catch (NoSuchMethodException e) {
            // Running on an older platform.
            mStartForeground = mStopForeground = null;
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(LOG_NAME, "onStartCommand()");
        Bundle extras = intent.getExtras();

        // Get everything we need from the database
        mRowId = extras.getLong("rowId");
        Log.d(LOG_NAME, "row Id for alarm is " + mRowId);
        mNick = db.getNick(mRowId);
        mFinalDestination.setLatitude(db.getLatitude(mRowId));
        mFinalDestination.setLongitude(db.getLongitude(mRowId));
        Log.d(LOG_NAME,
                "Passed latlong: " + mFinalDestination.getLatitude() + ", " + mFinalDestination.getLongitude());
        mPreset = db.getPreset(mRowId);
        Presets presetObj = new Presets(this, mPreset);
        // If this is set to custom, use the values from the database
        // If not, use the values from the preset
        if (presetObj.isCustom()) {
            mRadius = db.getRadius(mRowId);
            mProvider = db.getProvider(mRowId);
            mUnit = db.getUnit(mRowId);
        } else {
            mRadius = presetObj.getRadius();
            mProvider = presetObj.getLocProv();
            mUnit = presetObj.getUnit();
        }
        mWarnSound = db.getWarnSound(mRowId);
        mWarnVibrate = db.getWarnVibrate(mRowId);
        mWarnToast = db.getWarnToast(mRowId);
        // We turn the warning on if the global warning flag is true, and if
        // at least one of the warning types is true
        mWarningOn = db.getWarning(mRowId) && (mWarnSound || mWarnVibrate || mWarnToast);
        Log.d(LOG_NAME, "Provider: \"" + mProvider + "\"");

        String lp = this.getResources().getStringArray(R.array.locProvAndroid)[mProvider];
        if ("gps".equals(lp)) {
            mMinTime = 10 * SECONDS;
            mNoLocationWarningTime = 30 * SECONDS;
        } else {
            mMinTime = 45 * SECONDS;
            mNoLocationWarningTime = 90 * SECONDS;
        }
        mWarningRepeat = mMinTime;

        uc = new UnitConverter(this, mUnit);

        mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        registerLocationListener();

        if (!mLocationManager.isProviderEnabled(lp)) {
            stopService();
            return START_NOT_STICKY;

        }
        createNotification(intent);

        serviceRunning = true;
        updateAlarm();

        Toast.makeText(getApplicationContext(), R.string.foreground_service_started, Toast.LENGTH_SHORT).show();

        // We want this service to continue running until it is explicitly
        // stopped, so return sticky.
        return START_STICKY;
    }

    private void stopService() {
        // Make sure our notification is gone.
        stopForegroundCompat(ALARMNOTIFY_ID);
        unregisterLocationListener();
        mHandler.removeCallbacks(mCheckLocationAge);

        // Set everything back to default values, and tell the alarm activity
        mRowId = -1;
        mAlarm = false;
        mMetresAway = -1;
        updateAlarm();
    }

    @Override
    public void onDestroy() {
        Log.d(LOG_NAME, "WakeMeAtService.onDestroy()");
        if (wl != null) {
            wl.release();
        }
        Toast.makeText(getApplicationContext(), R.string.foreground_service_stopped, Toast.LENGTH_SHORT).show();
        stopService();

        serviceRunning = false;
        db.close();
        if (mAlarmIntent != null && mAlarmIntent.getClass() != null) {
            removeStickyBroadcast(mAlarmIntent);
        }
        super.onDestroy();
    }

    /**
     * Method that registers the service as a location listener
     */
    public void registerLocationListener() {
        Log.d(LOG_NAME, "registerLocationListener()");
        if (mLocationManager == null) {
            Log.e(LOG_NAME, "TrackRecordingService: Do not have any location manager.");
            return;
        }
        Log.d(LOG_NAME, "Preparing to register loc listener w/TrackRecordingService...");
        try {
            String lp = this.getResources().getStringArray(R.array.locProvAndroid)[mProvider];
            mLocationManager.requestLocationUpdates(lp, mMinTime, minDistance, WakeMeAtService.this);
        } catch (RuntimeException e) {
            Log.e(LOG_NAME, "Couldn't register location listener: " + e.getMessage(), e);
        }
    }

    /**
     * Unregister the location listener. Called in onDestroy.
     */
    public void unregisterLocationListener() {
        if (mLocationManager == null) {
            Log.e(LOG_NAME, "locationManager is null");
            return;
        }
        mLocationManager.removeUpdates(this);
        Log.d(LOG_NAME, "Location listener is unregistered");
    }

    void createNotification(Intent intent) {
        if (ACTION_FOREGROUND.equals(intent.getAction())) {
            // The text to use as the title of our notification
            CharSequence text = getText(R.string.foreground_service_started);
            Bitmap icon = BitmapFactory.decodeResource(this.getResources(), R.drawable.iconstar);
            // Set the icon, scrolling text and timestamp
            mBuilder = new NotificationCompat.Builder(this).setContentTitle(mNick).setContentText(text)
                    .setSmallIcon(R.drawable.icontaskbar).setLargeIcon(icon);

            // The PendingIntent to launch our activity if the user
            // selects this notification
            Intent i = new Intent(this, Alarm.class).putExtra("rowId", mRowId).putExtra("metresAway", mMetresAway)
                    .putExtra("alarm", mAlarm);

            // It appears that the extras aren't updated, so we use
            // FLAG_CANCEL_CURRENT to completely start from scratch
            mIntentOnSelect = PendingIntent.getActivity(this, ALARM_INTENT_REF, i,
                    PendingIntent.FLAG_CANCEL_CURRENT);
            mBuilder.setContentIntent(mIntentOnSelect);

            i = new Intent(this, Alarm.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra("cancel_all", true);
            mCancelIntent = PendingIntent.getActivity(this, CANCEL_INTENT_REF, i,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            mBuilder.addAction(R.drawable.ic_menu_close_clear_cancel, "Cancel alarm", mCancelIntent);

            startForegroundCompat(ALARMNOTIFY_ID);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onLocationChanged(Location location) {
        lastLocation.setToNow();
        Log.v(LOG_NAME, "onLocationChanged(" + lastLocation.toMillis(false) + ")");
        mHandler.removeCallbacks(mCheckLocationAge);
        if (mWarningOn) {
            mHandler.postDelayed(mCheckLocationAge, mMinTime);
        }

        mCurrLocation = location;
        mMetresAway = location.distanceTo(mFinalDestination);
        // message is, e.g. You are 200m from Welwyn North
        String message = String.format(getString(R.string.notif_full), uc.out(mMetresAway), mNick);
        mBuilder.setContentText(message);
        // Turn off the sound and vibrate, in case they were turned
        // on by the old location warning
        //mNotification.defaults &= ~Notification.DEFAULT_SOUND;
        //mNotification.defaults &= ~Notification.DEFAULT_VIBRATE;
        mNM.notify(ALARMNOTIFY_ID, mBuilder.build());
        if (mMetresAway < uc.toMetres(mRadius)) {
            soundAlarm();
        }
        updateAlarm();
    }

    public void updateAlarm() {
        Log.d(LOG_NAME, "Changing the distance away");
        if (mAlarmIntent == null || mAlarmIntent.getClass() != null) {
            mAlarmIntent = new Intent(BROADCAST_UPDATE);
        }
        mAlarmIntent.putExtra("rowId", mRowId);
        mAlarmIntent.putExtra("metresAway", mMetresAway);
        mAlarmIntent.putExtra("alarm", mAlarm);
        mAlarmIntent.putExtra("currLat", mCurrLocation.getLatitude());
        mAlarmIntent.putExtra("currLong", mCurrLocation.getLongitude());
        mAlarmIntent.putExtra("locTime", lastLocation.toMillis(true));
        Log.d(LOG_NAME, "Sending broadcast");
        Log.d(LOG_NAME, "mRowId: " + mRowId);
        sendStickyBroadcast(mAlarmIntent);
    }

    public void cancelAlarm() {
        Log.d(LOG_NAME, "WakeMeAtService.cancelAlarm");
        mAlarm = false;
        if (wl != null) {
            wl.release();
        }
        Intent alarmIntent = new Intent(WakeMeAtService.this.getApplication(), Alarm.class);
        alarmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        alarmIntent.putExtra("rowId", mRowId);
        alarmIntent.putExtra("metresAway", mMetresAway);
        alarmIntent.putExtra("alarm", mAlarm);

        startActivity(alarmIntent);
    }

    public void soundAlarm() {
        mAlarm = true;
        // This method of waking up the device seems to be required on <= 4.0
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, LOG_NAME);
        if ((wl != null) && (wl.isHeld() == false)) {
            wl.acquire();
        }
        Intent alarmIntent = new Intent(WakeMeAtService.this.getApplication(), Alarm.class);
        alarmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        alarmIntent.putExtra("rowId", mRowId);
        alarmIntent.putExtra("metresAway", mMetresAway);
        alarmIntent.putExtra("alarm", mAlarm);

        startActivity(alarmIntent);
        updateAlarm();
    }

    @Override
    public void onProviderDisabled(String provider) {
        String lp = this.getResources().getStringArray(R.array.locProvAndroid)[mProvider];
        if (provider != lp) {
            Log.wtf(LOG_NAME,
                    "Current provider (" + lp + ") doesn't match the listener provider (" + provider + ")");

        }
        String message = String.format(getString(R.string.providerDisabledMessage),
                this.getResources().getStringArray(R.array.locProvHuman)[mProvider]);
        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onProviderEnabled(String provider) {
        Log.d(LOG_NAME, "onProviderEnabled(" + provider + ")");
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        Log.d(LOG_NAME, "onStatusChanged(" + provider + ", " + status + ")");
    }

    private Runnable mCheckLocationAge = new Runnable() {
        public void run() {
            Time currTime = new Time();
            currTime.setToNow();
            long millis = currTime.toMillis(false) - lastLocation.toMillis(false);

            Log.d(LOG_NAME, "Curr: " + currTime.toMillis(false) + ", last: " + lastLocation.toMillis(false));
            Log.d(LOG_NAME, "Diff: " + millis + " vs limit: " + mNoLocationWarningTime);
            if (millis >= mNoLocationWarningTime) {
                oldLocationWarning(millis / 1000);
                mHandler.removeCallbacks(mCheckLocationAge);
                mHandler.postDelayed(mCheckLocationAge, mWarningRepeat);
                return;
            }
            mHandler.removeCallbacks(mCheckLocationAge);
            mHandler.postDelayed(mCheckLocationAge, mMinTime);
        }

    };

    private void oldLocationWarning(long age) {
        String msg = String.format(getString(R.string.oldLocationWarning), age);
        if (mWarnToast) {
            Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
        }

        // We should still add a notification that the location is old, even
        // if none of the warning settings are turned on
        Context context = getApplicationContext();
        CharSequence contentTitle = "Old location";
        CharSequence contentText = msg;

        Intent notificationIntent = new Intent(this, Alarm.class);
        PendingIntent contentIntent;
        contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
        if (mWarnSound) {
            //    mNotification.defaults |= Notification.DEFAULT_SOUND;
        }
        if (mWarnVibrate) {
            //    mNotification.defaults |= Notification.DEFAULT_VIBRATE;
        }

        //mNotification.setLatestEventInfo(context, contentTitle,
        //                                 contentText, contentIntent);
        //mNM.notify(ALARMNOTIFY_ID, mNotification);
    }
}