Skip to content

Commit

Permalink
Merge pull request #357 from meshtastic/companion-pairing
Browse files Browse the repository at this point in the history
1.2.55
  • Loading branch information
andrekir authored Feb 1, 2022
2 parents 3e32368 + 066027c commit 88a4825
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 72 deletions.
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ android {
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
versionCode 20254 // format is Mmmss (where M is 1+the numeric major number
versionName "1.2.54"
versionCode 20255 // format is Mmmss (where M is 1+the numeric major number
versionName "1.2.55"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

// per https://developer.android.com/studio/write/vector-asset-studio
Expand Down Expand Up @@ -88,10 +88,10 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}

lintOptions {
lint {
abortOnError false
}

}

play {
Expand Down
54 changes: 20 additions & 34 deletions app/src/main/java/com/geeksville/mesh/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,13 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.android.BindFailedException
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.android.getLocationPermissions
import com.geeksville.mesh.android.getBackgroundPermissions
import com.geeksville.mesh.android.getCameraPermissions
import com.geeksville.mesh.android.getMissingPermissions
import com.geeksville.mesh.android.getScanPermissions
import com.geeksville.mesh.android.*
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet
Expand All @@ -70,7 +65,6 @@ import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
import kotlinx.coroutines.*
import java.io.FileOutputStream
import java.lang.Runnable
import java.nio.charset.Charset
import java.text.DateFormat
import java.util.*
Expand Down Expand Up @@ -198,7 +192,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
}

private val btStateReceiver = BluetoothStateReceiver { _ ->
private val btStateReceiver = BluetoothStateReceiver {
updateBluetoothEnabled()
}

Expand Down Expand Up @@ -249,15 +243,8 @@ class MainActivity : AppCompatActivity(), Logging,
*/
private fun updateBluetoothEnabled() {
var enabled = false // assume failure
val requiredPerms: MutableList<String> = mutableListOf()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
requiredPerms.add(Manifest.permission.BLUETOOTH)
}

if (getMissingPermissions(requiredPerms).isEmpty()) {
if (hasConnectPermission()) {
/// ask the adapter if we have access
bluetoothAdapter?.apply {
enabled = isEnabled
Expand Down Expand Up @@ -309,6 +296,7 @@ class MainActivity : AppCompatActivity(), Logging,
/**
* @return a localized string warning user about missing permissions. Or null if everything is find
*/
@SuppressLint("InlinedApi")
fun getMissingMessage(
missingPerms: List<String> = getMinimumPermissions()
): String? {
Expand Down Expand Up @@ -338,7 +326,7 @@ class MainActivity : AppCompatActivity(), Logging,
}

/** Possibly prompt user to grant permissions
* @param shouldShowDialog usually true, but in cases where we've already shown a dialog elsewhere we skip it.
* @param shouldShowDialog usually false in cases where we've already shown a dialog elsewhere we skip it.
*
* @return true if we already have the needed permissions
*/
Expand Down Expand Up @@ -542,9 +530,9 @@ class MainActivity : AppCompatActivity(), Logging,
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
}.attach()

model.isConnected.observe(this, Observer { connected ->
model.isConnected.observe(this) { connected ->
updateConnectionStatusImage(connected)
})
}

// Handle any intent
handleIntent(intent)
Expand Down Expand Up @@ -640,7 +628,7 @@ class MainActivity : AppCompatActivity(), Logging,
/**
* Dispatch incoming result to the correct fragment.
*/
@SuppressLint("InlinedApi")
@SuppressLint("InlinedApi", "MissingPermission")
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
Expand Down Expand Up @@ -937,10 +925,10 @@ class MainActivity : AppCompatActivity(), Logging,
private var connectionJob: Job? = null

private val mesh = object :
ServiceClient<com.geeksville.mesh.IMeshService>({
com.geeksville.mesh.IMeshService.Stub.asInterface(it)
ServiceClient<IMeshService>({
IMeshService.Stub.asInterface(it)
}) {
override fun onConnected(service: com.geeksville.mesh.IMeshService) {
override fun onConnected(service: IMeshService) {

/*
Note: we must call this callback in a coroutine. Because apparently there is only a single activity looper thread. and if that onConnected override
Expand Down Expand Up @@ -1075,18 +1063,21 @@ class MainActivity : AppCompatActivity(), Logging,
super.onStop()
}

@SuppressLint("MissingPermission")
override fun onStart() {
super.onStart()

// Ask to start bluetooth if no USB devices are visible
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
if (!isInTestLab && !hasUSB) {
bluetoothAdapter?.let {
if (!it.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
if (hasConnectPermission()) {
bluetoothAdapter?.let {
if (!it.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
}
}
} else requestPermission()
}

try {
Expand Down Expand Up @@ -1154,12 +1145,7 @@ class MainActivity : AppCompatActivity(), Logging,
val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM)
.format(Date(System.currentTimeMillis()))
model.messagesState.sendMessage(str)
handler.postDelayed(
Runnable {
postPing()
},
30000
)
handler.postDelayed({ postPing() }, 30000)
}
item.isChecked = !item.isChecked // toggle ping test
if (item.isChecked)
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ fun Context.getMissingPermissions(perms: List<String>) = perms.filter {
) != PackageManager.PERMISSION_GRANTED
}

/**
* Bluetooth connect permissions (or empty if we already have what we need)
*/
fun Context.getConnectPermissions(): List<String> {
val perms = mutableListOf<String>()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
perms.add(Manifest.permission.BLUETOOTH)
}

return getMissingPermissions(perms)
}

/** @return true if the user already has Bluetooth connect permission */
fun Context.hasConnectPermission() = getConnectPermissions().isEmpty()

/**
* Bluetooth scan/discovery permissions (or empty if we already have what we need)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.geeksville.mesh.service

import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
Expand Down Expand Up @@ -91,13 +93,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
}

/// this service UUID is publically visible for scanning
val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")

val BTM_FROMRADIO_CHARACTER =
val BTM_FROMRADIO_CHARACTER: UUID =
UUID.fromString("8ba2bcc2-ee02-4a55-a531-c525c5e454d5")
val BTM_TORADIO_CHARACTER =
val BTM_TORADIO_CHARACTER: UUID =
UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7")
val BTM_FROMNUM_CHARACTER =
val BTM_FROMNUM_CHARACTER: UUID =
UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")

/// Get our bluetooth adapter (should always succeed except on emulator
Expand All @@ -108,10 +110,15 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
}

/** Return true if this address is still acceptable. For BLE that means, still bonded */
@SuppressLint("NewApi", "MissingPermission")
override fun addressValid(context: Context, rest: String): Boolean {
val allPaired =
getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()

val allPaired = if (hasCompanionDeviceApi(context)) {
val deviceManager = context.getSystemService(CompanionDeviceManager::class.java)
deviceManager.associations.map { it }.toSet()
} else {
getBluetoothAdapter(context)?.bondedDevices.orEmpty()
.map { it.address }.toSet()
}
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
false
Expand Down Expand Up @@ -191,7 +198,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
?: throw RadioNotConnectedException("No GATT")

/// Our service - note - it is possible to get back a null response for getService if the device services haven't yet been found
val bservice
private val bservice
get(): BluetoothGattService = device.getService(BTM_SERVICE_UUID)
?: throw RadioNotConnectedException("BLE service not found")

Expand Down Expand Up @@ -263,7 +270,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
/**
* We had some problem, schedule a reconnection attempt (if one isn't already queued)
*/
fun scheduleReconnect(reason: String) {
private fun scheduleReconnect(reason: String) {
if (reconnectJob == null) {
warn("Scheduling reconnect because $reason")
reconnectJob = service.serviceScope.handledLaunch { retryDueToException() }
Expand Down
Loading

0 comments on commit 88a4825

Please sign in to comment.