Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save zirman/7319d2b6aa109710c2cf91ca36aca802 to your computer and use it in GitHub Desktop.

Select an option

Save zirman/7319d2b6aa109710c2cf91ca36aca802 to your computer and use it in GitHub Desktop.
package com.example.robertchrzanowski.myapplication
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageManager
import android.location.Location
import android.os.Build
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.*
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.SingleEmitter
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Function
import java.util.concurrent.TimeUnit
// Possible Exceptions
class GoogleAPIConnectionSuspendedException(val errorCause: Int) : Exception()
class GoogleAPIConnectionException(detailMessage: String, val connectionResult: ConnectionResult?) :
Exception(detailMessage) {
fun wasResolutionUnsuccessful(): Boolean = connectionResult?.hasResolution() ?: false
}
class LocationUnavailableException : Exception()
class UserAbortException : Throwable()
// Sum type.
sealed class Sum<out A : Any, out B : Any> {
class kindA<out A : Any, out B : Any>(val value: A) : Sum<A, B>() {
override fun <C> with(l: (A) -> C, r: (B) -> C): C = l(value)
}
class kindB<out A : Any, out B : Any>(val value: B) : Sum<A, B>() {
override fun <C> with(l: (A) -> C, r: (B) -> C): C = r(value)
}
abstract fun <C> with(l: (A) -> C, r: (B) -> C): C
}
private val none = Sum.kindB<Any, Unit>(Unit)
fun <A : Any> Some(value: A): Sum<A, Unit> = Sum.kindA<A, Unit>(value)
@Suppress("UNCHECKED_CAST")
fun <A : Any> None(): Sum<A, Unit> = none as Sum<A, Unit>
fun <A : Any> Sum<A, Unit>.toNullable(): A? = with({ it }, { null })
abstract class RxLocationActivity : AppCompatActivity() {
private lateinit var googleApiAvailability: GoogleApiAvailability
private data class ObsDis<T>(val observable: Observable<T>, val disposable: Disposable)
private var nextRequestCode = 100
private val pendingRequests = mutableMapOf<Int, SingleEmitter<Unit>>()
private var permissionsObsSub = mutableMapOf<Set<String>, ObsDis<Unit>>()
private var googlePlayServicesObsSub: ObsDis<Unit>? = null
private var gpsLocationObsSub: ObsDis<Location>? = null
private var googleApiClientObservable: Observable<GoogleApiClient>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
googleApiAvailability = GoogleApiAvailability.getInstance()
}
final override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
pendingRequests.remove(requestCode)?.let { request ->
// Only run success when all permissions have been granted.
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
request.onSuccess(Unit)
} else {
request.onError(UserAbortException())
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(resultCode, resultCode, data)
pendingRequests.remove(requestCode)?.let { request ->
if (Activity.RESULT_OK == resultCode) {
request.onSuccess(Unit)
} else {
request.onError(UserAbortException())
}
}
}
private fun addRequest(makeRequest: (Int) -> SingleEmitter<Unit>): Unit {
val requestCode = nextRequestCode
// Only 16 bit request codes are allowed by Android.
do {
nextRequestCode = (nextRequestCode + 1) % 0xffff
} while (pendingRequests.contains(nextRequestCode))
pendingRequests[requestCode] = makeRequest(requestCode)
}
fun permissionsSingle(
permissions: Set<String>,
rationale: String,
title: String,
continueButton: String,
cancelButton: String): Single<Unit> {
val neededPermissions = permissions
.filter {
ActivityCompat.checkSelfPermission(this, it) !==
PackageManager.PERMISSION_GRANTED
}
.toSet()
return if (neededPermissions.isEmpty()) {
Single.just(Unit)
} else {
permissionsObsSub.getOrPut(neededPermissions) {
val observable = Single.create<Unit> { singleEmitter ->
addRequest { requestCode ->
fun requestPermissionsFromAndroid() {
ActivityCompat.requestPermissions(
this,
permissions.toTypedArray(),
requestCode)
}
// Display rationale if at least one permission needs it
val safeSubscriber = singleEmitter
if (neededPermissions.any {
ActivityCompat.shouldShowRequestPermissionRationale(this, it)
}) {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(rationale)
.setPositiveButton(
continueButton,
{ var1, var2 -> requestPermissionsFromAndroid() })
.setNegativeButton(
cancelButton,
{ var1, var2 ->
safeSubscriber.onError(UserAbortException())
})
.setCancelable(false)
.show()
} else {
requestPermissionsFromAndroid()
}
singleEmitter
}
}
.toObservable()
.replay()
.autoConnect()
val compositeDisposable = CompositeDisposable()
compositeDisposable.add(observable.subscribe(
{},
{ permissionsObsSub.remove(neededPermissions) },
{ permissionsObsSub.remove(neededPermissions) }))
ObsDis(observable, compositeDisposable)
}
.observable.singleOrError()
}
}
fun getBestLocationSumSingle(
title: String? = null,
rationale: String? = null,
continueButton: String? = null,
cancelButton: String? = null
): Single<Sum<Location, Unit>> =
getBestLocationMaybe(
title,
rationale,
continueButton,
cancelButton
)
.map { Some(it) }
.toSingle(None<Location>())
fun getBestLocationSingle(
title: String? = null,
rationale: String? = null,
continueButton: String? = null,
cancelButton: String? = null
): Single<Location> =
getBestLocationSumSingle(
title,
rationale,
continueButton,
cancelButton
)
.flatMap {
it.with({
Single.just(it)
}, {
Single.error<Location>(LocationUnavailableException())
})
}
fun getBestLocationMaybe(
title: String? = null,
rationale: String? = null,
continueButton: String? = null,
cancelButton: String? = null
): Maybe<Location> {
// lastKnownLocation may not return a location so we concat a stream with a single null to
// be able to continue
return getGooglePlayServicesAvailable()
.flatMap {
permissionsSingle(
permissions = setOf(Manifest.permission.ACCESS_FINE_LOCATION),
title = title ?: "Default permissions request title",
rationale = rationale ?: "This is my default rationale",
continueButton = continueButton ?: "OK",
cancelButton = cancelButton ?: "Cancel")
}
.flatMapMaybe {
getLastKnownLocation()
.map { Some(it) }.toSingle(None<Location>())
.flatMapMaybe { getLastKnownLocationOrGpsLocation(it) }
}
}
private fun getLastKnownLocationOrGpsLocation(
lastKnownLocation: Sum<Location, Unit>): Maybe<Location> {
// If the lastLocation is accurate enough we use that, otherwise we start
// location updates looking for a min accuracy within a time limit
fun minGpsLocation(): Maybe<Location> = getGpsLocation()
.reduce({ l1, l2 ->
if (l1.accuracy < l2.accuracy) l1 else l2
})
return lastKnownLocation.toNullable()?.let { lastKnownLocation ->
if ((System.currentTimeMillis() - lastKnownLocation.time) <
TimeUnit.MINUTES.toMillis(2) &&
goodEnoughLocation(lastKnownLocation)) {
Maybe.just(lastKnownLocation)
} else {
minGpsLocation()
}
} ?: minGpsLocation()
}
private fun getGpsLocation(): Observable<Location> {
if (gpsLocationObsSub == null) {
val timeoutSeconds = 6L // Maximum time to try to get location
fun locationRequest() = LocationRequest()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(TimeUnit.SECONDS.toMillis(1))
.setFastestInterval(200)
.setExpirationDuration(TimeUnit.SECONDS.toMillis(timeoutSeconds))
val observable = getCheckLocationSettings(
LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest())
.setAlwaysShow(true)
.build())
.flatMap { getLocationSettingsResolution(it) }
.flatMapObservable {
getConnectedGoogleApiClientObservable()
.flatMap { apiClient ->
Observable.create<Location> { observableEmitter ->
LocationServices
.FusedLocationApi
.requestLocationUpdates(
apiClient,
locationRequest())
{ observableEmitter.onNext(it) }
}
}
.filter(::isNotFromMockProvider)
.takeUntil(Observable.just(Unit).delay(
timeoutSeconds,
TimeUnit.SECONDS))
}
.takeUntil(::goodEnoughLocation)
.replay()
.autoConnect()
val compositeDisposable = CompositeDisposable()
compositeDisposable.add(observable.subscribe(
{},
{ gpsLocationObsSub = null },
{ gpsLocationObsSub = null }))
gpsLocationObsSub = ObsDis(
observable,
compositeDisposable)
}
return gpsLocationObsSub!!.observable
}
private fun getLocationSettingsResolution(
locationSettingsResult: LocationSettingsResult): Single<Unit> {
val status = locationSettingsResult.status
return if (status.statusCode == LocationSettingsStatusCodes.RESOLUTION_REQUIRED) {
if (status.hasResolution()) {
Single.create { singleEmitter ->
addRequest { requestCode ->
try {
status.startResolutionForResult(this, requestCode)
} catch (error: IntentSender.SendIntentException) {
singleEmitter.onError(Exception("Error opening settings activity"))
}
singleEmitter
}
}
} else {
Single.error(Exception("No Resolution"))
}
} else {
Single.just(Unit)
}
}
private fun getLastKnownLocation(): Maybe<Location> = getConnectedGoogleApiClientObservable()
.flatMap { apiClient ->
val location = LocationServices.FusedLocationApi.getLastLocation(apiClient)
Observable.just(if (location != null) Some(location) else None())
}
.take(1)
.filter { it.with({ true }, { false }) }
.singleElement()
.map { it.toNullable() }
private fun getCheckLocationSettings(locationRequest: LocationSettingsRequest):
Single<LocationSettingsResult>
= getConnectedGoogleApiClientObservable()
.flatMap<LocationSettingsResult>(Function { googleApiClient ->
Observable.create<LocationSettingsResult> { emitter ->
LocationServices.SettingsApi.checkLocationSettings(
googleApiClient,
locationRequest
)
.setResultCallback { result ->
emitter.onNext(result)
}
}
})
.take(1) // This is used to not receive client disconnect errors
.singleOrError()
private fun getConnectedGoogleApiClientObservable(): Observable<GoogleApiClient> {
val googleApiClient = googleApiClientObservable ?: {
val obs = Observable.create<GoogleApiClient> { observableEmitter ->
val apiClientBuilder = GoogleApiClient.Builder(this)
apiClientBuilder.addApi(LocationServices.API)
var apiClient: GoogleApiClient? = null
val apiClientConnectionCallbacks = object :
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
override fun onConnected(bundle: Bundle?) {
try {
observableEmitter.onNext(apiClient)
} catch (ex: Throwable) {
observableEmitter.onError(ex)
}
}
override fun onConnectionSuspended(cause: Int) {
observableEmitter.onError(GoogleAPIConnectionSuspendedException(cause))
}
override fun onConnectionFailed(connectionResult: ConnectionResult) {
observableEmitter.onError(GoogleAPIConnectionException(
"Error connecting to GoogleApiClient.",
connectionResult))
}
}
apiClientBuilder.addConnectionCallbacks(apiClientConnectionCallbacks)
apiClientBuilder.addOnConnectionFailedListener(apiClientConnectionCallbacks)
apiClient = apiClientBuilder.build()
apiClient!!.connect()
}
.replay()
.autoConnect()
obs.subscribe(
{},
{ googleApiClientObservable = null },
{})
obs
}()
googleApiClientObservable = googleApiClient
return googleApiClient!!
}
private fun getGooglePlayServicesAvailable(): Single<Unit> {
val errorCode = googleApiAvailability.isGooglePlayServicesAvailable(this)
return if (errorCode == ConnectionResult.SUCCESS) {
Single.just(Unit)
} else if (googleApiAvailability.isUserResolvableError(errorCode)) {
if (googlePlayServicesObsSub == null) {
val observable = Single.create<Unit> { singleEmitter ->
addRequest { requestCode ->
googleApiAvailability
.getErrorDialog(this, errorCode, requestCode) {
singleEmitter.onError(UserAbortException())
}
.show()
singleEmitter
}
}
.toObservable()
.replay()
.autoConnect()
val compositeDisposable = CompositeDisposable()
compositeDisposable.add(observable?.subscribe(
{},
{ googlePlayServicesObsSub = null },
{ googlePlayServicesObsSub = null }))
googlePlayServicesObsSub = ObsDis(
observable,
compositeDisposable)
}
googlePlayServicesObsSub!!.observable.singleOrError()
} else {
Single.error(Exception("Google Play Services Unresolvable Error"))
}
}
override fun onDestroy() {
permissionsObsSub.forEach { it.value.disposable.dispose() }
googlePlayServicesObsSub?.disposable?.dispose()
gpsLocationObsSub?.disposable?.dispose()
super.onDestroy()
}
}
// Minimum accuracy in meters and not from a mock provider
private fun goodEnoughLocation(location: Location): Boolean = location.accuracy <= 120f &&
isNotFromMockProvider(location)
// Note: when we tested this, it was not always accurate.
private fun isNotFromMockProvider(location: Location): Boolean =
!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
location.isFromMockProvider)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment