diff --git a/app/build.gradle b/app/build.gradle index caa6a720..9e3bb0f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlinx-serialization' apply plugin: 'dagger.hilt.android.plugin' -apply plugin: 'com.google.gms.google-services' apply plugin: 'com.github.triplet.play' apply plugin: 'de.mobilej.unmock' // apply plugin: "app.brant.amazonappstorepublisher" -// Apply the Crashlytics Gradle plugin +// Firebase Crashlytics +apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' // protobuf @@ -38,11 +38,10 @@ android { } compileSdkVersion 31 // leave undefined to use version plugin wants - // buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI defaultConfig { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) - targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added + targetSdkVersion 30 versionCode 20339 // format is Mmmss (where M is 1+the numeric major number versionName "1.3.39" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -169,18 +168,10 @@ dependencies { // implementation 'com.google.android.things:androidthings:1.0' implementation 'com.github.mik3y:usb-serial-for-android:3.4.6' - // location services - implementation 'com.google.android.gms:play-services-location:19.0.1' - // For Google Sign-In (owner name accesss) - implementation 'com.google.android.gms:play-services-auth:20.2.0' - // Add the Firebase SDK for Crashlytics. implementation 'com.google.firebase:firebase-crashlytics:18.2.6' implementation 'com.google.firebase:firebase-analytics:20.1.0' - // geeksville-androidlib - // compileOnly 'com.google.android.gms:play-services-base:17.6.0' - // alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 1ddf491d..556eecb2 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -33,6 +33,7 @@ import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.ServiceClient import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.android.getMissingPermissions +import com.geeksville.mesh.android.isGooglePlayAvailable import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.model.BTScanModel import com.geeksville.mesh.model.BluetoothViewModel @@ -45,8 +46,6 @@ import com.geeksville.mesh.service.* import com.geeksville.mesh.ui.* import com.geeksville.mesh.util.Exceptions import com.geeksville.mesh.util.exceptionReporter -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator @@ -252,15 +251,12 @@ class MainActivity : BaseActivity(), Logging { private fun askToRate() { exceptionReporter { // Got one IllegalArgumentException from inside this lib, but we don't want to crash our app because of bugs in this optional feature - val hasGooglePlay = GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(this) != ConnectionResult.SERVICE_MISSING - val rater = AppRate.with(this) .setInstallDays(10.toByte()) // default is 10, 0 means install day, 10 means app is launched 10 or more days later than installation .setLaunchTimes(10.toByte()) // default is 10, 3 means app is launched 3 or more times .setRemindInterval(1.toByte()) // default is 1, 1 means app is launched 1 or more days after neutral button clicked .setRemindLaunchesNumber(1.toByte()) // default is 0, 1 means app is launched 1 or more times after neutral button clicked - .setStoreType(if (hasGooglePlay) StoreType.GOOGLEPLAY else StoreType.AMAZON) + .setStoreType(StoreType.GOOGLEPLAY) rater.monitor() // Monitors the app launch times @@ -310,7 +306,7 @@ class MainActivity : BaseActivity(), Logging { // Handle any intent handleIntent(intent) - askToRate() + if (isGooglePlayAvailable(this)) askToRate() // if (!isInTestLab) - very important - even in test lab we must request permissions because we need location perms for some of our tests to pass requestPermission() diff --git a/app/src/main/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/main/java/com/geeksville/mesh/android/GeeksvilleApplication.kt index 09077b72..d5aa4703 100644 --- a/app/src/main/java/com/geeksville/mesh/android/GeeksvilleApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/android/GeeksvilleApplication.kt @@ -10,11 +10,11 @@ import android.provider.Settings import androidx.core.content.edit import com.geeksville.mesh.analytics.AnalyticsProvider import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.common.GoogleApiAvailabilityLight fun isGooglePlayAvailable(context: Context): Boolean { - val a = GoogleApiAvailability.getInstance() + val a = GoogleApiAvailabilityLight.getInstance() val r = a.isGooglePlayServicesAvailable(context) return r != ConnectionResult.SERVICE_MISSING && r != ConnectionResult.SERVICE_INVALID } diff --git a/app/src/main/java/com/geeksville/mesh/android/PlayClient.kt b/app/src/main/java/com/geeksville/mesh/android/PlayClient.kt deleted file mode 100644 index 27b55714..00000000 --- a/app/src/main/java/com/geeksville/mesh/android/PlayClient.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.geeksville.mesh.android - -import android.app.Activity -import android.os.Bundle -import com.google.android.gms.common.api.Api -import com.google.android.gms.common.api.Api.ApiOptions.NotRequiredOptions -import com.google.android.gms.common.api.Scope -import com.google.android.gms.common.api.GoogleApiClient -import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GooglePlayServicesUtil -import android.content.IntentSender -import android.content.Intent -import android.util.Log - - -interface PlayClientCallbacks /* : Activity */ { - /** - * Called to tell activity we've lost connection to play - */ - fun onPlayConnectionSuspended() :Unit - - /** - * Called to tell activity we are now connected to play - * Do remaining init here - */ - fun onPlayConnected() : Unit - - /** - * Called when this machine does not have a valid form of play. - */ - fun onPlayUnavailable() : Unit - -} - -/** - * Created by kevinh on 1/5/15. - */ - -public class PlayClient(val context: Activity, val playCallbacks: PlayClientCallbacks) : Logging { - - var apiClient: GoogleApiClient? = null - var authInProgress: Boolean = false - - companion object { - val PLAY_OAUTH_REQUEST_CODE = 901 - val AUTH_PENDING = "authPend" - } - - - /** - * Must be called from onCreate - */ - fun playOnCreate(savedInstanceState: Bundle?, apis: Array>, scopes: Array = arrayOf()) { - - if(savedInstanceState != null) - authInProgress = savedInstanceState.getBoolean(AUTH_PENDING) - - if(hasPlayServices()) { - var builder = GoogleApiClient.Builder(context) - .addConnectionCallbacks(object : GoogleApiClient.ConnectionCallbacks { - - override fun onConnected(p0: Bundle?) { - // Connected to Google Play services! - // The good stuff goes here. - - playCallbacks.onPlayConnected() - } - - override fun onConnectionSuspended(i: Int) { - // If your connection to the sensor gets lost at some point, - // you'll be able to determine the reason and react to it here. - if (i == ConnectionCallbacks.CAUSE_NETWORK_LOST) { - info("Connection lost. Cause: Network Lost."); - } else if (i == ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) { - info("Connection lost. Reason: Service Disconnected"); - } else - errormsg("Unknown play kode $i") - - playCallbacks.onPlayConnectionSuspended() - } - }) - .addOnConnectionFailedListener(object : GoogleApiClient.OnConnectionFailedListener { - override fun onConnectionFailed(result: ConnectionResult) { - info("Play connection failed $result") - if (!result.hasResolution()) { - showErrorDialog(result.errorCode) - } else { - // The failure has a resolution. Resolve it. - // Called typically when the app is not yet authorized, and an - // authorization dialog is displayed to the user. - if (!authInProgress) { - try { - info("Attempting to resolve failed connection"); - authInProgress = true; - result.startResolutionForResult(context, - PLAY_OAUTH_REQUEST_CODE); - } catch (e: IntentSender.SendIntentException) { - errormsg("Exception while starting resolution activity") - playCallbacks.onPlayUnavailable() - } - } - } - } - }) - - apis.forEach { api -> - builder = builder.addApi(api) - } - - scopes.forEach { s -> - builder = builder.addScope(s) - } - - apiClient = builder.build() - } - } - - private fun showErrorDialog(code: Int) { - // Show the localized error dialog - GooglePlayServicesUtil.getErrorDialog(code, - context, 0)?.show(); - playCallbacks.onPlayUnavailable() - } - - fun hasPlayServices(): Boolean { - // Check that Google Play services is available - val resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context) - // For testing - //val resultCode = ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED - - if (ConnectionResult.SUCCESS == resultCode) { - // In debug mode, log the status - Log.d("Geofence Detection", - "Google Play services is available."); - - // getAnalytics().track("Has Play") - - // Continue - return true - // Google Play services was not available for some reason - } else { - showErrorDialog(resultCode) - - return false - } - } - - /** - * Must be called from onActivityResult - * @return true if we handled this - */ - fun playOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean = - if (requestCode == PLAY_OAUTH_REQUEST_CODE) { - authInProgress = false; - if (resultCode == Activity.RESULT_OK) { - // Make sure the app is not already connected or attempting to connect - if (!apiClient!!.isConnecting && !apiClient!!.isConnected) { - apiClient!!.connect(); - } - } - else { - // User opted to not install play - errormsg("User declined play") - context.finish() - } - true - } - else - false - - fun playOnStart() { - if(apiClient != null) - apiClient!!.connect() - } - - fun playOnStop() { - if(apiClient != null && apiClient!!.isConnected) - apiClient!!.disconnect() - } - - fun playSaveInstanceState(outState: Bundle) { - outState.putBoolean(AUTH_PENDING, authInProgress) - } -} diff --git a/app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt b/app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt index 96aa1907..03e24e21 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt @@ -3,14 +3,11 @@ package com.geeksville.mesh.repository.location import android.annotation.SuppressLint import android.content.Context import android.location.Location -import android.os.Looper +import android.location.LocationListener +import android.location.LocationManager import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.hasBackgroundPermission -import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationResult -import com.google.android.gms.location.LocationServices import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -35,47 +32,43 @@ class SharedLocationManager constructor( val receivingLocationUpdates: StateFlow get() = _receivingLocationUpdates // TODO use positionBroadcastSecs / test locationRequest settings - private val desiredInterval = 1 * 60 * 1000L // if unset, use positionBroadcastSecs default // positionBroadcastSecs.takeIf { it != 0L }?.times(1000L) ?: (15 * 60 * 1000L) - - // Set up the Fused Location Provider and LocationRequest - private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) - private val locationRequest = LocationRequest.create().apply { - interval = desiredInterval - fastestInterval = 30 * 1000L - // smallestDisplacement = 50F // 50 meters - priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY - } + private val fastestInterval = 30 * 1000L + private val smallestDisplacement = 50F // 50 meters @SuppressLint("MissingPermission") private val _locationUpdates = callbackFlow { - val callback = object : LocationCallback() { - override fun onLocationResult(result: LocationResult) { - // info("New location: ${result.lastLocation}") - trySend(result.lastLocation) - } + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val callback = LocationListener { location -> + // info("New location: ${result.lastLocation}") + trySend(location) } + if (!context.hasBackgroundPermission()) close() - info("Starting location requests with interval=${desiredInterval}ms") + + info("Starting location updates with minTime=${fastestInterval}ms and minDistance=${smallestDisplacement}m") _receivingLocationUpdates.value = true GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS - fusedLocationClient.requestLocationUpdates( - locationRequest, - callback, - Looper.getMainLooper() - ).addOnFailureListener { ex -> - errormsg("Failed to listen to GPS error: ${ex.message}") - close(ex) // in case of exception, close the Flow + try { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + fastestInterval, + smallestDisplacement, + callback, + context.mainLooper + ) + } catch (e: Exception) { + close(e) // in case of exception, close the Flow } awaitClose { info("Stopping location requests") _receivingLocationUpdates.value = false GeeksvilleApplication.analytics.track("location_stop") - fusedLocationClient.removeLocationUpdates(callback) // clean up when Flow collection ends + locationManager.removeUpdates(callback) // clean up when Flow collection ends } }.shareIn( externalScope, diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 1a91181b..a97e9564 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -9,7 +9,6 @@ import androidx.core.content.edit import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.android.isGooglePlayAvailable import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.* import com.geeksville.mesh.MeshProtos.MeshPacket @@ -153,7 +152,7 @@ class MeshService : Service(), Logging { // If we're already observing updates, don't register again if (locationFlow?.isActive == true) return - if (hasBackgroundPermission() && isGooglePlayAvailable(this)) { + if (hasBackgroundPermission()) { locationFlow = locationRepository.getLocations() .onEach { location -> sendPosition( diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index fab749ea..3051b8c1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -16,7 +16,6 @@ import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.hideKeyboard -import com.geeksville.mesh.android.isGooglePlayAvailable import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.ConfigProtos @@ -154,7 +153,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { model.provideLocation.value = false binding.provideLocationCheckbox.isChecked = false } else { - binding.provideLocationCheckbox.isEnabled = isGooglePlayAvailable(requireContext()) + binding.provideLocationCheckbox.isEnabled = true } // update the region selection from the device @@ -582,7 +581,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { scanModel.setupScan() // system permissions might have changed while we were away - binding.provideLocationCheckbox.isChecked = myActivity.hasBackgroundPermission() && (model.provideLocation.value ?: false) && isGooglePlayAvailable(requireContext()) + binding.provideLocationCheckbox.isChecked = myActivity.hasBackgroundPermission() && (model.provideLocation.value ?: false) myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter) diff --git a/build.gradle b/build.gradle index 57b5b693..df1e70ee 100644 --- a/build.gradle +++ b/build.gradle @@ -19,9 +19,7 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files - // Add the Crashlytics Gradle plugin. - // Check that you have the Google Services Gradle plugin v4.3.2 or later - // (if not, add it). + // Firebase Crashlytics classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'