org.unchiujar.umbra2.services.LocationService.java Source code

Java tutorial

Introduction

Here is the source code for org.unchiujar.umbra2.services.LocationService.java

Source

/*******************************************************************************
 * This file is part of Umbra.
 *
 *     Umbra 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.
 *
 *     Umbra 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 Umbra.  If not, see <http://www.gnu.org/licenses/>.
 *
 *     Copyright (c) 2011 Vasile Jureschi <vasile.jureschi@gmail.com>.
 *     All rights reserved. This program and the accompanying materials
 *     are made available under the terms of the GNU Public License v3.0
 *     which accompanies this distribution, and is available at
 *
 *    http://www.gnu.org/licenses/gpl-3.0.html
 *
 *     Contributors:
 *        Vasile Jureschi <vasile.jureschi@gmail.com> - initial API and implementation
 *        Yen-Liang, Shen - Simplified Chinese and Traditional Chinese translations
 ******************************************************************************/

package org.unchiujar.umbra2.services;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.*;
import android.support.v4.app.NotificationCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unchiujar.umbra2.R;
import org.unchiujar.umbra2.activities.FogOfExplore;
import org.unchiujar.umbra2.location.LocationOrder;

import java.util.ArrayList;

public class LocationService extends Service {
    public static final String MOVEMENT_UPDATE = "org.unchiujar.umbra.MOVEMENT_UPDATE";
    public static final String LATITUDE = "org.unchiujar.umbra.LocationService.LATITUDE";
    public static final String LONGITUDE = "org.unchiujar.umbra.LocationService.LONGITUDE";
    public static final String ACCURACY = "org.unchiujar.umbra.LocationService.ACCURACY";
    /**
     * Command to the service to register a client, receiving callbacks from the service. The
     * Message's replyTo field must be a Messenger of the client where callbacks should be sent.
     */
    public static final int MSG_REGISTER_CLIENT = 1;
    /**
     * Command to the service to un`register a client, or stop receiving callbacks from the service.
     * The Message's replyTo field must be a Messenger of the client as previously given with
     * MSG_REGISTER_CLIENT.
     */
    public static final int MSG_UNREGISTER_CLIENT = 2;
    public static final int MSG_UNREGISTER_INTERFACE = 4;
    public static final int MSG_REGISTER_INTERFACE = 5;
    public static final int MSG_WALK = 6;
    public static final int MSG_DRIVE = 7;
    public static final int MSG_SHOW_NOTIFICATION = 8;
    public static final int MSG_HIDE_NOTIFICATION = 9;

