From a9c8502eb863fba522652031693c3e66e31eee6b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Jul 2026 15:01:44 +0200 Subject: [PATCH 1/4] catch security exception Signed-off-by: alperozturk96 --- .../thirdparties/sufficientlysecure/AndroidCalendar.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java b/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java index c2af23591899..0891bd86ca5d 100644 --- a/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java +++ b/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java @@ -112,9 +112,14 @@ private static String getString(Cursor cur, String dbName) { } private static boolean missing(ContentResolver resolver, Uri uri) { - // Determine if a provider is missing + // Determine if a provider is missing or inaccessible. + // acquireContentProviderClient throws SecurityException when the calendar + // permission has not been granted, so treat that as missing too. try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { return provider == null; + } catch (SecurityException e) { + Log_OC.w(TAG, "Calendar provider is not accessible: " + e.getMessage()); + return true; } } From f2ab75b5386006b06afcce0acfa17a5606326e4b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Jul 2026 15:01:53 +0200 Subject: [PATCH 2/4] expose reloadCalendars method Signed-off-by: alperozturk96 --- .../android/ui/fragment/contactsbackup/BackupListAdapter.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt index 8e2d87edc295..a40acc31921b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt @@ -126,6 +126,12 @@ class BackupListAdapter( notifyItemInserted(calendarFiles.lastIndex) } + @SuppressLint("NotifyDataSetChanged") + fun reloadCalendars() { + cachedAndroidCalendars = AndroidCalendar.loadAll(context.contentResolver) + notifyDataSetChanged() + } + @SuppressLint("NotifyDataSetChanged") fun replaceVcards(vCards: List) { contacts.clear() From 77d8123c146cb176b6f1e41e23e69287a01a9b4a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 10 Jul 2026 15:02:03 +0200 Subject: [PATCH 3/4] handle permissions Signed-off-by: alperozturk96 --- .../contactsbackup/BackupListFragment.java | 100 +++++++++++------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java index 20148300aa88..d5d86af88d7c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java @@ -58,6 +58,8 @@ import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.LinearLayoutManager; @@ -218,6 +220,10 @@ public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup conta } } + if (listAdapter.hasCalendarEntry()) { + checkAndAskForCalendarReadPermission(); + } + binding.restoreSelected.setOnClickListener(v -> { if (checkAndAskForCalendarWritePermission()) { importCalendar(); @@ -408,58 +414,72 @@ private void closeFragment() { } } + // region permission check result listener + private final ActivityResultLauncher contactsWritePermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + importContacts(selectedAccount); + } else { + showPermissionErrorMessage(); + } + }); + + private final ActivityResultLauncher calendarReadPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + listAdapter.reloadCalendars(); + } + }); + + private final ActivityResultLauncher calendarWritePermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + importCalendar(); + } else { + showPermissionErrorMessage(); + } + }); + // endregion + + // region permission checks private boolean checkAndAskForContactsWritePermission() { - // check permissions - if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) { - requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS}, - PermissionUtil.PERMISSIONS_WRITE_CONTACTS); + final var context = getContext(); + if (context == null) { return false; - } else { - return true; } - } - private boolean checkAndAskForCalendarWritePermission() { - // check permissions - if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CALENDAR)) { - requestPermissions(new String[]{Manifest.permission.WRITE_CALENDAR}, - PermissionUtil.PERMISSIONS_WRITE_CALENDAR); + if (!PermissionUtil.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS)) { + contactsWritePermissionLauncher.launch(Manifest.permission.WRITE_CONTACTS); return false; - } else { - return true; } + + return true; } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionUtil.PERMISSIONS_WRITE_CONTACTS) { - for (int index = 0; index < permissions.length; index++) { - if (Manifest.permission.WRITE_CONTACTS.equalsIgnoreCase(permissions[index])) { - if (grantResults[index] >= 0) { - importContacts(selectedAccount); - } else { - showPermissionErrorMessage(); - } - break; - } - } + private void checkAndAskForCalendarReadPermission() { + final var context = getContext(); + if (context == null) { + return; } - if (requestCode == PermissionUtil.PERMISSIONS_WRITE_CALENDAR) { - for (int index = 0; index < permissions.length; index++) { - if (Manifest.permission.WRITE_CALENDAR.equalsIgnoreCase(permissions[index])) { - if (grantResults[index] >= 0) { - importContacts(selectedAccount); - } else { - showPermissionErrorMessage(); - } - break; - } - } + if (!PermissionUtil.checkSelfPermission(context, Manifest.permission.READ_CALENDAR)) { + calendarReadPermissionLauncher.launch(Manifest.permission.READ_CALENDAR); + } + } + + private boolean checkAndAskForCalendarWritePermission() { + final var context = getContext(); + if (context == null) { + return false; + } + + if (!PermissionUtil.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR)) { + calendarWritePermissionLauncher.launch(Manifest.permission.WRITE_CALENDAR); + return false; } + return true; } + // endregion private void showPermissionErrorMessage() { DisplayUtils.showSnackMessage(this, R.string.contactlist_no_permission); From d3e10c09684e6e2fffeadd9a8a94c57a7c9f429c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 22 Jul 2026 08:36:01 +0200 Subject: [PATCH 4/4] check permissions before load calendar Signed-off-by: alperozturk96 --- .../fragment/contactsbackup/BackupListAdapter.kt | 16 ++++++++++++++-- .../contactsbackup/BackupListFragment.java | 6 +----- .../sufficientlysecure/AndroidCalendar.java | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt index a40acc31921b..431c6fd83abc 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt @@ -8,6 +8,7 @@ */ package com.owncloud.android.ui.fragment.contactsbackup +import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.content.res.Resources @@ -36,6 +37,7 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.TextDrawable import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment.getDisplayName import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.theme.ViewThemeUtils import ezvcard.VCard import ezvcard.property.Photo @@ -68,7 +70,17 @@ class BackupListAdapter( init { shouldShowHeadersForEmptySections(false) shouldShowFooters(false) - cachedAndroidCalendars = AndroidCalendar.loadAll(context.contentResolver) + cachedAndroidCalendars = loadCalendarsIfPermitted() + } + + private fun loadCalendarsIfPermitted(): List { + if (!PermissionUtil.checkSelfPermission(context, Manifest.permission.READ_CALENDAR)) { + Log_OC.d(BackupListFragment.TAG, "READ_CALENDAR permission is not granted, requesting it") + backupListFragment.checkAndAskForCalendarReadPermission() + return emptyList() + } + + return AndroidCalendar.loadAll(context) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder = when (viewType) { @@ -128,7 +140,7 @@ class BackupListAdapter( @SuppressLint("NotifyDataSetChanged") fun reloadCalendars() { - cachedAndroidCalendars = AndroidCalendar.loadAll(context.contentResolver) + cachedAndroidCalendars = loadCalendarsIfPermitted() notifyDataSetChanged() } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java index d5d86af88d7c..fd1b2ee25297 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java @@ -220,10 +220,6 @@ public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup conta } } - if (listAdapter.hasCalendarEntry()) { - checkAndAskForCalendarReadPermission(); - } - binding.restoreSelected.setOnClickListener(v -> { if (checkAndAskForCalendarWritePermission()) { importCalendar(); @@ -456,7 +452,7 @@ private boolean checkAndAskForContactsWritePermission() { return true; } - private void checkAndAskForCalendarReadPermission() { + public void checkAndAskForCalendarReadPermission() { final var context = getContext(); if (context == null) { return; diff --git a/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java b/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java index 0891bd86ca5d..afa6e973802a 100644 --- a/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java +++ b/app/src/main/java/thirdparties/sufficientlysecure/AndroidCalendar.java @@ -7,8 +7,11 @@ */ package thirdparties.sufficientlysecure; +import android.Manifest; import android.content.ContentProviderClient; import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.provider.CalendarContract.Calendars; @@ -19,6 +22,8 @@ import java.util.ArrayList; import java.util.List; +import androidx.core.content.ContextCompat; + public class AndroidCalendar { private static final String TAG = "ICS_AndroidCalendar"; @@ -48,7 +53,16 @@ public class AndroidCalendar { private static final String[] CAL_ID_COLS = new String[]{Events._ID}; private static final String CAL_ID_WHERE = Events.CALENDAR_ID + "=?"; - // Load all available calendars. + public static List loadAll(Context context) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + Log_OC.w(TAG, "Cannot load calendars, READ_CALENDAR permission is not granted"); + return new ArrayList<>(); + } + + return loadAll(context.getContentResolver()); + } + // If an empty list is returned the caller probably needs to enable calendar // read permissions in App Ops/XPrivacy etc. public static List loadAll(ContentResolver resolver) {