From 1b281c7c62bd19b11c2b368cf966f46daf30574a Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 26 Jun 2023 12:00:05 +0100 Subject: [PATCH] Fixing SecurityErrors on Android 12+ discovered via Play Store (after API31 update)... * Check for bluetooth permissions in DiscoveryActivity * At startup we now pop up a dialog explaining why we want *any* permissions * Fixing ControlCenterv2 permissions requests for Android S and later (requesting background location stopped *any* dialog appearing) * Fixing all errors in DiscoveryActivity from Android Studio by catching errors * Move permission requests around to ensure that we only call RequestMultiplePermissions from onCreate * Only show dialog if we have permissions to request * Fix "LifecycleOwners must call register before they are STARTED" on some Android devices: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3192/files#issuecomment-967267 --- .../activities/ControlCenterv2.java | 114 +++++++++---- .../activities/DiscoveryActivity.java | 153 +++++++++++++++--- app/src/main/res/values/strings.xml | 3 + 3 files changed, 220 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java index be89f5b18..68c5855c4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java @@ -17,6 +17,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast; + import android.Manifest; import android.annotation.TargetApi; import android.app.AlertDialog; @@ -61,6 +63,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.navigation.NavigationView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; @@ -94,9 +99,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class ControlCenterv2 extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, GBActivity { + private static final Logger LOG = LoggerFactory.getLogger(ControlCenterv2.class); public static final int MENU_REFRESH_CODE = 1; public static final String ACTION_REQUEST_PERMISSIONS = "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestpermissions"; + public static final String ACTION_REQUEST_LOCATION_PERMISSIONS + = "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestlocationpermissions"; private static PhoneStateListener fakeStateListener; //needed for KK compatibility @@ -133,8 +141,12 @@ public class ControlCenterv2 extends AppCompatActivity handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE)); break; case ACTION_REQUEST_PERMISSIONS: - checkAndRequestPermissions(false); + checkAndRequestPermissions(); break; + case ACTION_REQUEST_LOCATION_PERMISSIONS: + checkAndRequestLocationPermissions(); + break; + } } }; @@ -255,6 +267,7 @@ public class ControlCenterv2 extends AppCompatActivity filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED); filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES); filterLocal.addAction(ACTION_REQUEST_PERMISSIONS); + filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); refreshPairedDevices(); @@ -265,6 +278,10 @@ public class ControlCenterv2 extends AppCompatActivity Prefs prefs = GBApplication.getPrefs(); pesterWithPermissions = prefs.getBoolean("permission_pestering", true); + boolean displayPermissionDialog = !prefs.getBoolean("permission_dialog_displayed", false); + prefs.getPreferences().edit().putBoolean("permission_dialog_displayed", true).apply(); + + Set set = NotificationManagerCompat.getEnabledListenerPackages(this); if (pesterWithPermissions) { if (!set.contains(this.getPackageName())) { // If notification listener access hasn't been granted @@ -275,6 +292,13 @@ public class ControlCenterv2 extends AppCompatActivity } } + /* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy). + + Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first + on the page, and as they are accepted the permissions are requested in turn. + + When accepted, we request it or open the Activity for permission to display over other apps. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { /* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver the permission to access notifications is needed above Android M @@ -298,12 +322,23 @@ public class ControlCenterv2 extends AppCompatActivity } } - - // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy) - // When accepted, we open the Activity for permission to display over other apps. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) { + if (pesterWithPermissions) { + DialogFragment dialog = new LocationPermissionsDialogFragment(); + dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment"); + } + } // Check all the other permissions that we need to for Android M + later - checkAndRequestPermissions(true); + if (getWantedPermissions().isEmpty()) + displayPermissionDialog = false; + if (displayPermissionDialog && pesterWithPermissions) { + DialogFragment dialog = new PermissionsDialogFragment(); + dialog.show(getSupportFragmentManager(), "PermissionsDialogFragment"); + // when 'ok' clicked, checkAndRequestPermissions() is called + } else + checkAndRequestPermissions(); } ChangeLog cl = createChangeLog(); @@ -439,8 +474,16 @@ public class ControlCenterv2 extends AppCompatActivity } } + private void checkAndRequestLocationPermissions() { + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) { + LOG.error("No permission to access background location!"); + toast(ControlCenterv2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR); + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0); + } + } + @TargetApi(Build.VERSION_CODES.M) - private void checkAndRequestPermissions(boolean showDialogFirst) { + private List getWantedPermissions() { List wantedPermissions = new ArrayList<>(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) @@ -486,12 +529,6 @@ public class ControlCenterv2 extends AppCompatActivity } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { - if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) { - wantedPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION); - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.QUERY_ALL_PACKAGES) == PackageManager.PERMISSION_DENIED) { wantedPermissions.add(Manifest.permission.QUERY_ALL_PACKAGES); @@ -513,6 +550,13 @@ public class ControlCenterv2 extends AppCompatActivity } } + return wantedPermissions; + } + + @TargetApi(Build.VERSION_CODES.M) + private void checkAndRequestPermissions() { + List wantedPermissions = getWantedPermissions(); + if (!wantedPermissions.isEmpty()) { Prefs prefs = GBApplication.getPrefs(); // If this is not the first run, we can rely on @@ -528,25 +572,17 @@ public class ControlCenterv2 extends AppCompatActivity } } wantedPermissions.removeAll(shouldNotAsk); - } else if (!showDialogFirst) { + } else { // Permissions have not been asked yet, but now will be prefs.getPreferences().edit().putBoolean("permissions_asked", true).apply(); } if (!wantedPermissions.isEmpty()) { - if (showDialogFirst) { - // Show a dialog - this will then call checkAndRequestPermissions(false) - DialogFragment dialog = new LocationPermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment"); - //requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0])); + GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0); } else { - GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0); - } else { - requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0])); - //ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0); //Actually this still works if I test it, not sure if the new way is more reliable or not... - } + requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0])); } } } @@ -674,10 +710,8 @@ public class ControlCenterv2 extends AppCompatActivity } - /// Called from checkAndRequestPermissions - this puts up a dialog explaining we need permissions, and then calls checkAndRequestPermissions (via an intent) when 'ok' pressed + /// Called from onCreate - this puts up a dialog explaining we need backgound location permissions, and then requests permissions when 'ok' pressed public static class LocationPermissionsDialogFragment extends DialogFragment { - ControlCenterv2 controlCenter; - @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the Builder class for convenient dialog construction @@ -688,7 +722,7 @@ public class ControlCenterv2 extends AppCompatActivity getContext().getString(R.string.ok))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { - Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS); + Intent intent = new Intent(ACTION_REQUEST_LOCATION_PERMISSIONS); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } }); @@ -699,6 +733,8 @@ public class ControlCenterv2 extends AppCompatActivity // Register the permissions callback, which handles the user's response to the // system permissions dialog. Save the return value, an instance of // ActivityResultLauncher, as an instance variable. + // This is required here rather than where it is used because it'll cause a + // "LifecycleOwners must call register before they are STARTED" if not called from onCreate public ActivityResultLauncher requestMultiplePermissionsLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> { if (isGranted.containsValue(true)) { @@ -713,4 +749,24 @@ public class ControlCenterv2 extends AppCompatActivity GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR); } }); + + /// Called from onCreate - this puts up a dialog explaining we need permissions, and then requests permissions when 'ok' pressed + public static class PermissionsDialogFragment extends DialogFragment { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + Context context = getContext(); + builder.setMessage(context.getString(R.string.permission_request, + getContext().getString(R.string.app_name), + getContext().getString(R.string.ok))) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + }); + return builder.create(); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index 15e9e9b1b..5b0b3d872 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -58,6 +58,8 @@ import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; @@ -212,7 +214,16 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView uuids = serviceUuids.toArray(new ParcelUuid[0]); } } - LOG.warn(result.getDevice().getName() + ": " + + String name = "[unknown]"; + try { + name = result.getDevice().getName(); + } catch (SecurityException e) { + /* This should never happen because we need all the permissions + to get to the point where we can even scan, but 'SecurityException' check + is added to stop Android Studio errors */ + LOG.error("SecurityException on device.getName()"); + } + LOG.warn(name + ": " + ((scanRecord != null) ? scanRecord.getBytes().length : -1)); addToCandidateListIfNotAlreadyProcessed(result.getDevice(), (short) result.getRssi(), uuids); @@ -268,7 +279,11 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView checkAndRequestLocationPermission(); - startDiscovery(); + if (!startDiscovery()) { + /* if we couldn't start scanning, go back to the main page. + A toast will have been shown explaining what's wrong */ + finish(); + } } public void onStartButtonClick(View button) { @@ -328,7 +343,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView super.onResume(); } - private void handleDeviceFound(BluetoothDevice device, short rssi) { + private void handleDeviceFound(BluetoothDevice device, short rssi) throws SecurityException { if (device.getName() != null) { if (handleDeviceFound(device, rssi, null)) { LOG.info("found supported device " + device.getName() + " without scanning services, skipping service scan."); @@ -358,7 +373,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } } - private boolean handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) { + private boolean handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) throws SecurityException { LOG.debug("found device: " + device.getName() + ", " + device.getAddress()); if (LOG.isDebugEnabled()) { if (uuids != null && uuids.length > 0) { @@ -368,7 +383,16 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } } - if (device.getBondState() == BluetoothDevice.BOND_BONDED && ignoreBonded) { + boolean bonded = false; + try { + bonded = device.getBondState() == BluetoothDevice.BOND_BONDED; + } catch (SecurityException e) { + /* This should never happen because we need all the permissions + to get to the point where we can even scan, but 'SecurityException' check + is added to stop Android Studio errors */ + LOG.error("SecurityException on device.getBondState"); + } + if (bonded && ignoreBonded) { return true; // Ignore already bonded devices } @@ -389,10 +413,10 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView return false; } - private void startDiscovery() { + private boolean startDiscovery() { if (isScanning()) { LOG.warn("Not starting discovery, because already scanning."); - return; + return false; } LOG.info("Starting discovery"); @@ -407,7 +431,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView setScanning(true); } else { toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR); + return false; } + return true; } private void stopDiscovery() { @@ -441,7 +467,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView // Filters being non-null would be a very good idea with background scan, but in this case, // not really required. - adapter.getBluetoothLeScanner().startScan(null, getScanSettings(), getScanCallback()); + try { + adapter.getBluetoothLeScanner().startScan(null, getScanSettings(), getScanCallback()); + } catch (SecurityException e) { + /* This should never happen because we call this from startDiscovery, + which checks ensureBluetoothReady. But we add try...catch to stop Android Studio errors */ + LOG.error("SecurityException on startScan"); + } LOG.debug("Bluetooth LE discovery started successfully"); bluetoothLEProgress.setVisibility(View.VISIBLE); @@ -467,6 +499,10 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } catch (NullPointerException e) { LOG.warn("Internal NullPointerException when stopping the scan!"); return; + } catch (SecurityException e) { + /* This should never happen because ensureBluetoothReady should set adaptor=null, + but we add try...catch to stop Android Studio errors */ + LOG.error("SecurityException on adapter.stopScan"); } LOG.debug("Stopped BLE discovery"); @@ -485,19 +521,30 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } handler.removeMessages(0, stopRunnable); handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION); - if (adapter.startDiscovery()) { - LOG.debug("Discovery started successfully"); - bluetoothProgress.setVisibility(View.VISIBLE); - } else { - LOG.error("Discovery starting failed"); + try { + if (adapter.startDiscovery()) { + LOG.debug("Discovery started successfully"); + bluetoothProgress.setVisibility(View.VISIBLE); + } else { + LOG.error("Discovery starting failed"); + } + } catch (SecurityException e) { + /* This should never happen because we call this from startDiscovery, + which checks ensureBluetoothReady. But we add try...catch to stop Android Studio errors */ + LOG.error("BluetoothAdaptor.startDiscovery failed with SecurityException"); } } private void stopBTDiscovery() { - if (adapter != null) { + if (adapter == null) return; + try { adapter.cancelDiscovery(); - LOG.info("Stopped BT discovery"); + } catch (SecurityException e) { + /* This should never happen because ensureBluetoothReady should set adaptor=null, + but we add try...catch to stop Android Studio errors */ + LOG.error("BluetoothAdaptor.cancelDiscovery failed with SecurityException"); } + LOG.info("Stopped BT discovery"); } private void bluetoothStateChanged(int newState) { @@ -513,6 +560,18 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } private boolean checkBluetoothAvailable() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { + LOG.warn("No BLUETOOTH_SCAN permission"); + this.adapter = null; + return false; + } + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + LOG.warn("No BLUETOOTH_CONNECT permission"); + this.adapter = null; + return false; + } + } BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); if (bluetoothService == null) { LOG.warn("No bluetooth service available"); @@ -528,7 +587,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView if (!adapter.isEnabled()) { LOG.warn("Bluetooth not enabled"); Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivity(enableBtIntent); + try { + startActivity(enableBtIntent); + } catch (SecurityException e) { + /* This should never happen because we did checkSelfPermission above. + But we add try...catch to stop Android Studio errors */ + LOG.warn("startActivity(enableBtIntent) failed with SecurityException"); + } this.adapter = null; return false; } @@ -542,7 +607,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView boolean available = checkBluetoothAvailable(); startButton.setEnabled(available); if (available) { - adapter.cancelDiscovery(); + try { + adapter.cancelDiscovery(); + } catch (SecurityException e) { + /* This should never happen because checkBluetoothAvailable should return false + if we don't have permissions. But we add try...catch to stop Android Studio errors */ + LOG.error("SecurityException on adapter.cancelDiscovery, but checkBluetoothAvailable()=true!"); + } // must not return the result of cancelDiscovery() // appears to return false when currently not scanning return true; @@ -588,16 +659,25 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } private void checkAndRequestLocationPermission() { + /* This is more or less a copy of what's in ControlCenterv2, but + we do this in case the permissions weren't requested since there + is no way we can scan without this stuff */ + List wantedPermissions = new ArrayList<>(); if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { LOG.error("No permission to access coarse location!"); - toast(DiscoveryActivity.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR); - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0); + wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); } if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { LOG.error("No permission to access fine location!"); - toast(DiscoveryActivity.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR); - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 0); + wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); } + // if we need location permissions, request both together to avoid a bunch of dialogs + if (wantedPermissions.size() > 0) { + toast(DiscoveryActivity.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR); + ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0); + wantedPermissions.clear(); + } + // Now we have to request background location separately! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) { LOG.error("No permission to access background location!"); @@ -605,6 +685,37 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0); } } + // Now, we can request Bluetooth permissions.... + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { + LOG.error("No permission to access Bluetooth scanning!"); + toast(DiscoveryActivity.this, getString(R.string.error_no_bluetooth_scan), Toast.LENGTH_SHORT, GB.ERROR); + wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN); + } + if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + LOG.error("No permission to access Bluetooth connection!"); + toast(DiscoveryActivity.this, getString(R.string.error_no_bluetooth_connect), Toast.LENGTH_SHORT, GB.ERROR); + wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT); + } + } + if (wantedPermissions.size() > 0) { + GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0); + } else { + ActivityResultLauncher requestMultiplePermissionsLauncher = + registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> { + if (!isGranted.containsValue(false)) { + // Permission is granted. Continue the action or workflow in your app. + // should we do startDiscovery here?? + } else { + // Explain to the user that the feature is unavailable because the feature requires a permission that the user has denied. + GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR); + } + }); + requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0])); + } + } LocationManager locationManager = (LocationManager) DiscoveryActivity.this.getSystemService(Context.LOCATION_SERVICE); try { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f6ab2a66..3284b3a02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1517,6 +1517,8 @@ Detailed button press settings Long press button action Location access must be granted and enabled for scanning to work properly + Bluetooth scan access must be granted and enabled for scanning to work properly + Bluetooth connection access must be granted and enabled for scanning to work properly Draw widget circles Save raw activity files Last notification @@ -1536,6 +1538,7 @@ João Paulo Barraca (HPlus)\nVitaly Svyastyn (NO.1 F1)\nSami Alaoui (Teclast H30)\n“ladbsoft” (XWatch)\nSebastian Kranz (ZeTime)\nVadim Kaushan (ID115)\n“maxirnilian” (Lenovo Watch 9)\n“ksiwczynski”, “mkusnierz”, “mamutcho” (Lenovo Watch X Plus)\nAndreas Böhler (Casio GB-6900B, Casio GB-5600B, Casio GBX-100)\nJean-François Greffier (Mi Scale 2)\nJohannes Schmitt (BFH-16)\nLukas Schwichtenberg (Makibes HR3)\nDaniel Dakhno (Fossil Q Hybrid, Fossil Hybrid HR)\nGordon Williams (Bangle.js)\nPavel Elagin (JYou Y5)\nTaavi Eomäe (iTag)\nJohannes Krude(Casio GW-B5600) Many thanks to all unlisted contributors for contributing code, translations, support, ideas, motivation, bug reports, money… ✊ Links + %1$s allows you to send messages and other data from Android to your device. To do this it requires permission to access that data and without it, it might not function properly.\n\nYou\'ll now be presented with a few Android dialogs requesting those permissions.\n\nPlease tap \'%2$s\' to continue. All these permissions are required and instability might occur if not granted %1$s needs access to Notifications in order to display them on your watch when your phone\'s screen is off.\n\nPlease tap \'%2$s\' then \'%1$s\' and enable \'Allow Notification Access\', then tap \'Back\' to return to %1$s. %1$s needs access to Do Not Disturb settings in order to honour them on your watch when your phone\'s screen is off.\n\nPlease tap \'%2$s\' then \'%1$s\' and enable \'Allow Do Not Disturb\', then tap \'Back\' to return to %1$s.