nl.dobots.presence.PresenceDetectionApp.java Source code

Java tutorial

Introduction

Here is the source code for nl.dobots.presence.PresenceDetectionApp.java

Source

package nl.dobots.presence;

import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.Date;

import nl.dobots.bluenet.ble.extended.structs.BleDevice;
import nl.dobots.presence.ask.AskWrapper;
import nl.dobots.presence.cfg.Config;
import nl.dobots.presence.cfg.Settings;
import nl.dobots.presence.gui.MainActivity;
import nl.dobots.bluenet.localization.Localization;
import nl.dobots.bluenet.localization.SimpleLocalization;
import nl.dobots.bluenet.localization.locations.Location;
import nl.dobots.bluenet.service.BleScanService;
import nl.dobots.bluenet.service.callbacks.EventListener;
import nl.dobots.bluenet.service.callbacks.IntervalScanListener;

/**
 * Copyright (c) 2015 Dominik Egger <dominik@dobots.nl>. All rights reserved.
 * <p/>
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as
 * published by the Free Software Foundation.
 * <p/>
 * This code 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
 * version 3 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 * <p/>
 * Created on 5-8-15
 *
 * @author Dominik Egger
 */
public class PresenceDetectionApp extends Application implements IntervalScanListener, EventListener {

    private static final String TAG = PresenceDetectionApp.class.getCanonicalName();

    private static PresenceDetectionApp instance = null;

    public static PresenceDetectionApp getInstance() {
        return instance;
    }

    private Settings _settings;
    private AskWrapper _ask;

    private NotificationManager _notificationManager;

    private Boolean _currentPresence = null;
    private String _currentLocation = "";
    private String _currentAdditionalInfo;

    private boolean _retry = false;
    private boolean _updatingPresence;

    private boolean _updateWaiting;
    private boolean _updateWaitingPresence;
    private String _updateWaitingLocation;
    private String _updateWaitingAdditionalInfo;

    private BleScanService _service;
    private boolean _bound;

    //   private boolean _highFrequencyDetection;

    private Localization _localization;

    private boolean _detectionPaused = false;
    private boolean _scanning;

    // the time at which the manual override will expire and the detection will change to auto
    private Date _manualExpirationDate;

    private ArrayList<PresenceUpdateListener> _listenerList = new ArrayList<>();

    private boolean _networkErrorActive;

    private Handler _networkHandler;

    private Handler _watchdogHandler;
    private Runnable _watchdogRunner = new Runnable() {

        @Override
        public void run() {
            if (System.currentTimeMillis() - _localization.getLastDetectionTime() > Config.PRESENCE_TIMEOUT) {
                if (_currentPresence) {
                    Log.i(TAG, String.format(
                            "Watchdog timeout. No beacon seen within %d seconds. Changing state to not present.",
                            Config.PRESENCE_TIMEOUT / 1000));

                    _networkHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            updatePresence(false, "", "");
                        }
                    });
                }

