Java tutorial
/* * Copyright (C) 2013 Jorrit "Chainfire" Jongma * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package eu.chainfire.geolog.service; import java.util.Locale; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks; import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener; import com.google.android.gms.location.ActivityRecognitionClient; import com.google.android.gms.location.ActivityRecognitionResult; import com.google.android.gms.location.DetectedActivity; import com.google.android.gms.location.LocationClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import eu.chainfire.geolog.Debug; import eu.chainfire.geolog.R; import eu.chainfire.geolog.data.Database; import eu.chainfire.geolog.data.Database.Accuracy; import eu.chainfire.geolog.data.Database.Activity; import eu.chainfire.geolog.data.Database.Profile.Type; import eu.chainfire.geolog.ui.MainActivity; import eu.chainfire.geolog.ui.SettingsFragment; import android.annotation.SuppressLint; import android.app.AlarmManager; 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.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.location.Location; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; public class BackgroundService extends Service { public static void startService(Context context) { context.startService(new Intent(context.getApplicationContext(), BackgroundService.class)); } private static String EXTRA_ALARM_CALLBACK = "eu.chainfire.geolog.EXTRA.ALARM_CALLBACK"; private volatile ServiceThread thread = null; private volatile PowerManager.WakeLock wakelock = null; private volatile NotificationManager notificationManager; private volatile PendingIntent notificationIntent; private volatile Notification.Builder notificationBuilder; @SuppressLint("NewApi") @Override public void onCreate() { super.onCreate(); Debug.log("Service created"); if (thread == null) { Debug.log("Launching thread"); thread = new ServiceThread(); thread.setContext(getApplicationContext()); thread.start(); } PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GeoLog Wakelock"); Intent i = new Intent(); i.setAction(Intent.ACTION_MAIN); i.addCategory(Intent.CATEGORY_LAUNCHER); i.setClass(this, MainActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_NEW_TASK); notificationIntent = PendingIntent.getActivity(this, 0, i, 0); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationBuilder = (new Notification.Builder(this)).setSmallIcon(R.drawable.ic_stat_service) .setContentIntent(notificationIntent).setWhen(System.currentTimeMillis()).setAutoCancel(false) .setOngoing(true).setContentTitle(getString(R.string.service_title)) .setContentText(getString(R.string.service_waiting)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { notificationBuilder.setShowWhen(false); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { /* quick turn off, maybe ? if added, make sure to add a button to preferences to disable these buttons notificationBuilder. setPriority(Notification.PRIORITY_MAX). addAction(0, "A", notificationIntent). addAction(0, "B", notificationIntent). addAction(0, "C", notificationIntent); */ } updateNotification(); } @SuppressWarnings("deprecation") @SuppressLint("NewApi") private void updateNotification() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { notificationManager.notify(1, notificationBuilder.build()); } else { notificationManager.notify(1, notificationBuilder.getNotification()); } } @Override public void onDestroy() { Debug.log("Stopping thread"); thread.signalStop(); try { thread.join(); } catch (Exception e) { } thread = null; ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancelAll(); Debug.log("Service destroyed"); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { thread.processIntent(intent); return START_STICKY; } @Override public IBinder onBind(Intent intent) { return null; } private class ServiceThread extends Thread { private static final int FLAG_SETUP = 1; private static final int FLAG_ACTIVITY_UPDATE = 2; private static final int FLAG_LOCATION_UPDATE = 4; private static final int FLAG_PROFILE = 8; private volatile Context context = null; private volatile Handler handler = null; private volatile AlarmManager alarm = null; private volatile PendingIntent alarmCallback = null; private volatile ActivityRecognitionClient activityClient = null; private volatile boolean activityConnected = false; private volatile PendingIntent activityIntent = null; private volatile LocationClient locationClient = null; private volatile boolean locationConnected = false; private volatile Database.Helper databaseHelper = null; private volatile boolean metric = true; private volatile Database.Activity lastActivity = Database.Activity.UNKNOWN; private volatile int lastConfidence = 0; private volatile Location lastLocLoc = null; private volatile Database.Location lastLocation = null; private volatile long lastLocationDuplicates = 0; private volatile long lastNonUnknown = 0; private volatile Database.Accuracy lastLocationAccuracy = Database.Accuracy.NONE; private volatile int lastLocationInterval = -1; private volatile int lastActivityInterval = -1; private volatile int lastBatteryLevel = 0; private volatile boolean isSegmentStart = true; private volatile long lastProfileUpdate = SystemClock.elapsedRealtime(); private volatile long scheduledReduceAccuracyTime = 0; private volatile SharedPreferences prefs = null; private volatile Database.Profile currentProfile = null; // Main thread public void setContext(Context context) { this.context = context; } public void signalStop() { if (handler != null) { handler.post(new Runnable() { @Override public void run() { Looper.myLooper().quit(); } }); } } public boolean processIntent(Intent intent) { if (intent == null) return false; if (ActivityRecognitionResult.hasResult(intent)) { processActivity(intent); return true; } else if (intent.hasExtra(EXTRA_ALARM_CALLBACK)) { processAlarmCallback(intent); return true; } return false; } private void processActivity(Intent intent) { if (handler != null) { final DetectedActivity activity = ActivityRecognitionResult.extractResult(intent) .getMostProbableActivity(); if (Database.isDetectedActivityValid(activity)) { wakelock.acquire(); handler.post(new Runnable() { @Override public void run() { setActivity(Database.activityFromDetectedActivity(activity), activity.getConfidence()); wakelock.release(); } }); } } } private void processAlarmCallback(Intent intent) { if (handler != null) { wakelock.acquire(); handler.post(new Runnable() { @Override public void run() { updateListeners(0); wakelock.release(); } }); } } // Service thread private void setActivity(Database.Activity activity, int confidence) { if ((activity == Activity.UNKNOWN) && (SystemClock.elapsedRealtime() < lastNonUnknown + (2 * 60 * 1000)) && (SystemClock.elapsedRealtime() > lastNonUnknown)) { return; } if (activity != Activity.UNKNOWN) { lastNonUnknown = SystemClock.elapsedRealtime(); } Debug.log( String.format(Locale.ENGLISH, "A: %s (%d%%)", Database.activityToString(activity), confidence)); lastActivity = activity; lastConfidence = confidence; updateListeners(FLAG_ACTIVITY_UPDATE); } private void setLocation(Location location) { Debug.log(String.format(Locale.ENGLISH, "L: lat=%.8f long=%.5f alt=%.5f bearing=%.4f speed=%.4f accuracy=%.2f", location.getLatitude(), location.getLongitude(), location.getAltitude(), location.getBearing(), location.getSpeed(), location.getAccuracy())); lastLocLoc = location; updateListeners(FLAG_LOCATION_UPDATE); } @SuppressLint("NewApi") private void updateListeners(int flags) { if (!(activityConnected && locationConnected && (currentProfile != null))) return; if (currentProfile.getType() == Type.OFF) { stopSelf(); } if ((flags & FLAG_PROFILE) == FLAG_PROFILE) { Debug.log("Profile update"); lastActivity = Activity.UNKNOWN; lastConfidence = 0; scheduledReduceAccuracyTime = 0L; lastProfileUpdate = SystemClock.elapsedRealtime(); } Accuracy originalAccuracy = lastLocationAccuracy; Database.Profile.ActivitySettings wanted = currentProfile.getActivitySettings(lastActivity); Database.Accuracy wantedAccuracy = wanted.getAccuracy(); int wantedLocationInterval = wanted.getLocationInterval(); int wantedActivityInterval = wanted.getActivityInterval(); boolean allowUpdateActivityInterval = true; boolean allowUpdateLocationInterval = true; boolean allowUpdateLocationAccuracy = true; if (((SystemClock.elapsedRealtime() > lastProfileUpdate + (90 * 1000)) || (SystemClock.elapsedRealtime() < lastProfileUpdate)) && (currentProfile.getReduceAccuracyDelay() > 0) && ((wantedActivityInterval > lastActivityInterval) || (wantedLocationInterval > lastLocationInterval) || (Database.accuracyToInt(wantedAccuracy) < Database .accuracyToInt(lastLocationAccuracy)))) { long left = 0; if (scheduledReduceAccuracyTime == 0) { left = currentProfile.getReduceAccuracyDelay() * 1000; scheduledReduceAccuracyTime = SystemClock.elapsedRealtime() + left; alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, scheduledReduceAccuracyTime + 1000, alarmCallback); } else { left = scheduledReduceAccuracyTime - SystemClock.elapsedRealtime(); } if (left <= 0) scheduledReduceAccuracyTime = 0L; allowUpdateActivityInterval = ((left <= 0) || (wantedActivityInterval < lastActivityInterval) || (lastActivityInterval == -1)); allowUpdateLocationInterval = ((left <= 0) || (wantedLocationInterval < lastLocationInterval) || (lastLocationInterval == -1)); allowUpdateLocationAccuracy = ((left <= 0) || (Database.accuracyToInt(wantedAccuracy) > Database.accuracyToInt(lastLocationAccuracy))); if (!allowUpdateActivityInterval) wantedActivityInterval = lastActivityInterval; if (!allowUpdateLocationInterval) wantedLocationInterval = lastLocationInterval; if (!allowUpdateLocationAccuracy) wantedAccuracy = lastLocationAccuracy; if (!allowUpdateActivityInterval) Debug.log(String.format(Locale.ENGLISH, "ActivityInterval --> Delay (%ds remaining)", (left / 1000))); if (!allowUpdateLocationInterval) Debug.log(String.format(Locale.ENGLISH, "LocationInterval --> Delay (%ds remaining)", (left / 1000))); if (!allowUpdateLocationAccuracy) Debug.log(String.format(Locale.ENGLISH, "LocationAccuracy --> Delay (%ds remaining)", (left / 1000))); } else { scheduledReduceAccuracyTime = 0; alarm.cancel(alarmCallback); } if ((wantedAccuracy != lastLocationAccuracy) || (wantedLocationInterval != lastLocationInterval)) { String s = "NONE"; if (wantedAccuracy == Accuracy.LOW) s = "LOW"; if (wantedAccuracy == Accuracy.HIGH) s = "HIGH"; Debug.log(String.format(Locale.ENGLISH, "Location --> %s %ds", s, wantedLocationInterval)); locationClient.removeLocationUpdates(locationListener); if ((wantedAccuracy != Accuracy.NONE) && (wantedLocationInterval > 0)) { if ((lastLocationAccuracy == Accuracy.NONE) || (lastLocationInterval == 0)) isSegmentStart = true; LocationRequest req = new LocationRequest(); req.setFastestInterval(wantedLocationInterval * 250); req.setInterval(wantedLocationInterval * 1000); if (lastLocationAccuracy == Accuracy.NONE) req.setPriority(LocationRequest.PRIORITY_NO_POWER); if (lastLocationAccuracy == Accuracy.LOW) req.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); if (lastLocationAccuracy == Accuracy.HIGH) req.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); locationClient.requestLocationUpdates(req, locationListener); } lastLocationAccuracy = wantedAccuracy; lastLocationInterval = wantedLocationInterval; } if (wantedActivityInterval != lastActivityInterval) { Debug.log(String.format(Locale.ENGLISH, "Activity --> %ds", wantedActivityInterval)); activityClient.removeActivityUpdates(activityIntent); if ((wantedActivityInterval == 0) && (lastActivityInterval != 0)) { lastActivity = Activity.UNKNOWN; lastConfidence = 0; } if (wantedActivityInterval != 0) { activityClient.requestActivityUpdates(wantedActivityInterval * 1000, activityIntent); } lastActivityInterval = wantedActivityInterval; } if ((flags & FLAG_ACTIVITY_UPDATE) == FLAG_ACTIVITY_UPDATE) { if ((lastLocation == null) || (lastLocation.getActivity() != lastActivity) || (lastLocation.getConfidence() != lastConfidence)) { Debug.log("Activity update"); if (lastLocation == null) { lastLocationDuplicates = 0; lastLocation = new Database.Location(); lastLocation.setTime(System.currentTimeMillis()); } lastLocation.setActivity(lastActivity); lastLocation.setConfidence(lastConfidence); } } else if ((flags & FLAG_LOCATION_UPDATE) == FLAG_LOCATION_UPDATE) { if ((lastLocation == null) || (lastLocLoc == null) || (lastLocation.getLatitude() != lastLocLoc.getLatitude()) || (lastLocation.getLongitude() != lastLocLoc.getLongitude()) || (lastLocation.getAccuracyDistance() > lastLocLoc.getAccuracy()) || (lastLocation.getActivity() != lastActivity) || (lastLocation.getConfidence() != lastConfidence) || (lastLocation.getAccuracySetting() != originalAccuracy) || (isSegmentStart)) { Debug.log("Location update"); if (lastLocationDuplicates > 0) { Debug.log("Saving last duplicate (out of " + String.valueOf(lastLocationDuplicates) + ")"); Database.Location.copy(databaseHelper, lastLocation); } lastLocationDuplicates = 0; Database.Location loc = new Database.Location(); loc.setActivity(lastActivity); loc.setConfidence(lastConfidence); loc.setBattery(lastBatteryLevel); loc.setAccuracySetting(originalAccuracy); loc.isSegmentStart(isSegmentStart); loc.loadFromLocation(lastLocLoc); Debug.log("Saved to database: " + String.valueOf(loc.saveToDatabase(databaseHelper))); lastLocation = loc; isSegmentStart = false; } else if ((lastLocation != null) && (lastLocLoc != null)) { lastLocationDuplicates++; lastLocation.setActivity(lastActivity); lastLocation.setConfidence(lastConfidence); lastLocation.setBattery(lastBatteryLevel); lastLocation.setAccuracySetting(originalAccuracy); lastLocation.isSegmentStart(isSegmentStart); lastLocation.loadFromLocation(lastLocLoc); } } if (lastLocation != null) { notificationBuilder.setWhen(lastLocation.getTime()) .setContentText(String.format(Locale.ENGLISH, "%s ~ %d%% / %.5f, %.5f ~ %.0f%s", Database.activityToString(lastLocation.getActivity()), lastLocation.getConfidence(), lastLocation.getLatitude(), lastLocation.getLongitude(), metric ? lastLocation.getAccuracyDistance() : lastLocation.getAccuracyDistance() * SettingsFragment.METER_FEET_RATIO, metric ? "m" : "ft")); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { notificationBuilder.setShowWhen(true); } updateNotification(); } } private ConnectionCallbacks activityConnectionCallbacks = new ConnectionCallbacks() { @Override public void onConnected(Bundle arg0) { Debug.log("ActivityRecognitionClient connected"); activityConnected = true; updateListeners(FLAG_SETUP); } @Override public void onDisconnected() { Debug.log("ActivityRecognitionClient disconnected"); activityConnected = false; } }; private OnConnectionFailedListener activityConnectionFailed = new OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult arg0) { Debug.log("ActivityRecognitionClient connection failed"); activityConnected = false; signalStop(); } }; private LocationListener locationListener = new LocationListener() { @Override public void onLocationChanged(Location arg0) { wakelock.acquire(); try { setLocation(arg0); } finally { wakelock.release(); } } }; private ConnectionCallbacks locationConnectionCallbacks = new ConnectionCallbacks() { @Override public void onConnected(Bundle arg0) { Debug.log("LocationClient connected"); locationConnected = true; updateListeners(FLAG_SETUP); } @Override public void onDisconnected() { Debug.log("LocationClient disconnected"); locationConnected = false; } }; private OnConnectionFailedListener locationConnectionFailed = new OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult arg0) { Debug.log("LocationClient connection failed"); locationConnected = false; signalStop(); } }; private BroadcastReceiver databaseUpdated = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if ((currentProfile != null) && (intent != null) && intent.hasExtra(Database.Helper.EXTRA_TABLE) && intent.getStringExtra(Database.Helper.EXTRA_TABLE).equals(Database.Profile.TABLE_NAME) && intent.hasExtra(Database.Helper.EXTRA_ID) && (intent.getLongExtra(Database.Helper.EXTRA_ID, 0) == currentProfile.getId())) { currentProfile = Database.Profile.getById(databaseHelper, intent.getLongExtra(Database.Helper.EXTRA_ID, 0), currentProfile); updateListeners(FLAG_PROFILE); } } }; private OnSharedPreferenceChangeListener preferencesUpdated = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(SettingsFragment.PREF_UNITS)) { metric = !prefs.getString(SettingsFragment.PREF_UNITS, SettingsFragment.PREF_UNITS_DEFAULT) .equals(SettingsFragment.VALUE_UNITS_IMPERIAL); updateListeners(0); } if (key.equals(SettingsFragment.PREF_CURRENT_PROFILE)) { currentProfile = Database.Profile.getById(databaseHelper, sharedPreferences.getLong(key, 0), currentProfile); updateListeners(FLAG_PROFILE); } } }; private BroadcastReceiver batteryReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); boolean plugged = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0); boolean charging = ((status == BatteryManager.BATTERY_STATUS_CHARGING) || ((status == BatteryManager.BATTERY_STATUS_FULL) && plugged)); int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); if (charging) level += 100; lastBatteryLevel = level; } }; @Override public void run() { Debug.log("Thread init"); databaseHelper = Database.Helper.getInstance(context); alarm = (AlarmManager) context.getSystemService(ALARM_SERVICE); { Intent i = new Intent(context.getApplicationContext(), BackgroundService.class); i.putExtra(EXTRA_ALARM_CALLBACK, 1); alarmCallback = PendingIntent.getService(BackgroundService.this, 0, i, 0); } Looper.prepare(); handler = new Handler(); Debug.log("Registering for updates"); prefs = PreferenceManager.getDefaultSharedPreferences(context); long id = prefs.getLong(SettingsFragment.PREF_CURRENT_PROFILE, 0); if (id > 0) currentProfile = Database.Profile.getById(databaseHelper, id, null); if (currentProfile == null) currentProfile = Database.Profile.getOffProfile(databaseHelper); metric = !prefs.getString(SettingsFragment.PREF_UNITS, SettingsFragment.PREF_UNITS_DEFAULT) .equals(SettingsFragment.VALUE_UNITS_IMPERIAL); prefs.registerOnSharedPreferenceChangeListener(preferencesUpdated); LocalBroadcastManager.getInstance(context).registerReceiver(databaseUpdated, new IntentFilter(Database.Helper.NOTIFY_BROADCAST)); Debug.log("Registering for power levels"); context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); Debug.log("Connecting ActivityRecognitionClient"); activityIntent = PendingIntent.getService(context, 1, new Intent(context, BackgroundService.class), 0); activityClient = new ActivityRecognitionClient(context, activityConnectionCallbacks, activityConnectionFailed); activityClient.connect(); Debug.log("Connecting LocationClient"); locationClient = new LocationClient(context, locationConnectionCallbacks, locationConnectionFailed); locationClient.connect(); Debug.log("Entering loop"); handler.post(new Runnable() { @Override public void run() { updateListeners(FLAG_SETUP); } }); Looper.loop(); Debug.log("Exiting loop"); context.unregisterReceiver(batteryReceiver); LocalBroadcastManager.getInstance(context).unregisterReceiver(databaseUpdated); prefs.unregisterOnSharedPreferenceChangeListener(preferencesUpdated); if (activityConnected) { activityClient.removeActivityUpdates(activityIntent); activityClient.disconnect(); } if (locationConnected) { locationClient.removeLocationUpdates(locationListener); locationClient.disconnect(); } } } }