/* Copyright (C) 2024 Arjan Schrijver This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.util; import android.Manifest; import android.app.Activity; import android.app.NotificationManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.provider.Settings; import android.widget.Toast; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.R; public class PermissionsUtils { private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class); private static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener"; private static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service"; private static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over"; public static ArrayList getRequiredPermissionsList(Context context) { ArrayList permissionsList = new ArrayList<>(); permissionsList.add(new PermissionDetails(CUSTOM_PERM_NOTIFICATION_LISTENER, context.getString(R.string.menuitem_notifications), "Forwarding notifications to connected gadgets")); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { permissionsList.add(new PermissionDetails(CUSTOM_PERM_NOTIFICATION_SERVICE, "Manage Do Not Disturb", "Change DND notification policy")); permissionsList.add(new PermissionDetails(CUSTOM_PERM_DISPLAY_OVER, "Display over other apps", "Used by Bangle.js to start apps and other functionality on your phone")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { permissionsList.add(new PermissionDetails(Manifest.permission.ACCESS_BACKGROUND_LOCATION, "Background location", "Required for scanning for Bluetooth devices")); } permissionsList.add(new PermissionDetails(Manifest.permission.ACCESS_FINE_LOCATION, "Fine location", "Send location to gadgets which don't have GPS")); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { permissionsList.add(new PermissionDetails(Manifest.permission.BLUETOOTH, "Bluetooth", "Connect to Bluetooth devices")); permissionsList.add(new PermissionDetails(Manifest.permission.BLUETOOTH_ADMIN, "Bluetooth admin", "Discover and pair Bluetooth devices")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissionsList.add(new PermissionDetails(Manifest.permission.BLUETOOTH_SCAN, "Bluetooth scan", "Scan for Bluetooth devices")); permissionsList.add(new PermissionDetails(Manifest.permission.BLUETOOTH_CONNECT, "Bluetooth connect", "Connect to Bluetooth devices")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { permissionsList.add(new PermissionDetails(Manifest.permission.POST_NOTIFICATIONS, "Post notifications", "Post ongoing notification which keeps the service running")); } if (BuildConfig.INTERNET_ACCESS) { permissionsList.add(new PermissionDetails(Manifest.permission.INTERNET, "Internet access", "Synchronization with online resources")); } permissionsList.add(new PermissionDetails(Manifest.permission.MEDIA_CONTENT_CONTROL, "Media content control", "Read and control media playback")); permissionsList.add(new PermissionDetails(Manifest.permission.READ_CONTACTS, "Contacts", "Send contacts to gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.READ_CALENDAR, "Calendar", "Send calendar to gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.RECEIVE_SMS, "Receive SMS", "Forward SMS messages to gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.SEND_SMS, "Send SMS", "Send SMS (canned response) from gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.READ_CALL_LOG, "Read call log", "Forward call log to gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.READ_PHONE_STATE, "Read phone state", "Read status of ongoing calls")); permissionsList.add(new PermissionDetails(Manifest.permission.CALL_PHONE, "Call phone", "Initiate phone calls from gadgets")); permissionsList.add(new PermissionDetails(Manifest.permission.PROCESS_OUTGOING_CALLS, "Process outgoing calls", "Read the number of an outgoing call to display it on a gadget")); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { permissionsList.add(new PermissionDetails(Manifest.permission.ANSWER_PHONE_CALLS, "Answer phone calls", "Answer phone calls from gadgets")); } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { permissionsList.add(new PermissionDetails(Manifest.permission.READ_EXTERNAL_STORAGE, "External storage", "Using images, ringtones, app files and more")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { permissionsList.add(new PermissionDetails(Manifest.permission.QUERY_ALL_PACKAGES, "Query all packages", "Read names and icons of all installed apps")); } return permissionsList; } public static boolean checkPermission(Context context, String permission) { if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) { Set set = NotificationManagerCompat.getEnabledListenerPackages(context); return set.contains(context.getPackageName()); } else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted(); } else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context); } else { return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED; } } public static boolean checkAllPermissions(Context context) { boolean result = true; for (PermissionDetails permission : getRequiredPermissionsList(context)) { if (!checkPermission(context, permission.getPermission())) { result = false; } } return result; } public static void requestAllPermissions(Activity activity) { } public static void requestPermission(Activity activity, String permission) { if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) { try { activity.startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")); } catch (ActivityNotFoundException e) { GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR); } } else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE)) { try { activity.startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)); } catch (ActivityNotFoundException e) { GB.toast(activity, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR); LOG.error("'Notification Policy' activity not found"); } } else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) { activity.startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION)); } else { ActivityCompat.requestPermissions(activity, new String[]{permission}, 0); } } public static class PermissionDetails { private String permission; private String title; private String summary; public PermissionDetails(String permission, String title, String summary) { this.permission = permission; this.title = title; this.summary = summary; } public String getPermission() { return permission; } public String getTitle() { return title; } public String getSummary() { return summary; } } // @TargetApi(Build.VERSION_CODES.M) // private List getWantedPermissions() { // List wantedPermissions = new ArrayList<>(); // // if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.BLUETOOTH); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_CONTACTS); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.CALL_PHONE); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_CALL_LOG); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_PHONE_STATE); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.RECEIVE_SMS); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_SMS); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.SEND_SMS); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.READ_CALENDAR); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); // // try { // if (ContextCompat.checkSelfPermission(this, Manifest.permission.MEDIA_CONTENT_CONTROL) == PackageManager.PERMISSION_DENIED) // wantedPermissions.add(Manifest.permission.MEDIA_CONTENT_CONTROL); // } catch (Exception ignored) { // } // // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // if (pesterWithPermissions) { // if (ContextCompat.checkSelfPermission(this, Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.ANSWER_PHONE_CALLS); // } // } // } // // 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 (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.ACCESS_FINE_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); // } // } // // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN); // } // if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT); // } // } // // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.POST_NOTIFICATIONS); // } // } // // if (BuildConfig.INTERNET_ACCESS) { // if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.INTERNET) == PackageManager.PERMISSION_DENIED) { // wantedPermissions.add(Manifest.permission.INTERNET); // } // } // // 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 // // shouldShowRequestPermissionRationale(String permission) // // and ignore permissions that shouldn't or can't be requested again // if (prefs.getBoolean("permissions_asked", false)) { // // Don't request permissions that we shouldn't show a prompt for // // e.g. permissions that are "Never" granted by the user or never granted by the system // Set shouldNotAsk = new HashSet<>(); // for (String wantedPermission : wantedPermissions) { // if (!shouldShowRequestPermissionRationale(wantedPermission)) { // shouldNotAsk.add(wantedPermission); // } // } // wantedPermissions.removeAll(shouldNotAsk); // } else { // // Permissions have not been asked yet, but now will be // prefs.getPreferences().edit().putBoolean("permissions_asked", true).apply(); // } // // if (!wantedPermissions.isEmpty()) { // 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])); // } // } // } // // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // The enclosed hack in it's current state cause crash on Banglejs builds tarkgetSDK=31 on a Android 13 device. // // HACK: On Lineage we have to do this so that the permission dialog pops up // if (fakeStateListener == null) { // fakeStateListener = new PhoneStateListener(); // TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); // telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_CALL_STATE); // telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_NONE); // } // } // } /// 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 { // @Override // public Dialog onCreateDialog(Bundle savedInstanceState) { // // Use the Builder class for convenient dialog construction // MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()); // Context context = getContext(); // builder.setMessage(context.getString(R.string.permission_location, // 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_LOCATION_PERMISSIONS); // LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); // } // }); // return builder.create(); // } // } // 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)) { // // Permission is granted. Continue the action or workflow in your // // app. // } else { // // Explain to the user that the feature is unavailable because the // // feature requires a permission that the user has denied. At the // // same time, respect the user's decision. Don't link to system // // settings in an effort to convince the user to change their // // decision. // 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 // MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(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(); // } // } }