diff --git a/app/build.gradle b/app/build.gradle index 8bcfb89..d97dbd5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,10 @@ dependencies { implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.google.android.gms:play-services-location:17.1.0' implementation 'com.google.android.gms:play-services-maps:17.0.0' testImplementation 'junit:junit:4.13.1' +// implementation "com.android.support:support-compat:28.0.0" // okhttp implementation 'com.squareup.okhttp3:okhttp:4.9.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d0c446..b2cdc54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/a1/nextlocation/data/Data.java b/app/src/main/java/com/a1/nextlocation/data/Data.java index 520538e..ec64862 100644 --- a/app/src/main/java/com/a1/nextlocation/data/Data.java +++ b/app/src/main/java/com/a1/nextlocation/data/Data.java @@ -20,6 +20,7 @@ public enum Data { private double zoom = 0; private SharedPreferences.Editor editor; private Context context; + private LocationProximityListener locationProximityListener; public void setContext(Context context) { this.context = context; @@ -29,6 +30,15 @@ public enum Data { this.editor = editor; } + + public LocationProximityListener getLocationProximityListener() { + return locationProximityListener; + } + + public void setLocationProximityListener(LocationProximityListener locationProximityListener) { + this.locationProximityListener = locationProximityListener; + } + public double getZoom() { return zoom; } @@ -72,6 +82,11 @@ public enum Data { return locationsVisited; } + @FunctionalInterface + public interface LocationProximityListener { + void onLocationVisited(Location location); + } + public void saveVisitedNamesList(){ Gson gson = new Gson(); String json = gson.toJson(visitedNames); diff --git a/app/src/main/java/com/a1/nextlocation/data/Location.java b/app/src/main/java/com/a1/nextlocation/data/Location.java index 86785cc..ae99308 100644 --- a/app/src/main/java/com/a1/nextlocation/data/Location.java +++ b/app/src/main/java/com/a1/nextlocation/data/Location.java @@ -25,6 +25,8 @@ public class Location implements Parcelable { private String imageUrl; private String iconUrl; + private boolean visited; + public Location(@NotNull String name, String coordinates, String description, @Nullable String imageUrl) { this.name = name; this.coordinates = coordinates; @@ -171,4 +173,12 @@ public class Location implements Parcelable { parcel.writeString(description); parcel.writeString(imageUrl); } + + public boolean isVisited() { + return visited; + } + + public void setVisited(boolean visited) { + this.visited = visited; + } } diff --git a/app/src/main/java/com/a1/nextlocation/data/RouteHandler.java b/app/src/main/java/com/a1/nextlocation/data/RouteHandler.java index a6959e7..331d28e 100644 --- a/app/src/main/java/com/a1/nextlocation/data/RouteHandler.java +++ b/app/src/main/java/com/a1/nextlocation/data/RouteHandler.java @@ -13,6 +13,15 @@ public enum RouteHandler { private int stepCount = 0; private RouteFinishedListener routeFinishedListener; private long startedTime; + private double currentRouteDuration; + + public void setCurrentRouteDuration(double currentRouteDuration) { + this.currentRouteDuration = currentRouteDuration; + } + + public double getCurrentRouteDuration() { + return currentRouteDuration; + } private Polyline currentRouteLine; @@ -20,6 +29,7 @@ public enum RouteHandler { this.currentRouteLine = currentRouteLine; } + public Polyline getCurrentRouteLine() { return currentRouteLine; } diff --git a/app/src/main/java/com/a1/nextlocation/fragments/HomeFragment.java b/app/src/main/java/com/a1/nextlocation/fragments/HomeFragment.java index 293cfae..4d4cc8b 100644 --- a/app/src/main/java/com/a1/nextlocation/fragments/HomeFragment.java +++ b/app/src/main/java/com/a1/nextlocation/fragments/HomeFragment.java @@ -2,6 +2,8 @@ package com.a1.nextlocation.fragments; import android.Manifest; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -20,6 +22,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -27,6 +30,7 @@ import androidx.fragment.app.FragmentActivity; import com.a1.nextlocation.R; import com.a1.nextlocation.data.Data; import com.a1.nextlocation.data.RouteHandler; +import com.a1.nextlocation.geofencing.GeofenceInitalizer; import com.a1.nextlocation.json.DirectionsResult; import com.a1.nextlocation.network.ApiHandler; import com.a1.nextlocation.recyclerview.LocationListManager; @@ -61,6 +65,8 @@ public class HomeFragment extends Fragment implements LocationListener { private int color; private Location currentLocation; private Overlay allLocationsOverlay; + private GeofenceInitalizer initializer; + private final static String CHANNEL_ID = "next_location01"; @Override public void onCreate(Bundle savedInstanceState) { @@ -72,6 +78,7 @@ public class HomeFragment extends Fragment implements LocationListener { Manifest.permission.WRITE_EXTERNAL_STORAGE); color = requireContext().getColor(R.color.red); + Data.INSTANCE.setLocationProximityListener(this::onLocationVisited); } @Override @@ -133,6 +140,7 @@ public class HomeFragment extends Fragment implements LocationListener { roadOverlay.setColor(color); // pass the line to the route handler + RouteHandler.INSTANCE.setCurrentRouteDuration(directionsResult.getDuration()); RouteHandler.INSTANCE.setCurrentRouteLine(roadOverlay); Log.d(TAG, "onDirectionsAvailable: successfully added road!"); @@ -142,6 +150,7 @@ public class HomeFragment extends Fragment implements LocationListener { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + initializer = new GeofenceInitalizer(requireContext(),requireActivity()); initMap(view); } @@ -218,6 +227,8 @@ public class HomeFragment extends Fragment implements LocationListener { } + + /** * displays the route that is currently being followed as a red line */ @@ -246,6 +257,7 @@ public class HomeFragment extends Fragment implements LocationListener { private void addLocations() { // get the locations of the current route or all locations List locations = RouteHandler.INSTANCE.isFollowingRoute() ? RouteHandler.INSTANCE.getCurrentRoute().getLocations() : LocationListManager.INSTANCE.getLocationList(); + initializer.removeGeoFences(); final ArrayList items = new ArrayList<>(locations.size()); // marker icon Drawable marker = ContextCompat.getDrawable(requireContext(), R.drawable.ic_baseline_location_on_24); @@ -300,6 +312,19 @@ public class HomeFragment extends Fragment implements LocationListener { mapView.getOverlays().add(allLocationsOverlay); Log.d(TAG, "addLocations: successfully added locations"); + addGeofences(locations); + + } + + /** + * adds the geofences for the currently active locations + * @param locations the locations to add geofences for + */ + private void addGeofences(List locations) { + + Log.d(TAG, "addGeofences: adding geofences!"); + + initializer.init(locations); } /** @@ -367,6 +392,33 @@ public class HomeFragment extends Fragment implements LocationListener { } + public void onLocationVisited(com.a1.nextlocation.data.Location location) { + Data.INSTANCE.visitLocation(location); + showNotification(location); + + } + + private void showNotification(com.a1.nextlocation.data.Location location) { + + NotificationManager mNotificationManager = (NotificationManager) requireActivity().getSystemService(Context.NOTIFICATION_SERVICE); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, "next_location", importance); + notificationChannel.enableLights(true); + notificationChannel.enableVibration(true); + notificationChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); + mNotificationManager.createNotificationChannel(notificationChannel); + } + + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(requireContext(),CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(getString(R.string.notification_text,location.getName())) + .setAutoCancel(true); + + mNotificationManager.notify(0,mBuilder.build()); + } + // empty override methods for the LocationListener @Override diff --git a/app/src/main/java/com/a1/nextlocation/geofencing/GeoFenceBroadcastReceiver.java b/app/src/main/java/com/a1/nextlocation/geofencing/GeoFenceBroadcastReceiver.java new file mode 100644 index 0000000..eff3ec2 --- /dev/null +++ b/app/src/main/java/com/a1/nextlocation/geofencing/GeoFenceBroadcastReceiver.java @@ -0,0 +1,61 @@ +package com.a1.nextlocation.geofencing; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.a1.nextlocation.data.Data; +import com.a1.nextlocation.data.Location; +import com.a1.nextlocation.recyclerview.LocationListManager; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofenceStatusCodes; +import com.google.android.gms.location.GeofencingEvent; + +import java.util.List; + +/** + * broadcast receiver for geofence events + */ +public class GeoFenceBroadcastReceiver extends BroadcastReceiver { + private final String TAG = GeoFenceBroadcastReceiver.class.getCanonicalName(); + + @Override + public void onReceive(Context context, Intent intent) { + + GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); + Log.i(TAG, "onReceive: RECEIVED GEOFENCE STUFF"); + + if (geofencingEvent.hasError()) { + String errorMessage = GeofenceStatusCodes + .getStatusCodeString(geofencingEvent.getErrorCode()); + Log.e(TAG, errorMessage); + return; + } + + // Get the transition type. + int geofenceTransition = geofencingEvent.getGeofenceTransition(); + + switch (geofenceTransition) { + case Geofence.GEOFENCE_TRANSITION_ENTER: + List geofenceList = geofencingEvent.getTriggeringGeofences(); + // loop through list of geofences + for (Geofence geofence : geofenceList) { + for (Location l : LocationListManager.INSTANCE.getLocationList()) { + if (geofence.getRequestId().equals(l.getName())) { + l.setVisited(true); + // let the homefragment know that we are close to a location + Data.INSTANCE.getLocationProximityListener().onLocationVisited(l); + Log.d(TAG, "onReceive: VISITED LOCATION " + l.getName()); + break; + } + } + } + + break; + case Geofence.GEOFENCE_TRANSITION_EXIT: + Log.d(TAG, "onReceive: exiting geofence..."); + break; + } + } +} diff --git a/app/src/main/java/com/a1/nextlocation/geofencing/GeoFencingHelper.java b/app/src/main/java/com/a1/nextlocation/geofencing/GeoFencingHelper.java new file mode 100644 index 0000000..01f6335 --- /dev/null +++ b/app/src/main/java/com/a1/nextlocation/geofencing/GeoFencingHelper.java @@ -0,0 +1,51 @@ +package com.a1.nextlocation.geofencing; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.provider.SyncStateContract; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingRequest; + +import org.osmdroid.util.GeoPoint; + +public class GeoFencingHelper extends ContextWrapper { + private PendingIntent pendingIntent; + + public GeoFencingHelper(Context base) { + super(base); + } + + public GeofencingRequest getGeoFencingRequest(Geofence geofence) { + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); + builder.addGeofence(geofence); + return builder.build(); + + } + + public Geofence getGeofence(String ID, GeoPoint point, float radius) { + + return new Geofence.Builder() + .setCircularRegion(point.getLatitude(), point.getLongitude(), radius) + .setRequestId(ID) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | + Geofence.GEOFENCE_TRANSITION_EXIT) + .setLoiteringDelay(5000) + .setExpirationDuration(Geofence.NEVER_EXPIRE) + .build(); + } + + public PendingIntent getPendingIntent() { + if (pendingIntent != null) { + return pendingIntent; + } + + Intent intent = new Intent(this, GeoFenceBroadcastReceiver.class); + pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + return pendingIntent; + } +} diff --git a/app/src/main/java/com/a1/nextlocation/geofencing/GeofenceInitalizer.java b/app/src/main/java/com/a1/nextlocation/geofencing/GeofenceInitalizer.java new file mode 100644 index 0000000..7a1af31 --- /dev/null +++ b/app/src/main/java/com/a1/nextlocation/geofencing/GeofenceInitalizer.java @@ -0,0 +1,109 @@ +package com.a1.nextlocation.geofencing; + +import android.Manifest; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.a1.nextlocation.data.Data; +import com.a1.nextlocation.data.Location; +import com.a1.nextlocation.recyclerview.LocationListManager; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; + +import org.osmdroid.util.GeoPoint; + +import java.util.List; + +import static android.content.ContentValues.TAG; + +public class GeofenceInitalizer { + private final Activity activity; + private GeofencingClient geofencingClient; + private GeoFencingHelper geoFencingHelper; + private final Context context; + private final String TAG = GeofenceInitalizer.class.getCanonicalName(); + private List locations; + private int BACKGROUND_LOCATION_ACCESS_REQUEST_CODE = 10002; + + public GeofenceInitalizer(Context context, Activity activity) { + this.context = context; + this.activity = activity; + } + + public void init(List locations) { + if (!checkFineLocationPermission()) return; + + geofencingClient = LocationServices.getGeofencingClient(context); + geoFencingHelper = new GeoFencingHelper(context); + this.locations = locations; + if (Build.VERSION.SDK_INT >= 29) { + //If API is higher then 29 we need background permission + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED) { + addFences(); + } else { + //Permission is not granted!! Need to request it.. + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_BACKGROUND_LOCATION)) { + //We show a dialog and ask for permission + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_ACCESS_REQUEST_CODE); + } else { + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_ACCESS_REQUEST_CODE); + + } + } + } else { + addFences(); + } + } + + private void addFences() { + for (Location location : locations) { + GeoPoint t = new GeoPoint(location.getLat(), location.getLong()); + addGeofence(t, location.getName()); + } + } + + public void removeGeoFences() { + geofencingClient = LocationServices.getGeofencingClient(context); + geoFencingHelper = new GeoFencingHelper(context); + + PendingIntent pendingIntent = geoFencingHelper.getPendingIntent(); + + geofencingClient.removeGeofences(pendingIntent) + .addOnSuccessListener(aVoid -> Log.d(TAG, "Geofence is removed... ")) + .addOnFailureListener(e -> Log.d(TAG, e.getLocalizedMessage())); + if (this.locations != null) this.locations.clear(); + + } + + private void addGeofence(GeoPoint p, String name) { + if (!checkFineLocationPermission()) return; + + Geofence geofence = geoFencingHelper.getGeofence(name, p, 45); + GeofencingRequest geofencingRequest = geoFencingHelper.getGeoFencingRequest(geofence); + PendingIntent pendingIntent = geoFencingHelper.getPendingIntent(); + + geofencingClient.addGeofences(geofencingRequest, pendingIntent).addOnSuccessListener(v -> { + Log.i(TAG, "addGeofence: added geofence"); + }).addOnFailureListener(v -> { + Log.e(TAG, "addGeofence: failure adding geofence " + v.getMessage()); + }); + Log.i(TAG, "addGeofence: added geofence to client"); + } + + private boolean checkFineLocationPermission() { + return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b18831e..367dd5e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -26,4 +26,6 @@ Chinees HELP Onderaan het scherm zijn verschillende knoppen te zien. Deze knoppen hebben de volgende functies: \n\nLocaties: toont een lijst met alle locaties die bezocht kunnen worden. Elke locatie wordt kort beschreven. \n\nRoutes: Toont een lijst met alle routes die gelopen kunnen worden. Van elke route wordt een omschrijving gegeven. \n\nStatistieken: Toont persoonlijke statistieken. \n\nInstellingen: Hier kunnen app-instellingen worden aangepast naar eigen voorkeur. \n\nEen locatie ingedrukt houden laat extra informatie zien over de gekozen locatie + Je bent dicht bij een locatie! + Je bent bijna bij %1$s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c965083..0c084f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,4 +25,6 @@ Chinese HELP Hasn\'t been translated yet + You\'re close to a location! + You\'re almost at %1$s \ No newline at end of file