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

Adds data layer for location background sample. #227

Merged
merged 2 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

// Not being used now but could in future versions.
/**
* Returns specific location in database.
*/
fun getLocation(id: UUID): LiveData<MyLocationEntity> = locationDao.getLocation(id)
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

// 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()
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Un-subscribes from location updates.
*/
@MainThread
fun stopLocationUpdates() = myLocationManager.stopLocationUpdates()
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

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

fun getInstance(context: Context, executor: ExecutorService): LocationRepository {
return INSTANCE ?: synchronized(this) {
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved
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) {
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

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 {
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved
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

florina-muntenescu marked this conversation as resolved.
Show resolved Hide resolved
// 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() {
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved
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 }
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
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 {
codingjeremy marked this conversation as resolved.
Show resolved Hide resolved

@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>

florina-muntenescu marked this conversation as resolved.
Show resolved Hide resolved
@Update
fun updateLocation(myLocationEntity: MyLocationEntity)

florina-muntenescu marked this conversation as resolved.
Show resolved Hide resolved
@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