From c2a4f13769fead5b21d6a597ba01b018a06725a8 Mon Sep 17 00:00:00 2001 From: Mieszko Date: Sat, 15 Sep 2018 19:38:58 +0200 Subject: [PATCH 01/16] Initial build --- .idea/assetWizardSettings.xml | 14 - app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 7 +- .../findmytutor/activity/MainActivity.java | 355 +++++++++++++++- .../service/LocationRequestHelper.java | 39 ++ .../service/LocationResultHelper.java | 158 +++++++ .../findmytutor/service/LocationService.java | 397 ++++++++++++++++++ .../LocationUpdatesBroadcastReceiver.java | 69 +++ .../service/LocationUpdatesIntentService.java | 74 ++++ app/src/main/res/layout/activity_login.xml | 2 + app/src/main/res/layout/activity_main.xml | 71 +++- app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 18 + build.gradle | 2 +- 14 files changed, 1149 insertions(+), 62 deletions(-) delete mode 100644 .idea/assetWizardSettings.xml create mode 100644 app/src/main/java/com/uam/wmi/findmytutor/service/LocationRequestHelper.java create mode 100644 app/src/main/java/com/uam/wmi/findmytutor/service/LocationResultHelper.java create mode 100644 app/src/main/java/com/uam/wmi/findmytutor/service/LocationService.java create mode 100644 app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesBroadcastReceiver.java create mode 100644 app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesIntentService.java diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 2a9c5e0..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a333d4f..5bd7c0a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ android { } defaultConfig { applicationId "com.uam.wmi.findmytutor" - minSdkVersion 19 + minSdkVersion 26 targetSdkVersion 27 versionCode 1 versionName "1.0" @@ -45,4 +45,5 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:3.11.0" implementation "com.squareup.okhttp3:okhttp-urlconnection:3.10.0" implementation "com.squareup.okhttp3:logging-interceptor:3.11.0" + implementation "com.google.android.gms:play-services-location:11.0.4" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55a5adf..ad432e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,13 +3,13 @@ package="com.uam.wmi.findmytutor"> - - - + + + + /> \ No newline at end of file diff --git a/app/src/main/java/com/uam/wmi/findmytutor/activity/MainActivity.java b/app/src/main/java/com/uam/wmi/findmytutor/activity/MainActivity.java index a443ed2..c1173fc 100644 --- a/app/src/main/java/com/uam/wmi/findmytutor/activity/MainActivity.java +++ b/app/src/main/java/com/uam/wmi/findmytutor/activity/MainActivity.java @@ -1,30 +1,81 @@ package com.uam.wmi.findmytutor.activity; +import android.Manifest; import android.app.Fragment; import android.app.FragmentTransaction; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.Settings; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.BottomNavigationView; import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; + import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.MenuItem; import android.view.View; +import android.widget.Button; import android.widget.FrameLayout; +import android.widget.TextView; + +import com.google.android.gms.common.ConnectionResult; import com.mapbox.mapboxsdk.Mapbox; -import com.mapbox.mapboxsdk.maps.MapView; +import com.uam.wmi.findmytutor.BuildConfig; import com.uam.wmi.findmytutor.R; -import com.uam.wmi.findmytutor.model.Coordinate; -import com.uam.wmi.findmytutor.network.ApiClient; -import com.uam.wmi.findmytutor.service.CoordinateService; +import com.uam.wmi.findmytutor.service.LocationRequestHelper; +import com.uam.wmi.findmytutor.service.LocationResultHelper; +import com.uam.wmi.findmytutor.service.LocationUpdatesBroadcastReceiver; -import java.util.List; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener, + SharedPreferences.OnSharedPreferenceChangeListener { + + + private static final String TAG = MainActivity.class.getSimpleName(); + private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; + /** + * The desired interval for location updates. Inexact. Updates may be more or less frequent. + */ + // FIXME: 5/16/17 + private static final long UPDATE_INTERVAL = 10 * 1000; + + /** + * The fastest rate for active location updates. Updates will never be more frequent + * than this value, but they may be less frequent. + */ + // FIXME: 5/14/17 + private static final long FASTEST_UPDATE_INTERVAL = UPDATE_INTERVAL / 2; + + /** + * The max time before batched results are delivered by location services. Results may be + * delivered sooner than this interval. + */ + private static final long MAX_WAIT_TIME = UPDATE_INTERVAL * 3; + + /** + * Stores parameters for requests to the FusedLocationProviderApi. + */ + private LocationRequest mLocationRequest; + + /** + * The entry point to Google Play Services. + */ + private GoogleApiClient mGoogleApiClient; private BottomNavigationView mMainNav; private FrameLayout mMainFrame; @@ -34,18 +85,10 @@ public class MainActivity extends AppCompatActivity { private ProfileFragment profileFragment; - - private MapView mapView; - - public List getCoordinates() { - return this.coordinates; - } - - public void setCoordinates(List coordinates) { - this.coordinates = coordinates; - } - - public List coordinates; + // UI Widgets. + private Button mRequestUpdatesButton; + private Button mRemoveUpdatesButton; + private TextView mLocationUpdatesResultView; @Override protected void onCreate(Bundle savedInstanceState) { @@ -54,6 +97,11 @@ public class MainActivity extends AppCompatActivity { setContentView(R.layout.activity_main); + mRequestUpdatesButton = (Button) findViewById(R.id.request_updates_button); + mRemoveUpdatesButton = (Button) findViewById(R.id.remove_updates_button); + mLocationUpdatesResultView = (TextView) findViewById(R.id.location_updates_result); + + mMainFrame = (FrameLayout) findViewById(R.id.main_frame); mMainNav = (BottomNavigationView) findViewById(R.id.main_nav); @@ -62,8 +110,8 @@ public class MainActivity extends AppCompatActivity { profileFragment = new ProfileFragment(); // Default frag here - setFragment(mapFragment); - mMainNav.setSelectedItemId(R.id.nav_map); + //setFragment(mapFragment); + //mMainNav.setSelectedItemId(R.id.nav_map); mMainNav.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @Override @@ -106,16 +154,279 @@ public class MainActivity extends AppCompatActivity { } }); -// CoordinateService service = ApiClient.getClient(getApplicationContext()) -// .create(CoordinateService.class); + + // Check if the user revoked runtime permissions. + if (!checkPermissions()) { + requestPermissions(); + } + + buildGoogleApiClient(); } + private void setFragment(Fragment fragment) { FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.main_frame, fragment); fragmentTransaction.commit(); } + + + @Override + protected void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); } + + @Override + protected void onResume() { + super.onResume(); + updateButtonsState(LocationRequestHelper.getRequesting(this)); + mLocationUpdatesResultView.setText(LocationResultHelper.getSavedLocationResult(this)); + } + + @Override + protected void onStop() { + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + super.onStop(); + } + + /** + * Sets up the location request. Android has two location request settings: + * {@code ACCESS_COARSE_LOCATION} and {@code ACCESS_FINE_LOCATION}. These settings control + * the accuracy of the current location. This sample uses ACCESS_FINE_LOCATION, as defined in + * the AndroidManifest.xml. + *

+ * When the ACCESS_FINE_LOCATION setting is specified, combined with a fast update + * interval (5 seconds), the Fused Location Provider API returns location updates that are + * accurate to within a few feet. + *

+ * These settings are appropriate for mapping applications that show real-time location + * updates. + */ + private void createLocationRequest() { + mLocationRequest = new LocationRequest(); + + mLocationRequest.setInterval(UPDATE_INTERVAL); + + // Sets the fastest rate for active location updates. This interval is exact, and your + // application will never receive updates faster than this value. + mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL); + mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + + // Sets the maximum time when batched location updates are delivered. Updates may be + // delivered sooner than this interval. + mLocationRequest.setMaxWaitTime(MAX_WAIT_TIME); + } + + /** + * Builds {@link GoogleApiClient}, enabling automatic lifecycle management using + * {@link GoogleApiClient.Builder#enableAutoManage(android.support.v4.app.FragmentActivity, + * int, GoogleApiClient.OnConnectionFailedListener)}. I.e., GoogleApiClient connects in + * {@link AppCompatActivity#onStart}, or if onStart() has already happened, it connects + * immediately, and disconnects automatically in {@link AppCompatActivity#onStop}. + */ + private void buildGoogleApiClient() { + if (mGoogleApiClient != null) { + return; + } + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addConnectionCallbacks(this) + .enableAutoManage(this, this) + .addApi(LocationServices.API) + .build(); + createLocationRequest(); + } + + @Override + public void onConnected(@Nullable Bundle bundle) { + Log.i(TAG, "GoogleApiClient connected"); + } + + private PendingIntent getPendingIntent() { + Intent intent = new Intent(this, LocationUpdatesBroadcastReceiver.class); + intent.setAction(LocationUpdatesBroadcastReceiver.ACTION_PROCESS_UPDATES); + return PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public void onConnectionSuspended(int i) { + final String text = "Connection suspended"; + Log.w(TAG, text + ": Error code: " + i); + showSnackbar("Connection suspended"); + } + + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + final String text = "Exception while connecting to Google Play services"; + Log.w(TAG, text + ": " + connectionResult.getErrorMessage()); + showSnackbar(text); + } + + /** + * Shows a {@link Snackbar} using {@code text}. + * + * @param text The Snackbar text. + */ + private void showSnackbar(final String text) { + View container = findViewById(R.id.activity_main); + if (container != null) { + Snackbar.make(container, text, Snackbar.LENGTH_LONG).show(); + } + } + + /** + * Return the current state of the permissions needed. + */ + private boolean checkPermissions() { + int permissionState = ActivityCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_FINE_LOCATION); + return permissionState == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermissions() { + boolean shouldProvideRationale = + ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.ACCESS_FINE_LOCATION); + + // Provide an additional rationale to the user. This would happen if the user denied the + // request previously, but didn't check the "Don't ask again" checkbox. + if (shouldProvideRationale) { + Log.i(TAG, "Displaying permission rationale to provide additional context."); + Snackbar.make( + findViewById(R.id.activity_main), + R.string.permission_rationale, + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.ok, new View.OnClickListener() { + @Override + public void onClick(View view) { + // Request permission + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERMISSIONS_REQUEST_CODE); + } + }) + .show(); + } else { + Log.i(TAG, "Requesting permission"); + // Request permission. It's possible this can be auto answered if device policy + // sets the permission in a given state or the user denied the permission + // previously and checked "Never ask again". + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERMISSIONS_REQUEST_CODE); + } + } + + /** + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + Log.i(TAG, "onRequestPermissionResult"); + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { + if (grantResults.length <= 0) { + // If user interaction was interrupted, the permission request is cancelled and you + // receive empty arrays. + Log.i(TAG, "User interaction was cancelled."); + } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission was granted. Kick off the process of building and connecting + // GoogleApiClient. + buildGoogleApiClient(); + } else { + // Permission denied. + + // Notify the user via a SnackBar that they have rejected a core permission for the + // app, which makes the Activity useless. In a real app, core permissions would + // typically be best requested during a welcome-screen flow. + + // Additionally, it is important to remember that a permission might have been + // rejected without asking the user for permission (device policy or "Never ask + // again" prompts). Therefore, a user interface affordance is typically implemented + // when permissions are denied. Otherwise, your app could appear unresponsive to + // touches or interactions which have required permissions. + Snackbar.make( + findViewById(R.id.activity_main), + R.string.permission_denied_explanation, + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.settings, new View.OnClickListener() { + @Override + public void onClick(View view) { + // Build intent that displays the App settings screen. + Intent intent = new Intent(); + intent.setAction( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", + BuildConfig.APPLICATION_ID, null); + intent.setData(uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + }) + .show(); + } + } + + + try { + Log.i(TAG, "Starting location updates"); + LocationRequestHelper.setRequesting(this, true); + LocationServices.FusedLocationApi.requestLocationUpdates( + mGoogleApiClient, mLocationRequest, getPendingIntent()); + } catch (SecurityException e) { + LocationRequestHelper.setRequesting(this, false); + e.printStackTrace(); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { + if (s.equals(LocationResultHelper.KEY_LOCATION_UPDATES_RESULT)) { + Log.e("Service",LocationResultHelper.getSavedLocationResult(this)); + } else if (s.equals(LocationRequestHelper.KEY_LOCATION_UPDATES_REQUESTED)) { + updateButtonsState(LocationRequestHelper.getRequesting(this)); + } + } + + /** + * Handles the Request Updates button and requests start of location updates. + */ + public void requestLocationUpdates(View view) { + try { + Log.i(TAG, "Starting location updates"); + LocationRequestHelper.setRequesting(this, true); + LocationServices.FusedLocationApi.requestLocationUpdates( + mGoogleApiClient, mLocationRequest, getPendingIntent()); + } catch (SecurityException e) { + LocationRequestHelper.setRequesting(this, false); + e.printStackTrace(); + } + } + + /** + * Handles the Remove Updates button, and requests removal of location updates. + */ + public void removeLocationUpdates(View view) { + Log.i(TAG, "Removing location updates"); + LocationRequestHelper.setRequesting(this, false); + LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, + getPendingIntent()); + } + + /** + * Ensures that only one button is enabled at any time. The Start Updates button is enabled + * if the user is not requesting location updates. The Stop Updates button is enabled if the + * user is requesting location updates. + */ + private void updateButtonsState(boolean requestingLocationUpdates) { + + } + + +} + diff --git a/app/src/main/java/com/uam/wmi/findmytutor/service/LocationRequestHelper.java b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationRequestHelper.java new file mode 100644 index 0000000..3169bfa --- /dev/null +++ b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationRequestHelper.java @@ -0,0 +1,39 @@ +package com.uam.wmi.findmytutor.service; + +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + + +import android.content.Context; +import android.preference.PreferenceManager; + + +public class LocationRequestHelper { + + public final static String KEY_LOCATION_UPDATES_REQUESTED = "location-updates-requested"; + + public static void setRequesting(Context context, boolean value) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(KEY_LOCATION_UPDATES_REQUESTED, value) + .apply(); + } + + public static boolean getRequesting(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(KEY_LOCATION_UPDATES_REQUESTED, false); + } +} diff --git a/app/src/main/java/com/uam/wmi/findmytutor/service/LocationResultHelper.java b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationResultHelper.java new file mode 100644 index 0000000..70ee270 --- /dev/null +++ b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationResultHelper.java @@ -0,0 +1,158 @@ +package com.uam.wmi.findmytutor.service; + +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.location.Location; +import android.os.Build; +import android.preference.PreferenceManager; +import android.support.annotation.RequiresApi; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.app.NotificationChannel; + + +import com.uam.wmi.findmytutor.R; +import com.uam.wmi.findmytutor.activity.MainActivity; + +import java.text.DateFormat; +import java.util.Date; +import java.util.List; + +/** + * Class to process location results. + */ +public class LocationResultHelper { + + public final static String KEY_LOCATION_UPDATES_RESULT = "location-update-result"; + + final private static String PRIMARY_CHANNEL = "default"; + + + private Context mContext; + private List mLocations; + private NotificationManager mNotificationManager; + + @RequiresApi(api = Build.VERSION_CODES.O) + LocationResultHelper(Context context, List locations) { + mContext = context; + mLocations = locations; + + NotificationChannel channel = new NotificationChannel(PRIMARY_CHANNEL, + context.getString(R.string.default_channel), NotificationManager.IMPORTANCE_DEFAULT); + channel.setLightColor(Color.GREEN); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + getNotificationManager().createNotificationChannel(channel); + } + + /** + * Returns the title for reporting about a list of {@link Location} objects. + */ + private String getLocationResultTitle() { + String numLocationsReported = mContext.getResources().getQuantityString( + R.plurals.num_locations_reported, mLocations.size(), mLocations.size()); + return numLocationsReported + ": " + DateFormat.getDateTimeInstance().format(new Date()); + } + + private String getLocationResultText() { + if (mLocations.isEmpty()) { + return mContext.getString(R.string.unknown_location); + } + StringBuilder sb = new StringBuilder(); + for (Location location : mLocations) { + sb.append("("); + sb.append(location.getLatitude()); + sb.append(", "); + sb.append(location.getLongitude()); + sb.append(")"); + sb.append("\n"); + } + return sb.toString(); + } + + /** + * Saves location result as a string to {@link android.content.SharedPreferences}. + */ + void saveResults() { + PreferenceManager.getDefaultSharedPreferences(mContext) + .edit() + .putString(KEY_LOCATION_UPDATES_RESULT, getLocationResultTitle() + "\n" + + getLocationResultText()) + .apply(); + } + + /** + * Fetches location results from {@link android.content.SharedPreferences}. + */ + public static String getSavedLocationResult(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LOCATION_UPDATES_RESULT, ""); + } + + /** + * Get the notification mNotificationManager. + *

+ * Utility method as this helper works with it a lot. + * + * @return The system service NotificationManager + */ + private NotificationManager getNotificationManager() { + if (mNotificationManager == null) { + mNotificationManager = (NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE); + } + return mNotificationManager; + } + + /** + * Displays a notification with the location results. + */ + void showNotification() { + Intent notificationIntent = new Intent(mContext, MainActivity.class); + + // Construct a task stack. + TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext); + + // Add the main Activity to the task stack as the parent. + stackBuilder.addParentStack(MainActivity.class); + + // Push the content Intent onto the stack. + stackBuilder.addNextIntent(notificationIntent); + + // Get a PendingIntent containing the entire back stack. + PendingIntent notificationPendingIntent = + stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Builder notificationBuilder = null; + + notificationBuilder = new Notification.Builder(mContext, + PRIMARY_CHANNEL) + .setContentTitle(getLocationResultTitle()) + .setContentText(getLocationResultText()) + .setSmallIcon(R.mipmap.ic_launcher) + .setAutoCancel(true) + .setContentIntent(notificationPendingIntent); + + getNotificationManager().notify(0, notificationBuilder.build()); + } +} diff --git a/app/src/main/java/com/uam/wmi/findmytutor/service/LocationService.java b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationService.java new file mode 100644 index 0000000..566ae34 --- /dev/null +++ b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationService.java @@ -0,0 +1,397 @@ +package com.uam.wmi.findmytutor.service; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.Criteria; +import android.location.GpsStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.BatteryManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.SystemClock; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +import static android.content.ContentValues.TAG; + +public class LocationService extends Service implements LocationListener, GpsStatus.Listener { + public static final String LOG_TAG = LocationService.class.getSimpleName(); + + private final LocationServiceBinder binder = new LocationServiceBinder(); + boolean isLocationManagerUpdatingLocation; + + + + ArrayList locationList; + + ArrayList oldLocationList; + ArrayList noAccuracyLocationList; + ArrayList inaccurateLocationList; + + + boolean isLogging; + + float currentSpeed = 0.0f; // meters/second + long runStartTimeInMillis; + + ArrayList batteryLevelArray; + ArrayList batteryLevelScaledArray; + int batteryScale; + int gpsCount; + + + public LocationService() { + + } + + @Override + public void onCreate() { + isLocationManagerUpdatingLocation = false; + locationList = new ArrayList<>(); + noAccuracyLocationList = new ArrayList<>(); + oldLocationList = new ArrayList<>(); + inaccurateLocationList = new ArrayList<>(); + + isLogging = false; + + batteryLevelArray = new ArrayList<>(); + batteryLevelScaledArray = new ArrayList<>(); + registerReceiver(this.batteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } + + + + @Override + public int onStartCommand(Intent i, int flags, int startId) { + super.onStartCommand(i, flags, startId); + return Service.START_STICKY; + } + + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + + @Override + public void onRebind(Intent intent) { + Log.d(LOG_TAG, "onRebind "); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.d(LOG_TAG, "onUnbind "); + + return true; + } + + @Override + public void onDestroy() { + Log.d(LOG_TAG, "onDestroy "); + + + } + + + //This is where we detect the app is being killed, thus stop service. + @Override + public void onTaskRemoved(Intent rootIntent) { + Log.d(LOG_TAG, "onTaskRemoved "); + this.stopUpdatingLocation(); + + stopSelf(); + } + + + + + /** + * Binder class + * + * @author Takamitsu Mizutori + * + */ + public class LocationServiceBinder extends Binder { + public LocationService getService() { + return LocationService.this; + } + } + + + + /* LocationListener implemenation */ + @Override + public void onProviderDisabled(String provider) { + if (provider.equals(LocationManager.GPS_PROVIDER)) { + notifyLocationProviderStatusUpdated(false); + } + + } + + @Override + public void onProviderEnabled(String provider) { + if (provider.equals(LocationManager.GPS_PROVIDER)) { + notifyLocationProviderStatusUpdated(true); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (provider.equals(LocationManager.GPS_PROVIDER)) { + if (status == LocationProvider.OUT_OF_SERVICE) { + notifyLocationProviderStatusUpdated(false); + } else { + notifyLocationProviderStatusUpdated(true); + } + } + } + + /* GpsStatus.Listener implementation */ + public void onGpsStatusChanged(int event) { + + + } + + private void notifyLocationProviderStatusUpdated(boolean isLocationProviderAvailable) { + //Broadcast location provider status change here + } + + public void startLogging(){ + isLogging = true; + } + + public void stopLogging(){ + if (locationList.size() > 1 && batteryLevelArray.size() > 1){ + long currentTimeInMillis = (long)(SystemClock.elapsedRealtimeNanos() / 1000000); + long elapsedTimeInSeconds = (currentTimeInMillis - runStartTimeInMillis) / 1000; + float totalDistanceInMeters = 0; + for(int i = 0; i < locationList.size() - 1; i++){ + totalDistanceInMeters += locationList.get(i).distanceTo(locationList.get(i + 1)); + } + int batteryLevelStart = batteryLevelArray.get(0).intValue(); + int batteryLevelEnd = batteryLevelArray.get(batteryLevelArray.size() - 1).intValue(); + + float batteryLevelScaledStart = batteryLevelScaledArray.get(0).floatValue(); + float batteryLevelScaledEnd = batteryLevelScaledArray.get(batteryLevelScaledArray.size() - 1).floatValue(); + + saveLog(elapsedTimeInSeconds, totalDistanceInMeters, gpsCount, batteryLevelStart, batteryLevelEnd, batteryLevelScaledStart, batteryLevelScaledEnd); + } + isLogging = false; + } + + + + public void startUpdatingLocation() { + if(this.isLocationManagerUpdatingLocation == false){ + isLocationManagerUpdatingLocation = true; + runStartTimeInMillis = (long)(SystemClock.elapsedRealtimeNanos() / 1000000); + + + locationList.clear(); + + oldLocationList.clear(); + noAccuracyLocationList.clear(); + inaccurateLocationList.clear(); + + LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + + //Exception thrown when GPS or Network provider were not available on the user's device. + try { + Criteria criteria = new Criteria(); + criteria.setAccuracy(Criteria.ACCURACY_FINE); //setAccuracyは内部では、https://stackoverflow.com/a/17874592/1709287の用にHorizontalAccuracyの設定に変換されている。 + criteria.setPowerRequirement(Criteria.POWER_HIGH); + criteria.setAltitudeRequired(false); + criteria.setSpeedRequired(true); + criteria.setCostAllowed(true); + criteria.setBearingRequired(false); + + //API level 9 and up + criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH); + criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH); + //criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH); + //criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH); + + Integer gpsFreqInMillis = 5000; + Integer gpsFreqInDistance = 5; // in meters + + locationManager.addGpsStatusListener(this); + + locationManager.requestLocationUpdates(gpsFreqInMillis, gpsFreqInDistance, criteria, this, null); + + /* Battery Consumption Measurement */ + gpsCount = 0; + batteryLevelArray.clear(); + batteryLevelScaledArray.clear(); + + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, e.getLocalizedMessage()); + } catch (SecurityException e) { + Log.e(LOG_TAG, e.getLocalizedMessage()); + } catch (RuntimeException e) { + Log.e(LOG_TAG, e.getLocalizedMessage()); + } + } + } + + + public void stopUpdatingLocation(){ + if(this.isLocationManagerUpdatingLocation == true){ + LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + locationManager.removeUpdates(this); + isLocationManagerUpdatingLocation = false; + } + } + + @Override + public void onLocationChanged(final Location newLocation) { + Log.d(TAG, "(" + newLocation.getLatitude() + "," + newLocation.getLongitude() + ")"); + + gpsCount++; + + if(isLogging){ + //locationList.add(newLocation); + filterAndAddLocation(newLocation); + } + + Intent intent = new Intent("LocationUpdated"); + intent.putExtra("location", newLocation); + + LocalBroadcastManager.getInstance(this.getApplication()).sendBroadcast(intent); + } + + @SuppressLint("NewApi") + private long getLocationAge(Location newLocation){ + long locationAge; + if(android.os.Build.VERSION.SDK_INT >= 17) { + long currentTimeInMilli = (long)(SystemClock.elapsedRealtimeNanos() / 1000000); + long locationTimeInMilli = (long)(newLocation.getElapsedRealtimeNanos() / 1000000); + locationAge = currentTimeInMilli - locationTimeInMilli; + }else{ + locationAge = System.currentTimeMillis() - newLocation.getTime(); + } + return locationAge; + } + + + private boolean filterAndAddLocation(Location location){ + + long age = getLocationAge(location); + + if(age > 5 * 1000){ //more than 5 seconds + Log.d(TAG, "Location is old"); + oldLocationList.add(location); + return false; + } + + if(location.getAccuracy() <= 0){ + Log.d(TAG, "Latitidue and longitude values are invalid."); + noAccuracyLocationList.add(location); + return false; + } + + //setAccuracy(newLocation.getAccuracy()); + float horizontalAccuracy = location.getAccuracy(); + if(horizontalAccuracy > 10){ //10meter filter + Log.d(TAG, "Accuracy is too low."); + inaccurateLocationList.add(location); + return false; + } + + + /* Kalman Filter */ + float Qvalue; + + long locationTimeInMillis = (long)(location.getElapsedRealtimeNanos() / 1000000); + long elapsedTimeInMillis = locationTimeInMillis - runStartTimeInMillis; + + if(currentSpeed == 0.0f){ + Qvalue = 3.0f; //3 meters per second + }else{ + Qvalue = currentSpeed; // meters per second + } + + + + Location predictedLocation = new Location("");//provider name is unecessary + float predictedDeltaInMeters = predictedLocation.distanceTo(location); + + + /* Notifiy predicted location to UI */ + Intent intent = new Intent("PredictLocation"); + intent.putExtra("location", predictedLocation); + LocalBroadcastManager.getInstance(this.getApplication()).sendBroadcast(intent); + + Log.d(TAG, "Location quality is good enough."); + currentSpeed = location.getSpeed(); + locationList.add(location); + + + return true; + } + + + + /* Battery Consumption */ + private BroadcastReceiver batteryInfoReceiver = new BroadcastReceiver(){ + @Override + public void onReceive(Context ctxt, Intent intent) { + int batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + + float batteryLevelScaled = batteryLevel / (float)scale; + + + + batteryLevelArray.add(Integer.valueOf(batteryLevel)); + batteryLevelScaledArray.add(Float.valueOf(batteryLevelScaled)); + batteryScale = scale; + } + }; + + /* Data Logging */ + public synchronized void saveLog(long timeInSeconds, double distanceInMeters, int gpsCount, int batteryLevelStart, int batteryLevelEnd, float batteryLevelScaledStart, float batteryLevelScaledEnd) { + SimpleDateFormat fileNameDateTimeFormat = new SimpleDateFormat("yyyy_MMdd_HHmm"); + String filePath = this.getExternalFilesDir(null).getAbsolutePath() + "/" + + fileNameDateTimeFormat.format(new Date()) + "_battery" + ".csv"; + + Log.d(TAG, "saving to " + filePath); + + FileWriter fileWriter = null; + try { + fileWriter = new FileWriter(filePath, false); + fileWriter.append("Time,Distance,GPSCount,BatteryLevelStart,BatteryLevelEnd,BatteryLevelStart(/" + batteryScale + ")," + "BatteryLevelEnd(/" + batteryScale + ")" + "\n"); + String record = "" + timeInSeconds + ',' + distanceInMeters + ',' + gpsCount + ',' + batteryLevelStart + ',' + batteryLevelEnd + ',' + batteryLevelScaledStart + ',' + batteryLevelScaledEnd + '\n'; + fileWriter.append(record); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fileWriter != null) { + try { + fileWriter.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } + } + + + +} + + diff --git a/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesBroadcastReceiver.java b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesBroadcastReceiver.java new file mode 100644 index 0000000..9e8a0dc --- /dev/null +++ b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesBroadcastReceiver.java @@ -0,0 +1,69 @@ +package com.uam.wmi.findmytutor.service; + +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.location.Location; +import android.util.Log; + +import com.google.android.gms.location.LocationResult; + +import java.util.List; + +/** + * Receiver for handling location updates. + * + * For apps targeting API level O + * {@link android.app.PendingIntent#getBroadcast(Context, int, Intent, int)} should be used when + * requesting location updates. Due to limits on background services, + * {@link android.app.PendingIntent#getService(Context, int, Intent, int)} should not be used. + * + * Note: Apps running on "O" devices (regardless of targetSdkVersion) may receive updates + * less frequently than the interval specified in the + * {@link com.google.android.gms.location.LocationRequest} when the app is no longer in the + * foreground. + */ +public class LocationUpdatesBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "LUBroadcastReceiver"; + + public static final String ACTION_PROCESS_UPDATES = + "com.google.android.gms.location.sample.backgroundlocationupdates.action" + + ".PROCESS_UPDATES"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + final String action = intent.getAction(); + if (ACTION_PROCESS_UPDATES.equals(action)) { + LocationResult result = LocationResult.extractResult(intent); + if (result != null) { + List locations = result.getLocations(); + LocationResultHelper locationResultHelper = new LocationResultHelper( + context, locations); + // Save the location data to SharedPreferences. + locationResultHelper.saveResults(); + // Show notification with the location data. + locationResultHelper.showNotification(); + Log.i(TAG, LocationResultHelper.getSavedLocationResult(context)); + } + } + } + } +} + diff --git a/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesIntentService.java b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesIntentService.java new file mode 100644 index 0000000..95c69d6 --- /dev/null +++ b/app/src/main/java/com/uam/wmi/findmytutor/service/LocationUpdatesIntentService.java @@ -0,0 +1,74 @@ +package com.uam.wmi.findmytutor.service;/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.location.Location; +import android.util.Log; + +import com.google.android.gms.location.LocationResult; + +import java.util.List; + +/** + * Handles incoming location updates and displays a notification with the location data. + * + * For apps targeting API level 25 ("Nougat") or lower, location updates may be requested + * using {@link android.app.PendingIntent#getService(Context, int, Intent, int)} or + * {@link android.app.PendingIntent#getBroadcast(Context, int, Intent, int)}. For apps targeting + * API level O, only {@code getBroadcast} should be used. + * + * Note: Apps running on "O" devices (regardless of targetSdkVersion) may receive updates + * less frequently than the interval specified in the + * {@link com.google.android.gms.location.LocationRequest} when the app is no longer in the + * foreground. + */ +public class LocationUpdatesIntentService extends IntentService { + + static final String ACTION_PROCESS_UPDATES = + "com.google.android.gms.location.sample.backgroundlocationupdates.action" + + ".PROCESS_UPDATES"; + private static final String TAG = LocationUpdatesIntentService.class.getSimpleName(); + + + public LocationUpdatesIntentService() { + // Name the worker thread. + super(TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent != null) { + final String action = intent.getAction(); + if (ACTION_PROCESS_UPDATES.equals(action)) { + LocationResult result = LocationResult.extractResult(intent); + if (result != null) { + List locations = result.getLocations(); + LocationResultHelper locationResultHelper = new LocationResultHelper(this, + locations); + // Save the location data to SharedPreferences. + locationResultHelper.saveResults(); + // Show notification with the location data. + locationResultHelper.showNotification(); + Log.i(TAG, LocationResultHelper.getSavedLocationResult(this)); + } + } + } + } +} + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index c2c77b7..70a3e8d 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -79,6 +79,8 @@ android:text="@string/action_sign_in" android:textStyle="bold" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b1e333b..37fcb88 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,20 +8,10 @@ tools:context=".activity.MainActivity"> - - - - - - - - - - - - + android:layout_height="wrap_content" + > - - + + android:layout_height="wrap_content" + android:visibility="gone"> + + + + +