                // todo: should we go into low frequency scanning, e.g. 2 sec per 5 min or so until at least one beacon is seen again?
            } else {
                Log.i(TAG, "watchdog ok.");
            }
            _watchdogHandler.postDelayed(_watchdogRunner, Config.WATCHDOG_INTERVAL);
        }
    };

    private BroadcastReceiver _receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                if (!intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
                    // connection reestablished, clear network error
                    _networkErrorActive = false;

                    Log.d(TAG, "Intent: " + intent.toString());
                    Log.d(TAG, "Extras: " + intent.getExtras().toString());

                    if (_updateWaiting) {
                        Log.i(TAG, "update waiting ...");
                        // if update is waiting, trigger presence update again ...
                        _networkHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                updatePresence(_updateWaitingPresence, _updateWaitingLocation,
                                        _updateWaitingAdditionalInfo);
                            }
                        });
                    } else {
                        // ... otherwise, request current presence in case it changed, or if we never
                        // new it in the first place
                        if (_currentPresence == null) {
                            Log.i(TAG, "no update, and presence unknown ...");
                            _networkHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    requestCurrentPresence();
                                }
                            });
                        }
                    }
                }
            }
        }
    };

    private ServiceConnection _connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.i(TAG, "connected to ble scan service ...");
            BleScanService.BleScanBinder binder = (BleScanService.BleScanBinder) service;
            _service = binder.getService();
            _service.registerIntervalScanListener(PresenceDetectionApp.this);
            _service.registerEventListener(PresenceDetectionApp.this);

            _service.setScanInterval(Config.LOW_SCAN_INTERVAL);
            _service.setScanPause(Config.LOW_SCAN_PAUSE);

            // check if login information is present, otherwise ..
            if (_ask.isLoginCredentialsValid(_settings.getUsername(), _settings.getPassword())
                    && !_settings.getLocationsList().isEmpty()) {
                // if login credentials are ok and locations are configured, start detection directly ..
                //            _service.startIntervalScan();
            } else {
                // .. otherwise, pause detection until both requirements are met
                pauseDetection();
            }
            _scanning = _service.isScanning();

            _bound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.i(TAG, "disconnected from service");
            _bound = false;
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;

        // load settings from persistent storage
        _settings = Settings.getInstance();
        _settings.readPersistentStorage(getApplicationContext());

        _settings.readPersistentLocations(getApplicationContext());

        // get localization algo
        _localization = new SimpleLocalization(_settings.getLocationsList(), _settings.getDetectionDistance());

        // get ask wrapper (wraps login and presence functions)
        _ask = AskWrapper.getInstance();

        _notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        // watchdog handler checks for non-presence and handles manual override
        HandlerThread watchdogThread = new HandlerThread("Watchdog");
        watchdogThread.start();
        _watchdogHandler = new Handler(watchdogThread.getLooper());
        _watchdogHandler.postDelayed(_watchdogRunner, Config.WATCHDOG_INTERVAL);

        // network handler used for network operations (login, updatePresence, etc.)
        HandlerThread networkThread = new HandlerThread("NetworkHandler");
        networkThread.start();
        _networkHandler = new Handler(networkThread.getLooper());

        // filter for connectivity broadcasts
        IntentFilter filter = new IntentFilter();
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        registerReceiver(_receiver, filter);

        //      Intent startServiceIntent = new Intent(this, BleScanService.class);
        //      this.startService(startServiceIntent);

        Intent intent = new Intent(this, BleScanService.class);
        bindService(intent, _connection, Context.BIND_AUTO_CREATE);

        // set expiration time for RSSI measurements. keeps measurements of 5 scan intervals
        // before throwing them out. i.e. averages over all measurements received in the last 5
        // scan intervals
        BleDevice.setExpirationTime(5 * (Config.LOW_SCAN_PAUSE + Config.LOW_SCAN_INTERVAL));

    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        if (_bound) {
            unbindService(_connection);
        }
        _settings.onDestroy();
    }

    private void requestCurrentPresence() {

        AskWrapper.PresenceCallback callback = new AskWrapper.PresenceCallback() {
            @Override
            public void onSuccess(boolean present, String location) {
                // let current values unassigned so that it will be populated the first time
                // that a device / location is found
                //            _currentPresence = present;
                //            _currentLocation = location;
                // only inform listener (e.g. to update the UI)
                Log.i(TAG, "Remote presence: " + present + " at " + location);
                notifyPresenceUpdate(present, location, "");
            }

            @Override
            public void onError(String errorMessage) {
                Log.e(TAG, String.format("could not get presence. Error: %s", errorMessage));
            }
        };

        if (!_ask.isLoggedIn()) {
            _ask.login(_settings.getUsername(), _settings.getPassword(), _settings.getServer(), callback);
        } else {
            _ask.getCurrentPresence(callback);
        }
    }

    public void pauseDetection() {
        if (!_detectionPaused) {
            _detectionPaused = true;
            if (_bound) {
                // if connected to service, store current scanning state
                _scanning = _service.isScanning();
                _service.stopIntervalScan();
            }
            Log.i(TAG, "stop watchdog");
            _watchdogHandler.removeCallbacks(_watchdogRunner);
        }
    }

    public void resumeDetection() {
        if (_detectionPaused) {
            _detectionPaused = false;
            if (_bound && _scanning) {
                // if connected to service and last state was scanning, resume scan
                _service.startIntervalScan();
            }
            Log.i(TAG, "resume watchdog");
            _watchdogHandler.postDelayed(_watchdogRunner, Config.WATCHDOG_INTERVAL);
        }
    }

    @Override
    public void onScanStart() {
        // don't care
    }

    @Override
    public void onScanEnd() {
        if (_detectionPaused)
            return;

        final ArrayList<BleDevice> devices = _service.getDeviceMap().getDistanceSortedList();

        Log.d(TAG, "search locations");
        if (!_updatingPresence && !devices.isEmpty()) {
            SimpleLocalization.LocalizationResult localizationResult = _localization.findLocation(devices);

            if (localizationResult != null) {
                final Location location = localizationResult.location;
                final BleDevice device = localizationResult.triggerDevice;
                Log.d(TAG, "post presence update");
                _networkHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        updatePresence(true, location.getName(),
                                String.format("%s at %.2f", device.getName(), device.getDistance()));
                    }
                });
            } else {
                Log.d(TAG, "no location found");
            }
        }
    }

    public Date getManualExpirationDate() {
        return _manualExpirationDate;
    }

    private Runnable _onManualPresenceExpired = new Runnable() {
        @Override
        public void run() {
            setAutoPresence();
        }
    };

    public void setManualPresence(final boolean present, long expirationTime) {
        _manualExpirationDate = new Date(new Date().getTime() + expirationTime);
        pauseDetection();
        _watchdogHandler.postDelayed(_onManualPresenceExpired, expirationTime);
        _networkHandler.post(new Runnable() {
            @Override
            public void run() {
                updatePresence(present, "Manual", "");
            }
        });
    }

    public void setAutoPresence() {
        _manualExpirationDate = null;
        _watchdogHandler.removeCallbacks(_onManualPresenceExpired);
        // force update
        _updatingPresence = false;
        resumeDetection();
    }

    private void onNetworkError(String error, boolean present, String location, String additionalInfo) {

        // only trigger notification once as long as the network error is active.
        if (!_networkErrorActive) {
            _networkErrorActive = true;

            if (_settings.isNotificationsEnabled()) {
                Intent contentIntent = new Intent(this, MainActivity.class);
                contentIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                PendingIntent piContent = PendingIntent.getActivity(this, 0, contentIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                Intent wifiSettingsIntent = new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK);
                PendingIntent piWifiSettings = PendingIntent.getActivity(this, 0, wifiSettingsIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                        .setSmallIcon(R.mipmap.ic_launcher).setContentTitle("Network Error").setContentText(error)
                        .setStyle(new NotificationCompat.BigTextStyle().bigText(error))
                        .addAction(android.R.drawable.ic_menu_manage, "Wifi Settings", piWifiSettings)
                        .setContentIntent(piContent).setDefaults(Notification.DEFAULT_SOUND)
                        .setLights(Color.BLUE, 500, 1000);
                _notificationManager.notify(Config.PRESENCE_NOTIFICATION_ID, builder.build());
                Toast.makeText(this, error, Toast.LENGTH_LONG).show();
            }
        }

        // set logged in as false (because of network error)
        _ask.setLoggedIn(false);

        // store the presence update, will be triggered once network connection is reestablished
        _updateWaiting = true;
        _updateWaitingPresence = present;
        _updateWaitingLocation = location;
        _updateWaitingAdditionalInfo = additionalInfo;

        // just to make sure we don't get stuck
        _updatingPresence = false;
    }

    private void updatePresence(final boolean present, final String location, final String additionalInfo) {

        //we only send the new presence if it differs from the old one, to avoid surcharging the server
        if ((_currentPresence == null) || (_currentPresence != present) || !_currentLocation.matches(location)) {

            _updatingPresence = true;

            //         // can't execute network operations in the main thread, so we have to delegate
            //         // the call to the network handler
            //         if (Looper.myLooper() == Looper.getMainLooper()) {
            //            _networkHandler.post(new Runnable() {
            //               @Override
            //               public void run() {
            //                  updatePresence(present, location, additionalInfo);
            //               }
            //            });
            //            return;
            //         }

            // check if we are logged in, and do so otherwise
            if (!_ask.isLoggedIn()) {
                _ask.login(_settings.getUsername(), _settings.getPassword(), _settings.getServer(),
                        new AskWrapper.PresenceCallback() {
                            @Override
                            public void onSuccess(boolean remotePresent, String remoteLocation) {
                                // login was successful, call update presence again
                                updatePresence(present, location, additionalInfo);

                                // just to be sure, it should already be set to false when
                                // connection is reestablished, but just in case we missed that
                                // broadcast, if login succeeded, we can set it to false for sure
                                _networkErrorActive = false;
                            }

                            @Override
                            public void onError(String errorMessage) {
                                Log.e(TAG, "failed to log in");

                                onNetworkError(
                                        String.format("Can't login, please check your internet!\n\n" + "Error: %s",
                                                errorMessage),
                                        present, location, additionalInfo);
                            }
                        });
                return;
            }

            Log.i(TAG, "Update presence to: " + present + " at " + location);

            _ask.updatePresence(present, location, new AskWrapper.StatusCallback() {
                @Override
                public void onSuccess() {
                    // set current presence values
                    _currentLocation = location;
                    _currentPresence = present;
                    _currentAdditionalInfo = additionalInfo;

                    // notify anybody listening for presence updates
                    notifyPresenceUpdate(present, location, additionalInfo);

                    // cancel any outstanding notification (network error or BT error)
                    _notificationManager.cancel(Config.PRESENCE_NOTIFICATION_ID);

                    // clear flags
                    _updatingPresence = false;
                    _updateWaiting = false;
                    _retry = false;

                    // just to be sure. it should already be set to false when
                    // connection is reestablished, but just in case we missed that
                    // broadcast, if updating the presence succeeded, we can set it
                    // to false for sure
                    _networkErrorActive = false;

                }

                @Override
                public void onError(String errorMessage) {
                    Log.e(TAG, "failed to update presence");

                    // most likely the presence update fails because the session expired
                    // so we try again, but this time, we make sure that we log in again first
                    // by setting logged in state to false
                    _ask.setLoggedIn(false);

                    if (!_retry) {

                        // set retry flag ..
                        _retry = true;

                        // .. and try calling update presence again
                        updatePresence(present, location, additionalInfo);

                    } else {
                        // if the second time it fails again after logging in, we abort

                        // clear flags ..
                        _updatingPresence = false;
                        _retry = false;

                        // .. inform user and store presence update ..
                        onNetworkError(String.format(
                                "Failed to update presence, please check your internet!\n\n" + "Error: %s",
                                errorMessage), present, location, additionalInfo);

                        // .. and abort
                    }
                }
            });
        }
    }

    private void notifyPresenceUpdate(boolean present, String location, String additionalInfo) {
        for (PresenceUpdateListener listener : _listenerList) {
            listener.onPresenceUpdate(present, location, additionalInfo);
        }
    }

    public void registerPresenceUpdateListener(PresenceUpdateListener listener) {
        if (_listenerList.indexOf(listener) == -1) {
            _listenerList.add(listener);
        }
    }

    public void unregisterPresenceUpdateListener(PresenceUpdateListener listener) {
        if (_listenerList.indexOf(listener) != -1) {
            _listenerList.remove(listener);
        }
    }

    public Boolean getCurrentPresence() {
        // should never be null, but just in case
        if (_currentPresence == null)
            return false;

        return _currentPresence;
    }

    public String getCurrentLocation() {
        return _currentLocation;
    }

    public String getCurrentAdditionalInfo() {
        return _currentAdditionalInfo;
    }

    @Override
    public void onEvent(EventListener.Event event) {
        switch (event) {
        case BLUETOOTH_INITIALIZED: {
            // if BLE init succeeded clear the notification again
            _notificationManager.cancel(Config.PRESENCE_NOTIFICATION_ID);
            break;
        }
        case BLUETOOTH_TURNED_OFF: {
            if (_settings.isNotificationsEnabled()) {
                Intent contentIntent = new Intent(this, MainActivity.class);
                PendingIntent piContent = PendingIntent.getActivity(this, 0, contentIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                Intent btEnableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                PendingIntent piBtEnable = PendingIntent.getActivity(this, 0, btEnableIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                String errorMessage = "Can't detect presence without BLE!";

                NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                        .setSmallIcon(R.mipmap.ic_launcher).setContentTitle("Presence Detection Error")
                        .setContentText(errorMessage)
                        .setStyle(new NotificationCompat.BigTextStyle().bigText(errorMessage))
                        .addAction(android.R.drawable.ic_menu_manage, "Enable Bluetooth", piBtEnable)
                        .setContentIntent(piContent).setDefaults(Notification.DEFAULT_SOUND)
                        .setLights(Color.BLUE, 500, 1000);
                _notificationManager.notify(Config.PRESENCE_NOTIFICATION_ID, builder.build());
            }
        }
        }
    }

    //   public void setHighFrequencyDetection(boolean enable) {
    //      if (_highFrequencyDetection == enable) return;
    //
    //      if (enable) {
    //         Log.i(TAG, "set scan frequency to high");
    //         _service.setScanInterval(HIGH_SCAN_PERIOD);
    //         _service.setScanPause(HIGH_SCAN_PAUSE);
    //         _service.updateScanParams();
    //         BleDevice.setExpirationTime(HIGH_SCAN_EXPIRATION);
    //      } else {
    //         Log.i(TAG, "set scan frequency to low");
    //         _service.setScanInterval(LOW_SCAN_PERIOD);
    //         _service.setScanPause(LOW_SCAN_PAUSE);
    //         _service.updateScanParams();
    //         BleDevice.setExpirationTime(LOW_SCAN_EXPIRATION);
    //      }
    //      _highFrequencyDetection = enable;
    //   }

}