Merge pull request #353 from meshtastic/perms

update permissions for Android 12 (API 31+)
pull/355/head
Andre Kirchhoff 2022-01-25 18:49:07 -03:00 zatwierdzone przez GitHub
commit fb48f379c7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 107 dodań i 64 usunięć

Wyświetl plik

@ -12,15 +12,22 @@
android:name="android.hardware.location.gps" android:name="android.hardware.location.gps"
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <!-- Request legacy Bluetooth permissions on older devices -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- only useful if this phone can do BTLE --> <!-- API 31+ Bluetooth permissions -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- needed to access bluetooth when app is background --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<!-- <!-- Permissions required for providing location (from phone GPS) to mesh -->
This permission is required for analytics - and soon the MQTT gateway <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
--> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- This permission is required for analytics - and soon the MQTT gateway -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -41,9 +48,6 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- For android >=26 we can use the new BLE scanning API, which allows auto launching our service when our device is seen --> <!-- For android >=26 we can use the new BLE scanning API, which allows auto launching our service when our device is seen -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" />
@ -124,7 +128,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -162,7 +167,8 @@
android:resource="@xml/device_filter" /> android:resource="@xml/device_filter" />
</activity> </activity>
<receiver android:name="com.geeksville.mesh.service.BootCompleteReceiver"> <receiver android:name="com.geeksville.mesh.service.BootCompleteReceiver"
android:exported="false">
<!-- handle boot events --> <!-- handle boot events -->
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

Wyświetl plik

@ -47,6 +47,7 @@ import com.geeksville.mesh.android.getLocationPermissions
import com.geeksville.mesh.android.getBackgroundPermissions import com.geeksville.mesh.android.getBackgroundPermissions
import com.geeksville.mesh.android.getCameraPermissions import com.geeksville.mesh.android.getCameraPermissions
import com.geeksville.mesh.android.getMissingPermissions import com.geeksville.mesh.android.getMissingPermissions
import com.geeksville.mesh.android.getScanPermissions
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.ChannelSet
@ -251,12 +252,9 @@ class MainActivity : AppCompatActivity(), Logging,
val requiredPerms: MutableList<String> = mutableListOf() val requiredPerms: MutableList<String> = mutableListOf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPerms.add(Manifest.permission.BLUETOOTH_SCAN)
requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT) requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else { } else {
requiredPerms.add(Manifest.permission.ACCESS_FINE_LOCATION)
requiredPerms.add(Manifest.permission.BLUETOOTH) requiredPerms.add(Manifest.permission.BLUETOOTH)
requiredPerms.add(Manifest.permission.BLUETOOTH_ADMIN)
} }
if (getMissingPermissions(requiredPerms).isEmpty()) { if (getMissingPermissions(requiredPerms).isEmpty()) {
@ -275,8 +273,6 @@ class MainActivity : AppCompatActivity(), Logging,
*/ */
private fun getMinimumPermissions(): List<String> { private fun getMinimumPermissions(): List<String> {
val perms = mutableListOf( val perms = mutableListOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.WAKE_LOCK Manifest.permission.WAKE_LOCK
// We only need this for logging to capture files for the simulator - turn off for most users // 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
perms.add(Manifest.permission.BLUETOOTH_SCAN)
perms.add(Manifest.permission.BLUETOOTH_CONNECT) perms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else { } else {
perms.add(Manifest.permission.BLUETOOTH) perms.add(Manifest.permission.BLUETOOTH)
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
} }
// Some old phones complain about requesting perms they don't understand // Some old phones complain about requesting perms they don't understand
@ -300,6 +294,9 @@ class MainActivity : AppCompatActivity(), Logging,
return getMissingPermissions(perms) return getMissingPermissions(perms)
} }
/** Ask the user to grant Bluetooth scan/discovery permission */
fun requestScanPermission() = requestPermission(getScanPermissions(), true)
/** Ask the user to grant camera permission */ /** Ask the user to grant camera permission */
fun requestCameraPermission() = requestPermission(getCameraPermissions(), false) 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 * @return a localized string warning user about missing permissions. Or null if everything is find
*/ */
fun getMissingMessage(): String? { fun getMissingMessage(
missingPerms: List<String> = getMinimumPermissions()
): String? {
val renamedPermissions = mapOf( val renamedPermissions = mapOf(
// Older versions of android don't know about these permissions - ignore failure to grant // Older versions of android don't know about these permissions - ignore failure to grant
Manifest.permission.ACCESS_COARSE_LOCATION to null, Manifest.permission.ACCESS_COARSE_LOCATION to null,
Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND to null, Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND to null,
Manifest.permission.REQUEST_COMPANION_USE_DATA_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)) if (renamedPermissions.containsKey(it))
renamedPermissions[it] renamedPermissions[it]
else // No localization found - just show the nasty android string 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 * @return true if we already have the needed permissions
*/ */
fun requestPermission( private fun requestPermission(
missingPerms: List<String> = getMinimumPermissions(), missingPerms: List<String> = getMinimumPermissions(),
shouldShowDialog: Boolean = true shouldShowDialog: Boolean = true
): Boolean = ): Boolean =
@ -369,7 +369,7 @@ class MainActivity : AppCompatActivity(), Logging,
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.required_permissions)) .setTitle(getString(R.string.required_permissions))
.setMessage(getMissingMessage()) .setMessage(getMissingMessage(missingPerms))
.setNeutralButton(R.string.cancel) { _, _ -> .setNeutralButton(R.string.cancel) { _, _ ->
warn("User bailed due to permissions") warn("User bailed due to permissions")
} }
@ -550,6 +550,9 @@ class MainActivity : AppCompatActivity(), Logging,
handleIntent(intent) handleIntent(intent)
askToRate() 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() { private fun initToolbar() {
@ -814,7 +817,7 @@ class MainActivity : AppCompatActivity(), Logging,
model.isConnected.value = oldConnection model.isConnected.value = oldConnection
} }
// if provideLocation enabled: Start providing location (from phone GPS) to mesh // 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() service.setupProvideLocation()
} }
} else { } else {

Wyświetl plik

@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.geeksville.mesh.service.BluetoothInterface
/** /**
* @return null on platforms without a BlueTooth driver (i.e. the emulator) * @return null on platforms without a BlueTooth driver (i.e. the emulator)
@ -28,6 +29,25 @@ fun Context.getMissingPermissions(perms: List<String>) = perms.filter {
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
} }
/**
* Bluetooth scan/discovery permissions (or empty if we already have what we need)
*/
fun Context.getScanPermissions(): List<String> {
val perms = mutableListOf<String>()
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) * Camera permission (or empty if we already have what we need)
*/ */
@ -41,7 +61,7 @@ fun Context.getCameraPermissions(): List<String> {
fun Context.hasCameraPermission() = getCameraPermissions().isEmpty() 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<String> { fun Context.getLocationPermissions(): List<String> {
val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION) val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)
@ -49,7 +69,7 @@ fun Context.getLocationPermissions(): List<String> {
return getMissingPermissions(perms) 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() fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
/** /**

Wyświetl plik

@ -34,6 +34,7 @@ import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.RadioConfigProtos import com.geeksville.mesh.RadioConfigProtos
import com.geeksville.mesh.android.bluetoothManager import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.android.hasScanPermission
import com.geeksville.mesh.android.hasLocationPermission import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.android.hasBackgroundPermission import com.geeksville.mesh.android.hasBackgroundPermission
import com.geeksville.mesh.android.usbManager import com.geeksville.mesh.android.usbManager
@ -447,7 +448,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private val guiJob = Job() private val guiJob = Job()
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob) private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
private val hasCompanionDeviceApi: Boolean by lazy { private val hasCompanionDeviceApi: Boolean by lazy {
BluetoothInterface.hasCompanionDeviceApi(requireContext()) BluetoothInterface.hasCompanionDeviceApi(requireContext())
} }
@ -477,7 +477,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
_binding = SettingsFragmentBinding.inflate(inflater, container, false) _binding = SettingsFragmentBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -561,7 +561,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val statusText = binding.scanStatusText val statusText = binding.scanStatusText
val permissionsWarning = myActivity.getMissingMessage() val permissionsWarning = myActivity.getMissingMessage()
when { when {
(!hasCompanionDeviceApi && permissionsWarning != null) -> (permissionsWarning != null) ->
statusText.text = permissionsWarning statusText.text = permissionsWarning
region == RadioConfigProtos.RegionCode.Unset -> region == RadioConfigProtos.RegionCode.Unset ->
@ -615,6 +615,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = regionAdapter spinner.adapter = regionAdapter
model.bluetoothEnabled.observe(
viewLifecycleOwner, {
if (it) binding.changeRadioButton.show()
else binding.changeRadioButton.hide()
})
model.ownerName.observe(viewLifecycleOwner, { name -> model.ownerName.observe(viewLifecycleOwner, { name ->
binding.usernameEditText.setText(name) binding.usernameEditText.setText(name)
}) })
@ -637,9 +643,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
} }
}) })
scanModel.devices.observe( scanModel.devices.observe(viewLifecycleOwner, { devices ->
viewLifecycleOwner, updateDevicesButtons(devices)
{ devices -> updateDevicesButtons(devices) }) })
binding.updateFirmwareButton.setOnClickListener { binding.updateFirmwareButton.setOnClickListener {
doFirmwareUpdate() doFirmwareUpdate()
@ -655,34 +661,31 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.provideLocationCheckbox.isEnabled = isGooglePlayAvailable(requireContext()) binding.provideLocationCheckbox.isEnabled = isGooglePlayAvailable(requireContext())
binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked -> 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) { if (!myActivity.hasLocationPermission()) // Make sure we have location permission (prerequisite)
debug("User changed location tracking to $isChecked") myActivity.requestLocationPermission()
if (view.isPressed) { // We want to ignore changes caused by code (as opposed to the user) else if (!myActivity.hasBackgroundPermission())
// Don't check the box until the system setting changes MaterialAlertDialogBuilder(requireContext())
view.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission() .setTitle(R.string.background_required)
.setMessage(R.string.why_background_required)
if (!myActivity.hasLocationPermission()) // Make sure we have location permission (prerequisite) .setNeutralButton(R.string.cancel) { _, _ ->
myActivity.requestLocationPermission() debug("User denied background permission")
else if (!myActivity.hasBackgroundPermission()) }
MaterialAlertDialogBuilder(requireContext()) .setPositiveButton(getString(R.string.accept)) { _, _ ->
.setTitle(R.string.background_required) myActivity.requestBackgroundPermission()
.setMessage(R.string.why_background_required) }
.setNeutralButton(R.string.cancel) { _, _ -> .show()
debug("User denied background permission") if (view.isChecked) {
} debug("User changed location tracking to $isChecked")
.setPositiveButton(getString(R.string.accept)) { _, _ -> model.provideLocation.value = isChecked
myActivity.requestBackgroundPermission() checkLocationEnabled(getString(R.string.location_disabled))
} model.meshService?.setupProvideLocation()
.show()
if (view.isChecked) {
checkLocationEnabled(getString(R.string.location_disabled))
model.meshService?.setupProvideLocation()
}
} }
} else { } else {
model.provideLocation.value = isChecked
model.meshService?.stopProvideLocation() model.meshService?.stopProvideLocation()
} }
} }
@ -730,8 +733,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.isChecked = b.isChecked =
scanModel.onSelected(myActivity, device) scanModel.onSelected(myActivity, device)
if (!b.isSelected) if (!b.isSelected) {
binding.scanStatusText.text = getString(R.string.please_pair) binding.scanStatusText.text = getString(R.string.please_pair)
}
} }
} }
@ -757,7 +761,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// and before use // and before use
val bleAddr = scanModel.selectedBluetooth val bleAddr = scanModel.selectedBluetooth
if (bleAddr != null && adapter != null && adapter.isEnabled) { if (bleAddr != null && adapter != null) {
val bDevice = val bDevice =
adapter.getRemoteDevice(bleAddr) adapter.getRemoteDevice(bleAddr)
if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared 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() { private fun initClassicScan() {
binding.changeRadioButton.setOnClickListener { binding.changeRadioButton.setOnClickListener {
if (myActivity.warnMissingPermissions()) { debug("User clicked changeRadioButton")
myActivity.requestPermission() if (!myActivity.hasScanPermission()) {
} else scanLeDevice() myActivity.requestScanPermission()
} else {
checkLocationEnabled()
scanLeDevice()
}
} }
} }
@ -828,7 +836,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initModernScan() { private fun initModernScan() {
binding.changeRadioButton.setOnClickListener { 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()
}
} }
} }