Skip to content
This repository has been archived by the owner on Aug 23, 2023. It is now read-only.

Location update ui #229

Open
wants to merge 10 commits into
base: LocationUpdateUI
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2020 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.
*/
package com.google.android.gms.location.sample.locationupdatesbackgroundkotlin

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar

/**
* Helper functions to simplify permission checks/requests.
*/
fun Context.hasPermission(permission: String): Boolean {

// Background permissions didn't exit prior to Q, so it's approved by default.
if (permission == Manifest.permission.ACCESS_BACKGROUND_LOCATION &&
android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) {
return true
}

return ActivityCompat.checkSelfPermission(this, permission) ==
PackageManager.PERMISSION_GRANTED
}

/**
* Requests permission and if the user denied a previous request, but didn't check
* "Don't ask again", we provide additional rationale.
*
* Note: The Snackbar should have an action to request the permission.
*/
fun Fragment.requestPermissionWithRationale(
permission: String,
requestCode: Int,
snackbar: Snackbar
) {
val provideRationale = shouldShowRequestPermissionRationale(permission)

if (provideRationale) {
snackbar.show()
} else {
requestPermissions(arrayOf(permission), requestCode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 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.
*/
package com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data

import android.content.Context
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data.db.MyLocationDatabase
import com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data.db.MyLocationEntity
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

private const val TAG = "LocationRepository"

/**
* Access point for database (MyLocation data) and location APIs (start/stop location tracking and
* checking tracking status).
*/
class LocationRepository private constructor(
private val myLocationDatabase: MyLocationDatabase,
private val myLocationManager: MyLocationManager,
private val executor:ExecutorService
) {

// Database related fields/methods:
private val locationDao = myLocationDatabase.locationDao()

/**
* Returns all recorded locations from database.
*/
fun getLocations(): LiveData<List<MyLocationEntity>> = locationDao.getLocations()

// Not being used now but could in future versions.
/**
* Returns specific location in database.
*/
fun getLocation(id: UUID): LiveData<MyLocationEntity> = locationDao.getLocation(id)

// Not being used now but could in future versions.
/**
* Updates location in database.
*/
fun updateLocation(myLocationEntity: MyLocationEntity) {
executor.execute {
locationDao.updateLocation(myLocationEntity)
}
}

/**
* Adds location to the database.
*/
fun addLocation(myLocationEntity: MyLocationEntity) {
executor.execute {
locationDao.addLocation(myLocationEntity)
}
}

// Location related fields/methods:
/**
* Tracks whether the app is actively subscribed to location changes.
*/
val trackingLocation: LiveData<Boolean> = myLocationManager.trackingLocation

/**
* Subscribes to location updates.
*/
@MainThread
fun startLocationUpdates() = myLocationManager.startLocationUpdates()

/**
* Un-subscribes from location updates.
*/
@MainThread
fun stopLocationUpdates() = myLocationManager.stopLocationUpdates()

companion object {
@Volatile private var INSTANCE: LocationRepository? = null

fun getInstance(context: Context, executor: ExecutorService): LocationRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: LocationRepository(
MyLocationDatabase.getInstance(context),
MyLocationManager.getInstance(context),
executor)
.also { INSTANCE = it }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2020 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.
*/
package com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data

import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.LocationUpdatesBroadcastReceiver
import com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.hasPermission
import java.util.concurrent.TimeUnit

private const val TAG = "MyLocationManager"

/**
* Manages all location related tasks for the app.
*/
class MyLocationManager private constructor(private val context: Context) {

private val _trackingLocation: MutableLiveData<Boolean> = MutableLiveData<Boolean>(false)

/**
* Tracks whether the app is actively subscribed to location changes.
*/
val trackingLocation: LiveData<Boolean>
get() = _trackingLocation

// The Fused Location Provider provides access to location tracking APIs.
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)

// Stores parameters for requests to the FusedLocationProviderApi.
private val locationRequest: LocationRequest = LocationRequest().apply {
// Sets the desired interval for active location updates. This interval is inexact. You
// may not receive updates at all if no location sources are available, or you may
// receive them slower than requested. You may also receive updates faster than
// requested if other applications are requesting location at a faster interval.
//
// IMPORTANT NOTE: Apps running on "O" devices (regardless of targetSdkVersion) may
// receive updates less frequently than this interval when the app is no longer in the
// foreground.
interval = TimeUnit.SECONDS.toMillis(60)

// Sets the fastest rate for active location updates. This interval is exact, and your
// application will never receive updates faster than this value.
fastestInterval = TimeUnit.SECONDS.toMillis(30)

// Sets the maximum time when batched location updates are delivered. Updates may be
// delivered sooner than this interval.
maxWaitTime = TimeUnit.MINUTES.toMillis(2)

priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}

/**
* Creates default PendingIntent for location changes.
*
* Note: We use a BroadcastReceiver because on API level 26 and above (Oreo+), Android places
* limits on Services.
*/
private val locationUpdatePendingIntent: PendingIntent by lazy {
val intent = Intent(context, LocationUpdatesBroadcastReceiver::class.java)
intent.action = LocationUpdatesBroadcastReceiver.ACTION_PROCESS_UPDATES
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

/**
* Uses the FusedLocationProvider to start tracking location if the correct fine locations are
* approved.
*
* @throws SecurityException if ACCESS_FINE_LOCATION permission is removed before the
* FusedLocationClient's requestLocationUpdates() has been completed.
*/
@Throws(SecurityException::class)
@MainThread
fun startLocationUpdates() {
Log.d(TAG, "startLocationUpdates()")

if (!context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) return

try {
_trackingLocation.value = true
// If the PendingIntent is the same as the last request (which it always is), this
// request will replace any requestLocationUpdates() called before.
fusedLocationClient.requestLocationUpdates(locationRequest, locationUpdatePendingIntent)
} catch (permissionRevoked: SecurityException) {
_trackingLocation.value = false

// Exception only occurs if the user revokes the FINE location permission before
// requestLocationUpdates() is finished executing (very rare).
Log.d(TAG, "Location permission revoked; details: $permissionRevoked")
throw permissionRevoked
}
}

@MainThread
fun stopLocationUpdates() {
Log.d(TAG, "stopLocationUpdates()")
_trackingLocation.value = false
fusedLocationClient.removeLocationUpdates(locationUpdatePendingIntent)
}

companion object {
@Volatile private var INSTANCE: MyLocationManager? = null

fun getInstance(context: Context): MyLocationManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MyLocationManager(context).also { INSTANCE = it }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 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.
*/
package com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data.db

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import java.util.UUID

/**
* Defines database operations.
*/
@Dao
interface MyLocationDao {

@Query("SELECT * FROM my_location_table ORDER BY date DESC")
fun getLocations(): LiveData<List<MyLocationEntity>>

@Query("SELECT * FROM my_location_table WHERE id=(:id)")
fun getLocation(id: UUID): LiveData<MyLocationEntity>

@Update
fun updateLocation(myLocationEntity: MyLocationEntity)

@Insert
fun addLocation(myLocationEntity: MyLocationEntity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2020 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.
*/
package com.google.android.gms.location.sample.locationupdatesbackgroundkotlin.data.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

private const val DATABASE_NAME = "my-location-database"

/**
* Database for storing all location data.
*/
@Database(entities = [MyLocationEntity::class], version = 1)
@TypeConverters(MyLocationTypeConverters::class)
abstract class MyLocationDatabase : RoomDatabase() {
abstract fun locationDao(): MyLocationDao

companion object {
// For Singleton instantiation
@Volatile private var INSTANCE: MyLocationDatabase? = null

fun getInstance(context: Context): MyLocationDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
}

private fun buildDatabase(context: Context): MyLocationDatabase {
return Room.databaseBuilder(
context,
MyLocationDatabase::class.java,
DATABASE_NAME
).build()
}
}
}
Loading