net.fabiszewski.ulogger.LoggerService.java Source code

Java tutorial

Introduction

Here is the source code for net.fabiszewski.ulogger.LoggerService.java

Source

/*
 * Copyright (c) 2017 Bartek Fabiszewski
 * http://www.fabiszewski.net
 *
 * This file is part of logger-android.
 * Licensed under GPL, either version 3, or any later.
 * See <http://www.gnu.org/licenses/>
 */

package net.fabiszewski.ulogger;

import android.Manifest;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import static android.location.LocationProvider.AVAILABLE;
import static android.location.LocationProvider.OUT_OF_SERVICE;
import static android.location.LocationProvider.TEMPORARILY_UNAVAILABLE;

/**
 * Background service logging positions to database
 * and synchronizing with remote server.
 *
 */

public class LoggerService extends Service {

    private static final String TAG = LoggerService.class.getSimpleName();
    public static final String BROADCAST_LOCATION_STARTED = "net.fabiszewski.ulogger.broadcast.location_started";
    public static final String BROADCAST_LOCATION_STOPPED = "net.fabiszewski.ulogger.broadcast.location_stopped";
    public static final String BROADCAST_LOCATION_UPDATED = "net.fabiszewski.ulogger.broadcast.location_updated";
    public static final String BROADCAST_LOCATION_PERMISSION_DENIED = "net.fabiszewski.ulogger.broadcast.location_permission_denied";
    public static final String BROADCAST_LOCATION_NETWORK_DISABLED = "net.fabiszewski.ulogger.broadcast.network_disabled";
    public static final String BROADCAST_LOCATION_GPS_DISABLED = "net.fabiszewski.ulogger.broadcast.gps_disabled";
    public static final String BROADCAST_LOCATION_NETWORK_ENABLED = "net.fabiszewski.ulogger.broadcast.network_enabled";
    public static final String BROADCAST_LOCATION_GPS_ENABLED = "net.fabiszewski.ulogger.broadcast.gps_enabled";
    public static final String BROADCAST_LOCATION_DISABLED = "net.fabiszewski.ulogger.broadcast.location_disabled";
    private boolean liveSync = false;
    private Intent syncIntent;

    private static volatile boolean isRunning = false;
    private LoggerThread thread;
    private Looper looper;
    private LocationManager locManager;
    private LocationListener locListener;
    private DbAccess db;
    private int maxAccuracy;
    private float minDistance;
    private long minTimeMillis;
    // max time tolerance is half min time, but not more that 5 min
    final private long minTimeTolerance = Math.min(minTimeMillis / 2, 5 * 60 * 1000);
    final private long maxTimeMillis = minTimeMillis + minTimeTolerance;

    private static Location lastLocation = null;
    private static volatile long lastUpdateRealtime = 0;

    private final int NOTIFICATION_ID = (int) (System.currentTimeMillis() / 1000L);
    private NotificationManager mNotificationManager;
    private boolean useGps;
    private boolean useNet;

    /**
     * Basic initializations.
     */
    @Override
    public void onCreate() {
        if (Logger.DEBUG) {
            Log.d(TAG, "[onCreate]");
        }

        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancelAll();

        locManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locListener = new mLocationListener();

        // read user preferences
        updatePreferences();

        boolean hasLocationUpdates = requestLocationUpdates();

        if (hasLocationUpdates) {
            isRunning = true;
            sendBroadcast(BROADCAST_LOCATION_STARTED);

            syncIntent = new Intent(getApplicationContext(), WebSyncService.class);

            thread = new LoggerThread();
            thread.start();
            looper = thread.getLooper();

            db = DbAccess.getInstance();
            db.open(this);

            // start websync service if needed
            if (liveSync && db.needsSync()) {
                startService(syncIntent);
            }
        }
    }

    /**
     * Start main thread, request location updates, start synchronization.
     *
     * @param intent Intent
     * @param flags Flags
     * @param startId Unique id
     * @return Always returns START_STICKY
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Logger.DEBUG) {
            Log.d(TAG, "[onStartCommand]");
        }

        final boolean prefsUpdated = (intent != null) && intent.getBooleanExtra(MainActivity.UPDATED_PREFS, false);
        if (prefsUpdated) {
            handlePrefsUpdated();
        } else if (isRunning) {
            // first start
            showNotification(mNotificationManager, NOTIFICATION_ID);
        } else {
            // onCreate failed to start updates
            stopSelf();
        }

        return START_STICKY;
    }

    /**
     * When user updated preferences, restart location updates, stop service on failure
     */
    private void handlePrefsUpdated() {
        // restart updates
        updatePreferences();
        if (isRunning && !restartUpdates()) {
            // no valid providers after preferences update
            stopSelf();
        }
    }

