org.fitchfamily.android.wifi_backend.backend.BackendService.java Source code

Java tutorial

Introduction

Here is the source code for org.fitchfamily.android.wifi_backend.backend.BackendService.java

Source

package org.fitchfamily.android.wifi_backend.backend;

/*
 *  WiFi Backend for Unified Network Location
 *  Copyright (C) 2014,2015  Tod Fitch
 *
 *  This program 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.
 *
 *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.location.Location;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.EService;
import org.androidannotations.annotations.SystemService;

import org.fitchfamily.android.wifi_backend.BuildConfig;
import org.fitchfamily.android.wifi_backend.Configuration;
import org.fitchfamily.android.wifi_backend.database.SamplerDatabase;
import org.fitchfamily.android.wifi_backend.R;
import org.fitchfamily.android.wifi_backend.ui.MainActivity_;
import org.fitchfamily.android.wifi_backend.ui.MainActivity;
import org.fitchfamily.android.wifi_backend.util.AgeValue;
import org.fitchfamily.android.wifi_backend.util.distanceCache;
import org.fitchfamily.android.wifi_backend.util.LocationUtil;
import org.fitchfamily.android.wifi_backend.util.SimpleLocation;
import org.fitchfamily.android.wifi_backend.wifi.WifiAccessPoint;
import org.fitchfamily.android.wifi_backend.wifi.WifiBlacklist;
import org.fitchfamily.android.wifi_backend.wifi.WifiReceiver;
import org.fitchfamily.android.wifi_backend.wifi.WifiReceiver.WifiReceivedCallback;

import org.microg.nlp.api.LocationBackendService;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

@EService
public class BackendService extends LocationBackendService implements WifiReceivedCallback {
    private static final String TAG = "WiFiBackendSrv";
    private static final boolean DEBUG = BuildConfig.DEBUG;

    private static final int NOTIFICATION = 42;

    private static BackendService instance;
    private SamplerDatabase database;
    private WifiReceiver wifiReceiver;
    private Thread thread;
    private boolean gpsMonitorRunning = false;
    private boolean permissionNotificationShown = false;

    private static distanceCache distanceResults = new distanceCache();

    private AgeValue<SimpleLocation> gpsLocation = AgeValue.create();
    private List<WifiAccessPoint> apList = null;

    @SystemService
    protected NotificationManager notificationManager;

    @SystemService
    protected WifiManager wifiManager;

    @AfterInject
    protected void init() {
        database = SamplerDatabase.getInstance(this);
        wifiReceiver = new WifiReceiver(wifiManager, this);
    }

    @Override
    protected void onOpen() {
        if (DEBUG) {
            Log.i(TAG, "onOpen()");
        }
        instance = this;
        registerReceiver(wifiReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));

        if (Configuration.with(this).hasLocationAccess()) {
            setgpsMonitorRunning(true);
        }
    }

    @Override
    protected void onClose() {
        if (DEBUG) {
            Log.i(TAG, "onClose()");
        }

        unregisterReceiver(wifiReceiver);
        setgpsMonitorRunning(false);
        setShowPermissionNotification(false);

        wifiReceiver = null;
    }

    @Override
    protected Location update() {
        if (!Configuration.with(this).hasLocationAccess()) {
            if (DEBUG) {
                Log.i(TAG, "update(): Permission missing");
            }
            setgpsMonitorRunning(false);
            setShowPermissionNotification(true);
        } else if (wifiReceiver != null) {
            setgpsMonitorRunning(true);
            setShowPermissionNotification(false);
            if (DEBUG) {
                Log.i(TAG, "Starting scan for WiFi APs");
            }
            wifiReceiver.startScan();
        } else {
            if (DEBUG) {
                Log.i(TAG, "update(): no wifiReceiver???");
            }
        }
        return null;
    }

    private void gpsLocationUpdated(final android.location.Location locReport) {
        if (DEBUG) {
            Log.i(TAG, "GPS Location update: " + locReport.toString());
        }
        gpsLocation(SimpleLocation.fromAndroidLocation(locReport));
        if (wifiReceiver != null) {
            wifiReceiver.startScan();
            calculate();
        }
    }

    public static void instanceGpsLocationUpdated(final android.location.Location locReport) {
        if (instance != null) {
            instance.gpsLocationUpdated(locReport);
        }
    }

    // WiFi scanning can several seconds (seen over 11 seconds on my phone). So store
    // our latest seen results and we will update our calculations when we get either a
    // new WiFi scan or a GPS location.
    @Override
    public void processWiFiScanResults(@NonNull List<WifiAccessPoint> apList) {
        if (DEBUG) {
            Log.i(TAG, "processWiFiScanResults() - entry.");
        }
        this.apList = apList;
        calculate();
    }

    private synchronized void calculate() {
        if (thread != null) {
            Log.d(TAG, "calculate() : Thread busy?!");
            return;
        }
        final List<WifiAccessPoint> accessPoints = this.apList;
        final SimpleLocation gpsLocation = gpsLocation();

        if (accessPoints == null) {
            return;
        }
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (gpsLocation != null) {
                    Log.d(TAG, "GPS Location fresh.");
                    updateWiFiDatabase(gpsLocation, accessPoints);
                }
                Location result = wiFiBasedLocation(accessPoints);
                report(result);
                thread = null;
            }
        });
        thread.start();
    }

    // Stuff for binding to (basically starting) background AP location
    // collection
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder binder) {

            if (DEBUG) {
                Log.i(TAG, "mConnection.onServiceConnected()");
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            if (DEBUG) {
                Log.i(TAG, "mConnection.onServiceDisconnected()");
            }
        }
    };

    private void setgpsMonitorRunning(boolean enable) {
        if (enable != gpsMonitorRunning) {
            if (enable) {
                bindService(new Intent(this, gpsMonitor_.class), mConnection, Context.BIND_AUTO_CREATE);
            } else {
                unbindService(mConnection);
            }

            gpsMonitorRunning = enable;
        }
    }

    private void setShowPermissionNotification(boolean visible) {
        if (visible != permissionNotificationShown) {
            if (visible) {
                if (DEBUG) {
                    Log.i(TAG, "setShowPermissionNotification(true)");
                }

                notificationManager.notify(NOTIFICATION,
                        new NotificationCompat.Builder(this).setWhen(0).setShowWhen(false).setAutoCancel(false)
                                .setOngoing(true)
                                .setContentIntent(PendingIntent.getActivity(this, 0,
                                        MainActivity_.intent(this).action(MainActivity.Action.request_permission)
                                                .get(),
                                        PendingIntent.FLAG_UPDATE_CURRENT))
                                .setContentTitle(getString(R.string.app_title))
                                .setContentText(getString(R.string.preference_grant_permission))
                                .setSmallIcon(R.drawable.ic_stat_no_location).build());
            } else {
                if (DEBUG) {
                    Log.i(TAG, "setShowPermissionNotification(false)");
                }

                notificationManager.cancel(NOTIFICATION);
            }

            permissionNotificationShown = visible;
        }
    }

    private Location wiFiBasedLocation(@NonNull List<WifiAccessPoint> accessPoints) {
        if (accessPoints.isEmpty()) {
            return null;
        } else {
            Set<Location> locations = new HashSet<>(accessPoints.size());

            for (WifiAccessPoint accessPoint : accessPoints) {
                SimpleLocation result = database.getLocation(accessPoint.rfId());

                if (result != null) {
                    Bundle extras = new Bundle();
                    extras.putInt(Configuration.EXTRA_SIGNAL_LEVEL, accessPoint.level());

                    Location location = result.toAndroidLocation();
                    location.setExtras(extras);
                    locations.add(location);
                }
            }

            if (locations.isEmpty()) {
                if (DEBUG) {
                    Log.i(TAG, "WifiDBResolver.process(): No APs with known locations");
                }
                return null;
            } else {
                // Find largest group of AP locations. If we don't have at
                // least two near each other then we don't have enough
                // information to get a good location.
                locations = LocationUtil.culledAPs(locations, BackendService.this);

                if (locations == null || locations.size() < 2) {
                    if (DEBUG) {
                        Log.i(TAG,
                                "WifiDBResolver.process(): Insufficient number of WiFi hotspots to resolve location");
                    }
                    return null;
                } else {
                    Location avgLoc = LocationUtil.weightedAverage("wifi", locations);

                    if (avgLoc == null) {
                        if (DEBUG) {
                            Log.e(TAG, "Averaging locations did not work.");
                        }
                        return null;
                    } else {
                        avgLoc.setTime(System.currentTimeMillis());
                        return avgLoc;
                    }
                }
            }
        }
    }

    private void updateWiFiDatabase(SimpleLocation location, final List<WifiAccessPoint> accessPoints) {
        long entryTime = System.currentTimeMillis();

        try {
            database.beginTransaction();
            for (WifiAccessPoint accessPoint : accessPoints) {
                if (WifiBlacklist.ignore(accessPoint.ssid())) {
                    database.dropAccessPoint(accessPoint.rfId());
                } else {
                    database.addSample(accessPoint.rfType(), accessPoint.ssid(), accessPoint.rfId(), location);
                }
            }
            database.commitTransaction();
        } catch (Exception ex) {
            Log.i(TAG, "updateWiFiDatabase(): " + ex);
        }

        if (DEBUG) {
            distanceResults.logCacheStats();
        }
    }

    // gpsLocation is set and used in different thread contexts, make accessors for them
    // synchronized.
    private synchronized void gpsLocation(SimpleLocation locReport) {
        gpsLocation.value(locReport);
    }

    private synchronized SimpleLocation gpsLocation() {

        // Only use for GPS location is for updating the AP database and we only want
        // to do that if the location is relatively fresh. So if not fresh, return null.
        long locationAge = gpsLocation.age();
        if (locationAge < 500
                || (locationAge < Configuration.with(BackendService.this).validGpsTimeInMilliseconds())) {
            return gpsLocation.value();
        }
        return null;
    }
}