/* 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.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.widget.Toast; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; public class PermissionsUtils { private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class); public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener"; public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service"; public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over"; public static final List specialPermissions = new ArrayList() {{ add(CUSTOM_PERM_NOTIFICATION_LISTENER); add(CUSTOM_PERM_NOTIFICATION_SERVICE); add(CUSTOM_PERM_DISPLAY_OVER); add(Manifest.permission.ACCESS_FINE_LOCATION); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { add(Manifest.permission.ACCESS_BACKGROUND_LOCATION); } }}; public static ArrayList getRequiredPermissionsList(Activity activity) { ArrayList permissionsList = new ArrayList<>(); permissionsList.add(new PermissionDetails( CUSTOM_PERM_NOTIFICATION_LISTENER, activity.getString(R.string.menuitem_notifications), activity.getString(R.string.permission_notifications_summary))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { permissionsList.add(new PermissionDetails( CUSTOM_PERM_NOTIFICATION_SERVICE, activity.getString(R.string.permission_manage_dnd_title), activity.getString(R.string.permission_manage_dnd_summary))); permissionsList.add(new PermissionDetails( CUSTOM_PERM_DISPLAY_OVER, activity.getString(R.string.permission_displayover_title), activity.getString(R.string.permission_displayover_summary))); } permissionsList.add(new PermissionDetails( Manifest.permission.ACCESS_FINE_LOCATION, activity.getString(R.string.permission_fine_location_title), activity.getString(R.string.permission_fine_location_summary))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { permissionsList.add(new PermissionDetails( Manifest.permission.ACCESS_BACKGROUND_LOCATION, activity.getString(R.string.permission_background_location_title), activity.getString(R.string.permission_background_location_summary))); } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { permissionsList.add(new PermissionDetails( Manifest.permission.BLUETOOTH, activity.getString(R.string.permission_bluetooth_title), activity.getString(R.string.permission_bluetooth_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.BLUETOOTH_ADMIN, activity.getString(R.string.permission_bluetooth_admin_title), activity.getString(R.string.permission_bluetooth_admin_summary))); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissionsList.add(new PermissionDetails( Manifest.permission.BLUETOOTH_SCAN, activity.getString(R.string.permission_bluetooth_scan_title), activity.getString(R.string.permission_bluetooth_scan_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.BLUETOOTH_CONNECT, activity.getString(R.string.permission_bluetooth_connect_title), activity.getString(R.string.permission_bluetooth_connect_summary))); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { permissionsList.add(new PermissionDetails( Manifest.permission.POST_NOTIFICATIONS, activity.getString(R.string.permission_post_notification_title), activity.getString(R.string.permission_post_notification_summary))); } if (BuildConfig.INTERNET_ACCESS) { permissionsList.add(new PermissionDetails( Manifest.permission.INTERNET, activity.getString(R.string.permission_internet_access_title), activity.getString(R.string.permission_internet_access_summary))); } // permissionsList.add(new PermissionDetails( // NOTE: can't request this, it's only allowed for system apps // Manifest.permission.MEDIA_CONTENT_CONTROL, // "Media content control", // "Read and control media playback")); permissionsList.add(new PermissionDetails( Manifest.permission.READ_CONTACTS, activity.getString(R.string.permission_contacts_title), activity.getString(R.string.permission_contacts_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.READ_CALENDAR, activity.getString(R.string.permission_calendar_title), activity.getString(R.string.permission_calendar_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.RECEIVE_SMS, activity.getString(R.string.permission_receive_sms_title), activity.getString(R.string.permission_receive_sms_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.SEND_SMS, activity.getString(R.string.permission_send_sms_title), activity.getString(R.string.permission_send_sms_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.READ_CALL_LOG, activity.getString(R.string.permission_read_call_log_title), activity.getString(R.string.permission_read_call_log_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.READ_PHONE_STATE, activity.getString(R.string.permission_read_phone_state_title), activity.getString(R.string.permission_read_phone_state_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.CALL_PHONE, activity.getString(R.string.permission_call_phone_title), activity.getString(R.string.permission_call_phone_summary))); permissionsList.add(new PermissionDetails( Manifest.permission.PROCESS_OUTGOING_CALLS, activity.getString(R.string.permission_process_outgoing_calls_title), activity.getString(R.string.permission_process_outgoing_calls_summary))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { permissionsList.add(new PermissionDetails( Manifest.permission.ANSWER_PHONE_CALLS, activity.getString(R.string.permission_answer_phone_calls_title), activity.getString(R.string.permission_answer_phone_calls_summary))); } if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { permissionsList.add(new PermissionDetails( Manifest.permission.READ_EXTERNAL_STORAGE, activity.getString(R.string.permission_external_storage_title), activity.getString(R.string.permission_external_storage_summary))); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { permissionsList.add(new PermissionDetails( Manifest.permission.QUERY_ALL_PACKAGES, activity.getString(R.string.permission_query_all_packages_title), activity.getString(R.string.permission_query_all_packages_summary))); } 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(Activity activity) { boolean result = true; for (PermissionDetails permission : getRequiredPermissionsList(activity)) { if (!checkPermission(activity, permission.getPermission())) { result = false; } } return result; } public static void requestPermission(Activity activity, String permission) { if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) { showNotifyListenerPermissionsDialog(activity); } else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) { showNotifyPolicyPermissionsDialog(activity); } else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) { showDisplayOverOthersPermissionsDialog(activity); } else if (permission.equals(Manifest.permission.ACCESS_BACKGROUND_LOCATION) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)) { showBackgroundLocationPermissionsDialog(activity); } 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; } } private static void showNotifyListenerPermissionsDialog(Activity activity) { new MaterialAlertDialogBuilder(activity) .setMessage(activity.getString(R.string.permission_notification_listener, activity.getString(R.string.app_name), activity.getString(R.string.ok))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) public void onClick(DialogInterface dialog, int id) { try { Intent intent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS); intent.putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME, new ComponentName(BuildConfig.APPLICATION_ID, NotificationListener.class.getName()).flattenToString()); } else { intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS); } String showArgs = BuildConfig.APPLICATION_ID + "/" + NotificationListener.class.getName(); intent.putExtra(":settings:fragment_args_key", showArgs); Bundle bundle = new Bundle(); bundle.putString(":settings:fragment_args_key", showArgs); intent.putExtra(":settings:show_fragment_args", bundle); activity.startActivity(intent); } catch (ActivityNotFoundException e) { GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR); LOG.error("'Notification Listener Settings' activity not found"); } } }) .show(); } private static void showNotifyPolicyPermissionsDialog(Activity activity) { new MaterialAlertDialogBuilder(activity) .setMessage(activity.getString(R.string.permission_notification_policy_access, activity.getString(R.string.app_name), activity.getString(R.string.ok))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.M) public void onClick(DialogInterface dialog, int id) { 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"); } } }) .show(); } private static void showDisplayOverOthersPermissionsDialog(Activity activity) { new MaterialAlertDialogBuilder(activity) .setMessage(activity.getString(R.string.permission_display_over_other_apps, activity.getString(R.string.app_name), activity.getString(R.string.ok))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @RequiresApi(api = Build.VERSION_CODES.M) public void onClick(DialogInterface dialog, int id) { Intent enableIntent = new Intent( android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + BuildConfig.APPLICATION_ID) ); activity.startActivity(enableIntent); } }) .setNegativeButton(R.string.dismiss, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }) .show(); } @RequiresApi(api = Build.VERSION_CODES.R) private static void showBackgroundLocationPermissionsDialog(Activity activity) { new MaterialAlertDialogBuilder(activity) .setMessage(activity.getString(R.string.permission_location, activity.getString(R.string.app_name), activity.getPackageManager().getBackgroundPermissionOptionLabel())) .setPositiveButton(R.string.ok, (dialog, id) -> { ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0); }) .show(); } }