incorporate androidlib

This commit is contained in:
andrekir 2022-09-04 22:52:40 -03:00
parent 20cf3f0825
commit 5eb5cd1421
63 changed files with 1451 additions and 108 deletions

View 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
}
}

View 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"
}

View file

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

View file

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

View 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())
}
}

View file

@ -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) {
}

View file

@ -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()
}
}

View file

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

View 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)
}
}

View 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)
}
}

View 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()
}
}
}