    /**
     * Check if user granted permission to access location.
     *
     * @return True if permission granted, false otherwise
     */
    private boolean canAccessLocation() {
        return (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED);
    }

    /**
     * Check if given provider exists on device
     * @param provider Provider
     * @return True if exists, false otherwise
     */
    private boolean providerExists(String provider) {
        return locManager.getAllProviders().contains(provider);
    }

    /**
     * Reread preferences
     */
    private void updatePreferences() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        minTimeMillis = Long.parseLong(prefs.getString("prefMinTime", getString(R.string.pref_mintime_default)))
                * 1000;
        minDistance = Float
                .parseFloat(prefs.getString("prefMinDistance", getString(R.string.pref_mindistance_default)));
        maxAccuracy = Integer
                .parseInt(prefs.getString("prefMinAccuracy", getString(R.string.pref_minaccuracy_default)));
        useGps = prefs.getBoolean("prefUseGps", providerExists(LocationManager.GPS_PROVIDER));
        useNet = prefs.getBoolean("prefUseNet", providerExists(LocationManager.NETWORK_PROVIDER));
        liveSync = prefs.getBoolean("prefLiveSync", false);
    }

    /**
     * Restart request for location updates
     *
     * @return True if succeeded, false otherwise (eg. disabled all providers)
     */
    private boolean restartUpdates() {
        if (Logger.DEBUG) {
            Log.d(TAG, "[location updates restart]");
        }

        locManager.removeUpdates(locListener);
        return requestLocationUpdates();
    }

    /**
     * Request location updates
     * @return True if succeeded from at least one provider
     */
    private boolean requestLocationUpdates() {
        boolean hasLocationUpdates = false;
        if (canAccessLocation()) {
            if (useNet) {
                //noinspection MissingPermission
                locManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minTimeMillis, minDistance,
                        locListener, looper);
                if (locManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                    hasLocationUpdates = true;
                    if (Logger.DEBUG) {
                        Log.d(TAG, "[Using net provider]");
                    }
                }
            }
            if (useGps) {
                //noinspection MissingPermission
                locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTimeMillis, minDistance,
                        locListener, looper);
                if (locManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                    hasLocationUpdates = true;
                    if (Logger.DEBUG) {
                        Log.d(TAG, "[Using gps provider]");
                    }
                }
            }
            if (!hasLocationUpdates) {
                // no location provider available
                sendBroadcast(BROADCAST_LOCATION_DISABLED);
                if (Logger.DEBUG) {
                    Log.d(TAG, "[No available location updates]");
                }
            }
        } else {
            // can't access location
            sendBroadcast(BROADCAST_LOCATION_PERMISSION_DENIED);
            if (Logger.DEBUG) {
                Log.d(TAG, "[Location permission denied]");
            }
        }

        return hasLocationUpdates;
    }

    /**
     * Service cleanup
     */
    @Override
    public void onDestroy() {
        if (Logger.DEBUG) {
            Log.d(TAG, "[onDestroy]");
        }

        if (canAccessLocation()) {
            //noinspection MissingPermission
            locManager.removeUpdates(locListener);
        }
        if (db != null) {
            db.close();
        }

        isRunning = false;

        mNotificationManager.cancel(NOTIFICATION_ID);
        sendBroadcast(BROADCAST_LOCATION_STOPPED);

        if (thread != null) {
            thread.interrupt();
        }
        thread = null;

    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Check if logger service is running.
     *
     * @return True if running, false otherwise
     */
    public static boolean isRunning() {
        return isRunning;
    }

    /**
     * Return realtime of last update in milliseconds
     *
     * @return Time or zero if not set
     */
    public static long lastUpdateRealtime() {
        return lastUpdateRealtime;
    }

    /**
     * Reset realtime of last update
     */
    public static void resetUpdateRealtime() {

        lastUpdateRealtime = 0;
    }

    /**
     * Main service thread class handling location updates.
     */
    private class LoggerThread extends HandlerThread {
        LoggerThread() {
            super("LoggerThread");
        }

        private final String TAG = LoggerThread.class.getSimpleName();

        @Override
        public void interrupt() {
            if (Logger.DEBUG) {
                Log.d(TAG, "[interrupt]");
            }
        }

        @Override
        public void finalize() throws Throwable {
            if (Logger.DEBUG) {
                Log.d(TAG, "[finalize]");
            }
            super.finalize();
        }

        @Override
        public void run() {
            if (Logger.DEBUG) {
                Log.d(TAG, "[run]");
            }
            super.run();
        }
    }

    /**
     * Show notification
     *
     * @param mNotificationManager Notification manager
     * @param mId Notification Id
     */
    private void showNotification(NotificationManager mNotificationManager, int mId) {
        if (Logger.DEBUG) {
            Log.d(TAG, "[showNotification " + mId + "]");
        }

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_notify_24dp).setContentTitle(getString(R.string.app_name))
                .setContentText(String.format(getString(R.string.is_running), getString(R.string.app_name)));
        Intent resultIntent = new Intent(this, MainActivity.class);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(resultIntent);
        PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);
        mNotificationManager.notify(mId, mBuilder.build());
    }

    /**
     * Send broadcast message
     * @param broadcast Broadcast message
     */
    private void sendBroadcast(String broadcast) {
        Intent intent = new Intent(broadcast);
        sendBroadcast(intent);
    }

    /**
     * Location listener class
     */
    private class mLocationListener implements LocationListener {

        @Override
        public void onLocationChanged(Location loc) {

            if (Logger.DEBUG) {
                Log.d(TAG, "[location changed: " + loc + "]");
            }

            if (!skipLocation(loc)) {

                lastLocation = loc;
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    lastUpdateRealtime = SystemClock.elapsedRealtime();
                } else {
                    lastUpdateRealtime = loc.getElapsedRealtimeNanos() / 1000000;
                }
                db.writeLocation(loc);
                sendBroadcast(BROADCAST_LOCATION_UPDATED);
                if (liveSync) {
                    startService(syncIntent);
                }
            }
        }

        /**
         * Should the location be logged or skipped
         * @param loc Location
         * @return True if skipped
         */
        private boolean skipLocation(Location loc) {
            // accuracy radius too high
            if (loc.hasAccuracy() && loc.getAccuracy() > maxAccuracy) {
                if (Logger.DEBUG) {
                    Log.d(TAG, "[location accuracy above limit: " + loc.getAccuracy() + " > " + maxAccuracy + "]");
                }
                // reset gps provider to get better accuracy even if time and distance criteria don't change
                if (loc.getProvider().equals(LocationManager.GPS_PROVIDER)) {
                    restartUpdates();
                }
                return true;
            }
            // use network provider only if recent gps data is missing
            if (loc.getProvider().equals(LocationManager.NETWORK_PROVIDER) && lastLocation != null) {
                // we received update from gps provider not later than after maxTime period
                long elapsedMillis = SystemClock.elapsedRealtime() - lastUpdateRealtime;
                if (lastLocation.getProvider().equals(LocationManager.GPS_PROVIDER)
                        && elapsedMillis < maxTimeMillis) {
                    // skip network provider
                    if (Logger.DEBUG) {
                        Log.d(TAG, "[location network provider skipped]");
                    }
                    return true;
                }
            }
            return false;
        }

        /**
         * Callback on provider disabled
         * @param provider Provider
         */
        @Override
        public void onProviderDisabled(String provider) {
            if (Logger.DEBUG) {
                Log.d(TAG, "[location provider " + provider + " disabled]");
            }
            if (provider.equals(LocationManager.GPS_PROVIDER)) {
                sendBroadcast(BROADCAST_LOCATION_GPS_DISABLED);
            } else if (provider.equals(LocationManager.NETWORK_PROVIDER)) {
                sendBroadcast(BROADCAST_LOCATION_NETWORK_DISABLED);
            }
        }

        /**
         * Callback on provider enabled
         * @param provider Provider
         */
        @Override
        public void onProviderEnabled(String provider) {
            if (Logger.DEBUG) {
                Log.d(TAG, "[location provider " + provider + " enabled]");
            }
            if (provider.equals(LocationManager.GPS_PROVIDER)) {
                sendBroadcast(BROADCAST_LOCATION_GPS_ENABLED);
            } else if (provider.equals(LocationManager.NETWORK_PROVIDER)) {
                sendBroadcast(BROADCAST_LOCATION_NETWORK_ENABLED);
            }
        }

        /**
         * Callback on provider status change
         * @param provider Provider
         * @param status Status
         * @param extras Extras
         */
        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            if (Logger.DEBUG) {
                final String statusString;
                switch (status) {
                case OUT_OF_SERVICE:
                    statusString = "out of service";
                    break;
                case TEMPORARILY_UNAVAILABLE:
                    statusString = "temporarily unavailable";
                    break;
                case AVAILABLE:
                    statusString = "available";
                    break;
                default:
                    statusString = "unknown";
                    break;
                }
                if (Logger.DEBUG) {
                    Log.d(TAG, "[location status for " + provider + " changed: " + statusString + "]");
                }
            }
        }
    }
}