diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 488604f..956d6fd 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -7,11 +7,11 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index a5bcd75..f1cf180 100644 Binary files a/README.md and b/README.md differ diff --git a/app/build.gradle b/app/build.gradle index 85ca93e..bf52d70 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,6 +34,7 @@ android { buildFeatures { viewBinding true } + namespace 'com.ar.application' } dependencies { @@ -43,7 +44,9 @@ dependencies { implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.gms:play-services-maps:18.1.0' + implementation 'com.google.android.gms:play-services-location:20.0.0' implementation 'com.google.firebase:firebase-messaging-ktx:23.0.8' + implementation 'com.google.firebase:firebase-database-ktx:20.0.6' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f39a1b2..bf56e08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,12 +1,13 @@ + xmlns:tools="http://schemas.android.com/tools"> - - - - + + + + + + + android:value="AIzaSyBesYoZOX1GYDg1n5JySelTKaTPafyfm1w" /> + \ No newline at end of file diff --git a/app/src/main/java/com/ar/application/Fence.kt b/app/src/main/java/com/ar/application/Fence.kt new file mode 100644 index 0000000..eb11fdc --- /dev/null +++ b/app/src/main/java/com/ar/application/Fence.kt @@ -0,0 +1,7 @@ +package com.ar.application + +data class Fence( + var key: String = "", + var lat: Double = 0.0, + var lng: Double = 0.0, +) \ No newline at end of file diff --git a/app/src/main/java/com/ar/application/GeofenceReceiver.kt b/app/src/main/java/com/ar/application/GeofenceReceiver.kt new file mode 100644 index 0000000..3834420 --- /dev/null +++ b/app/src/main/java/com/ar/application/GeofenceReceiver.kt @@ -0,0 +1,54 @@ +package com.ar.application + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.ar.application.Geofencing.Companion.showNotification +import com.google.android.gms.location.Geofence +import com.google.android.gms.location.GeofencingEvent +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.ValueEventListener +import com.google.firebase.database.ktx.database +import com.google.firebase.database.ktx.getValue +import com.google.firebase.ktx.Firebase + +class GeofenceReceiver: BroadcastReceiver() { + lateinit var key: String + lateinit var message: String + override fun onReceive(p0: Context?, p1: Intent?) { + if (p0 != null) { + val geofencingEvent = p1?.let { GeofencingEvent.fromIntent(it) } + val geofenceTransition = geofencingEvent?.geofenceTransition + + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER + || geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) { + if (p1 != null) { + key = p1.getStringExtra("key")!! + message = p1.getStringExtra("message")!! + } + + // retrieving data from database + val database = Firebase.database + val reference = database.getReference("fences") + val fenceListener = object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + val fence = snapshot.getValue() + if (fence != null) { + showNotification( + p0.applicationContext, + message + ) + } + } + + override fun onCancelled(error: DatabaseError) { + println("Fence: onCancelled: ${error.details}") + } + } + val child = reference.child(key) + child.addValueEventListener(fenceListener) + } + } + } +} diff --git a/app/src/main/java/com/ar/application/Geofencing.kt b/app/src/main/java/com/ar/application/Geofencing.kt index 35bca36..3e6d3fc 100644 --- a/app/src/main/java/com/ar/application/Geofencing.kt +++ b/app/src/main/java/com/ar/application/Geofencing.kt @@ -1,21 +1,54 @@ package com.ar.application +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.os.Build import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Toast +import androidx.annotation.RequiresPermission +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat -import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.OnMapReadyCallback import com.google.android.gms.maps.SupportMapFragment +import com.ar.application.databinding.ActivityGeofencingBinding +import com.google.android.gms.location.FusedLocationProviderClient +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.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.MarkerOptions -import com.ar.application.databinding.ActivityGeofencingBinding +import com.google.firebase.database.ktx.database +import com.google.firebase.ktx.Firebase +import kotlin.random.Random class Geofencing : AppCompatActivity(), OnMapReadyCallback { - private lateinit var mMap: GoogleMap + private val LOCATION_REQUEST_CODE = 123 + private val GEOFENCE_REQUEST_CODE = 456 // only for android version >= 10 + private val CAMERA_ZOOM_LEVEL = 18f + private val GEOFENCE_RADIUS = 500 // change later + private val GEOFENCE_ID = "REMINDER_GEOFENCE_ID" + private val GEOFENCE_EXPIRATION = 10 * 24 * 60 * 60 * 1000 // 10 days, change later + private val GEOFENCE_DWELL_DELAY = 10 * 1000 // 10 secs, change later + private val TAG: String = Geofencing::class.java.simpleName + + private lateinit var map: GoogleMap private lateinit var binding: ActivityGeofencingBinding + private lateinit var fusedLocationClient: FusedLocationProviderClient + private lateinit var geofencingClient: GeofencingClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,7 +61,8 @@ class Geofencing : AppCompatActivity(), OnMapReadyCallback { .findFragmentById(R.id.mapGeofence) as SupportMapFragment mapFragment.getMapAsync(this) - + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + geofencingClient = LocationServices.getGeofencingClient(this) } /** @@ -40,12 +74,264 @@ class Geofencing : AppCompatActivity(), OnMapReadyCallback { * it inside the SupportMapFragment. This method will only be triggered once the user has * installed Google Play services and returned to the app. */ + //@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) override fun onMapReady(googleMap: GoogleMap) { - mMap = googleMap + map = googleMap + map.uiSettings.isZoomControlsEnabled = true + + if (!areLocationPermsGranted()) { + askForLocationPerms() + } + else { + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED) { +// askForLocationPerms() + } + this.map.isMyLocationEnabled = true + + // getting last known location data + fusedLocationClient.lastLocation.addOnSuccessListener { + if (it != null) { + with (map) { + val latLng = LatLng(it.latitude, it.longitude) + addMarker(MarkerOptions().position(latLng)) + moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, CAMERA_ZOOM_LEVEL)) + } + } + else { + with (map) { + addMarker(MarkerOptions().position(LatLng(17.429, 78.455))) + moveCamera(CameraUpdateFactory.newLatLngZoom( + LatLng(17.429, 78.455), CAMERA_ZOOM_LEVEL) + // Panjagutta coordinates + ) + } + TODO("Manually setting the last known location for testing," + + "but that doesn't make sense." + + "So, think of something else.") + } + } + } + setLongClick(map) + setPoiClick(map) + } + + private fun areLocationPermsGranted(): Boolean { // checks if location permissions have been granted + return ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + + private fun askForLocationPerms() { // requests for location permissions + val permissions = mutableListOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + permissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + + ActivityCompat.requestPermissions( + this, + permissions.toTypedArray(), + LOCATION_REQUEST_CODE + ) + } + + private fun askForBackgroundLocationPerm() { // requests for only background location + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), + GEOFENCE_REQUEST_CODE + ) + } + } + + private fun setPoiClick(map: GoogleMap) { + map.setOnPoiClickListener { poi -> + map.addMarker( + MarkerOptions() + .position(poi.latLng) + .title(poi.name) + )?.showInfoWindow() + + //scheduleJob() + } + } + + private fun setLongClick(map: GoogleMap) { + map.setOnMapLongClickListener { latlng -> + map.addMarker( + MarkerOptions().position(latlng) + .title("Geofence centre") + )?.showInfoWindow() + map.addCircle( + CircleOptions() + .center(latlng) + .strokeColor(Color.argb(50, 70, 70, 70)) + .fillColor(Color.argb(70, 150, 150, 150)) + .radius(GEOFENCE_RADIUS.toDouble()) + ) + map.moveCamera(CameraUpdateFactory.newLatLngZoom(latlng, CAMERA_ZOOM_LEVEL)) + + val database = Firebase.database + val reference = database.getReference("fences") + val key = reference.push().key + if (key != null) { + val reminder = Fence(key, latlng.latitude, latlng.longitude) + reference.child(key).setValue(reminder) + } + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + } + createGeofence(latlng, key!!, geofencingClient) + } + } + + @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) + private fun createGeofence(location: LatLng, key: String, geofencingClient: GeofencingClient) { + val geofence = Geofence.Builder() + .setRequestId(GEOFENCE_ID) + .setCircularRegion(location.latitude, location.longitude, GEOFENCE_RADIUS.toFloat()) + .setExpirationDuration(GEOFENCE_EXPIRATION.toLong()) + // defines the lifetime of the geofence + .setTransitionTypes( + Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_DWELL + ) + // transitions of the user when actions are to be triggered + .setLoiteringDelay(GEOFENCE_DWELL_DELAY) + // min duration of user in geofence for action to be triggered + .build() + + val geofenceRequest = GeofencingRequest.Builder() + .setInitialTrigger(Geofence.GEOFENCE_TRANSITION_ENTER) + .addGeofence(geofence) + .build() + + val intent = Intent(this, GeofenceReceiver::class.java) + .putExtra("key", key) + .putExtra("message", "Geofence detected!") + + val pendingIntent = PendingIntent.getBroadcast( + applicationContext, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (ContextCompat.checkSelfPermission( + applicationContext, + Manifest.permission.ACCESS_BACKGROUND_LOCATION + ) != PackageManager.PERMISSION_GRANTED) { + askForBackgroundLocationPerm() + } + else { + geofencingClient.addGeofences(geofenceRequest, pendingIntent) + } + } + else { + geofencingClient.addGeofences(geofenceRequest, pendingIntent) + } + } + + @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == GEOFENCE_REQUEST_CODE) { + if (permissions.isNotEmpty() && grantResults[0] != PackageManager.PERMISSION_GRANTED) { + Toast.makeText( + this, + "Please grant background location permission!", + Toast.LENGTH_SHORT + ) + .show() + askForBackgroundLocationPerm() + } + } + else if (requestCode == LOCATION_REQUEST_CODE) { + if (permissions.isNotEmpty() && + (grantResults[0] == PackageManager.PERMISSION_GRANTED + || grantResults[1] == PackageManager.PERMISSION_GRANTED)) { + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED) { + askForLocationPerms() + } + map.isMyLocationEnabled = true + onMapReady(map) + } + else { + Toast.makeText( + this, + "Please grant location permissions!", + Toast.LENGTH_SHORT + ) + .show() + askForLocationPerms() + } + } + } + + companion object { + fun showNotification(context: Context, message: String) { + val channelID = "FENCE_NOTIFICATION_CHANNEL" + var notifID = 420 + notifID += Random(notifID).nextInt(1, 69) + + val notifBuilder = NotificationCompat.Builder(context.applicationContext, channelID) + .setSmallIcon(R.drawable.ic_fence_detected) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText(message) + .setStyle( + NotificationCompat.BigTextStyle().bigText(message) + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + + val notifManager = context.getSystemService(Context.NOTIFICATION_SERVICE) + as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelID, + context.getString(R.string.app_name), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { description = context.getString(R.string.app_name) } - // Add a marker in Sydney and move the camera - val sydney = LatLng(-34.0, 151.0) - mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney")) - mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney)) + notifManager.createNotificationChannel(channel) + } + notifManager.notify(notifID, notifBuilder.build()) + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_fence_detected.xml b/app/src/main/res/drawable/ic_fence_detected.xml new file mode 100644 index 0000000..11fdbe5 --- /dev/null +++ b/app/src/main/res/drawable/ic_fence_detected.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd40ee5..16a2a29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,4 +6,5 @@ Go to Trace Route Page Go to Geo-fencing SOS + MapsActivity \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7ce7085..6581c07 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ buildscript { dependencies { - classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.google.gms:google-services:4.3.14' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.2.2' apply false - id 'com.android.library' version '7.2.2' apply false + id 'com.android.application' version '7.3.0' apply false + id 'com.android.library' version '7.3.0' apply false id 'org.jetbrains.kotlin.android' version '1.7.10' apply false id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2df3794..aea147c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Sep 09 10:29:15 IST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME