Browse Source

Merge master

Signed-off-by: alperozturk <alper_ozturk@proton.me>
alperozturk 1 năm trước cách đây
mục cha
commit
e202ce33a9

BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png


+ 0 - 691
app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.java

@@ -1,691 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * @author TSI-mc
- * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2017 Nextcloud GmbH.
- * Copyright (C) 2023 TSI-mc
- *
- * This program 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.
- *
- * This program 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 <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.ui.fragment.contactsbackup;
-
-import android.Manifest;
-import android.app.DatePickerDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CompoundButton;
-import android.widget.DatePicker;
-import android.widget.Toast;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.jobs.BackgroundJobManager;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.BackupFragmentBinding;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.operations.RefreshFolderOperation;
-import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
-import com.owncloud.android.ui.activity.SettingsActivity;
-import com.owncloud.android.ui.fragment.FileFragment;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.MimeTypeUtil;
-import com.owncloud.android.utils.PermissionUtil;
-import com.owncloud.android.utils.theme.ThemeUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.fragment.app.Fragment;
-import third_parties.daveKoeller.AlphanumComparator;
-
-import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP;
-import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP;
-
-public class BackupFragment extends FileFragment implements DatePickerDialog.OnDateSetListener, Injectable {
-    public static final String TAG = BackupFragment.class.getSimpleName();
-    private static final String ARG_SHOW_SIDEBAR = "SHOW_SIDEBAR";
-    private static final String KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN";
-    private static final String KEY_CALENDAR_DAY = "CALENDAR_DAY";
-    private static final String KEY_CALENDAR_MONTH = "CALENDAR_MONTH";
-    private static final String KEY_CALENDAR_YEAR = "CALENDAR_YEAR";
-
-    public static final String PREFERENCE_CONTACTS_BACKUP_ENABLED = "PREFERENCE_CONTACTS_BACKUP_ENABLED";
-    public static final String PREFERENCE_CALENDAR_BACKUP_ENABLED = "PREFERENCE_CALENDAR_BACKUP_ENABLED";
-
-
-    private BackupFragmentBinding binding;
-
-    @Inject BackgroundJobManager backgroundJobManager;
-    @Inject ThemeUtils themeUtils;
-
-    @Inject ArbitraryDataProvider arbitraryDataProvider;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    private Date selectedDate;
-    private boolean calendarPickerOpen;
-
-    private DatePickerDialog datePickerDialog;
-
-    private CompoundButton.OnCheckedChangeListener dailyBackupCheckedChangeListener;
-    private CompoundButton.OnCheckedChangeListener contactsCheckedListener;
-    private CompoundButton.OnCheckedChangeListener calendarCheckedListener;
-    private User user;
-    private boolean showSidebar = true;
-    //flag to check if calendar backup should be shown and backup should be done or not
-    private boolean showCalendarBackup = true;
-    public static BackupFragment create(boolean showSidebar) {
-        BackupFragment fragment = new BackupFragment();
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(ARG_SHOW_SIDEBAR, showSidebar);
-        fragment.setArguments(bundle);
-        return fragment;
-    }
-
-    private boolean isCalendarBackupEnabled() {
-        return arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CALENDAR_BACKUP_ENABLED);
-    }
-
-    private void setCalendarBackupEnabled(final boolean enabled) {
-        arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREFERENCE_CALENDAR_BACKUP_ENABLED, enabled);
-    }
-
-    private boolean isContactsBackupEnabled() {
-        return arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CONTACTS_BACKUP_ENABLED);
-    }
-
-    private void setContactsBackupEnabled(final boolean enabled) {
-        arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREFERENCE_CONTACTS_BACKUP_ENABLED, enabled);
-    }
-
-    @Override
-    public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
-        // use grey as fallback for elements where custom theming is not available
-        if (themeUtils.themingEnabled(getContext())) {
-            getContext().getTheme().applyStyle(R.style.FallbackThemingTheme, true);
-        }
-
-        binding = BackupFragmentBinding.inflate(inflater, container, false);
-        View view = binding.getRoot();
-
-        setHasOptionsMenu(true);
-
-        if (getArguments() != null) {
-            showSidebar = getArguments().getBoolean(ARG_SHOW_SIDEBAR);
-        }
-
-        showCalendarBackup = requireContext().getResources().getBoolean(R.bool.show_calendar_backup);
-
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-        user = contactsPreferenceActivity.getUser().orElseThrow(RuntimeException::new);
-
-        ActionBar actionBar = contactsPreferenceActivity != null ? contactsPreferenceActivity.getSupportActionBar() : null;
-
-        if (actionBar != null) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
-            viewThemeUtils.files.themeActionBar(requireContext(), actionBar,
-                                                showCalendarBackup ? R.string.backup_title : R.string.contact_backup_title);
-        }
-
-
-        viewThemeUtils.androidx.colorSwitchCompat(binding.contacts);
-        viewThemeUtils.androidx.colorSwitchCompat(binding.calendar);
-        viewThemeUtils.androidx.colorSwitchCompat(binding.dailyBackup);
-        binding.dailyBackup.setChecked(arbitraryDataProvider.getBooleanValue(user,
-                                                                             PREFERENCE_CONTACTS_AUTOMATIC_BACKUP));
-
-        binding.contacts.setChecked(isContactsBackupEnabled() && checkContactBackupPermission());
-        binding.calendar.setChecked(isCalendarBackupEnabled() && checkCalendarBackupPermission(getContext()));
-
-        binding.calendar.setVisibility(showCalendarBackup ? View.VISIBLE : View.GONE);
-
-        setupCheckListeners();
-
-        setBackupNowButtonVisibility();
-
-        binding.backupNow.setOnClickListener(v -> backupNow());
-
-        binding.contactsDatepicker.setOnClickListener(v -> openCleanDate());
-
-        // display last backup
-        Long lastBackupTimestamp = arbitraryDataProvider.getLongValue(user, PREFERENCE_CONTACTS_LAST_BACKUP);
-
-        if (lastBackupTimestamp == -1) {
-            binding.lastBackupWithDate.setVisibility(View.GONE);
-        } else {
-            binding.lastBackupWithDate.setText(
-                String.format(getString(R.string.last_backup),
-                              DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp)));
-        }
-
-        if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) {
-            if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 &&
-                savedInstanceState.getInt(KEY_CALENDAR_MONTH, -1) != -1 &&
-                savedInstanceState.getInt(KEY_CALENDAR_DAY, -1) != -1) {
-                selectedDate = new Date(savedInstanceState.getInt(KEY_CALENDAR_YEAR),
-                                        savedInstanceState.getInt(KEY_CALENDAR_MONTH), savedInstanceState.getInt(KEY_CALENDAR_DAY));
-            }
-            calendarPickerOpen = true;
-        }
-
-        viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.backupNow);
-        viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.contactsDatepicker);
-
-        viewThemeUtils.platform.colorTextView(binding.dataToBackUpTitle);
-        viewThemeUtils.platform.colorTextView(binding.backupSettingsTitle);
-
-        return view;
-    }
-
-    private void setupCheckListeners() {
-        dailyBackupCheckedChangeListener = (buttonView, isChecked) -> {
-            if (checkAndAskForContactsReadPermission()) {
-                setAutomaticBackup(isChecked);
-            }
-        };
-        binding.dailyBackup.setOnCheckedChangeListener(dailyBackupCheckedChangeListener);
-
-
-        contactsCheckedListener = (buttonView, isChecked) -> {
-            if (isChecked) {
-                if (checkAndAskForContactsReadPermission()) {
-                    setContactsBackupEnabled(true);
-                }
-            } else {
-                setContactsBackupEnabled(false);
-            }
-            setBackupNowButtonVisibility();
-            setAutomaticBackup(binding.dailyBackup.isChecked());
-        };
-        binding.contacts.setOnCheckedChangeListener(contactsCheckedListener);
-
-        calendarCheckedListener = (buttonView, isChecked) -> {
-            if (isChecked) {
-                if (checkAndAskForCalendarReadPermission()) {
-                    setCalendarBackupEnabled(true);
-                }
-            } else {
-                setCalendarBackupEnabled(false);
-            }
-            setBackupNowButtonVisibility();
-            setAutomaticBackup(binding.dailyBackup.isChecked());
-        };
-        binding.calendar.setOnCheckedChangeListener(calendarCheckedListener);
-    }
-
-    private void setBackupNowButtonVisibility() {
-        if (binding.contacts.isChecked() || binding.calendar.isChecked()) {
-            binding.backupNow.setVisibility(View.VISIBLE);
-        } else {
-            binding.backupNow.setVisibility(View.INVISIBLE);
-        }
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        if (calendarPickerOpen) {
-            if (selectedDate != null) {
-                openDate(selectedDate);
-            } else {
-                openDate(null);
-            }
-        }
-
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-        if (contactsPreferenceActivity != null) {
-            String backupFolderPath = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
-            refreshBackupFolder(backupFolderPath,
-                                contactsPreferenceActivity.getApplicationContext(),
-                                contactsPreferenceActivity.getStorageManager());
-        }
-    }
-
-    private void refreshBackupFolder(final String backupFolderPath,
-                                     final Context context,
-                                     final FileDataStorageManager storageManager) {
-        AsyncTask<String, Integer, Boolean> task = new AsyncTask<String, Integer, Boolean>() {
-            @Override
-            protected Boolean doInBackground(String... path) {
-                OCFile folder = storageManager.getFileByPath(path[0]);
-
-                if (folder != null) {
-                    RefreshFolderOperation operation = new RefreshFolderOperation(folder, System.currentTimeMillis(),
-                                                                                  false, false, storageManager, user, context);
-
-                    RemoteOperationResult result = operation.execute(user, context);
-                    return result.isSuccess();
-                } else {
-                    return Boolean.FALSE;
-                }
-            }
-
-            @Override
-            protected void onPostExecute(Boolean result) {
-                if (result && binding != null) {
-                    OCFile backupFolder = storageManager.getFileByPath(backupFolderPath);
-
-                    List<OCFile> backupFiles = storageManager
-                        .getFolderContent(backupFolder, false);
-
-                    Collections.sort(backupFiles, new AlphanumComparator<>());
-
-                    if (backupFiles == null || backupFiles.isEmpty()) {
-                        binding.contactsDatepicker.setVisibility(View.INVISIBLE);
-                    } else {
-                        binding.contactsDatepicker.setVisibility(View.VISIBLE);
-                    }
-                }
-            }
-        };
-
-        task.execute(backupFolderPath);
-    }
-
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
-        boolean retval;
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                if (showSidebar) {
-                    if (contactsPreferenceActivity.isDrawerOpen()) {
-                        contactsPreferenceActivity.closeDrawer();
-                    } else {
-                        contactsPreferenceActivity.openDrawer();
-                    }
-                } else if (getActivity() != null) {
-                    getActivity().finish();
-                } else {
-                    Intent settingsIntent = new Intent(getContext(), SettingsActivity.class);
-                    settingsIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-                    startActivity(settingsIntent);
-                }
-                retval = true;
-                break;
-
-            default:
-                retval = super.onOptionsItemSelected(item);
-                break;
-        }
-        return retval;
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-
-        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) {
-            for (int index = 0; index < permissions.length; index++) {
-                if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) {
-                    if (grantResults[index] >= 0) {
-                        // if approved, exit for loop
-                        setContactsBackupEnabled(true);
-                        break;
-                    }
-
-                    // if not accepted, disable again
-                    binding.contacts.setOnCheckedChangeListener(null);
-                    binding.contacts.setChecked(false);
-                    binding.contacts.setOnCheckedChangeListener(contactsCheckedListener);
-                }
-            }
-        }
-
-        if (requestCode == PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC) {
-            boolean readGranted = false;
-            boolean writeGranted = false;
-            for (int index = 0; index < permissions.length; index++) {
-                if (Manifest.permission.WRITE_CALENDAR.equalsIgnoreCase(permissions[index]) && grantResults[index] >= 0) {
-                    writeGranted = true;
-                } else if (Manifest.permission.READ_CALENDAR.equalsIgnoreCase(permissions[index]) && grantResults[index] >= 0) {
-                    readGranted = true;
-                }
-            }
-            if (!readGranted || !writeGranted) {
-                // if not accepted, disable again
-                binding.calendar.setOnCheckedChangeListener(null);
-                binding.calendar.setChecked(false);
-                binding.calendar.setOnCheckedChangeListener(calendarCheckedListener);
-            } else {
-                setCalendarBackupEnabled(true);
-            }
-        }
-
-        setBackupNowButtonVisibility();
-        setAutomaticBackup(binding.dailyBackup.isChecked());
-    }
-
-    public void backupNow() {
-        if (isContactsBackupEnabled() && checkContactBackupPermission()) {
-            startContactsBackupJob();
-        }
-
-        if (showCalendarBackup && isCalendarBackupEnabled() && checkCalendarBackupPermission(requireContext())) {
-            startCalendarBackupJob();
-        }
-
-        DisplayUtils.showSnackMessage(requireView().findViewById(R.id.contacts_linear_layout),
-                                      R.string.contacts_preferences_backup_scheduled);
-    }
-
-    private void startContactsBackupJob() {
-        ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity();
-        if (activity != null) {
-            Optional<User> optionalUser = activity.getUser();
-            if (optionalUser.isPresent()) {
-                backgroundJobManager.startImmediateContactsBackup(optionalUser.get());
-            }
-        }
-    }
-
-    private void startCalendarBackupJob() {
-        ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity();
-        if (activity != null) {
-            Optional<User> optionalUser = activity.getUser();
-            if (optionalUser.isPresent()) {
-                backgroundJobManager.startImmediateCalendarBackup(optionalUser.get());
-            }
-        }
-    }
-
-    private void setAutomaticBackup(final boolean enabled) {
-
-        final ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity();
-        if (activity == null) {
-            return;
-        }
-        Optional<User> optionalUser = activity.getUser();
-        if (!optionalUser.isPresent()) {
-            return;
-        }
-        User user = optionalUser.get();
-        if (enabled) {
-            if (isContactsBackupEnabled()) {
-                Log_OC.d(TAG, "Scheduling contacts backup job");
-                backgroundJobManager.schedulePeriodicContactsBackup(user);
-            } else {
-                Log_OC.d(TAG, "Cancelling contacts backup job");
-                backgroundJobManager.cancelPeriodicContactsBackup(user);
-            }
-            if (isCalendarBackupEnabled()) {
-                Log_OC.d(TAG, "Scheduling calendar backup job");
-                backgroundJobManager.schedulePeriodicCalendarBackup(user);
-            } else {
-                Log_OC.d(TAG, "Cancelling calendar backup job");
-                backgroundJobManager.cancelPeriodicCalendarBackup(user);
-            }
-        } else {
-            Log_OC.d(TAG, "Cancelling all backup jobs");
-            backgroundJobManager.cancelPeriodicContactsBackup(user);
-            backgroundJobManager.cancelPeriodicCalendarBackup(user);
-        }
-
-        arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                    PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
-                                                    String.valueOf(enabled));
-    }
-
-    private boolean checkAndAskForContactsReadPermission() {
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
-        // check permissions
-        if (PermissionUtil.checkSelfPermission(contactsPreferenceActivity, Manifest.permission.READ_CONTACTS)) {
-            return true;
-        } else {
-            // No explanation needed, request the permission.
-            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},
-                               PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC);
-            return false;
-        }
-    }
-
-    private boolean checkAndAskForCalendarReadPermission() {
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
-        // check permissions
-        if (checkCalendarBackupPermission(contactsPreferenceActivity)) {
-            return true;
-        } else {
-            // No explanation needed, request the permission.
-            requestPermissions(new String[]{Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR},
-                               PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC);
-            return false;
-        }
-    }
-
-    private boolean checkCalendarBackupPermission(final Context context) {
-        return PermissionUtil.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) && PermissionUtil.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR);
-    }
-
-    private boolean checkContactBackupPermission() {
-        return PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.READ_CONTACTS);
-    }
-
-    public void openCleanDate() {
-        if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) {
-            openDate(null);
-        }
-    }
-
-    public void openDate(@Nullable Date savedDate) {
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
-        if (contactsPreferenceActivity == null) {
-            Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        String contactsBackupFolderString =
-            getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
-        String calendarBackupFolderString =
-            getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR;
-
-        FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager();
-
-        OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString);
-        OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString);
-
-        List<OCFile> backupFiles = storageManager.getFolderContent(contactsBackupFolder, false);
-        backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false));
-
-        Collections.sort(backupFiles, (o1, o2) -> {
-            return Long.compare(o1.getModificationTimestamp(), o2.getModificationTimestamp());
-        });
-
-        Calendar cal = Calendar.getInstance();
-        int year;
-        int month;
-        int day;
-
-        if (savedDate == null) {
-            year = cal.get(Calendar.YEAR);
-            month = cal.get(Calendar.MONTH) + 1;
-            day = cal.get(Calendar.DAY_OF_MONTH);
-        } else {
-            year = savedDate.getYear();
-            month = savedDate.getMonth();
-            day = savedDate.getDay();
-        }
-
-        if (backupFiles.size() > 0 && backupFiles.get(backupFiles.size() - 1) != null) {
-            datePickerDialog = new DatePickerDialog(contactsPreferenceActivity, this, year, month, day);
-            datePickerDialog.getDatePicker().setMaxDate(backupFiles.get(backupFiles.size() - 1)
-                                                            .getModificationTimestamp());
-            datePickerDialog.getDatePicker().setMinDate(backupFiles.get(0).getModificationTimestamp());
-
-            datePickerDialog.setOnDismissListener(dialog -> selectedDate = null);
-
-            datePickerDialog.setTitle("");
-            datePickerDialog.show();
-
-            viewThemeUtils.platform.colorTextButtons(datePickerDialog.getButton(DatePickerDialog.BUTTON_NEGATIVE),
-                                                     datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE));
-
-            // set background to transparent
-            datePickerDialog.getButton(DatePickerDialog.BUTTON_NEGATIVE).setBackgroundColor(0x00000000);
-            datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE).setBackgroundColor(0x00000000);
-        } else {
-            DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout),
-                                          R.string.contacts_preferences_something_strange_happened);
-        }
-    }
-
-    @Override
-    public void onDestroyView() {
-        binding = null;
-        super.onDestroyView();
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        if (datePickerDialog != null) {
-            datePickerDialog.dismiss();
-        }
-    }
-
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        super.onSaveInstanceState(outState);
-        if (datePickerDialog != null) {
-            outState.putBoolean(KEY_CALENDAR_PICKER_OPEN, datePickerDialog.isShowing());
-
-            if (datePickerDialog.isShowing()) {
-                outState.putInt(KEY_CALENDAR_DAY, datePickerDialog.getDatePicker().getDayOfMonth());
-                outState.putInt(KEY_CALENDAR_MONTH, datePickerDialog.getDatePicker().getMonth());
-                outState.putInt(KEY_CALENDAR_YEAR, datePickerDialog.getDatePicker().getYear());
-            }
-        }
-    }
-
-    @Override
-    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
-        final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
-        if (contactsPreferenceActivity == null) {
-            Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        selectedDate = new Date(year, month, dayOfMonth);
-
-        String contactsBackupFolderString =
-            getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR;
-        String calendarBackupFolderString =
-            getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR;
-
-        FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager();
-
-        OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString);
-        OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString);
-
-        List<OCFile> backupFiles = storageManager.getFolderContent(contactsBackupFolder, false);
-        backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false));
-
-        // find file with modification with date and time between 00:00 and 23:59
-        // if more than one file exists, take oldest
-        Calendar date = Calendar.getInstance();
-        date.set(year, month, dayOfMonth);
-
-        // start
-        date.set(Calendar.HOUR, 0);
-        date.set(Calendar.MINUTE, 0);
-        date.set(Calendar.SECOND, 1);
-        date.set(Calendar.MILLISECOND, 0);
-        date.set(Calendar.AM_PM, Calendar.AM);
-        long start = date.getTimeInMillis();
-
-        // end
-        date.set(Calendar.HOUR, 23);
-        date.set(Calendar.MINUTE, 59);
-        date.set(Calendar.SECOND, 59);
-        long end = date.getTimeInMillis();
-
-        OCFile contactsBackupToRestore = null;
-        List<OCFile> calendarBackupsToRestore = new ArrayList<>();
-
-        for (OCFile file : backupFiles) {
-            if (start < file.getModificationTimestamp() && end > file.getModificationTimestamp()) {
-                // contact
-                if (MimeTypeUtil.isVCard(file)) {
-                    if (contactsBackupToRestore == null) {
-                        contactsBackupToRestore = file;
-                    } else if (contactsBackupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) {
-                        contactsBackupToRestore = file;
-                    }
-                }
-
-                // calendars
-                if (showCalendarBackup && MimeTypeUtil.isCalendar(file)) {
-                    calendarBackupsToRestore.add(file);
-                }
-            }
-        }
-
-        List<OCFile> backupToRestore = new ArrayList<>();
-
-        if (contactsBackupToRestore != null) {
-            backupToRestore.add(contactsBackupToRestore);
-        }
-
-        backupToRestore.addAll(calendarBackupsToRestore);
-
-
-        if (backupToRestore.isEmpty()) {
-            DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout),
-                                          R.string.contacts_preferences_no_file_found);
-        } else {
-            final User user = contactsPreferenceActivity.getUser().orElseThrow(RuntimeException::new);
-            OCFile[] files = new OCFile[backupToRestore.size()];
-            Fragment contactListFragment = BackupListFragment.newInstance(backupToRestore.toArray(files), user);
-
-            contactsPreferenceActivity.getSupportFragmentManager().
-                beginTransaction()
-                .replace(R.id.frame_container, contactListFragment, BackupListFragment.TAG)
-                .addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST)
-                .commit();
-        }
-    }
-}

+ 726 - 0
app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt

@@ -0,0 +1,726 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author TSI-mc
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud GmbH.
+ * Copyright (C) 2023 TSI-mc
+ *
+ * This program 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.
+ *
+ * This program 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 <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.fragment.contactsbackup
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.DatePickerDialog
+import android.app.DatePickerDialog.OnDateSetListener
+import android.content.Context
+import android.content.Intent
+import android.os.AsyncTask
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CompoundButton
+import android.widget.DatePicker
+import android.widget.Toast
+import com.nextcloud.client.account.User
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.owncloud.android.R
+import com.owncloud.android.databinding.BackupFragmentBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.RefreshFolderOperation
+import com.owncloud.android.ui.activity.ContactsPreferenceActivity
+import com.owncloud.android.ui.activity.SettingsActivity
+import com.owncloud.android.ui.fragment.FileFragment
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.PermissionUtil
+import com.owncloud.android.utils.PermissionUtil.checkSelfPermission
+import com.owncloud.android.utils.theme.ThemeUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import third_parties.daveKoeller.AlphanumComparator
+import java.util.Calendar
+import java.util.Collections
+import java.util.Date
+import javax.inject.Inject
+
+@Suppress("TooManyFunctions")
+class BackupFragment : FileFragment(), OnDateSetListener, Injectable {
+    private lateinit var binding: BackupFragmentBinding
+
+    @JvmField
+    @Inject
+    var backgroundJobManager: BackgroundJobManager? = null
+
+    @JvmField
+    @Inject
+    var themeUtils: ThemeUtils? = null
+
+    @JvmField
+    @Inject
+    var arbitraryDataProvider: ArbitraryDataProvider? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var selectedDate: Date? = null
+    private var calendarPickerOpen = false
+    private var datePickerDialog: DatePickerDialog? = null
+    private var contactsCheckedListener: CompoundButton.OnCheckedChangeListener? = null
+    private var calendarCheckedListener: CompoundButton.OnCheckedChangeListener? = null
+    private var user: User? = null
+    private var showSidebar = true
+
+    // flag to check if calendar backup should be shown and backup should be done or not
+    private var showCalendarBackup = true
+    private var isCalendarBackupEnabled: Boolean
+        get() = user?.let { arbitraryDataProvider?.getBooleanValue(it, PREFERENCE_CALENDAR_BACKUP_ENABLED) } ?: false
+        private set(enabled) {
+            arbitraryDataProvider!!.storeOrUpdateKeyValue(
+                user!!.accountName,
+                PREFERENCE_CALENDAR_BACKUP_ENABLED,
+                enabled
+            )
+        }
+
+    private var isContactsBackupEnabled: Boolean
+        get() = user?.let { arbitraryDataProvider?.getBooleanValue(it, PREFERENCE_CONTACTS_BACKUP_ENABLED) } ?: false
+        private set(enabled) {
+            arbitraryDataProvider!!.storeOrUpdateKeyValue(
+                user!!.accountName,
+                PREFERENCE_CONTACTS_BACKUP_ENABLED,
+                enabled
+            )
+        }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        // use grey as fallback for elements where custom theming is not available
+        if (themeUtils?.themingEnabled(context) == true) {
+            requireContext().theme.applyStyle(R.style.FallbackThemingTheme, true)
+        }
+
+        binding = BackupFragmentBinding.inflate(inflater, container, false)
+        val view: View = binding.root
+
+        setHasOptionsMenu(true)
+
+        if (arguments != null) {
+            showSidebar = requireArguments().getBoolean(ARG_SHOW_SIDEBAR)
+        }
+
+        showCalendarBackup = requireContext().resources.getBoolean(R.bool.show_calendar_backup)
+
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+        user = contactsPreferenceActivity?.user?.orElseThrow { RuntimeException() }
+
+        setupSwitches(user)
+
+        setupCheckListeners()
+        setBackupNowButtonVisibility()
+
+        setOnClickListeners()
+
+        contactsPreferenceActivity?.let {
+            displayLastBackup(it)
+            applyUserColorToActionBar(it)
+        }
+
+        setupDates(savedInstanceState)
+        applyUserColor()
+
+        return view
+    }
+
+    private fun setupSwitches(user: User?) {
+        user?.let {
+            binding.dailyBackup.isChecked = arbitraryDataProvider?.getBooleanValue(
+                it,
+                ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP
+            ) ?: false
+        }
+
+        binding.contacts.isChecked = isContactsBackupEnabled && checkContactBackupPermission()
+        binding.calendar.isChecked = isCalendarBackupEnabled && checkCalendarBackupPermission(requireContext())
+        binding.calendar.visibility = if (showCalendarBackup) View.VISIBLE else View.GONE
+    }
+
+    private fun setupCheckListeners() {
+        binding.dailyBackup.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+            if (checkAndAskForContactsReadPermission()) {
+                setAutomaticBackup(isChecked)
+            }
+        }
+
+        initContactsCheckedListener()
+        binding.contacts.setOnCheckedChangeListener(contactsCheckedListener)
+
+        initCalendarCheckedListener()
+        binding.calendar.setOnCheckedChangeListener(calendarCheckedListener)
+    }
+
+    private fun initContactsCheckedListener() {
+        contactsCheckedListener =
+            CompoundButton.OnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+                if (isChecked) {
+                    if (checkAndAskForContactsReadPermission()) {
+                        isContactsBackupEnabled = true
+                    }
+                } else {
+                    isContactsBackupEnabled = false
+                }
+                setBackupNowButtonVisibility()
+                setAutomaticBackup(binding.dailyBackup.isChecked)
+            }
+    }
+
+    private fun initCalendarCheckedListener() {
+        calendarCheckedListener =
+            CompoundButton.OnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+                if (isChecked) {
+                    if (checkAndAskForCalendarReadPermission()) {
+                        isCalendarBackupEnabled = true
+                    }
+                } else {
+                    isCalendarBackupEnabled = false
+                }
+                setBackupNowButtonVisibility()
+                setAutomaticBackup(binding.dailyBackup.isChecked)
+            }
+    }
+
+    private fun setBackupNowButtonVisibility() {
+        binding.backupNow.visibility =
+            if (binding.contacts.isChecked || binding.calendar.isChecked) View.VISIBLE else View.INVISIBLE
+    }
+
+    private fun setOnClickListeners() {
+        binding.backupNow.setOnClickListener { backupNow() }
+        binding.contactsDatepicker.setOnClickListener { openCleanDate() }
+    }
+
+    private fun displayLastBackup(contactsPreferenceActivity: ContactsPreferenceActivity) {
+        val lastBackupTimestamp = user?.let {
+            arbitraryDataProvider?.getLongValue(
+                it,
+                ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP
+            )
+        } ?: -1L
+
+        if (lastBackupTimestamp == -1L) {
+            binding.lastBackupWithDate.visibility = View.GONE
+        } else {
+            binding.lastBackupWithDate.text = String.format(
+                getString(R.string.last_backup),
+                DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp)
+            )
+        }
+    }
+
+    private fun applyUserColorToActionBar(contactsPreferenceActivity: ContactsPreferenceActivity) {
+        val actionBar = contactsPreferenceActivity.supportActionBar
+
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true)
+            viewThemeUtils?.files?.themeActionBar(
+                requireContext(),
+                actionBar,
+                if (showCalendarBackup) R.string.backup_title else R.string.contact_backup_title
+            )
+        }
+    }
+
+    private fun setupDates(savedInstanceState: Bundle?) {
+        if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) {
+            if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 && savedInstanceState.getInt(
+                    KEY_CALENDAR_MONTH,
+                    -1
+                ) != -1 && savedInstanceState.getInt(
+                    KEY_CALENDAR_DAY, -1
+                ) != -1
+            ) {
+                val cal = Calendar.getInstance()
+                cal[Calendar.YEAR] = savedInstanceState.getInt(KEY_CALENDAR_YEAR)
+                cal[Calendar.MONTH] = savedInstanceState.getInt(KEY_CALENDAR_MONTH)
+                cal[Calendar.DAY_OF_MONTH] = savedInstanceState.getInt(KEY_CALENDAR_DAY)
+                selectedDate = cal.time
+            }
+            calendarPickerOpen = true
+        }
+    }
+
+    private fun applyUserColor() {
+        viewThemeUtils?.androidx?.colorSwitchCompat(binding.contacts)
+        viewThemeUtils?.androidx?.colorSwitchCompat(binding.calendar)
+        viewThemeUtils?.androidx?.colorSwitchCompat(binding.dailyBackup)
+
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.backupNow)
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryOutlined(binding.contactsDatepicker)
+
+        viewThemeUtils?.platform?.colorTextView(binding.dataToBackUpTitle)
+        viewThemeUtils?.platform?.colorTextView(binding.backupSettingsTitle)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        if (calendarPickerOpen) {
+            if (selectedDate != null) {
+                openDate(selectedDate)
+            } else {
+                openDate(null)
+            }
+        }
+
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+        if (contactsPreferenceActivity != null) {
+            val backupFolderPath = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR
+            refreshBackupFolder(
+                backupFolderPath,
+                contactsPreferenceActivity.applicationContext,
+                contactsPreferenceActivity.storageManager
+            )
+        }
+    }
+
+    private fun refreshBackupFolder(
+        backupFolderPath: String,
+        context: Context,
+        storageManager: FileDataStorageManager
+    ) {
+        val task: AsyncTask<String, Int, Boolean> =
+            @SuppressLint("StaticFieldLeak")
+            object : AsyncTask<String, Int, Boolean>() {
+                @Deprecated("Deprecated in Java")
+                override fun doInBackground(vararg path: String): Boolean {
+                    val folder = storageManager.getFileByPath(path[0])
+                    return if (folder != null) {
+                        val operation = RefreshFolderOperation(
+                            folder,
+                            System.currentTimeMillis(),
+                            false,
+                            false,
+                            storageManager,
+                            user,
+                            context
+                        )
+                        val result = operation.execute(user, context)
+                        result.isSuccess
+                    } else {
+                        java.lang.Boolean.FALSE
+                    }
+                }
+
+                @Deprecated("Deprecated in Java")
+                override fun onPostExecute(result: Boolean) {
+                    if (result) {
+                        val backupFolder = storageManager.getFileByPath(backupFolderPath)
+                        val backupFiles = storageManager
+                            .getFolderContent(backupFolder, false)
+                        Collections.sort(backupFiles, AlphanumComparator())
+                        if (backupFiles == null || backupFiles.isEmpty()) {
+                            binding.contactsDatepicker.visibility = View.INVISIBLE
+                        } else {
+                            binding.contactsDatepicker.visibility = View.VISIBLE
+                        }
+                    }
+                }
+            }
+
+        task.execute(backupFolderPath)
+    }
+
+    @Deprecated("Deprecated in Java")
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+
+        val retval: Boolean
+        when (item.itemId) {
+            android.R.id.home -> {
+                if (showSidebar) {
+                    if (contactsPreferenceActivity!!.isDrawerOpen) {
+                        contactsPreferenceActivity.closeDrawer()
+                    } else {
+                        contactsPreferenceActivity.openDrawer()
+                    }
+                } else if (activity != null) {
+                    requireActivity().finish()
+                } else {
+                    val settingsIntent = Intent(context, SettingsActivity::class.java)
+                    settingsIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
+                    startActivity(settingsIntent)
+                }
+                retval = true
+            }
+
+            else -> retval = super.onOptionsItemSelected(item)
+        }
+        return retval
+    }
+
+    @Deprecated("Deprecated in Java")
+    @Suppress("NestedBlockDepth")
+    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+        if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) {
+            for (index in permissions.indices) {
+                if (Manifest.permission.READ_CONTACTS.equals(permissions[index], ignoreCase = true)) {
+                    if (grantResults[index] >= 0) {
+                        // if approved, exit for loop
+                        isContactsBackupEnabled = true
+                        break
+                    }
+
+                    // if not accepted, disable again
+                    binding.contacts.setOnCheckedChangeListener(null)
+                    binding.contacts.isChecked = false
+                    binding.contacts.setOnCheckedChangeListener(contactsCheckedListener)
+                }
+            }
+        }
+        if (requestCode == PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC) {
+            var readGranted = false
+            var writeGranted = false
+            for (index in permissions.indices) {
+                if (Manifest.permission.WRITE_CALENDAR.equals(
+                        permissions[index],
+                        ignoreCase = true
+                    ) && grantResults[index] >= 0
+                ) {
+                    writeGranted = true
+                } else if (Manifest.permission.READ_CALENDAR.equals(
+                        permissions[index],
+                        ignoreCase = true
+                    ) && grantResults[index] >= 0
+                ) {
+                    readGranted = true
+                }
+            }
+            if (!readGranted || !writeGranted) {
+                // if not accepted, disable again
+                binding.calendar.setOnCheckedChangeListener(null)
+                binding.calendar.isChecked = false
+                binding.calendar.setOnCheckedChangeListener(calendarCheckedListener)
+            } else {
+                isCalendarBackupEnabled = true
+            }
+        }
+        setBackupNowButtonVisibility()
+        setAutomaticBackup(binding.dailyBackup.isChecked)
+    }
+
+    private fun backupNow() {
+        if (isContactsBackupEnabled && checkContactBackupPermission()) {
+            startContactsBackupJob()
+        }
+        if (showCalendarBackup && isCalendarBackupEnabled && checkCalendarBackupPermission(requireContext())) {
+            startCalendarBackupJob()
+        }
+        DisplayUtils.showSnackMessage(
+            requireView().findViewById<View>(R.id.contacts_linear_layout),
+            R.string.contacts_preferences_backup_scheduled
+        )
+    }
+
+    private fun startContactsBackupJob() {
+        val activity = activity as ContactsPreferenceActivity?
+        if (activity != null) {
+            val optionalUser = activity.user
+            if (optionalUser.isPresent) {
+                backgroundJobManager!!.startImmediateContactsBackup(optionalUser.get())
+            }
+        }
+    }
+
+    private fun startCalendarBackupJob() {
+        val activity = activity as ContactsPreferenceActivity?
+        if (activity != null) {
+            val optionalUser = activity.user
+            if (optionalUser.isPresent) {
+                backgroundJobManager!!.startImmediateCalendarBackup(optionalUser.get())
+            }
+        }
+    }
+
+    private fun setAutomaticBackup(enabled: Boolean) {
+        val activity = activity as ContactsPreferenceActivity? ?: return
+        val optionalUser = activity.user
+        if (!optionalUser.isPresent) {
+            return
+        }
+        val user = optionalUser.get()
+        if (enabled) {
+            if (isContactsBackupEnabled) {
+                Log_OC.d(TAG, "Scheduling contacts backup job")
+                backgroundJobManager?.schedulePeriodicContactsBackup(user)
+            } else {
+                Log_OC.d(TAG, "Cancelling contacts backup job")
+                backgroundJobManager?.cancelPeriodicContactsBackup(user)
+            }
+            if (isCalendarBackupEnabled) {
+                Log_OC.d(TAG, "Scheduling calendar backup job")
+                backgroundJobManager?.schedulePeriodicCalendarBackup(user)
+            } else {
+                Log_OC.d(TAG, "Cancelling calendar backup job")
+                backgroundJobManager?.cancelPeriodicCalendarBackup(user)
+            }
+        } else {
+            Log_OC.d(TAG, "Cancelling all backup jobs")
+            backgroundJobManager?.cancelPeriodicContactsBackup(user)
+            backgroundJobManager?.cancelPeriodicCalendarBackup(user)
+        }
+        arbitraryDataProvider?.storeOrUpdateKeyValue(
+            user.accountName,
+            ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP,
+            enabled.toString()
+        )
+    }
+
+    private fun checkAndAskForContactsReadPermission(): Boolean {
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+
+        // check permissions
+        return if (checkSelfPermission(contactsPreferenceActivity!!, Manifest.permission.READ_CONTACTS)) {
+            true
+        } else {
+            // No explanation needed, request the permission.
+            requestPermissions(
+                arrayOf(Manifest.permission.READ_CONTACTS),
+                PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC
+            )
+            false
+        }
+    }
+
+    private fun checkAndAskForCalendarReadPermission(): Boolean {
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+
+        // check permissions
+        return if (contactsPreferenceActivity?.let { checkCalendarBackupPermission(it) } == true) {
+            true
+        } else {
+            // No explanation needed, request the permission.
+            requestPermissions(
+                arrayOf(
+                    Manifest.permission.READ_CALENDAR,
+                    Manifest.permission.WRITE_CALENDAR
+                ),
+                PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC
+            )
+            false
+        }
+    }
+
+    private fun checkCalendarBackupPermission(context: Context): Boolean {
+        return checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) && checkSelfPermission(
+            context, Manifest.permission.WRITE_CALENDAR
+        )
+    }
+
+    private fun checkContactBackupPermission(): Boolean {
+        return checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS)
+    }
+
+    private fun openCleanDate() {
+        if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) {
+            openDate(null)
+        }
+    }
+
+    private fun openDate(savedDate: Date?) {
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+        if (contactsPreferenceActivity == null) {
+            Toast.makeText(context, getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show()
+            return
+        }
+
+        val contactsBackupFolderString = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR
+        val calendarBackupFolderString = resources.getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR
+        val storageManager = contactsPreferenceActivity.storageManager
+        val contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString)
+        val calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString)
+
+        val backupFiles = storageManager.getFolderContent(contactsBackupFolder, false)
+        backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false))
+        backupFiles.sortWith { o1: OCFile?, o2: OCFile? ->
+            if (o1 != null && o2 != null) {
+                o1.modificationTimestamp.compareTo(o2.modificationTimestamp)
+            } else {
+                -1
+            }
+        }
+
+        val cal = Calendar.getInstance()
+        val year: Int
+        val month: Int
+        val day: Int
+        if (savedDate == null) {
+            year = cal[Calendar.YEAR]
+            month = cal[Calendar.MONTH] + 1
+            day = cal[Calendar.DAY_OF_MONTH]
+        } else {
+            year = savedDate.year
+            month = savedDate.month
+            day = savedDate.day
+        }
+        if (backupFiles.size > 0 && backupFiles[backupFiles.size - 1] != null) {
+            datePickerDialog = DatePickerDialog(contactsPreferenceActivity, this, year, month, day)
+            datePickerDialog?.datePicker?.maxDate = backupFiles[backupFiles.size - 1]!!
+                .modificationTimestamp
+            datePickerDialog?.datePicker?.minDate = backupFiles[0]!!.modificationTimestamp
+            datePickerDialog?.setOnDismissListener { selectedDate = null }
+            datePickerDialog?.setTitle("")
+            datePickerDialog?.show()
+
+            viewThemeUtils?.platform?.colorTextButtons(
+                datePickerDialog!!.getButton(DatePickerDialog.BUTTON_NEGATIVE),
+                datePickerDialog!!.getButton(DatePickerDialog.BUTTON_POSITIVE)
+            )
+
+            // set background to transparent
+            datePickerDialog?.getButton(DatePickerDialog.BUTTON_NEGATIVE)?.setBackgroundColor(0x00000000)
+            datePickerDialog?.getButton(DatePickerDialog.BUTTON_POSITIVE)?.setBackgroundColor(0x00000000)
+        } else {
+            DisplayUtils.showSnackMessage(
+                requireView().findViewById<View>(R.id.contacts_linear_layout),
+                R.string.contacts_preferences_something_strange_happened
+            )
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        datePickerDialog?.dismiss()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+
+        datePickerDialog?.let {
+            outState.putBoolean(KEY_CALENDAR_PICKER_OPEN, it.isShowing)
+
+            if (it.isShowing) {
+                outState.putInt(KEY_CALENDAR_DAY, it.datePicker.dayOfMonth)
+                outState.putInt(KEY_CALENDAR_MONTH, it.datePicker.month)
+                outState.putInt(KEY_CALENDAR_YEAR, it.datePicker.year)
+            }
+        }
+    }
+
+    @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod", "MagicNumber")
+    override fun onDateSet(view: DatePicker, year: Int, month: Int, dayOfMonth: Int) {
+        val contactsPreferenceActivity = activity as ContactsPreferenceActivity?
+        if (contactsPreferenceActivity == null) {
+            Toast.makeText(context, getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show()
+            return
+        }
+
+        selectedDate = Date(year, month, dayOfMonth)
+        val contactsBackupFolderString = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR
+        val calendarBackupFolderString = resources.getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR
+        val storageManager = contactsPreferenceActivity.storageManager
+        val contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString)
+        val calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString)
+        val backupFiles = storageManager.getFolderContent(contactsBackupFolder, false)
+        backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false))
+
+        // find file with modification with date and time between 00:00 and 23:59
+        // if more than one file exists, take oldest
+        val date = Calendar.getInstance()
+        date[year, month] = dayOfMonth
+
+        // start
+        date[Calendar.HOUR] = 0
+        date[Calendar.MINUTE] = 0
+        date[Calendar.SECOND] = 1
+        date[Calendar.MILLISECOND] = 0
+        date[Calendar.AM_PM] = Calendar.AM
+        val start = date.timeInMillis
+
+        // end
+        date[Calendar.HOUR] = 23
+        date[Calendar.MINUTE] = 59
+        date[Calendar.SECOND] = 59
+        val end = date.timeInMillis
+        var contactsBackupToRestore: OCFile? = null
+        val calendarBackupsToRestore: MutableList<OCFile> = ArrayList()
+        for (file in backupFiles) {
+            if (start < file.modificationTimestamp && end > file.modificationTimestamp) {
+                // contact
+                if (MimeTypeUtil.isVCard(file)) {
+                    if (contactsBackupToRestore == null) {
+                        contactsBackupToRestore = file
+                    } else if (contactsBackupToRestore.modificationTimestamp < file.modificationTimestamp) {
+                        contactsBackupToRestore = file
+                    }
+                }
+
+                // calendars
+                if (showCalendarBackup && MimeTypeUtil.isCalendar(file)) {
+                    calendarBackupsToRestore.add(file)
+                }
+            }
+        }
+        val backupToRestore: MutableList<OCFile> = ArrayList()
+        if (contactsBackupToRestore != null) {
+            backupToRestore.add(contactsBackupToRestore)
+        }
+        backupToRestore.addAll(calendarBackupsToRestore)
+        if (backupToRestore.isEmpty()) {
+            DisplayUtils.showSnackMessage(
+                requireView().findViewById<View>(R.id.contacts_linear_layout),
+                R.string.contacts_preferences_no_file_found
+            )
+        } else {
+            val user = contactsPreferenceActivity.user.orElseThrow { RuntimeException() }
+            val files: Array<OCFile?> = arrayOfNulls(backupToRestore.size)
+
+            val contactListFragment = BackupListFragment.newInstance(files, user)
+
+            contactsPreferenceActivity.supportFragmentManager.beginTransaction()
+                .replace(R.id.frame_container, contactListFragment, BackupListFragment.TAG)
+                .addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST)
+                .commit()
+        }
+    }
+
+    companion object {
+        val TAG = BackupFragment::class.java.simpleName
+        private const val ARG_SHOW_SIDEBAR = "SHOW_SIDEBAR"
+        private const val KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN"
+        private const val KEY_CALENDAR_DAY = "CALENDAR_DAY"
+        private const val KEY_CALENDAR_MONTH = "CALENDAR_MONTH"
+        private const val KEY_CALENDAR_YEAR = "CALENDAR_YEAR"
+        const val PREFERENCE_CONTACTS_BACKUP_ENABLED = "PREFERENCE_CONTACTS_BACKUP_ENABLED"
+        const val PREFERENCE_CALENDAR_BACKUP_ENABLED = "PREFERENCE_CALENDAR_BACKUP_ENABLED"
+
+        @JvmStatic
+        fun create(showSidebar: Boolean): BackupFragment {
+            val fragment = BackupFragment()
+            val bundle = Bundle()
+            bundle.putBoolean(ARG_SHOW_SIDEBAR, showSidebar)
+            fragment.arguments = bundle
+            return fragment
+        }
+    }
+}

+ 16 - 22
app/src/main/res/layout/backup_fragment.xml

@@ -17,8 +17,8 @@
   You should have received a copy of the GNU Affero General Public
   License along with this program. If not, see <http://www.gnu.org/licenses/>.
 -->
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/contacts_linear_layout"
     android:layout_width="match_parent"
@@ -30,14 +30,14 @@
         android:layout_margin="@dimen/standard_margin"
         android:orientation="vertical">
 
-        <TextView
+        <com.google.android.material.textview.MaterialTextView
             android:id="@+id/data_to_back_up_title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/data_to_back_up"
             android:textStyle="bold" />
 
-        <androidx.appcompat.widget.SwitchCompat
+        <com.google.android.material.materialswitch.MaterialSwitch
             android:id="@+id/contacts"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
@@ -46,22 +46,21 @@
             android:textColor="@color/text_color"
             android:textSize="@dimen/two_line_primary_text_size" />
 
-        <androidx.appcompat.widget.SwitchCompat
+        <com.google.android.material.materialswitch.MaterialSwitch
             android:id="@+id/calendar"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:minHeight="48dp"
             android:text="@string/calendar"
             android:textColor="@color/text_color"
-            android:textSize="@dimen/two_line_primary_text_size"
-            />
+            android:textSize="@dimen/two_line_primary_text_size" />
 
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:orientation="vertical">
 
-            <TextView
+            <com.google.android.material.textview.MaterialTextView
                 android:id="@+id/backup_settings_title"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -73,7 +72,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="48dp">
 
-                <androidx.appcompat.widget.SwitchCompat
+                <com.google.android.material.materialswitch.MaterialSwitch
                     android:id="@+id/daily_backup"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
@@ -89,14 +88,14 @@
                     android:orientation="vertical"
                     android:gravity="center_vertical">
 
-                    <TextView
+                    <com.google.android.material.textview.MaterialTextView
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:text="@string/daily_backup"
                         android:textColor="@color/text_color"
                         android:textSize="@dimen/two_line_primary_text_size" />
 
-                    <TextView
+                    <com.google.android.material.textview.MaterialTextView
                         android:id="@+id/last_backup_with_date"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
@@ -113,32 +112,27 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_marginTop="@dimen/standard_margin"
-            android:gravity="center"
-            android:orientation="horizontal"
-            android:weightSum="1.0">
+            android:gravity="end"
+            android:orientation="horizontal">
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/contacts_datepicker"
-                style="@style/OutlinedButton"
-                android:layout_width="0dp"
+                style="@style/Widget.Material3.Button.OutlinedButton"
+                android:layout_width="@dimen/backup_button_width"
                 android:layout_height="match_parent"
