Created
February 2, 2017 14:57
-
-
Save zirman/7319d2b6aa109710c2cf91ca36aca802 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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