    public static final int MSG_LOCATION_CHANGED = 9000;
    public static final String POWER_EVENT = "org.unchiujar.services.LocationService.POWER_EVENT";
    /**
     * The maximum duration the location listeners should be put to sleep.
     */
    protected static final long MAX_BACKOFF_INTERVAL = 10 * 60 * 1000;
    private static final int APPLICATION_ID = 1241241;
    private static final Logger LOGGER = LoggerFactory.getLogger(LocationService.class);
    /**
     * Walk update frequency for average walking speed. Average distance covered by humans while
     * walking is 1.3 m/s. Using quadruple update time for safety.
     */
    private static final long WALK_UPDATE_INTERVAL = (long) (LocationOrder.METERS_RADIUS * 4 / 1.3 * 1000) / 2;
    /**
     * Update frequency for driving at 50 km/h. Using quadruple update time for safety.
     */
    private static final long DRIVE_UPDATE_INTERVAL = (long) (LocationOrder.METERS_RADIUS * 4 / 13 * 1000) / 2;
    /**
     * Fast update frequency for screen on state.
     */
    private static final long SCREEN_ON_UPDATE_INTERVAL = 1000;
    /**
     * Update distance for screen on state.
     */
    private static final long SCREEN_ON_UPDATE_DISTANCE = 1;
    /**
     * Initial backoff interval is double the {@link LocationService#WALK_UPDATE_INTERVAL}.
     */
    private static final long INITIAL_BACKOFF_INTERVAL = WALK_UPDATE_INTERVAL * 2;
    /**
     * Location search duration.
     */
    private static final long LOCATION_SEARCH_DURATION = 30 * 1000;
    /**
     * Target we publish for clients to send messages to IncomingHandler.
     */
    private final Messenger mMessenger = new Messenger(new IncomingHandler());
    protected boolean mPowerConnected;
    private final BroadcastReceiver receiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {

            String action = intent.getAction();

            if (action.equals(Intent.ACTION_POWER_CONNECTED)) {

                LOGGER.debug("Power connected, increasing location frequency update.");
                setOnScreeState();
                mPowerConnected = true;
            } else if (action.equals(Intent.ACTION_POWER_DISCONNECTED)) {
                LOGGER.debug("Power disconnected, restoring location frequency update.");
                mPowerConnected = false;
                PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
                // if we received a power disconnected event and the screen is off then
                // set the location update frequency to off screen
                if (!pm.isScreenOn()) {
                    setOffScreenState();
                }
            }
        }
    };
    private NotificationManager notificationManager;
    private boolean mWalking = true;
    /**
     * Keeps track of all current registered clients.
     */
    private ArrayList<Messenger> mClients = new ArrayList<>();
    private LocationManager mLocationManager;
    private volatile boolean mOnScreen;

    // ==================== LIFECYCLE METHODS ====================
    private LocationListener mFine = new LocationListener() {

        @Override
        public void onLocationChanged(Location location) {
            LOGGER.debug("Sending fine fast location : {}", location);
            // send the location before doing any other work
            sendLocation(location);
            // only start the algorithm if the app is running in the background
            if (!mOnScreen) {
                restartBackoff();
            }
        }

        @Override
        public void onProviderDisabled(String provider) {
            // NO-OP
        }

        @Override
        public void onProviderEnabled(String provider) {
            // NO-OP
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            // NO-OP
        }

    };
    /**
     * Handler to post start and stop location updates runnables.
     */
    private Handler mBackoffHandler = new Handler();
    /**
     * The shortest duration of backoff time. The initial value is twice the frequency of
     * {@link #WALK_UPDATE_INTERVAL} since it doesn't make sense to request locations faster when we
     * have GPS fix problems than we there is a normal location update.
     */
    private long mBackoffTime = INITIAL_BACKOFF_INTERVAL;
    // =================END LIFECYCLE METHODS ====================
    private volatile boolean mBackoffStarted;
    /**
     * Starts the location updates and post a request to stop them using
     * {@link #stopLocationRequest} after {@link #LOCATION_SEARCH_DURATION} interval.
     */
    private Runnable startLocationRequests = new Runnable() {

        @Override
        public void run() {
            LOGGER.debug("Location requests started and will be stopped in {} milliseconds",
                    LOCATION_SEARCH_DURATION);

            mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                    mWalking ? WALK_UPDATE_INTERVAL : DRIVE_UPDATE_INTERVAL, 0, mFine);
            // stop location requests after we waited for the location search
            // duration
            mBackoffHandler.postDelayed(stopLocationRequest, LOCATION_SEARCH_DURATION);
        }
    };
    /**
     * Stops the location updates and posts a request to start them in after {@link #mBackoffTime}
     * interval .
     */
    private Runnable stopLocationRequest = new Runnable() {

        @Override
        public void run() {
            mLocationManager.removeUpdates(mFine);
            // only start if the backoff algorithm is enabled
            // necessary as the stopBackoff method may try to remove the
            // runnables while the
            // runnables are running with the effect that the algorithm doesn't
            // stop
            if (mBackoffStarted) {
                // start location requests after we have waited the backoff time
                mBackoffHandler.postDelayed(startLocationRequests, mBackoffTime);
            }
            // if the backoff interval has not increased to the max value
            // double the interval
            // need in order not to grow the backoff interval indefinitely
            LOGGER.debug("Location requests stopped and will be started in  {} milliseconds", mBackoffTime);

            if (mBackoffTime < MAX_BACKOFF_INTERVAL) {
                mBackoffTime *= 2;
            }

        }
    };

    private void sendLocation(Location location) {
        LOGGER.debug("Location changed: {}", location);
        for (int i = mClients.size() - 1; i >= 0; i--) {

            try {
                // Send data as an Integer
                mClients.get(i).send(Message.obtain(null, MSG_LOCATION_CHANGED, location));

            } catch (RemoteException e) {
                // The client is dead. Remove it from the list; we are going
                // through the list from
                // back to front so this is safe to do inside the loop.
                mClients.remove(i);
            }
        }

        Message message = Message.obtain();
        message.obj = location;

        try {
            mMessenger.send(message);
        } catch (RemoteException e) {
            LOGGER.error("Error sending location", e);
        }

    }

    @Override
    public void onCreate() {
        LOGGER.debug("On create");
        super.onCreate();
        displayRunningNotification();
        LOGGER.debug("Location manager set up.");

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

        setOffScreenState();
        registerPowerReceiver();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    // *----------- Exponential backoff code ---------------

    @Override
    public void onDestroy() {
        LOGGER.debug("On destroy");
        super.onDestroy();
        // stop listening for power events
        unregisterReceiver(receiver);

        mLocationManager.removeUpdates(mFine);
        notificationManager.cancel(APPLICATION_ID);
        LOGGER.debug("Service on destroy called.");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        LOGGER.debug("Unbind called.");
        return super.onUnbind(intent);
    }

    private void displayRunningNotification() {
        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // display notification only if users says so

        notificationManager.notify(APPLICATION_ID, createNotification());
    }

    private Notification createNotification() {
        String contentTitle = getString(R.string.app_name);
        String running = getString(R.string.running);

        CharSequence tickerText = contentTitle + " " + running;

        // instantiate notification
        Notification notification = new NotificationCompat.Builder(this).setTicker(tickerText)
                .setWhen(System.currentTimeMillis()).setSmallIcon(R.drawable.icon).build();

        notification.flags |= Notification.FLAG_NO_CLEAR;
        notification.flags |= Notification.FLAG_ONGOING_EVENT;

        // Define the Notification's expanded message and Intent:
        CharSequence contentText = contentTitle + " " + running;
        Intent notificationIntent = new Intent(this, FogOfExplore.class);
        // notificationIntent.setAction("android.intent.action.VIEW");
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        notification.setLatestEventInfo(this, contentTitle, contentText, contentIntent);
        return notification;
    }

    /**
     * Turns off fast GPS updates when the application is not in foreground.
     */
    private void setOffScreenState() {
        mOnScreen = false;
        // move from screen on updates to regular speed updates
        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                mWalking ? WALK_UPDATE_INTERVAL : DRIVE_UPDATE_INTERVAL, 0, mFine);
        restartBackoff();
    }

    /**
     * Turns on fast GPS updates when the application is in foreground and tries to display the last
     * known location.
     */
    private void setOnScreeState() {
        mOnScreen = true;
        stopBackoff();
        Location network = mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        Location gps = mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
        // Location passive =
        // mLocationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);

        // Location toSend = (gps != null) ? gps : (network != null) ? network
        // : (passive != null) ? passive : null;
        Location toSend = (gps != null) ? gps : (network != null) ? network : null;

        if (toSend != null) {
            sendLocation(toSend);
        }

        // register fast location listener
        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, SCREEN_ON_UPDATE_INTERVAL,
                SCREEN_ON_UPDATE_DISTANCE, mFine);

    }

    /**
     * Restarts the entire backoff algorithm. Called every time we have a location fix.
     */
    private void restartBackoff() {
        LOGGER.debug("Restarting backoff handler, last backoff time was {}", mBackoffTime);
        stopBackoff();
        mBackoffStarted = true;

        // post a request to stop the location updates but give a chance to the
        // location listeners to get a location and restart the backoff
        // algorithm again
        long actualBackoff = mBackoffTime * 2;
        LOGGER.debug("Initial backoff stop listener request. The updates will be stopped in  {} milliseconds",
                actualBackoff);
        mBackoffHandler.postDelayed(stopLocationRequest, actualBackoff);
    }

    // *----------- Exponential backoff code end ---------------

    /**
     * Stop the exponential backoff algorithm.
     */
    private void stopBackoff() {
        LOGGER.debug("Stopping backoff last backoff time was {}", mBackoffTime);
        mBackoffTime = mWalking ? WALK_UPDATE_INTERVAL * 2 : DRIVE_UPDATE_INTERVAL * 2;
        mBackoffStarted = false;
        LOGGER.debug("Backoff time reset to  {}", mBackoffTime);
        // remove any runnables from backoff handler
        mBackoffHandler.removeCallbacks(startLocationRequests);
        mBackoffHandler.removeCallbacks(stopLocationRequest);

    }

    /**
     * When binding to the service, we return an interface to our messenger for sending messages to
     * the service.
     */
    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    // ------------ Power connected broadcast receiver ------------
    private void registerPowerReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.intent.action.ACTION_POWER_CONNECTED");
        filter.addAction("android.intent.action.ACTION_POWER_DISCONNECTED");
        registerReceiver(receiver, filter);
    }

    /**
     * Handler of incoming messages from clients.
     */
    private class IncomingHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            LOGGER.debug("Message received: {}", msg.what);
            switch (msg.what) {
            case MSG_WALK:
                LOGGER.debug("Walk message received.");
                mWalking = true;
                break;
            case MSG_DRIVE:
                LOGGER.debug("Drive message received.");
                mWalking = false;
                break;
            case MSG_UNREGISTER_CLIENT:
                mClients.remove(msg.replyTo);
                break;
            case MSG_UNREGISTER_INTERFACE:
                // remove client
                mClients.remove(msg.replyTo);
                LOGGER.debug("Setting the service to off screen state.");
                // if the power is connected do not change the
                // the location update frequency
                if (mPowerConnected) {
                    LOGGER.debug("Power is connected, not changing location update frequency");
                    break;
                }
                setOffScreenState();
                break;
            case MSG_REGISTER_INTERFACE:
                LOGGER.debug("Setting the service to on screen state.");
                // if the power is connected do not change the
                // the location update frequency
                if (mPowerConnected) {
                    LOGGER.debug("Power is connected, not changing location update frequency");
                    break;
                }
                setOnScreeState();
                break;
            case MSG_REGISTER_CLIENT:
                LOGGER.debug("Registering new client.");
                mClients.add(msg.replyTo);
                break;
            case MSG_SHOW_NOTIFICATION:
                LOGGER.debug("Showing notification");
                displayRunningNotification();
                break;
            case MSG_HIDE_NOTIFICATION:
                LOGGER.debug("Hiding notification");

                notificationManager.cancelAll();
                break;
            default:
                super.handleMessage(msg);
            }
        }

    }

}