mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
incorporate androidlib
This commit is contained in:
parent
20cf3f0825
commit
5eb5cd1421
63 changed files with 1451 additions and 108 deletions
110
app/src/main/java/com/geeksville/mesh/android/AppPrefs.kt
Normal file
110
app/src/main/java/com/geeksville/mesh/android/AppPrefs.kt
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/4/15.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* A delegate for "foo by FloatPref"
|
||||
*/
|
||||
class FloatPref {
|
||||
fun get(thisRef: AppPrefs, prop: KProperty<Float>): Float = thisRef.getPrefs().getFloat(thisRef.makeName(prop.name), java.lang.Float.MIN_VALUE)
|
||||
|
||||
fun set(thisRef: AppPrefs, prop: KProperty<Float>, value: Float) {
|
||||
thisRef.setPrefs { e -> e.putFloat(thisRef.makeName(prop.name), value)}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A delegate for "foo by StringPref"
|
||||
*/
|
||||
class StringPref(val default: String) {
|
||||
fun get(thisRef: AppPrefs, prop: KProperty<String>): String = thisRef.getPrefs().getString(thisRef.makeName(prop.name), default)!!
|
||||
|
||||
fun set(thisRef: AppPrefs, prop: KProperty<String>, value: String) {
|
||||
thisRef.setPrefs { e ->
|
||||
e.putString(thisRef.makeName(prop.name), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin for accessing android prefs for the app
|
||||
*/
|
||||
public open class AppPrefs(val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val baseName = "appPrefs_"
|
||||
}
|
||||
|
||||
fun makeName(s: String) = baseName + s
|
||||
|
||||
fun getPrefs() = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
|
||||
fun setPrefs(body: (SharedPreferences.Editor) -> Unit) {
|
||||
val e = getPrefs().edit()
|
||||
body(e)
|
||||
e.commit()
|
||||
}
|
||||
|
||||
fun incPref(name: String) {
|
||||
setPrefs { e ->
|
||||
e.putInt(name, 1 + getPrefs().getInt(name, 0))
|
||||
}
|
||||
}
|
||||
|
||||
fun removePref(name: String) {
|
||||
setPrefs { e ->
|
||||
e.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
fun putPref(name: String, b: Boolean) {
|
||||
setPrefs { e ->
|
||||
e.putBoolean(name, b)
|
||||
}
|
||||
}
|
||||
|
||||
fun putPref(name: String, b: Float) {
|
||||
setPrefs { e ->
|
||||
e.putFloat(name, b)
|
||||
}
|
||||
}
|
||||
|
||||
fun putPref(name: String, b: Int) {
|
||||
setPrefs { e ->
|
||||
e.putInt(name, b)
|
||||
}
|
||||
}
|
||||
|
||||
fun putPref(name: String, b: Set<String>) {
|
||||
setPrefs { e ->
|
||||
e.putStringSet(name, b)
|
||||
}
|
||||
}
|
||||
|
||||
fun putPref(name: String, b: String) {
|
||||
setPrefs { e ->
|
||||
e.putString(name, b)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a persistent installation ID
|
||||
*/
|
||||
fun getInstallId(): String {
|
||||
var r = getPrefs().getString(makeName("installId"), "")!!
|
||||
if(r == "") {
|
||||
r = UUID.randomUUID().toString()
|
||||
putPref(makeName("installId"), r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt
Normal file
24
app/src/main/java/com/geeksville/mesh/android/BuildUtils.kt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/14/16.
|
||||
*/
|
||||
object BuildUtils : Logging {
|
||||
|
||||
fun is64Bit(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 21)
|
||||
return false
|
||||
else
|
||||
return Build.SUPPORTED_64_BIT_ABIS.size > 0
|
||||
}
|
||||
|
||||
fun isBuggyMoto(): Boolean {
|
||||
debug("Device type is: ${Build.DEVICE}")
|
||||
return Build.DEVICE == "osprey_u2" // Moto G
|
||||
}
|
||||
|
||||
/// Are we running on the emulator?
|
||||
val isEmulator get() = Build.MODEL == "Android SDK built for x86" || Build.MODEL == "sdk_gphone_x86" || Build.MODEL == "Android SDK built for x86_64"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
|
||||
/// show a toast
|
||||
fun Context.toast(message: CharSequence) =
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
/// Utility function to hide the soft keyboard per stack overflow
|
||||
fun Activity.hideKeyboard() {
|
||||
// Check if no view has focus:
|
||||
currentFocus?.let { v ->
|
||||
val imm =
|
||||
getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import android.content.pm.PackageManager
|
|||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.MainActivity
|
||||
|
||||
/**
|
||||
|
|
|
|||
14
app/src/main/java/com/geeksville/mesh/android/DateUtils.kt
Normal file
14
app/src/main/java/com/geeksville/mesh/android/DateUtils.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/13/16.
|
||||
*/
|
||||
object DateUtils {
|
||||
fun dateUTC(year: Int, month: Int, day: Int): Date {
|
||||
val cal = GregorianCalendar(TimeZone.getTimeZone("GMT"))
|
||||
cal.set(year, month, day, 0, 0, 0);
|
||||
return Date(cal.getTime().getTime())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.PrintWriter
|
||||
|
||||
/**
|
||||
* Create a debug log on the SD card (if needed and allowed and app is configured for debugging (FIXME)
|
||||
*
|
||||
* write strings to that file
|
||||
*/
|
||||
class DebugLogFile(context: Context, name: String) {
|
||||
val stream = FileOutputStream(File(context.getExternalFilesDir(null), name), true)
|
||||
val file = PrintWriter(stream)
|
||||
|
||||
fun close() {
|
||||
file.close()
|
||||
}
|
||||
|
||||
fun log(s: String) {
|
||||
file.println(s) // FIXME, optionally include timestamps
|
||||
file.flush() // for debugging
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a debug log on the SD card (if needed and allowed and app is configured for debugging (FIXME)
|
||||
*
|
||||
* write strings to that file
|
||||
*/
|
||||
class BinaryLogFile(context: Context, name: String) :
|
||||
FileOutputStream(File(context.getExternalFilesDir(null), name), true) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/13/16.
|
||||
*/
|
||||
class ExpireChecker(val context: Activity) : Logging {
|
||||
|
||||
fun check(year: Int, month: Int, day: Int) {
|
||||
val expireDate = DateUtils.dateUTC(year, month, day)
|
||||
val now = Date()
|
||||
|
||||
debug("Expire check $now vs $expireDate")
|
||||
if (now.after(expireDate))
|
||||
doExpire()
|
||||
}
|
||||
|
||||
private fun doExpire() {
|
||||
val packageName = context.packageName
|
||||
errormsg("$packageName is too old and must be updated at the Play store")
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
"This application is out of date and must be updated",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
val i = Intent(Intent.ACTION_VIEW)
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
i.setData(Uri.parse("market://details?id=$packageName&referrer=utm_source%3Dexpired"))
|
||||
context.startActivity(i)
|
||||
context.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.edit
|
||||
import com.geeksville.mesh.analytics.AnalyticsProvider
|
||||
import com.geeksville.mesh.analytics.MixpanelAnalytics
|
||||
import com.geeksville.mesh.analytics.TeeAnalytics
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
|
||||
fun isGooglePlayAvailable(context: Context): Boolean {
|
||||
val a = GoogleApiAvailability.getInstance()
|
||||
val r = a.isGooglePlayServicesAvailable(context)
|
||||
return r != ConnectionResult.SERVICE_MISSING && r != ConnectionResult.SERVICE_INVALID
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/4/15.
|
||||
*/
|
||||
|
||||
open class GeeksvilleApplication(
|
||||
val splunkKey: String? = null,
|
||||
val mixpanelKey: String? = null,
|
||||
val pushKey: String? = null
|
||||
) : Application(), Logging {
|
||||
|
||||
companion object {
|
||||
lateinit var analytics: AnalyticsProvider
|
||||
var currentActivity: Activity? = null
|
||||
}
|
||||
|
||||
var splunk: AnalyticsProvider? = null
|
||||
var mixAnalytics: MixpanelAnalytics? = null
|
||||
|
||||
private val lifecycleCallbacks = object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
currentActivity = activity
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Are we running inside the testlab?
|
||||
val isInTestLab: Boolean
|
||||
get() {
|
||||
val testLabSetting =
|
||||
Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
|
||||
if(testLabSetting != null)
|
||||
info("Testlab is $testLabSetting")
|
||||
return "true" == testLabSetting
|
||||
}
|
||||
|
||||
private val analyticsPrefs: SharedPreferences by lazy {
|
||||
getSharedPreferences(
|
||||
"analytics-prefs",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
|
||||
var isAnalyticsAllowed: Boolean
|
||||
get() = analyticsPrefs.getBoolean("allowed", true)
|
||||
set(value) {
|
||||
analyticsPrefs.edit(commit = true) {
|
||||
putBoolean("allowed", value)
|
||||
}
|
||||
|
||||
// Change the flag with the providers
|
||||
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
|
||||
/*
|
||||
if(splunkKey != null)
|
||||
splunk = SplunkAnalytics(this, splunkKey) // Only used for crash reports
|
||||
*/
|
||||
|
||||
val googleAnalytics = com.geeksville.mesh.analytics.GoogleAnalytics(this)
|
||||
if (mixpanelKey != null) {
|
||||
val mix = com.geeksville.mesh.analytics.MixpanelAnalytics(this, mixpanelKey, pushKey)
|
||||
mixAnalytics = mix
|
||||
|
||||
analytics = TeeAnalytics(googleAnalytics, mix)
|
||||
} else
|
||||
analytics = googleAnalytics
|
||||
|
||||
// Set analytics per prefs
|
||||
isAnalyticsAllowed = isAnalyticsAllowed
|
||||
|
||||
registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
||||
}
|
||||
|
||||
fun isInternetConnected(): Boolean {
|
||||
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
val activeNetwork = cm.getActiveNetworkInfo();
|
||||
val isConnected = activeNetwork != null &&
|
||||
activeNetwork.isConnectedOrConnecting();
|
||||
|
||||
return isConnected
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun geeksvilleApp(context: Context) = context.applicationContext as GeeksvilleApplication
|
||||
|
||||
|
||||
interface GeeksvilleApplicationClient {
|
||||
|
||||
fun getAnalytics() = GeeksvilleApplication.analytics
|
||||
|
||||
}
|
||||
83
app/src/main/java/com/geeksville/mesh/android/Logging.kt
Normal file
83
app/src/main/java/com/geeksville/mesh/android/Logging.kt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.util.Exceptions
|
||||
|
||||
/**
|
||||
* Created by kevinh on 12/24/14.
|
||||
*/
|
||||
|
||||
typealias LogPrinter = (Int, String, String) -> Unit
|
||||
|
||||
interface Logging {
|
||||
|
||||
companion object {
|
||||
/** Some vendors strip log messages unless the severity is super high.
|
||||
*
|
||||
* alps == Soyes
|
||||
* HMD Global == mfg of the Nokia 7.2
|
||||
*/
|
||||
private val badVendors = setOf("OnePlus", "alps", "HMD Global", "Sony")
|
||||
|
||||
/// if false NO logs will be shown, set this in the application based on BuildConfig.DEBUG
|
||||
var showLogs = true
|
||||
|
||||
/** if true, all logs will be printed at error level. Sometimes necessary for buggy ROMs
|
||||
* that filter logcat output below this level.
|
||||
*
|
||||
* Since there are so many bad vendors, we just always lie if we are a release build
|
||||
*/
|
||||
var forceErrorLevel = !BuildConfig.DEBUG || badVendors.contains(Build.MANUFACTURER)
|
||||
|
||||
/// If false debug logs will not be shown (but others might)
|
||||
var showDebug = true
|
||||
|
||||
/**
|
||||
* By default all logs are printed using the standard android Log class. But clients
|
||||
* can change printlog to a different implementation (for logging to files or via
|
||||
* google crashlytics)
|
||||
*/
|
||||
var printlog: LogPrinter = { level, tag, message ->
|
||||
if (showLogs) {
|
||||
if (showDebug || level > Log.DEBUG) {
|
||||
Log.println(if (forceErrorLevel) Log.ERROR else level, tag, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tag(): String = this.javaClass.getName()
|
||||
|
||||
fun info(msg: String) = printlog(Log.INFO, tag(), msg)
|
||||
fun verbose(msg: String) = printlog(Log.VERBOSE, tag(), msg)
|
||||
fun debug(msg: String) = printlog(Log.DEBUG, tag(), msg)
|
||||
fun warn(msg: String) = printlog(Log.WARN, tag(), msg)
|
||||
|
||||
/**
|
||||
* Log an error message, note - we call this errormsg rather than error because error() is
|
||||
* a stdlib function in kotlin in the global namespace and we don't want users to accidentally call that.
|
||||
*/
|
||||
fun errormsg(msg: String, ex: Throwable? = null) {
|
||||
if (ex?.message != null)
|
||||
printlog(Log.ERROR, tag(), "$msg (exception ${ex.message})")
|
||||
else
|
||||
printlog(Log.ERROR, tag(), "$msg")
|
||||
}
|
||||
|
||||
/// Kotlin assertions are disabled on android, so instead we use this assert helper
|
||||
fun logAssert(f: Boolean) {
|
||||
if (!f) {
|
||||
val ex = AssertionError("Assertion failed")
|
||||
|
||||
// if(!Debug.isDebuggerConnected())
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
/// Report an error (including messaging our crash reporter service if allowed
|
||||
fun reportError(s: String) {
|
||||
Exceptions.report(Exception("logging reportError: $s"), s)
|
||||
}
|
||||
}
|
||||
185
app/src/main/java/com/geeksville/mesh/android/PlayClient.kt
Normal file
185
app/src/main/java/com/geeksville/mesh/android/PlayClient.kt
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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<Api<out NotRequiredOptions>>, scopes: Array<Scope> = 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)
|
||||
}
|
||||
}
|
||||
114
app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt
Normal file
114
app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.os.IInterface
|
||||
import com.geeksville.mesh.util.exceptionReporter
|
||||
import java.io.Closeable
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class BindFailedException : Exception("bindService failed")
|
||||
|
||||
/**
|
||||
* A wrapper that cleans up the service binding process
|
||||
*/
|
||||
open class ServiceClient<T : IInterface>(private val stubFactory: (IBinder) -> T) : Closeable,
|
||||
Logging {
|
||||
|
||||
var serviceP: T? = null
|
||||
|
||||
/// A getter that returns the bound service or throws if not bound
|
||||
val service: T
|
||||
get() {
|
||||
waitConnect() // Wait for at least the initial connection to happen
|
||||
return serviceP ?: throw Exception("Service not bound")
|
||||
}
|
||||
|
||||
private var context: Context? = null
|
||||
|
||||
private var isClosed = true
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
private val condition = lock.newCondition()
|
||||
|
||||
/** Call this if you want to stall until the connection is completed */
|
||||
fun waitConnect() {
|
||||
// Wait until this service is connected
|
||||
lock.withLock {
|
||||
if (context == null)
|
||||
throw Exception("Haven't called connect")
|
||||
|
||||
if (serviceP == null)
|
||||
condition.await()
|
||||
}
|
||||
}
|
||||
|
||||
fun connect(c: Context, intent: Intent, flags: Int) {
|
||||
context = c
|
||||
if (isClosed) {
|
||||
isClosed = false
|
||||
if (!c.bindService(intent, connection, flags)) {
|
||||
|
||||
// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try
|
||||
// a short sleep to see if that helps
|
||||
errormsg("Needed to use the second bind attempt hack")
|
||||
Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1
|
||||
if (!c.bindService(intent, connection, flags)) {
|
||||
throw BindFailedException()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn("Ignoring rebind attempt for service")
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
isClosed = true
|
||||
try {
|
||||
context?.unbindService(connection)
|
||||
}
|
||||
catch(ex: IllegalArgumentException) {
|
||||
// Autobugs show this can generate an illegal arg exception for "service not registered" during reinstall?
|
||||
warn("Ignoring error in ServiceClient.close, probably harmless")
|
||||
}
|
||||
serviceP = null
|
||||
context = null
|
||||
}
|
||||
|
||||
/// Called when we become connected
|
||||
open fun onConnected(service: T) {
|
||||
}
|
||||
|
||||
/// called on loss of connection
|
||||
open fun onDisconnected() {
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) = exceptionReporter {
|
||||
if (!isClosed) {
|
||||
val s = stubFactory(binder)
|
||||
serviceP = s
|
||||
onConnected(s)
|
||||
|
||||
// after calling our handler, tell anyone who was waiting for this connection to complete
|
||||
lock.withLock {
|
||||
condition.signalAll()
|
||||
}
|
||||
} else {
|
||||
// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue
|
||||
// for us. Be careful not to process that stale event
|
||||
warn("A service connected while we were closing it, ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) = exceptionReporter {
|
||||
serviceP = null
|
||||
onDisconnected()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue