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