-                android:layout_weight="0.5"
                 android:minHeight="@dimen/minimum_size_for_touchable_area"
                 android:text="@string/restore_backup"
                 android:visibility="invisible"
-                app:cornerRadius="@dimen/button_corner_radius"
                 tools:visibility="visible" />
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/backup_now"
-                android:layout_width="0dp"
+                android:layout_width="@dimen/backup_button_width"
                 android:layout_height="match_parent"
                 android:layout_marginStart="@dimen/standard_half_margin"
-                android:layout_weight="0.5"
                 android:minHeight="@dimen/minimum_size_for_touchable_area"
                 android:text="@string/contacts_backup_button"
-                android:theme="@style/Button.Primary"
-                app:cornerRadius="@dimen/button_corner_radius" />
+                android:theme="@style/Widget.Material3.Button.IconButton.Filled" />
 
         </LinearLayout>
 

+ 1 - 0
app/src/main/res/values-vi/strings.xml

@@ -660,6 +660,7 @@
     <string name="share_no_password_title">Đặt mật khẩu</string>
     <string name="share_password_title">Mật khẩu được bảo vệ</string>
     <string name="share_permission_can_edit">Có thể chỉnh sửa</string>
+    <string name="share_permission_file_drop">Thả file</string>
     <string name="share_permission_view_only">Chỉ xem</string>
     <string name="share_permissions">Quyền kho</string>
     <string name="share_remote_clarification">%1$s (từ xa)</string>

+ 2 - 0
app/src/main/res/values/dims.xml

@@ -133,6 +133,8 @@
     <dimen name="bottom_sheet_text_size">16sp</dimen>
     <dimen name="permission_dialog_text_size">18sp</dimen>
     <dimen name="button_corner_radius">24dp</dimen>
+    <dimen name="backup_button_width">160dp</dimen>
+
     <integer name="media_grid_width">4</integer>
     <dimen name="account_action_button_margin">12dp</dimen>
     <dimen name="account_action_button_height">50dp</dimen>

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 80 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 79 warnings</span>