diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7e2a26a86..94c6aab71 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,15 +12,22 @@
android:name="android.hardware.location.gps"
android:required="false" />
-
-
+
+
+
-
-
+
+
+
-
+
+
+
+
+
@@ -41,9 +48,6 @@
-
-
@@ -124,7 +128,8 @@
android:label="@string/app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
- android:theme="@style/AppTheme">
+ android:theme="@style/AppTheme"
+ android:exported="true">
@@ -162,7 +167,8 @@
android:resource="@xml/device_filter" />
-
+
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 5d7e432e1..b3509b158 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -47,6 +47,7 @@ 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.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet
@@ -251,12 +252,9 @@ class MainActivity : AppCompatActivity(), Logging,
val requiredPerms: MutableList = mutableListOf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- requiredPerms.add(Manifest.permission.BLUETOOTH_SCAN)
requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
- requiredPerms.add(Manifest.permission.ACCESS_FINE_LOCATION)
requiredPerms.add(Manifest.permission.BLUETOOTH)
- requiredPerms.add(Manifest.permission.BLUETOOTH_ADMIN)
}
if (getMissingPermissions(requiredPerms).isEmpty()) {
@@ -275,8 +273,6 @@ class MainActivity : AppCompatActivity(), Logging,
*/
private fun getMinimumPermissions(): List {
val perms = mutableListOf(
- Manifest.permission.ACCESS_COARSE_LOCATION,
- Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.WAKE_LOCK
// We only need this for logging to capture files for the simulator - turn off for most users
@@ -284,11 +280,9 @@ class MainActivity : AppCompatActivity(), Logging,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- perms.add(Manifest.permission.BLUETOOTH_SCAN)
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
perms.add(Manifest.permission.BLUETOOTH)
- perms.add(Manifest.permission.BLUETOOTH_ADMIN)
}
// Some old phones complain about requesting perms they don't understand
@@ -300,6 +294,9 @@ class MainActivity : AppCompatActivity(), Logging,
return getMissingPermissions(perms)
}
+ /** Ask the user to grant Bluetooth scan/discovery permission */
+ fun requestScanPermission() = requestPermission(getScanPermissions(), true)
+
/** Ask the user to grant camera permission */
fun requestCameraPermission() = requestPermission(getCameraPermissions(), false)
@@ -312,16 +309,19 @@ class MainActivity : AppCompatActivity(), Logging,
/**
* @return a localized string warning user about missing permissions. Or null if everything is find
*/
- fun getMissingMessage(): String? {
+ fun getMissingMessage(
+ missingPerms: List = getMinimumPermissions()
+ ): String? {
val renamedPermissions = mapOf(
// Older versions of android don't know about these permissions - ignore failure to grant
Manifest.permission.ACCESS_COARSE_LOCATION to null,
Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND to null,
Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND to null,
- Manifest.permission.ACCESS_FINE_LOCATION to getString(R.string.location)
+ Manifest.permission.ACCESS_FINE_LOCATION to getString(R.string.location),
+ Manifest.permission.BLUETOOTH_CONNECT to "Bluetooth"
)
- val deniedPermissions = getMinimumPermissions().mapNotNull {
+ val deniedPermissions = missingPerms.mapNotNull {
if (renamedPermissions.containsKey(it))
renamedPermissions[it]
else // No localization found - just show the nasty android string
@@ -342,7 +342,7 @@ class MainActivity : AppCompatActivity(), Logging,
*
* @return true if we already have the needed permissions
*/
- fun requestPermission(
+ private fun requestPermission(
missingPerms: List = getMinimumPermissions(),
shouldShowDialog: Boolean = true
): Boolean =
@@ -369,7 +369,7 @@ class MainActivity : AppCompatActivity(), Logging,
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.required_permissions))
- .setMessage(getMissingMessage())
+ .setMessage(getMissingMessage(missingPerms))
.setNeutralButton(R.string.cancel) { _, _ ->
warn("User bailed due to permissions")
}
@@ -550,6 +550,9 @@ class MainActivity : AppCompatActivity(), Logging,
handleIntent(intent)
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()
}
private fun initToolbar() {
@@ -814,7 +817,7 @@ class MainActivity : AppCompatActivity(), Logging,
model.isConnected.value = oldConnection
}
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
- if (model.provideLocation.value == true && (oldConnection != connected))
+ if (model.provideLocation.value == true)
service.setupProvideLocation()
}
} else {
diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
index 093d58f69..71e22b0b3 100644
--- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
+++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
@@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.os.Build
import androidx.core.content.ContextCompat
+import com.geeksville.mesh.service.BluetoothInterface
/**
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
@@ -28,6 +29,25 @@ fun Context.getMissingPermissions(perms: List) = perms.filter {
) != PackageManager.PERMISSION_GRANTED
}
+/**
+ * Bluetooth scan/discovery permissions (or empty if we already have what we need)
+ */
+fun Context.getScanPermissions(): List {
+ val perms = mutableListOf()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ perms.add(Manifest.permission.BLUETOOTH_SCAN)
+ } else if (!BluetoothInterface.hasCompanionDeviceApi(this)) {
+ perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
+ perms.add(Manifest.permission.BLUETOOTH_ADMIN)
+ }
+
+ return getMissingPermissions(perms)
+}
+
+/** @return true if the user already has Bluetooth scan/discovery permission */
+fun Context.hasScanPermission() = getScanPermissions().isEmpty()
+
/**
* Camera permission (or empty if we already have what we need)
*/
@@ -41,7 +61,7 @@ fun Context.getCameraPermissions(): List {
fun Context.hasCameraPermission() = getCameraPermissions().isEmpty()
/**
- * Camera permission (or empty if we already have what we need)
+ * Location permission (or empty if we already have what we need)
*/
fun Context.getLocationPermissions(): List {
val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)
@@ -49,7 +69,7 @@ fun Context.getLocationPermissions(): List {
return getMissingPermissions(perms)
}
-/** @return true if the user already has camera permission */
+/** @return true if the user already has location permission */
fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
/**
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 8567c653c..b19290a2e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -34,6 +34,7 @@ import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.RadioConfigProtos
import com.geeksville.mesh.android.bluetoothManager
+import com.geeksville.mesh.android.hasScanPermission
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.android.hasBackgroundPermission
import com.geeksville.mesh.android.usbManager
@@ -447,7 +448,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private val guiJob = Job()
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
-
private val hasCompanionDeviceApi: Boolean by lazy {
BluetoothInterface.hasCompanionDeviceApi(requireContext())
}
@@ -477,7 +477,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
_binding = SettingsFragmentBinding.inflate(inflater, container, false)
return binding.root
}
@@ -561,7 +561,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val statusText = binding.scanStatusText
val permissionsWarning = myActivity.getMissingMessage()
when {
- (!hasCompanionDeviceApi && permissionsWarning != null) ->
+ (permissionsWarning != null) ->
statusText.text = permissionsWarning
region == RadioConfigProtos.RegionCode.Unset ->
@@ -615,6 +615,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = regionAdapter
+ model.bluetoothEnabled.observe(
+ viewLifecycleOwner, {
+ if (it) binding.changeRadioButton.show()
+ else binding.changeRadioButton.hide()
+ })
+
model.ownerName.observe(viewLifecycleOwner, { name ->
binding.usernameEditText.setText(name)
})
@@ -637,9 +643,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
})
- scanModel.devices.observe(
- viewLifecycleOwner,
- { devices -> updateDevicesButtons(devices) })
+ scanModel.devices.observe(viewLifecycleOwner, { devices ->
+ updateDevicesButtons(devices)
+ })
binding.updateFirmwareButton.setOnClickListener {
doFirmwareUpdate()
@@ -655,34 +661,31 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.provideLocationCheckbox.isEnabled = isGooglePlayAvailable(requireContext())
binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked ->
- model.provideLocation.value = isChecked
+ if (view.isPressed && isChecked) { // We want to ignore changes caused by code (as opposed to the user)
+ // Don't check the box until the system setting changes
+ view.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission()
- if (view.isChecked) {
- debug("User changed location tracking to $isChecked")
- if (view.isPressed) { // We want to ignore changes caused by code (as opposed to the user)
- // Don't check the box until the system setting changes
- view.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission()
-
- if (!myActivity.hasLocationPermission()) // Make sure we have location permission (prerequisite)
- myActivity.requestLocationPermission()
- else if (!myActivity.hasBackgroundPermission())
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.background_required)
- .setMessage(R.string.why_background_required)
- .setNeutralButton(R.string.cancel) { _, _ ->
- debug("User denied background permission")
- }
- .setPositiveButton(getString(R.string.accept)) { _, _ ->
- myActivity.requestBackgroundPermission()
- }
- .show()
-
- if (view.isChecked) {
- checkLocationEnabled(getString(R.string.location_disabled))
- model.meshService?.setupProvideLocation()
- }
+ if (!myActivity.hasLocationPermission()) // Make sure we have location permission (prerequisite)
+ myActivity.requestLocationPermission()
+ else if (!myActivity.hasBackgroundPermission())
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.background_required)
+ .setMessage(R.string.why_background_required)
+ .setNeutralButton(R.string.cancel) { _, _ ->
+ debug("User denied background permission")
+ }
+ .setPositiveButton(getString(R.string.accept)) { _, _ ->
+ myActivity.requestBackgroundPermission()
+ }
+ .show()
+ if (view.isChecked) {
+ debug("User changed location tracking to $isChecked")
+ model.provideLocation.value = isChecked
+ checkLocationEnabled(getString(R.string.location_disabled))
+ model.meshService?.setupProvideLocation()
}
} else {
+ model.provideLocation.value = isChecked
model.meshService?.stopProvideLocation()
}
}
@@ -730,8 +733,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.isChecked =
scanModel.onSelected(myActivity, device)
- if (!b.isSelected)
+ if (!b.isSelected) {
binding.scanStatusText.text = getString(R.string.please_pair)
+ }
}
}
@@ -757,7 +761,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// and before use
val bleAddr = scanModel.selectedBluetooth
- if (bleAddr != null && adapter != null && adapter.isEnabled) {
+ if (bleAddr != null && adapter != null) {
val bDevice =
adapter.getRemoteDevice(bleAddr)
if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared
@@ -798,9 +802,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initClassicScan() {
binding.changeRadioButton.setOnClickListener {
- if (myActivity.warnMissingPermissions()) {
- myActivity.requestPermission()
- } else scanLeDevice()
+ debug("User clicked changeRadioButton")
+ if (!myActivity.hasScanPermission()) {
+ myActivity.requestScanPermission()
+ } else {
+ checkLocationEnabled()
+ scanLeDevice()
+ }
}
}
@@ -828,7 +836,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initModernScan() {
binding.changeRadioButton.setOnClickListener {
- myActivity.startCompanionScan()
+ debug("User clicked changeRadioButton")
+ if (!myActivity.hasScanPermission()) {
+ myActivity.requestScanPermission()
+ } else {
+ // checkLocationEnabled() // ? some phones still need location turned on
+ myActivity.startCompanionScan()
+ }
}
}