+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import android.Manifest;
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.files.downloader.DownloadRequest;
+import com.nextcloud.client.files.downloader.Request;
+import com.nextcloud.client.files.downloader.Transfer;
+import com.nextcloud.client.files.downloader.TransferManagerConnection;
+import com.nextcloud.client.files.downloader.TransferState;
+import com.nextcloud.client.jobs.BackgroundJobManager;
+import com.nextcloud.client.network.ClientFactory;
+import com.owncloud.android.R;
+import com.owncloud.android.databinding.BackuplistFragmentBinding;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
+import com.owncloud.android.ui.asynctasks.LoadContactsTask;
+import com.owncloud.android.ui.events.VCardToggleEvent;
+import com.owncloud.android.ui.fragment.FileFragment;
+import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.PermissionUtil;
+import com.owncloud.android.utils.theme.ThemeColorUtils;
+import com.owncloud.android.utils.theme.ThemeToolbarUtils;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import ezvcard.VCard;
+import kotlin.Unit;
+
+/**
+ * This fragment shows all contacts or calendars from files and allows to import them.
+ */
+public class BackupListFragment extends FileFragment implements Injectable {
+ public static final String TAG = BackupListFragment.class.getSimpleName();
+
+ public static final String FILE_NAMES = "FILE_NAMES";
+ public static final String FILE_NAME = "FILE_NAME";
+ public static final String USER = "USER";
+ public static final String CHECKED_CALENDAR_ITEMS_ARRAY_KEY = "CALENDAR_CHECKED_ITEMS";
+ public static final String CHECKED_CONTACTS_ITEMS_ARRAY_KEY = "CONTACTS_CHECKED_ITEMS";
+
+ private BackuplistFragmentBinding binding;
+
+ private BackupListAdapter listAdapter;
+ private final List vCards = new ArrayList<>();
+ private final List ocFiles = new ArrayList<>();
+ @Inject UserAccountManager accountManager;
+ @Inject ClientFactory clientFactory;
+ @Inject BackgroundJobManager backgroundJobManager;
+ private TransferManagerConnection fileDownloader;
+ private LoadContactsTask loadContactsTask = null;
+ private ContactsAccount selectedAccount;
+
+ public static BackupListFragment newInstance(OCFile file, User user) {
+ BackupListFragment frag = new BackupListFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(FILE_NAME, file);
+ arguments.putParcelable(USER, user);
+ frag.setArguments(arguments);
+
+ return frag;
+ }
+
+ public static BackupListFragment newInstance(OCFile[] files, User user) {
+ BackupListFragment frag = new BackupListFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelableArray(FILE_NAMES, files);
+ arguments.putParcelable(USER, user);
+ frag.setArguments(arguments);
+
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.fragment_contact_list, menu);
+ }
+
+ @Override
+ public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ binding = BackuplistFragmentBinding.inflate(inflater, container, false);
+ View view = binding.getRoot();
+
+ setHasOptionsMenu(true);
+
+ ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+
+ if (contactsPreferenceActivity != null) {
+ ActionBar actionBar = contactsPreferenceActivity.getSupportActionBar();
+ if (actionBar != null) {
+ ThemeToolbarUtils.setColoredTitle(actionBar, R.string.actionbar_calendar_contacts_restore, getContext());
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
+ }
+
+ if (savedInstanceState == null) {
+ listAdapter = new BackupListAdapter(accountManager,
+ clientFactory,
+ new HashSet<>(),
+ new HashMap<>(),
+ this,
+ requireContext());
+ } else {
+ HashMap checkedCalendarItems = new HashMap<>();
+ String[] checkedCalendarItemsArray = savedInstanceState.getStringArray(CHECKED_CALENDAR_ITEMS_ARRAY_KEY);
+ if (checkedCalendarItemsArray != null) {
+ for (String checkedItem : checkedCalendarItemsArray) {
+ checkedCalendarItems.put(checkedItem, -1);
+ }
+ }
+ if (checkedCalendarItems.size() > 0) {
+ showRestoreButton(true);
+ }
+
+ HashSet checkedContactsItems = new HashSet<>();
+ int[] checkedContactsItemsArray = savedInstanceState.getIntArray(CHECKED_CONTACTS_ITEMS_ARRAY_KEY);
+ if (checkedContactsItemsArray != null) {
+ for (int checkedItem : checkedContactsItemsArray) {
+ checkedContactsItems.add(checkedItem);
+ }
+ }
+ if (checkedContactsItems.size() > 0) {
+ showRestoreButton(true);
+ }
+
+ listAdapter = new BackupListAdapter(accountManager,
+ clientFactory,
+ checkedContactsItems,
+ checkedCalendarItems,
+ this,
+ requireContext());
+ }
+
+ binding.list.setAdapter(listAdapter);
+ binding.list.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ Bundle arguments = getArguments();
+ if (arguments == null) {
+ return view;
+ }
+
+ if (arguments.getParcelable(FILE_NAME) != null) {
+ ocFiles.add(arguments.getParcelable(FILE_NAME));
+ } else if (arguments.getParcelableArray(FILE_NAMES) != null) {
+ for (Parcelable file : arguments.getParcelableArray(FILE_NAMES)) {
+ ocFiles.add((OCFile) file);
+ }
+ } else {
+ return view;
+ }
+
+ User user = getArguments().getParcelable(USER);
+ fileDownloader = new TransferManagerConnection(getActivity(), user);
+ fileDownloader.registerTransferListener(this::onDownloadUpdate);
+ fileDownloader.bind();
+
+ for (OCFile file : ocFiles) {
+ if (!file.isDown()) {
+ Request request = new DownloadRequest(user, file);
+ fileDownloader.enqueue(request);
+ }
+
+ if (MimeTypeUtil.isVCard(file) && file.isDown()) {
+ setFile(file);
+ loadContactsTask = new LoadContactsTask(this, file);
+ loadContactsTask.execute();
+ }
+
+ if (MimeTypeUtil.isCalendar(file) && file.isDown()) {
+ showLoadingMessage(false);
+ listAdapter.addCalendar(file);
+ }
+ }
+
+ binding.restoreSelected.setOnClickListener(v -> {
+ if (checkAndAskForCalendarWritePermission()) {
+ importCalendar();
+ }
+
+ if (listAdapter.getCheckedContactsIntArray().length > 0 && checkAndAskForContactsWritePermission()) {
+ importContacts(selectedAccount);
+ return;
+ }
+
+ Snackbar
+ .make(
+ binding.list,
+ R.string.contacts_preferences_import_scheduled,
+ Snackbar.LENGTH_LONG
+ )
+ .show();
+
+ closeFragment();
+ });
+
+ binding.restoreSelected.setTextColor(ThemeColorUtils.primaryAccentColor(getContext()));
+
+ return view;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (fileDownloader != null) {
+ fileDownloader.unbind();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putStringArray(CHECKED_CALENDAR_ITEMS_ARRAY_KEY, listAdapter.getCheckedCalendarStringArray());
+ outState.putIntArray(CHECKED_CONTACTS_ITEMS_ARRAY_KEY, listAdapter.getCheckedContactsIntArray());
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onMessageEvent(VCardToggleEvent event) {
+ if (event.showRestoreButton) {
+ binding.contactlistRestoreSelectedContainer.setVisibility(View.VISIBLE);
+ } else {
+ binding.contactlistRestoreSelectedContainer.setVisibility(View.GONE);
+ }
+ }
+
+ public void showRestoreButton(boolean show) {
+ binding.contactlistRestoreSelectedContainer.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+ contactsPreferenceActivity.setDrawerIndicatorEnabled(true);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+
+ public void onResume() {
+ super.onResume();
+ ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+ contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ if (loadContactsTask != null) {
+ loadContactsTask.cancel(true);
+ }
+ super.onStop();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ boolean retval;
+ int itemId = item.getItemId();
+
+ if (itemId == android.R.id.home) {
+ closeFragment();
+ retval = true;
+ } else if (itemId == R.id.action_select_all) {
+ item.setChecked(!item.isChecked());
+ setSelectAllMenuItem(item, item.isChecked());
+ listAdapter.selectAll(item.isChecked());
+ retval = true;
+ } else {
+ retval = super.onOptionsItemSelected(item);
+ }
+
+ return retval;
+ }
+
+ public void showLoadingMessage(boolean showIt) {
+ binding.loadingListContainer.setVisibility(showIt ? View.VISIBLE : View.GONE);
+ }
+
+ private void setSelectAllMenuItem(MenuItem selectAll, boolean checked) {
+ selectAll.setChecked(checked);
+ if (checked) {
+ selectAll.setIcon(R.drawable.ic_select_none);
+ } else {
+ selectAll.setIcon(R.drawable.ic_select_all);
+ }
+ }
+
+ private void importContacts(ContactsAccount account) {
+ backgroundJobManager.startImmediateContactsImport(account.getName(),
+ account.getType(),
+ getFile().getStoragePath(),
+ listAdapter.getCheckedContactsIntArray());
+
+ Snackbar
+ .make(
+ binding.list,
+ R.string.contacts_preferences_import_scheduled,
+ Snackbar.LENGTH_LONG
+ )
+ .show();
+
+ closeFragment();
+ }
+
+ private void importCalendar() {
+ backgroundJobManager.startImmediateCalendarImport(listAdapter.getCheckedCalendarPathsArray());
+
+ Snackbar
+ .make(
+ binding.list,
+ R.string.contacts_preferences_import_scheduled,
+ Snackbar.LENGTH_LONG
+ )
+ .show();
+
+ closeFragment();
+ }
+
+ private void closeFragment() {
+ ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
+ if (contactsPreferenceActivity != null) {
+ contactsPreferenceActivity.onBackPressed();
+ }
+ }
+
+ private boolean checkAndAskForContactsWritePermission() {
+ // check permissions
+ if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) {
+ requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
+ PermissionUtil.PERMISSIONS_WRITE_CONTACTS);
+ 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);
+ return false;
+ } else {
+ 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 {
+ if (getView() != null) {
+ Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
+ .show();
+ } else {
+ Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ 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 {
+ if (getView() != null) {
+ Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
+ .show();
+ } else {
+ Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private Unit onDownloadUpdate(Transfer download) {
+ final Activity activity = getActivity();
+ if (download.getState() == TransferState.COMPLETED && activity != null) {
+ OCFile ocFile = download.getFile();
+
+ if (MimeTypeUtil.isVCard(ocFile)) {
+ setFile(ocFile);
+ loadContactsTask = new LoadContactsTask(this, ocFile);
+ loadContactsTask.execute();
+ }
+ }
+ return Unit.INSTANCE;
+ }
+
+ public void loadVCards(List cards) {
+ showLoadingMessage(false);
+ vCards.clear();
+ vCards.addAll(cards);
+ listAdapter.replaceVcards(vCards);
+ }
+
+ public static String getDisplayName(VCard vCard) {
+ if (vCard.getFormattedName() != null) {
+ return vCard.getFormattedName().getValue();
+ } else if (vCard.getTelephoneNumbers() != null && vCard.getTelephoneNumbers().size() > 0) {
+ return vCard.getTelephoneNumbers().get(0).getText();
+ } else if (vCard.getEmails() != null && vCard.getEmails().size() > 0) {
+ return vCard.getEmails().get(0).getValue();
+ }
+
+ return "";
+ }
+
+ public boolean hasCalendarEntry() {
+ return listAdapter.hasCalendarEntry();
+ }
+
+ public void setSelectedAccount(ContactsAccount account) {
+ selectedAccount = account;
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListHeaderViewHolder.kt b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListHeaderViewHolder.kt
new file mode 100644
index 0000000000..73a76cb534
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListHeaderViewHolder.kt
@@ -0,0 +1,49 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup
+
+import android.content.Context
+import android.widget.ArrayAdapter
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.owncloud.android.databinding.BackupListItemHeaderBinding
+import java.util.ArrayList
+
+class BackupListHeaderViewHolder(
+ val binding: BackupListItemHeaderBinding,
+ val context: Context
+) : SectionedViewHolder(binding.root) {
+ val adapter = ArrayAdapter(
+ context,
+ android.R.layout.simple_spinner_dropdown_item,
+ ArrayList()
+ )
+
+ init {
+ binding.spinner.adapter = adapter
+ }
+
+ fun setContactsAccount(accounts: List) {
+ adapter.clear()
+ adapter.addAll(accounts)
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListItemViewHolder.kt b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListItemViewHolder.kt
new file mode 100644
index 0000000000..b3d8b97117
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListItemViewHolder.kt
@@ -0,0 +1,28 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup
+
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.owncloud.android.databinding.BackupListItemBinding
+
+class BackupListItemViewHolder(val binding: BackupListItemBinding) : SectionedViewHolder(binding.root)
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/CalendarItemViewHolder.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/CalendarItemViewHolder.java
new file mode 100644
index 0000000000..9f65d45655
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/CalendarItemViewHolder.java
@@ -0,0 +1,79 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Toast;
+
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
+import com.owncloud.android.R;
+import com.owncloud.android.databinding.CalendarlistListItemBinding;
+
+import java.util.ArrayList;
+
+import third_parties.sufficientlysecure.AndroidCalendar;
+
+class CalendarItemViewHolder extends SectionedViewHolder {
+ public CalendarlistListItemBinding binding;
+ private final ArrayAdapter adapter;
+ private final Context context;
+
+ CalendarItemViewHolder(CalendarlistListItemBinding binding, Context context) {
+ super(binding.getRoot());
+
+ this.binding = binding;
+ this.context = context;
+
+ adapter = new ArrayAdapter<>(context,
+ android.R.layout.simple_spinner_dropdown_item,
+ new ArrayList<>());
+
+ binding.spinner.setAdapter(adapter);
+ }
+
+ public void setCalendars(ArrayList calendars) {
+ adapter.clear();
+ adapter.addAll(calendars);
+ }
+
+ public void setListener(View.OnClickListener onClickListener) {
+ itemView.setOnClickListener(onClickListener);
+ }
+
+ public void showCalendars(boolean show) {
+ if (show) {
+ if (adapter.isEmpty()) {
+ Toast.makeText(context,
+ context.getResources().getString(R.string.no_calendar_exists),
+ Toast.LENGTH_LONG)
+ .show();
+ } else {
+ binding.spinner.setVisibility(View.VISIBLE);
+ }
+ } else {
+ binding.spinner.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactItemViewHolder.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactItemViewHolder.java
new file mode 100644
index 0000000000..0ff7f05841
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactItemViewHolder.java
@@ -0,0 +1,43 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import android.view.View;
+
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
+import com.owncloud.android.databinding.ContactlistListItemBinding;
+
+public class ContactItemViewHolder extends SectionedViewHolder {
+ public ContactlistListItemBinding binding;
+
+ ContactItemViewHolder(ContactlistListItemBinding binding) {
+ super(binding.getRoot());
+
+ this.binding = binding;
+ binding.getRoot().setTag(this);
+ }
+
+ public void setVCardListener(View.OnClickListener onClickListener) {
+ itemView.setOnClickListener(onClickListener);
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListAdapter.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListAdapter.java
new file mode 100644
index 0000000000..26d89bd805
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListAdapter.java
@@ -0,0 +1,250 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.CheckedTextView;
+import android.widget.ImageView;
+
+import com.bumptech.glide.request.animation.GlideAnimation;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.client.network.ClientFactory;
+import com.owncloud.android.R;
+import com.owncloud.android.databinding.ContactlistListItemBinding;
+import com.owncloud.android.ui.TextDrawable;
+import com.owncloud.android.ui.events.VCardToggleEvent;
+import com.owncloud.android.utils.BitmapUtils;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.theme.ThemeColorUtils;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.recyclerview.widget.RecyclerView;
+import ezvcard.VCard;
+import ezvcard.property.Photo;
+
+import static com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment.getDisplayName;
+
+class ContactListAdapter extends RecyclerView.Adapter {
+ private static final int SINGLE_SELECTION = 1;
+
+ private List vCards;
+ private Set checkedVCards;
+
+ private Context context;
+
+ private UserAccountManager accountManager;
+ private ClientFactory clientFactory;
+
+ ContactListAdapter(UserAccountManager accountManager, ClientFactory clientFactory, Context context,
+ List vCards) {
+ this.vCards = vCards;
+ this.context = context;
+ this.checkedVCards = new HashSet<>();
+ this.accountManager = accountManager;
+ this.clientFactory = clientFactory;
+ }
+
+ ContactListAdapter(UserAccountManager accountManager,
+ Context context,
+ List vCards,
+ Set checkedVCards) {
+ this.vCards = vCards;
+ this.context = context;
+ this.checkedVCards = checkedVCards;
+ this.accountManager = accountManager;
+ }
+
+ public int getCheckedCount() {
+ if (checkedVCards != null) {
+ return checkedVCards.size();
+ } else {
+ return 0;
+ }
+ }
+
+ public void replaceVCards(List vCards) {
+ this.vCards = vCards;
+ notifyDataSetChanged();
+ }
+
+ public int[] getCheckedIntArray() {
+ int[] intArray;
+ if (checkedVCards != null && checkedVCards.size() > 0) {
+ intArray = new int[checkedVCards.size()];
+ int i = 0;
+ for (int position : checkedVCards) {
+ intArray[i] = position;
+ i++;
+ }
+ return intArray;
+ } else {
+ return new int[0];
+ }
+ }
+
+ @NonNull
+ @Override
+ public ContactItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ContactItemViewHolder(ContactlistListItemBinding.inflate(LayoutInflater.from(parent.getContext()),
+ parent,
+ false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final ContactItemViewHolder holder, final int position) {
+ final int verifiedPosition = holder.getAdapterPosition();
+ final VCard vcard = vCards.get(verifiedPosition);
+
+ if (vcard != null) {
+
+ setChecked(checkedVCards.contains(position), holder.binding.name);
+
+ holder.binding.name.setText(getDisplayName(vcard));
+
+ // photo
+ if (vcard.getPhotos().size() > 0) {
+ setPhoto(holder.binding.icon, vcard.getPhotos().get(0));
+ } else {
+ try {
+ holder.binding.icon.setImageDrawable(
+ TextDrawable.createNamedAvatar(
+ holder.binding.name.getText().toString(),
+ context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius)
+ )
+ );
+ } catch (Exception e) {
+ holder.binding.icon.setImageResource(R.drawable.ic_user);
+ }
+ }
+
+ holder.setVCardListener(v -> toggleVCard(holder, verifiedPosition));
+ }
+ }
+
+ private void setPhoto(ImageView imageView, Photo firstPhoto) {
+ String url = firstPhoto.getUrl();
+ byte[] data = firstPhoto.getData();
+
+ if (data != null && data.length > 0) {
+ Bitmap thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length);
+ RoundedBitmapDrawable drawable = BitmapUtils.bitmapToCircularBitmapDrawable(context.getResources(),
+ thumbnail);
+
+ imageView.setImageDrawable(drawable);
+ } else if (url != null) {
+ SimpleTarget target = new SimpleTarget() {
+ @Override
+ public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) {
+ imageView.setImageDrawable(resource);
+ }
+
+ @Override
+ public void onLoadFailed(Exception e, Drawable errorDrawable) {
+ super.onLoadFailed(e, errorDrawable);
+ imageView.setImageDrawable(errorDrawable);
+ }
+ };
+ DisplayUtils.downloadIcon(accountManager,
+ clientFactory,
+ context,
+ url,
+ target,
+ R.drawable.ic_user,
+ imageView.getWidth(),
+ imageView.getHeight());
+ }
+ }
+
+ private void setChecked(boolean checked, CheckedTextView checkedTextView) {
+ checkedTextView.setChecked(checked);
+
+ if (checked) {
+ checkedTextView.getCheckMarkDrawable()
+ .setColorFilter(ThemeColorUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP);
+ } else {
+ checkedTextView.getCheckMarkDrawable().clearColorFilter();
+ }
+ }
+
+ private void toggleVCard(ContactItemViewHolder holder, int verifiedPosition) {
+ holder.binding.name.setChecked(!holder.binding.name.isChecked());
+
+ if (holder.binding.name.isChecked()) {
+ holder.binding.name.getCheckMarkDrawable().setColorFilter(ThemeColorUtils.primaryColor(context),
+ PorterDuff.Mode.SRC_ATOP);
+
+ checkedVCards.add(verifiedPosition);
+ if (checkedVCards.size() == SINGLE_SELECTION) {
+ EventBus.getDefault().post(new VCardToggleEvent(true));
+ }
+ } else {
+ holder.binding.name.getCheckMarkDrawable().clearColorFilter();
+
+ checkedVCards.remove(verifiedPosition);
+
+ if (checkedVCards.isEmpty()) {
+ EventBus.getDefault().post(new VCardToggleEvent(false));
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return vCards.size();
+ }
+
+ public void selectAllFiles(boolean select) {
+ checkedVCards = new HashSet<>();
+ if (select) {
+ for (int i = 0; i < vCards.size(); i++) {
+ checkedVCards.add(i);
+ }
+ }
+
+ if (checkedVCards.size() > 0) {
+ EventBus.getDefault().post(new VCardToggleEvent(true));
+ } else {
+ EventBus.getDefault().post(new VCardToggleEvent(false));
+ }
+
+ notifyDataSetChanged();
+ }
+
+ public boolean isEmpty() {
+ return getItemCount() == 0;
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
deleted file mode 100644
index 6b2b6f730f..0000000000
--- a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
+++ /dev/null
@@ -1,735 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2017 Tobias Kaminsky
- * Copyright (C) 2017 Nextcloud GmbH.
- * Copyright (C) 2020 Chris Narkiewicz
- *
- * 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 .
- */
-
-package com.owncloud.android.ui.fragment.contactsbackup;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.ContactsContract;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.CheckedTextView;
-import android.widget.ImageView;
-import android.widget.Toast;
-
-import com.bumptech.glide.request.animation.GlideAnimation;
-import com.bumptech.glide.request.target.SimpleTarget;
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.files.downloader.Direction;
-import com.nextcloud.client.files.downloader.DownloadRequest;
-import com.nextcloud.client.files.downloader.Request;
-import com.nextcloud.client.files.downloader.Transfer;
-import com.nextcloud.client.files.downloader.TransferManagerConnection;
-import com.nextcloud.client.files.downloader.TransferState;
-import com.nextcloud.client.jobs.BackgroundJobManager;
-import com.nextcloud.client.network.ClientFactory;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.ContactlistFragmentBinding;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.TextDrawable;
-import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
-import com.owncloud.android.ui.events.VCardToggleEvent;
-import com.owncloud.android.ui.fragment.FileFragment;
-import com.owncloud.android.utils.BitmapUtils;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.PermissionUtil;
-import com.owncloud.android.utils.theme.ThemeColorUtils;
-import com.owncloud.android.utils.theme.ThemeToolbarUtils;
-
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import ezvcard.Ezvcard;
-import ezvcard.VCard;
-import ezvcard.property.Photo;
-import kotlin.Unit;
-
-import static com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment.getDisplayName;
-
-/**
- * This fragment shows all contacts from a file and allows to import them.
- */
-public class ContactListFragment extends FileFragment implements Injectable {
- public static final String TAG = ContactListFragment.class.getSimpleName();
-
- public static final String FILE_NAME = "FILE_NAME";
- public static final String USER = "USER";
- public static final String CHECKED_ITEMS_ARRAY_KEY = "CHECKED_ITEMS";
-
- private static final int SINGLE_ACCOUNT = 1;
-
- private ContactlistFragmentBinding binding;
-
- private ContactListAdapter contactListAdapter;
- private final List vCards = new ArrayList<>();
- private OCFile ocFile;
- @Inject UserAccountManager accountManager;
- @Inject ClientFactory clientFactory;
- @Inject BackgroundJobManager backgroundJobManager;
- private TransferManagerConnection fileDownloader;
-
- public static ContactListFragment newInstance(OCFile file, User user) {
- ContactListFragment frag = new ContactListFragment();
- Bundle arguments = new Bundle();
- arguments.putParcelable(FILE_NAME, file);
- arguments.putParcelable(USER, user);
- frag.setArguments(arguments);
- return frag;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- inflater.inflate(R.menu.fragment_contact_list, menu);
- }
-
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
- binding = ContactlistFragmentBinding.inflate(inflater, container, false);
- View view = binding.getRoot();
-
- setHasOptionsMenu(true);
-
- ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
-
- if (contactsPreferenceActivity != null) {
- ActionBar actionBar = contactsPreferenceActivity.getSupportActionBar();
- if (actionBar != null) {
- ThemeToolbarUtils.setColoredTitle(actionBar, R.string.actionbar_contacts_restore, getContext());
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
- }
-
- if (savedInstanceState == null) {
- contactListAdapter = new ContactListAdapter(accountManager, clientFactory, getContext(), vCards);
- } else {
- Set checkedItems = new HashSet<>();
- int[] itemsArray = savedInstanceState.getIntArray(CHECKED_ITEMS_ARRAY_KEY);
- if (itemsArray != null) {
- for (int checkedItem : itemsArray) {
- checkedItems.add(checkedItem);
- }
- }
- if (checkedItems.size() > 0) {
- onMessageEvent(new VCardToggleEvent(true));
- }
- contactListAdapter = new ContactListAdapter(accountManager, getContext(), vCards, checkedItems);
- }
- binding.contactlistRecyclerview.setAdapter(contactListAdapter);
- binding.contactlistRecyclerview.setLayoutManager(new LinearLayoutManager(getContext()));
-
- ocFile = getArguments().getParcelable(FILE_NAME);
- setFile(ocFile);
- User user = getArguments().getParcelable(USER);
- fileDownloader = new TransferManagerConnection(getActivity(), user);
- fileDownloader.registerTransferListener(this::onDownloadUpdate);
- fileDownloader.bind();
- if (!ocFile.isDown()) {
- Request request = new DownloadRequest(user, ocFile);
- fileDownloader.enqueue(request);
- } else {
- loadContactsTask.execute();
- }
-
- binding.contactlistRestoreSelected.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (checkAndAskForContactsWritePermission()) {
- getAccountForImport();
- }
- }
- });
-
- binding.contactlistRestoreSelected.setTextColor(ThemeColorUtils.primaryAccentColor(getContext()));
-
- return view;
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- if (fileDownloader != null) {
- fileDownloader.unbind();
- }
- }
-
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putIntArray(CHECKED_ITEMS_ARRAY_KEY, contactListAdapter.getCheckedIntArray());
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onMessageEvent(VCardToggleEvent event) {
- if (event.showRestoreButton) {
- binding.contactlistRestoreSelectedContainer.setVisibility(View.VISIBLE);
- } else {
- binding.contactlistRestoreSelectedContainer.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
- contactsPreferenceActivity.setDrawerIndicatorEnabled(true);
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- binding = null;
- }
-
- public void onResume() {
- super.onResume();
- ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
- contactsPreferenceActivity.setDrawerIndicatorEnabled(false);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- EventBus.getDefault().register(this);
- }
-
- @Override
- public void onStop() {
- EventBus.getDefault().unregister(this);
- if (loadContactsTask != null) {
- loadContactsTask.cancel(true);
- }
- super.onStop();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- boolean retval;
- int itemId = item.getItemId();
-
- if (itemId == android.R.id.home) {
- ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity();
- if (contactsPreferenceActivity != null) {
- contactsPreferenceActivity.onBackPressed();
- }
- retval = true;
- } else if (itemId == R.id.action_select_all) {
- item.setChecked(!item.isChecked());
- setSelectAllMenuItem(item, item.isChecked());
- contactListAdapter.selectAllFiles(item.isChecked());
- retval = true;
- } else {
- retval = super.onOptionsItemSelected(item);
- }
-
- return retval;
- }
-
- private void setLoadingMessage() {
- binding.loadingListContainer.setVisibility(View.VISIBLE);
- }
-
- private void setSelectAllMenuItem(MenuItem selectAll, boolean checked) {
- selectAll.setChecked(checked);
- if (checked) {
- selectAll.setIcon(R.drawable.ic_select_none);
- } else {
- selectAll.setIcon(R.drawable.ic_select_all);
- }
- }
-
- static class ContactItemViewHolder extends RecyclerView.ViewHolder {
- private ImageView badge;
- private CheckedTextView name;
-
- ContactItemViewHolder(View itemView) {
- super(itemView);
-
- badge = itemView.findViewById(R.id.contactlist_item_icon);
- name = itemView.findViewById(R.id.contactlist_item_name);
-
-
- itemView.setTag(this);
- }
-
- public void setVCardListener(View.OnClickListener onClickListener) {
- itemView.setOnClickListener(onClickListener);
- }
-
- public ImageView getBadge() {
- return badge;
- }
-
- public void setBadge(ImageView badge) {
- this.badge = badge;
- }
-
- public CheckedTextView getName() {
- return name;
- }
-
- public void setName(CheckedTextView name) {
- this.name = name;
- }
- }
-
- private void importContacts(ContactsAccount account) {
- backgroundJobManager.startImmediateContactsImport(account.name,
- account.type,
- getFile().getStoragePath(),
- contactListAdapter.getCheckedIntArray());
-
- Snackbar
- .make(
- binding.contactlistRecyclerview,
- R.string.contacts_preferences_import_scheduled,
- Snackbar.LENGTH_LONG
- )
- .show();
-
- Handler handler = new Handler();
- handler.postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getFragmentManager().getBackStackEntryCount() > 0) {
- getFragmentManager().popBackStack();
- } else {
- getActivity().finish();
- }
- }
- }, 1750);
- }
-
- private void getAccountForImport() {
- final ArrayList contactsAccounts = new ArrayList<>();
-
- // add local one
- contactsAccounts.add(new ContactsAccount("Local contacts", null, null));
-
- Cursor cursor = null;
- try {
- cursor = getContext().getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,
- new String[]{ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE},
- null,
- null,
- null);
-
- if (cursor != null && cursor.getCount() > 0) {
- while (cursor.moveToNext()) {
- String name = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME));
- String type = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE));
-
- ContactsAccount account = new ContactsAccount(name, name, type);
-
- if (!contactsAccounts.contains(account)) {
- contactsAccounts.add(account);
- }
- }
-
- cursor.close();
- }
- } catch (Exception e) {
- Log_OC.d(TAG, e.getMessage());
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- if (contactsAccounts.size() == SINGLE_ACCOUNT) {
- importContacts(contactsAccounts.get(0));
- } else {
- ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, contactsAccounts);
- AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setTitle(R.string.contactlist_account_chooser_title)
- .setAdapter(adapter, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- importContacts(contactsAccounts.get(which));
- }
- }).show();
- }
- }
-
- private boolean checkAndAskForContactsWritePermission() {
- // check permissions
- if (!PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.WRITE_CONTACTS)) {
- requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
- PermissionUtil.PERMISSIONS_WRITE_CONTACTS);
- return false;
- } else {
- 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) {
- getAccountForImport();
- } else {
- if (getView() != null) {
- Snackbar.make(getView(), R.string.contactlist_no_permission, Snackbar.LENGTH_LONG)
- .show();
- } else {
- Toast.makeText(getContext(), R.string.contactlist_no_permission, Toast.LENGTH_LONG).show();
- }
- }
- break;
- }
- }
- }
- }
-
- private class ContactsAccount {
- private String displayName;
- private String name;
- private String type;
-
- ContactsAccount(String displayName, String name, String type) {
- this.displayName = displayName;
- this.name = name;
- this.type = type;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof ContactsAccount) {
- ContactsAccount other = (ContactsAccount) obj;
- return this.name.equalsIgnoreCase(other.name) && this.type.equalsIgnoreCase(other.type);
- } else {
- return false;
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return displayName;
- }
-
- @Override
- public int hashCode() {
- return Arrays.hashCode(new Object[]{displayName, name, type});
- }
- }
-
- private Unit onDownloadUpdate(Transfer download) {
- final Activity activity = getActivity();
- if (download.getState() == TransferState.COMPLETED && activity != null) {
- ocFile = download.getFile();
- loadContactsTask.execute();
- }
- return Unit.INSTANCE;
- }
-
- public static class VCardComparator implements Comparator {
- @Override
- public int compare(VCard o1, VCard o2) {
- String contac1 = getDisplayName(o1);
- String contac2 = getDisplayName(o2);
-
- return contac1.compareToIgnoreCase(contac2);
- }
-
-
- }
-
- private AsyncTask loadContactsTask = new AsyncTask() {
-
- @Override
- protected void onPreExecute() {
- setLoadingMessage();
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- if (!isCancelled()) {
- File file = new File(ocFile.getStoragePath());
- try {
- vCards.addAll(Ezvcard.parse(file).all());
- Collections.sort(vCards, new VCardComparator());
- } catch (IOException e) {
- Log_OC.e(TAG, "IO Exception: " + file.getAbsolutePath());
- return Boolean.FALSE;
- }
- return Boolean.TRUE;
- }
- return Boolean.FALSE;
- }
-
- @Override
- protected void onPostExecute(Boolean bool) {
- if (!isCancelled()) {
- binding.loadingListContainer.setVisibility(View.GONE);
- contactListAdapter.replaceVCards(vCards);
- }
- }
- };
-
- public static String getDisplayName(VCard vCard) {
- if (vCard.getFormattedName() != null) {
- return vCard.getFormattedName().getValue();
- } else if (vCard.getTelephoneNumbers() != null && vCard.getTelephoneNumbers().size() > 0) {
- return vCard.getTelephoneNumbers().get(0).getText();
- } else if (vCard.getEmails() != null && vCard.getEmails().size() > 0) {
- return vCard.getEmails().get(0).getValue();
- }
-
- return "";
- }
-}
-
-class ContactListAdapter extends RecyclerView.Adapter {
- private static final int SINGLE_SELECTION = 1;
-
- private List vCards;
- private Set checkedVCards;
-
- private Context context;
-
- private UserAccountManager accountManager;
- private ClientFactory clientFactory;
-
- ContactListAdapter(UserAccountManager accountManager, ClientFactory clientFactory, Context context,
- List vCards) {
- this.vCards = vCards;
- this.context = context;
- this.checkedVCards = new HashSet<>();
- this.accountManager = accountManager;
- this.clientFactory = clientFactory;
- }
-
- ContactListAdapter(UserAccountManager accountManager,
- Context context,
- List vCards,
- Set checkedVCards) {
- this.vCards = vCards;
- this.context = context;
- this.checkedVCards = checkedVCards;
- this.accountManager = accountManager;
- }
-
- public int getCheckedCount() {
- if (checkedVCards != null) {
- return checkedVCards.size();
- } else {
- return 0;
- }
- }
-
- public void replaceVCards(List vCards) {
- this.vCards = vCards;
- notifyDataSetChanged();
- }
-
- public int[] getCheckedIntArray() {
- int[] intArray;
- if (checkedVCards != null && checkedVCards.size() > 0) {
- intArray = new int[checkedVCards.size()];
- int i = 0;
- for (int position : checkedVCards) {
- intArray[i] = position;
- i++;
- }
- return intArray;
- } else {
- return new int[0];
- }
- }
-
- @NonNull
- @Override
- public ContactListFragment.ContactItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(context).inflate(R.layout.contactlist_list_item, parent, false);
-
- return new ContactListFragment.ContactItemViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(@NonNull final ContactListFragment.ContactItemViewHolder holder, final int position) {
- final int verifiedPosition = holder.getAdapterPosition();
- final VCard vcard = vCards.get(verifiedPosition);
-
- if (vcard != null) {
-
- setChecked(checkedVCards.contains(position), holder.getName());
-
- holder.getName().setText(getDisplayName(vcard));
-
- // photo
- if (vcard.getPhotos().size() > 0) {
- setPhoto(holder.getBadge(), vcard.getPhotos().get(0));
- } else {
- try {
- holder.getBadge().setImageDrawable(
- TextDrawable.createNamedAvatar(
- holder.getName().getText().toString(),
- context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius)
- )
- );
- } catch (Exception e) {
- holder.getBadge().setImageResource(R.drawable.ic_user);
- }
- }
-
- holder.setVCardListener(v -> toggleVCard(holder, verifiedPosition));
- }
- }
-
- private void setPhoto(ImageView imageView, Photo firstPhoto) {
- String url = firstPhoto.getUrl();
- byte[] data = firstPhoto.getData();
-
- if (data != null && data.length > 0) {
- Bitmap thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length);
- RoundedBitmapDrawable drawable = BitmapUtils.bitmapToCircularBitmapDrawable(context.getResources(),
- thumbnail);
-
- imageView.setImageDrawable(drawable);
- } else if (url != null) {
- SimpleTarget target = new SimpleTarget() {
- @Override
- public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) {
- imageView.setImageDrawable(resource);
- }
-
- @Override
- public void onLoadFailed(Exception e, Drawable errorDrawable) {
- super.onLoadFailed(e, errorDrawable);
- imageView.setImageDrawable(errorDrawable);
- }
- };
- DisplayUtils.downloadIcon(accountManager,
- clientFactory,
- context,
- url,
- target,
- R.drawable.ic_user,
- imageView.getWidth(),
- imageView.getHeight());
- }
- }
-
- private void setChecked(boolean checked, CheckedTextView checkedTextView) {
- checkedTextView.setChecked(checked);
-
- if (checked) {
- checkedTextView.getCheckMarkDrawable()
- .setColorFilter(ThemeColorUtils.primaryColor(context), PorterDuff.Mode.SRC_ATOP);
- } else {
- checkedTextView.getCheckMarkDrawable().clearColorFilter();
- }
- }
-
- private void toggleVCard(ContactListFragment.ContactItemViewHolder holder, int verifiedPosition) {
- holder.getName().setChecked(!holder.getName().isChecked());
-
- if (holder.getName().isChecked()) {
- holder.getName().getCheckMarkDrawable().setColorFilter(ThemeColorUtils.primaryColor(context),
- PorterDuff.Mode.SRC_ATOP);
-
- checkedVCards.add(verifiedPosition);
- if (checkedVCards.size() == SINGLE_SELECTION) {
- EventBus.getDefault().post(new VCardToggleEvent(true));
- }
- } else {
- holder.getName().getCheckMarkDrawable().clearColorFilter();
-
- checkedVCards.remove(verifiedPosition);
-
- if (checkedVCards.isEmpty()) {
- EventBus.getDefault().post(new VCardToggleEvent(false));
- }
- }
- }
-
- @Override
- public int getItemCount() {
- return vCards.size();
- }
-
- public void selectAllFiles(boolean select) {
- checkedVCards = new HashSet<>();
- if (select) {
- for (int i = 0; i < vCards.size(); i++) {
- checkedVCards.add(i);
- }
- }
-
- if (checkedVCards.size() > 0) {
- EventBus.getDefault().post(new VCardToggleEvent(true));
- } else {
- EventBus.getDefault().post(new VCardToggleEvent(false));
- }
-
- notifyDataSetChanged();
- }
-
-}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsAccount.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsAccount.java
new file mode 100644
index 0000000000..dde131d16b
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactsAccount.java
@@ -0,0 +1,68 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import java.util.Arrays;
+
+import androidx.annotation.NonNull;
+
+public class ContactsAccount {
+ private final String displayName;
+ private final String name;
+ private final String type;
+
+ ContactsAccount(String displayName, String name, String type) {
+ this.displayName = displayName;
+ this.name = name;
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ContactsAccount) {
+ ContactsAccount other = (ContactsAccount) obj;
+ return this.name.equalsIgnoreCase(other.name) && this.type.equalsIgnoreCase(other.type);
+ } else {
+ return false;
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return displayName;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[]{displayName, name, type});
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/VCardComparator.java b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/VCardComparator.java
new file mode 100644
index 0000000000..0a101ab056
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/VCardComparator.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package com.owncloud.android.ui.fragment.contactsbackup;
+
+import java.util.Comparator;
+
+import ezvcard.VCard;
+
+public class VCardComparator implements Comparator {
+ @Override
+ public int compare(VCard o1, VCard o2) {
+ String contact1 = BackupListFragment.getDisplayName(o1);
+ String contact2 = BackupListFragment.getDisplayName(o2);
+
+ return contact1.compareToIgnoreCase(contact2);
+ }
+}
diff --git a/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java b/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
index 5a76065539..6f06fff178 100644
--- a/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
+++ b/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
@@ -328,6 +328,14 @@ public final class MimeTypeUtil {
return isVCard(file.getMimeType()) || isVCard(getMimeTypeFromPath(file.getRemotePath()));
}
+ public static boolean isCalendar(OCFile file) {
+ return isCalendar(file.getMimeType()) || isCalendar(getMimeTypeFromPath(file.getRemotePath()));
+ }
+
+ public static boolean isCalendar(String mimeType) {
+ return "text/calendar".equalsIgnoreCase(mimeType);
+ }
+
public static boolean isFolder(String mimeType) {
return MimeType.DIRECTORY.equalsIgnoreCase(mimeType);
}
diff --git a/src/main/java/com/owncloud/android/utils/PermissionUtil.java b/src/main/java/com/owncloud/android/utils/PermissionUtil.java
index c276dffaa6..0829b3169b 100644
--- a/src/main/java/com/owncloud/android/utils/PermissionUtil.java
+++ b/src/main/java/com/owncloud/android/utils/PermissionUtil.java
@@ -13,9 +13,10 @@ import androidx.core.content.ContextCompat;
public final class PermissionUtil {
public static final int PERMISSIONS_WRITE_EXTERNAL_STORAGE = 1;
public static final int PERMISSIONS_READ_CONTACTS_AUTOMATIC = 2;
- public static final int PERMISSIONS_READ_CONTACTS_MANUALLY = 3;
public static final int PERMISSIONS_WRITE_CONTACTS = 4;
public static final int PERMISSIONS_CAMERA = 5;
+ public static final int PERMISSIONS_READ_CALENDAR_AUTOMATIC = 6;
+ public static final int PERMISSIONS_WRITE_CALENDAR = 7;
private PermissionUtil() {
// utility class -> private constructor
diff --git a/src/main/java/third_parties/sufficientlysecure/AndroidCalendar.java b/src/main/java/third_parties/sufficientlysecure/AndroidCalendar.java
new file mode 100644
index 0000000000..770c8cf298
--- /dev/null
+++ b/src/main/java/third_parties/sufficientlysecure/AndroidCalendar.java
@@ -0,0 +1,160 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+package third_parties.sufficientlysecure;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class AndroidCalendar {
+ private static final String TAG = "ICS_AndroidCalendar";
+
+ public long mId;
+ public String mIdStr;
+ public String mName;
+ public String mDisplayName;
+ public String mAccountName;
+ public String mAccountType;
+ public String mOwner;
+ public boolean mIsActive;
+ public String mTimezone;
+ public int mNumEntries;
+
+ private static final String[] CAL_COLS = new String[]{
+ Calendars._ID,
+ Calendars.DELETED,
+ Calendars.NAME,
+ Calendars.CALENDAR_DISPLAY_NAME,
+ Calendars.ACCOUNT_NAME,
+ Calendars.ACCOUNT_TYPE,
+ Calendars.OWNER_ACCOUNT,
+ Calendars.VISIBLE,
+ Calendars.CALENDAR_TIME_ZONE};
+
+ 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.
+ // 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) {
+
+ if (missing(resolver, Calendars.CONTENT_URI) ||
+ missing(resolver, Events.CONTENT_URI)) {
+ return new ArrayList<>();
+ }
+
+ Cursor cur;
+ try {
+ cur = resolver.query(Calendars.CONTENT_URI, CAL_COLS, null, null, null);
+ } catch (Exception except) {
+ Log_OC.w(TAG, "Calendar provider is missing columns, continuing anyway");
+ cur = resolver.query(Calendars.CONTENT_URI, null, null, null, null);
+ }
+ List calendars = new ArrayList<>(cur.getCount());
+
+ while (cur.moveToNext()) {
+ if (getLong(cur, Calendars.DELETED) != 0) {
+ continue;
+ }
+
+ AndroidCalendar calendar = new AndroidCalendar();
+ calendar.mId = getLong(cur, Calendars._ID);
+ if (calendar.mId == -1) {
+ continue;
+ }
+ calendar.mIdStr = getString(cur, Calendars._ID);
+ calendar.mName = getString(cur, Calendars.NAME);
+ calendar.mDisplayName = getString(cur, Calendars.CALENDAR_DISPLAY_NAME);
+ calendar.mAccountName = getString(cur, Calendars.ACCOUNT_NAME);
+ calendar.mAccountType = getString(cur, Calendars.ACCOUNT_TYPE);
+ calendar.mOwner = getString(cur, Calendars.OWNER_ACCOUNT);
+ calendar.mIsActive = getLong(cur, Calendars.VISIBLE) == 1;
+ calendar.mTimezone = getString(cur, Calendars.CALENDAR_TIME_ZONE);
+
+ final String[] args = new String[]{calendar.mIdStr};
+ Cursor eventsCur = resolver.query(Events.CONTENT_URI, CAL_ID_COLS, CAL_ID_WHERE, args, null);
+ calendar.mNumEntries = eventsCur.getCount();
+ eventsCur.close();
+ calendars.add(calendar);
+ }
+ cur.close();
+
+ return calendars;
+ }
+
+ private static int getColumnIndex(Cursor cur, String dbName) {
+ return dbName == null ? -1 : cur.getColumnIndex(dbName);
+ }
+
+ private static long getLong(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? -1 : cur.getLong(i);
+ }
+
+ private static String getString(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? null : cur.getString(i);
+ }
+
+ private static boolean missing(ContentResolver resolver, Uri uri) {
+ // Determine if a provider is missing
+ ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
+ if (provider != null) {
+ provider.release();
+ }
+ return provider == null;
+ }
+
+ @Override
+ public String toString() {
+ return mDisplayName + " (" + mIdStr + ")";
+ }
+
+ private boolean differ(final String lhs, final String rhs) {
+ if (lhs == null) {
+ return rhs != null;
+ }
+ return rhs == null || !lhs.equals(rhs);
+ }
+
+ public boolean differsFrom(AndroidCalendar other) {
+ return mId != other.mId ||
+ mIsActive != other.mIsActive ||
+ mNumEntries != other.mNumEntries ||
+ differ(mName, other.mName) ||
+ differ(mDisplayName, other.mDisplayName) ||
+ differ(mAccountName, other.mAccountName) ||
+ differ(mAccountType, other.mAccountType) ||
+ differ(mOwner, other.mOwner) ||
+ differ(mTimezone, other.mTimezone);
+ }
+}
diff --git a/src/main/java/third_parties/sufficientlysecure/CalendarSource.java b/src/main/java/third_parties/sufficientlysecure/CalendarSource.java
new file mode 100644
index 0000000000..e9f2b58714
--- /dev/null
+++ b/src/main/java/third_parties/sufficientlysecure/CalendarSource.java
@@ -0,0 +1,96 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package third_parties.sufficientlysecure;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class CalendarSource {
+ private static final String HTTP_SEP = "://";
+
+ private URL mUrl = null;
+ private Uri mUri = null;
+ private final String mString;
+ private final String mUsername;
+ private final String mPassword;
+ private final Context context;
+
+ public CalendarSource(String url,
+ Uri uri,
+ String username,
+ String password,
+ Context context) throws MalformedURLException {
+ if (url != null) {
+ mUrl = new URL(url);
+ mString = mUrl.toString();
+ } else {
+ mUri = uri;
+ mString = uri.toString();
+ }
+ mUsername = username;
+ mPassword = password;
+ this.context = context;
+ }
+
+ public URLConnection getConnection() throws IOException {
+ if (mUsername != null) {
+ String protocol = mUrl.getProtocol();
+ String userPass = mUsername + ":" + mPassword;
+
+ if (protocol.equalsIgnoreCase("ftp") || protocol.equalsIgnoreCase("ftps")) {
+ String external = mUrl.toExternalForm();
+ String end = external.substring(protocol.length() + HTTP_SEP.length());
+ return new URL(protocol + HTTP_SEP + userPass + "@" + end).openConnection();
+ }
+
+ if (protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) {
+ String encoded = new String(new Base64().encode(userPass.getBytes("UTF-8")));
+ URLConnection connection = mUrl.openConnection();
+ connection.setRequestProperty("Authorization", "Basic " + encoded);
+ return connection;
+ }
+ }
+ return mUrl.openConnection();
+ }
+
+ public InputStream getStream() throws IOException {
+ if (mUri != null) {
+ return context.getContentResolver().openInputStream(mUri);
+ }
+ URLConnection c = this.getConnection();
+ return c == null ? null : c.getInputStream();
+ }
+
+ @Override
+ public String toString() {
+ return mString;
+ }
+}
diff --git a/src/main/java/third_parties/sufficientlysecure/DuplicateHandlingEnum.java b/src/main/java/third_parties/sufficientlysecure/DuplicateHandlingEnum.java
new file mode 100644
index 0000000000..85d846dd4b
--- /dev/null
+++ b/src/main/java/third_parties/sufficientlysecure/DuplicateHandlingEnum.java
@@ -0,0 +1,30 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2021 Tobias Kaminsky
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * 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 .
+ */
+
+package third_parties.sufficientlysecure;
+
+public enum DuplicateHandlingEnum {
+ DUP_REPLACE,
+ DUP_REPLACE_ANY,
+ DUP_IGNORE,
+ DUP_DONT_CHECK,
+}
diff --git a/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java b/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java
new file mode 100644
index 0000000000..60887c548b
--- /dev/null
+++ b/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2015 Jon Griffiths (jon_p_griffiths@yahoo.com)
+ * Copyright (C) 2013 Dominik Schürmann
+ * Copyright (C) 2010-2011 Lukas Aichbauer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package third_parties.sufficientlysecure;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.MailTo;
+import android.net.ParseException;
+import android.net.Uri;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.Reminders;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.nextcloud.client.preferences.AppPreferences;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import net.fortuna.ical4j.model.Calendar;
+import net.fortuna.ical4j.model.ComponentList;
+import net.fortuna.ical4j.model.DateTime;
+import net.fortuna.ical4j.model.Dur;
+import net.fortuna.ical4j.model.Parameter;
+import net.fortuna.ical4j.model.Property;
+import net.fortuna.ical4j.model.component.VAlarm;
+import net.fortuna.ical4j.model.component.VEvent;
+import net.fortuna.ical4j.model.parameter.FbType;
+import net.fortuna.ical4j.model.parameter.Related;
+import net.fortuna.ical4j.model.property.Action;
+import net.fortuna.ical4j.model.property.DateProperty;
+import net.fortuna.ical4j.model.property.Duration;
+import net.fortuna.ical4j.model.property.FreeBusy;
+import net.fortuna.ical4j.model.property.Transp;
+import net.fortuna.ical4j.model.property.Trigger;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+
+@SuppressLint("NewApi")
+public class ProcessVEvent {
+ private static final String TAG = "ICS_ProcessVEvent";
+
+ private static final Duration ONE_DAY = createDuration("P1D");
+ private static final Duration ZERO_SECONDS = createDuration("PT0S");
+
+ private static final String[] EVENT_QUERY_COLUMNS = new String[]{Events.CALENDAR_ID, Events._ID};
+ private static final int EVENT_QUERY_CALENDAR_ID_COL = 0;
+ private static final int EVENT_QUERY_ID_COL = 1;
+
+ private final Calendar mICalCalendar;
+ private final boolean mIsInserter;
+ private final AndroidCalendar selectedCal;
+
+ private Context context;
+
+ @Inject AppPreferences preferences;
+
+ // UID generation
+ long mUidMs = 0;
+ String mUidTail = null;
+
+ private final class Options {
+ private final List mDefaultReminders;
+
+ public Options(Context context) {
+ mDefaultReminders = new ArrayList<>(); // RemindersDialog.getSavedRemindersInMinutes(this); // TODO check
+ mDefaultReminders.add(0);
+ mDefaultReminders.add(5);
+ mDefaultReminders.add(10);
+ mDefaultReminders.add(30);
+ mDefaultReminders.add(60);
+ }
+
+ public List getReminders(List eventReminders) {
+ if (eventReminders.size() > 0 && getImportReminders()) {
+ return eventReminders;
+ }
+ return mDefaultReminders;
+ }
+
+ public boolean getKeepUids() {
+ return true; // upstream this is a setting // TODO check if we need to also have this as a setting
+ }
+
+ private boolean getImportReminders() {
+ return true; // upstream this is a setting // TODO check if we need to also have this as a setting
+ }
+
+ private boolean getGlobalUids() {
+ return false; // upstream this is a setting // TODO check if we need to also have this as a setting
+ }
+
+ private boolean getTestFileSupport() {
+ return false; // upstream this is a setting // TODO check if we need to also have this as a setting
+ }
+
+ public DuplicateHandlingEnum getDuplicateHandling() {
+// return DuplicateHandlingEnum.values()[getEnumInt(PREF_DUPLICATE_HANDLING, 0)];
+ return DuplicateHandlingEnum.values()[0]; // TODO is option needed?
+ }
+
+// private int getEnumInt(final String key, final int def) {
+// return Integer.parseInt(getString(key, String.valueOf(def)));
+// }
+ }
+
+ public ProcessVEvent(Context context, Calendar iCalCalendar, AndroidCalendar selectedCal, boolean isInserter) {
+ this.context = context;
+ mICalCalendar = iCalCalendar;
+ this.selectedCal = selectedCal;
+ mIsInserter = isInserter;
+ }
+
+ // TODO how to run?
+ public void run() throws Exception {
+ final Options options = new Options(context);
+ List reminders = new ArrayList<>();
+
+ ComponentList events = mICalCalendar.getComponents(VEvent.VEVENT);
+
+ ContentResolver resolver = context.getContentResolver();
+ int numDel = 0;
+ int numIns = 0;
+ int numDups = 0;
+
+ ContentValues cAlarm = new ContentValues();
+ cAlarm.put(Reminders.METHOD, Reminders.METHOD_ALERT);
+
+ final DuplicateHandlingEnum dupes = options.getDuplicateHandling();
+
+ Log_OC.i(TAG, (mIsInserter ? "Insert" : "Delete") + " for id " + selectedCal.mIdStr);
+ Log_OC.d(TAG, "Duplication option is " + dupes.ordinal());
+
+ for (Object ve : events) {
+ VEvent e = (VEvent) ve;
+ Log_OC.d(TAG, "source event: " + e.toString());
+
+ if (e.getRecurrenceId() != null) {
+ // FIXME: Support these edited instances
+ Log_OC.w(TAG, "Ignoring edited instance of a recurring event");
+ continue;
+ }
+
+ long insertCalendarId = selectedCal.mId; // Calendar id to insert to
+
+ ContentValues c = convertToDB(e, options, reminders, selectedCal.mId);
+
+ Cursor cur = null;
+ boolean mustDelete = !mIsInserter;
+
+ // Determine if we need to delete a duplicate event in order to update it
+ if (!mustDelete && dupes != DuplicateHandlingEnum.DUP_DONT_CHECK) {
+
+ cur = query(resolver, options, c);
+ while (!mustDelete && cur != null && cur.moveToNext()) {
+ if (dupes == DuplicateHandlingEnum.DUP_REPLACE) {
+ mustDelete = cur.getLong(EVENT_QUERY_CALENDAR_ID_COL) == selectedCal.mId;
+ } else {
+ mustDelete = true; // Replacing all (or ignoring, handled just below)
+ }
+ }
+
+ if (mustDelete) {
+ if (dupes == DuplicateHandlingEnum.DUP_IGNORE) {
+ Log_OC.i(TAG, "Avoiding inserting a duplicate event");
+ numDups++;
+ cur.close();
+ continue;
+ }
+ cur.moveToPosition(-1); // Rewind for use below
+ }
+ }
+
+ if (mustDelete) {
+ if (cur == null) {
+ cur = query(resolver, options, c);
+ }
+
+ while (cur != null && cur.moveToNext()) {
+ long rowCalendarId = cur.getLong(EVENT_QUERY_CALENDAR_ID_COL);
+
+ if (dupes == DuplicateHandlingEnum.DUP_REPLACE
+ && rowCalendarId != selectedCal.mId) {
+ Log_OC.i(TAG, "Avoiding deleting duplicate event in calendar " + rowCalendarId);
+ continue; // Not in the destination calendar
+ }
+
+ String id = cur.getString(EVENT_QUERY_ID_COL);
+ Uri eventUri = Uri.withAppendedPath(Events.CONTENT_URI, id);
+ numDel += resolver.delete(eventUri, null, null);
+ String where = Reminders.EVENT_ID + "=?";
+ resolver.delete(Reminders.CONTENT_URI, where, new String[]{id});
+ if (mIsInserter && rowCalendarId != selectedCal.mId
+ && dupes == DuplicateHandlingEnum.DUP_REPLACE_ANY) {
+ // Must update this event in the calendar this row came from
+ Log_OC.i(TAG, "Changing calendar: " + rowCalendarId + " to " + insertCalendarId);
+ insertCalendarId = rowCalendarId;
+ }
+ }
+ }
+
+ if (cur != null) {
+ cur.close();
+ }
+
+ if (!mIsInserter) {
+ continue;
+ }
+
+ if (Events.UID_2445 != null && !c.containsKey(Events.UID_2445)) {
+ // Create a UID for this event to use. We create it here so if
+ // exported multiple times it will always have the same id.
+ c.put(Events.UID_2445, generateUid()); // TODO use
+ }
+
+ c.put(Events.CALENDAR_ID, insertCalendarId);
+ if (options.getTestFileSupport()) {
+ processEventTests(e, c, reminders);
+ numIns++;
+ continue;
+ }
+
+ Uri uri = insertAndLog(resolver, Events.CONTENT_URI, c, "Event");
+ if (uri == null) {
+ continue;
+ }
+
+ final long id = Long.parseLong(uri.getLastPathSegment());
+
+ for (int time : options.getReminders(reminders)) {
+ cAlarm.put(Reminders.EVENT_ID, id);
+ cAlarm.put(Reminders.MINUTES, time);
+ insertAndLog(resolver, Reminders.CONTENT_URI, cAlarm, "Reminder");
+ }
+ numIns++;
+ }
+
+ selectedCal.mNumEntries += numIns;
+ selectedCal.mNumEntries -= numDel;
+
+ Resources res = context.getResources();
+ int n = mIsInserter ? numIns : numDel;
+ String msg = res.getQuantityString(R.plurals.processed_n_entries, n, n) + "\n";
+ if (mIsInserter) {
+ msg += "\n";
+ if (options.getDuplicateHandling() == DuplicateHandlingEnum.DUP_DONT_CHECK) {
+ msg += res.getString(R.string.did_not_check_for_dupes);
+ } else {
+ msg += res.getQuantityString(R.plurals.found_n_duplicates, numDups, numDups);
+ }
+ }
+
+ // TODO show failure in starting context
+ // DisplayUtils.showSnackMessage(context, msg);
+ }
+
+ // Munge a VEvent so Android won't reject it, then convert to ContentValues for inserting
+ private ContentValues convertToDB(VEvent e, Options options,
+ List reminders, long calendarId) {
+ reminders.clear();
+
+ boolean allDay = false;
+ boolean startIsDate = !(e.getStartDate().getDate() instanceof DateTime);
+ boolean isRecurring = hasProperty(e, Property.RRULE) || hasProperty(e, Property.RDATE);
+
+ if (startIsDate) {
+ // If the start date is a DATE we expect the end date to be a date too and the
+ // event is all-day, midnight to midnight (RFC 2445).
+ allDay = true;
+ }
+
+ if (!hasProperty(e, Property.DTEND) && !hasProperty(e, Property.DURATION)) {
+ // No end date or duration given.
+ // Since we added a duration above when the start date is a DATE:
+ // - The start date is a DATETIME, the event lasts no time at all (RFC 2445).
+ e.getProperties().add(ZERO_SECONDS);
+ // Zero time events are always free (RFC 2445), so override/set TRANSP accordingly.
+ removeProperty(e, Property.TRANSP);
+ e.getProperties().add(Transp.TRANSPARENT);
+ }
+
+ if (isRecurring) {
+ // Recurring event. Android insists on a duration.
+ if (!hasProperty(e, Property.DURATION)) {
+ // Calculate duration from start to end date
+ Duration d = new Duration(e.getStartDate().getDate(), e.getEndDate().getDate());
+ e.getProperties().add(d);
+ }
+ removeProperty(e, Property.DTEND);
+ } else {
+ // Non-recurring event. Android insists on an end date.
+ if (!hasProperty(e, Property.DTEND)) {
+ // Calculate end date from duration, set it and remove the duration.
+ e.getProperties().add(e.getEndDate());
+ }
+ removeProperty(e, Property.DURATION);
+ }
+
+ // Now calculate the db values for the event
+ ContentValues c = new ContentValues();
+
+ c.put(Events.CALENDAR_ID, calendarId);
+ copyProperty(c, Events.TITLE, e, Property.SUMMARY);
+ copyProperty(c, Events.DESCRIPTION, e, Property.DESCRIPTION);
+
+ if (e.getOrganizer() != null) {
+ URI uri = e.getOrganizer().getCalAddress();
+ try {
+ MailTo mailTo = MailTo.parse(uri.toString());
+ c.put(Events.ORGANIZER, mailTo.getTo());
+ c.put(Events.GUESTS_CAN_MODIFY, 1); // Ensure we can edit if not the organiser
+ } catch (ParseException ignored) {
+ Log_OC.e(TAG, "Failed to parse Organiser URI " + uri.toString());
+ }
+ }
+
+ copyProperty(c, Events.EVENT_LOCATION, e, Property.LOCATION);
+
+ if (hasProperty(e, Property.STATUS)) {
+ String status = e.getProperty(Property.STATUS).getValue();
+ switch (status) {
+ case "TENTATIVE":
+ c.put(Events.STATUS, Events.STATUS_TENTATIVE);
+ break;
+ case "CONFIRMED":
+ c.put(Events.STATUS, Events.STATUS_CONFIRMED);
+ break;
+ case "CANCELLED": // NOTE: In ical4j it is CANCELLED with two L
+ c.put(Events.STATUS, Events.STATUS_CANCELED);
+ break;
+ }
+ }
+
+ copyProperty(c, Events.DURATION, e, Property.DURATION);
+
+ if (allDay) {
+ c.put(Events.ALL_DAY, 1);
+ }
+
+ copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, e.getStartDate());
+ if (hasProperty(e, Property.DTEND)) {
+ copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, e.getEndDate());
+ }
+
+ if (hasProperty(e, Property.CLASS)) {
+ String access = e.getProperty(Property.CLASS).getValue();
+ int accessLevel = Events.ACCESS_DEFAULT;
+ switch (access) {
+ case "CONFIDENTIAL":
+ accessLevel = Events.ACCESS_CONFIDENTIAL;
+ break;
+ case "PRIVATE":
+ accessLevel = Events.ACCESS_PRIVATE;
+ break;
+ case "PUBLIC":
+ accessLevel = Events.ACCESS_PUBLIC;
+ break;
+ }
+
+ c.put(Events.ACCESS_LEVEL, accessLevel);
+ }
+
+ // Work out availability. This is confusing as FREEBUSY and TRANSP overlap.
+ if (Events.AVAILABILITY != null) {
+ int availability = Events.AVAILABILITY_BUSY;
+ if (hasProperty(e, Property.TRANSP)) {
+ if (e.getTransparency() == Transp.TRANSPARENT) {
+ availability = Events.AVAILABILITY_FREE;
+ }
+
+ } else if (hasProperty(e, Property.FREEBUSY)) {
+ FreeBusy fb = (FreeBusy) e.getProperty(Property.FREEBUSY);
+ FbType fbType = (FbType) fb.getParameter(Parameter.FBTYPE);
+ if (fbType != null && fbType == FbType.FREE) {
+ availability = Events.AVAILABILITY_FREE;
+ } else if (fbType != null && fbType == FbType.BUSY_TENTATIVE) {
+ availability = Events.AVAILABILITY_TENTATIVE;
+ }
+ }
+ c.put(Events.AVAILABILITY, availability);
+ }
+
+ copyProperty(c, Events.RRULE, e, Property.RRULE);
+ copyProperty(c, Events.RDATE, e, Property.RDATE);
+ copyProperty(c, Events.EXRULE, e, Property.EXRULE);
+ copyProperty(c, Events.EXDATE, e, Property.EXDATE);
+ copyProperty(c, Events.CUSTOM_APP_URI, e, Property.URL);
+ copyProperty(c, Events.UID_2445, e, Property.UID);
+ if (c.containsKey(Events.UID_2445) && TextUtils.isEmpty(c.getAsString(Events.UID_2445))) {
+ // Remove null/empty UIDs
+ c.remove(Events.UID_2445);
+ }
+
+ for (Object alarm : e.getAlarms()) {
+ VAlarm a = (VAlarm) alarm;
+
+ if (a.getAction() != Action.AUDIO && a.getAction() != Action.DISPLAY) {
+ continue; // Ignore email and procedure alarms
+ }
+
+ Trigger t = a.getTrigger();
+ final long startMs = e.getStartDate().getDate().getTime();
+ long alarmStartMs = startMs;
+ long alarmMs;
+
+ // FIXME: - Support for repeating alarms
+ // - Check the calendars max number of alarms
+ if (t.getDateTime() != null) {
+ alarmMs = t.getDateTime().getTime(); // Absolute
+ } else if (t.getDuration() != null && t.getDuration().isNegative()) {
+ Related rel = (Related) t.getParameter(Parameter.RELATED);
+ if (rel != null && rel == Related.END) {
+ alarmStartMs = e.getEndDate().getDate().getTime();
+ }
+ alarmMs = alarmStartMs - durationToMs(t.getDuration()); // Relative
+ } else {
+ continue;
+ }
+
+ int reminder = (int) ((startMs - alarmMs) / DateUtils.MINUTE_IN_MILLIS);
+ if (reminder >= 0 && !reminders.contains(reminder)) {
+ reminders.add(reminder);
+ }
+ }
+
+ if (options.getReminders(reminders).size() > 0) {
+ c.put(Events.HAS_ALARM, 1);
+ }
+
+ // FIXME: Attendees, SELF_ATTENDEE_STATUS
+ return c;
+ }
+
+ private static Duration createDuration(String value) {
+ Duration d = new Duration();
+ d.setValue(value);
+ return d;
+ }
+
+ private static long durationToMs(Dur d) {
+ long ms = 0;
+ ms += d.getSeconds() * DateUtils.SECOND_IN_MILLIS;
+ ms += d.getMinutes() * DateUtils.MINUTE_IN_MILLIS;
+ ms += d.getHours() * DateUtils.HOUR_IN_MILLIS;
+ ms += d.getDays() * DateUtils.DAY_IN_MILLIS;
+ ms += d.getWeeks() * DateUtils.WEEK_IN_MILLIS;
+ return ms;
+ }
+
+ private boolean hasProperty(VEvent e, String name) {
+ return e.getProperty(name) != null;
+ }
+
+ private void removeProperty(VEvent e, String name) {
+ Property p = e.getProperty(name);
+ if (p != null) {
+ e.getProperties().remove(p);
+ }
+ }
+
+ private void copyProperty(ContentValues c, String dbName, VEvent e, String evName) {
+ if (dbName != null) {
+ Property p = e.getProperty(evName);
+ if (p != null) {
+ c.put(dbName, p.getValue());
+ }
+ }
+ }
+
+ private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty date) {
+ if (dbName != null && date.getDate() != null) {
+ c.put(dbName, date.getDate().getTime()); // ms since epoc in GMT
+ if (dbTzName != null) {
+ if (date.isUtc() || date.getTimeZone() == null) {
+ c.put(dbTzName, "UTC");
+ } else {
+ c.put(dbTzName, date.getTimeZone().getID());
+ }
+ }
+ }
+ }
+
+ private Uri insertAndLog(ContentResolver resolver, Uri uri, ContentValues c, String type) {
+ Log_OC.d(TAG, "Inserting " + type + " values: " + c);
+
+ Uri result = resolver.insert(uri, c);
+ if (result == null) {
+ Log_OC.e(TAG, "failed to insert " + type);
+ Log_OC.e(TAG, "failed " + type + " values: " + c); // Not already logged, dump now
+ } else {
+ Log_OC.d(TAG, "Insert " + type + " returned " + result.toString());
+ }
+ return result;
+ }
+
+ private Cursor queryEvents(ContentResolver resolver, StringBuilder b, List argsList) {
+ final String where = b.toString();
+ final String[] args = argsList.toArray(new String[argsList.size()]);
+ return resolver.query(Events.CONTENT_URI, EVENT_QUERY_COLUMNS, where, args, null);
+ }
+
+ private Cursor query(ContentResolver resolver, Options options, ContentValues c) {
+
+ StringBuilder b = new StringBuilder();
+ List argsList = new ArrayList<>();
+
+ if (options.getKeepUids() && Events.UID_2445 != null && c.containsKey(Events.UID_2445)) {
+ // Use our UID to query, either globally or per-calendar unique
+ if (!options.getGlobalUids()) {
+ b.append(Events.CALENDAR_ID).append("=? AND ");
+ argsList.add(c.getAsString(Events.CALENDAR_ID));
+ }
+ b.append(Events.UID_2445).append("=?");
+ argsList.add(c.getAsString(Events.UID_2445));
+ return queryEvents(resolver, b, argsList);
+ }
+
+ // Without UIDs, the best we can do is check the start date and title within
+ // the current calendar, even though this may return false duplicates.
+ if (!c.containsKey(Events.CALENDAR_ID) || !c.containsKey(Events.DTSTART)) {
+ return null;
+ }
+
+ b.append(Events.CALENDAR_ID).append("=? AND ");
+ b.append(Events.DTSTART).append("=? AND ");
+ b.append(Events.TITLE);
+
+ argsList.add(c.getAsString(Events.CALENDAR_ID));
+ argsList.add(c.getAsString(Events.DTSTART));
+
+ if (c.containsKey(Events.TITLE)) {
+ b.append("=?");
+ argsList.add(c.getAsString(Events.TITLE));
+ } else {
+ b.append(" is null");
+ }
+
+ return queryEvents(resolver, b, argsList);
+ }
+
+ private void checkTestValue(VEvent e, ContentValues c, String keyValue, String testName) {
+ String[] parts = keyValue.split("=");
+ String key = parts[0];
+ String expected = parts.length > 1 ? parts[1] : "";
+ String got = c.getAsString(key);
+
+ if (expected.equals("") && got != null) {
+ got = ""; // Sentinel for testing present and non-null
+ }
+ if (got == null) {
+ got = ""; // Sentinel for testing not present values
+ }
+
+ if (!expected.equals(got)) {
+ Log_OC.e(TAG, " " + keyValue + " -> FAILED");
+ Log_OC.e(TAG, " values: " + c);
+ String error = "Test " + testName + " FAILED, expected '" + keyValue + "', got '" + got + "'";
+ throw new RuntimeException(error);
+ }
+ Log_OC.i(TAG, " " + keyValue + " -> PASSED");
+ }
+
+ private void processEventTests(VEvent e, ContentValues c, List reminders) {
+
+ Property testName = e.getProperty("X-TEST-NAME");
+ if (testName == null) {
+ return; // Not a test case
+ }
+
+ // This is a test event. Verify it using the embedded meta data.
+ Log_OC.i(TAG, "Processing test case " + testName.getValue() + "...");
+
+ String reminderValues = "";
+ String sep = "";
+ for (Integer i : reminders) {
+ reminderValues += sep + i;
+ sep = ",";
+ }
+ c.put("reminders", reminderValues);
+
+ for (Object o : e.getProperties()) {
+ Property p = (Property) o;
+ switch (p.getName()) {
+ case "X-TEST-VALUE":
+ checkTestValue(e, c, p.getValue(), testName.getValue());
+ break;
+ case "X-TEST-MIN-VERSION":
+ final int ver = Integer.parseInt(p.getValue());
+ if (android.os.Build.VERSION.SDK_INT < ver) {
+ Log_OC.e(TAG, " -> SKIPPED (MIN-VERSION < " + ver + ")");
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ // TODO move this to some common place
+ private String generateUid() {
+ // Generated UIDs take the form -@nextcloud.com.
+ if (mUidTail == null) {
+ String uidPid = preferences.getUidPid();
+ if (uidPid.length() == 0) {
+ uidPid = UUID.randomUUID().toString().replace("-", "");
+ preferences.setUidPid(uidPid);
+ }
+ mUidTail = uidPid + "@nextcloud.com";
+ }
+
+ mUidMs = Math.max(mUidMs, System.currentTimeMillis());
+ String uid = mUidMs + mUidTail;
+ mUidMs++;
+
+ return uid;
+ }
+}
diff --git a/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java b/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java
new file mode 100644
index 0000000000..7a89395180
--- /dev/null
+++ b/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2015 Jon Griffiths (jon_p_griffiths@yahoo.com)
+ * Copyright (C) 2013 Dominik Schürmann
+ * Copyright (C) 2010-2011 Lukas Aichbauer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package third_parties.sufficientlysecure;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.Reminders;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.files.downloader.PostUploadAction;
+import com.nextcloud.client.files.downloader.Request;
+import com.nextcloud.client.files.downloader.TransferManagerConnection;
+import com.nextcloud.client.files.downloader.UploadRequest;
+import com.nextcloud.client.files.downloader.UploadTrigger;
+import com.nextcloud.client.preferences.AppPreferences;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.services.NameCollisionPolicy;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import net.fortuna.ical4j.data.CalendarOutputter;
+import net.fortuna.ical4j.model.Calendar;
+import net.fortuna.ical4j.model.Date;
+import net.fortuna.ical4j.model.DateTime;
+import net.fortuna.ical4j.model.Dur;
+import net.fortuna.ical4j.model.Period;
+import net.fortuna.ical4j.model.Property;
+import net.fortuna.ical4j.model.PropertyFactoryImpl;
+import net.fortuna.ical4j.model.PropertyList;
+import net.fortuna.ical4j.model.TimeZone;
+import net.fortuna.ical4j.model.TimeZoneRegistry;
+import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
+import net.fortuna.ical4j.model.component.VAlarm;
+import net.fortuna.ical4j.model.component.VEvent;
+import net.fortuna.ical4j.model.parameter.FbType;
+import net.fortuna.ical4j.model.property.Action;
+import net.fortuna.ical4j.model.property.CalScale;
+import net.fortuna.ical4j.model.property.Description;
+import net.fortuna.ical4j.model.property.DtEnd;
+import net.fortuna.ical4j.model.property.DtStamp;
+import net.fortuna.ical4j.model.property.DtStart;
+import net.fortuna.ical4j.model.property.Duration;
+import net.fortuna.ical4j.model.property.FreeBusy;
+import net.fortuna.ical4j.model.property.Method;
+import net.fortuna.ical4j.model.property.Organizer;
+import net.fortuna.ical4j.model.property.ProdId;
+import net.fortuna.ical4j.model.property.Transp;
+import net.fortuna.ical4j.model.property.Version;
+import net.fortuna.ical4j.model.property.XProperty;
+import net.fortuna.ical4j.util.CompatibilityHints;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+@SuppressLint("NewApi")
+public class SaveCalendar implements Injectable {
+ private static final String TAG = "ICS_SaveCalendar";
+
+ private final PropertyFactoryImpl mPropertyFactory = PropertyFactoryImpl.getInstance();
+ private TimeZoneRegistry mTzRegistry;
+ private final Set mInsertedTimeZones = new HashSet<>();
+ private final Set mFailedOrganisers = new HashSet<>();
+ boolean mAllCols;
+ private final Context activity;
+ private final AndroidCalendar selectedCal;
+ private final AppPreferences preferences;
+ private final User user;
+
+ // UID generation
+ long mUidMs = 0;
+ String mUidTail = null;
+
+ private static final List STATUS_ENUM = Arrays.asList("TENTATIVE", "CONFIRMED", "CANCELLED");
+ private static final List CLASS_ENUM = Arrays.asList(null, "CONFIDENTIAL", "PRIVATE", "PUBLIC");
+ private static final List AVAIL_ENUM = Arrays.asList(null, "FREE", "BUSY-TENTATIVE");
+
+ private static final String[] EVENT_COLS = new String[]{
+ Events._ID, Events.ORIGINAL_ID, Events.UID_2445, Events.TITLE, Events.DESCRIPTION,
+ Events.ORGANIZER, Events.EVENT_LOCATION, Events.STATUS, Events.ALL_DAY, Events.RDATE,
+ Events.RRULE, Events.DTSTART, Events.EVENT_TIMEZONE, Events.DURATION, Events.DTEND,
+ Events.EVENT_END_TIMEZONE, Events.ACCESS_LEVEL, Events.AVAILABILITY, Events.EXDATE,
+ Events.EXRULE, Events.CUSTOM_APP_PACKAGE, Events.CUSTOM_APP_URI, Events.HAS_ALARM
+ };
+
+ private static final String[] REMINDER_COLS = new String[]{
+ Reminders.MINUTES, Reminders.METHOD
+ };
+
+ public SaveCalendar(Context activity, AndroidCalendar calendar, AppPreferences preferences, User user) {
+ this.activity = activity; // TODO rename
+ this.selectedCal = calendar;
+ this.preferences = preferences;
+ this.user = user;
+ }
+
+ public void start() throws Exception {
+ mInsertedTimeZones.clear();
+ mFailedOrganisers.clear();
+ mAllCols = false;
+
+ String file = selectedCal.mDisplayName + "_" +
+ DateFormat.format("yyyy-MM-dd_HH-mm-ss", java.util.Calendar.getInstance()).toString() +
+ ".ics";
+
+ File fileName = new File(activity.getCacheDir(), file);
+
+ Log_OC.i(TAG, "Save id " + selectedCal.mIdStr + " to file " + fileName.getAbsolutePath());
+
+ String name = activity.getPackageName();
+ String ver;
+ try {
+ ver = activity.getPackageManager().getPackageInfo(name, 0).versionName;
+ } catch (NameNotFoundException e) {
+ ver = "Unknown Build";
+ }
+
+ String prodId = "-//" + selectedCal.mOwner + "//iCal Import/Export " + ver + "//EN";
+ Calendar cal = new Calendar();
+ cal.getProperties().add(new ProdId(prodId));
+ cal.getProperties().add(Version.VERSION_2_0);
+ cal.getProperties().add(Method.PUBLISH);
+ cal.getProperties().add(CalScale.GREGORIAN);
+
+ if (selectedCal.mTimezone != null) {
+ // We don't write any events with floating times, but export this
+ // anyway so the default timezone for new events is correct when
+ // the file is imported into a system that supports it.
+ cal.getProperties().add(new XProperty("X-WR-TIMEZONE", selectedCal.mTimezone));
+ }
+
+ // query events
+ ContentResolver resolver = activity.getContentResolver();
+ int numberOfCreatedUids = 0;
+ if (Events.UID_2445 != null) {
+ numberOfCreatedUids = ensureUids(activity, resolver, selectedCal);
+ }
+ boolean relaxed = true; // settings.getIcal4jValidationRelaxed(); // TODO is this option needed? default true
+ CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, relaxed);
+ List events = getEvents(resolver, selectedCal, cal);
+
+ for (VEvent v : events) {
+ cal.getComponents().add(v);
+ }
+
+ new CalendarOutputter().output(cal, new FileOutputStream(fileName));
+
+ Resources res = activity.getResources();
+ String msg = res.getQuantityString(R.plurals.wrote_n_events_to, events.size(), events.size(), file);
+ if (numberOfCreatedUids > 0) {
+ msg += "\n" + res.getQuantityString(R.plurals.created_n_uids_to, numberOfCreatedUids, numberOfCreatedUids);
+ }
+
+ // TODO replace DisplayUtils.showSnackMessage(activity, msg);
+
+ upload(fileName);
+ }
+
+ private int ensureUids(Context activity, ContentResolver resolver, AndroidCalendar cal) {
+ String[] cols = new String[]{Events._ID};
+ String[] args = new String[]{cal.mIdStr};
+ Map newUids = new HashMap<>();
+ Cursor cur = resolver.query(Events.CONTENT_URI, cols,
+ Events.CALENDAR_ID + " = ? AND " + Events.UID_2445 + " IS NULL", args, null);
+ while (cur.moveToNext()) {
+ Long id = getLong(cur, Events._ID);
+ String uid = generateUid();
+ newUids.put(id, uid);
+ }
+ for (Long id : newUids.keySet()) {
+ String uid = newUids.get(id);
+ Uri updateUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id);
+ ContentValues c = new ContentValues();
+ c.put(Events.UID_2445, uid);
+ resolver.update(updateUri, c, null, null);
+ Log_OC.i(TAG, "Generated UID " + uid + " for event " + id);
+ }
+ return newUids.size();
+ }
+
+ private List getEvents(ContentResolver resolver, AndroidCalendar cal_src, Calendar cal_dst) {
+ String where = Events.CALENDAR_ID + "=?";
+ String[] args = new String[]{cal_src.mIdStr};
+ String sortBy = Events.CALENDAR_ID + " ASC";
+ Cursor cur;
+ try {
+ cur = resolver.query(Events.CONTENT_URI, mAllCols ? null : EVENT_COLS,
+ where, args, sortBy);
+ } catch (Exception except) {
+ Log_OC.w(TAG, "Calendar provider is missing columns, continuing anyway");
+ int n = 0;
+ for (n = 0; n < EVENT_COLS.length; ++n) {
+ if (EVENT_COLS[n] == null) {
+ Log_OC.e(TAG, "Invalid EVENT_COLS index " + Integer.toString(n));
+ }
+ }
+ cur = resolver.query(Events.CONTENT_URI, null, where, args, sortBy);
+ }
+
+ DtStamp timestamp = new DtStamp(); // Same timestamp for all events
+
+ // Collect up events and add them after any timezones
+ List events = new ArrayList<>();
+ while (cur.moveToNext()) {
+ VEvent e = convertFromDb(cur, cal_dst, timestamp);
+ if (e != null) {
+ events.add(e);
+ Log_OC.d(TAG, "Adding event: " + e.toString());
+ }
+ }
+ cur.close();
+ return events;
+ }
+
+ private String calculateFileName(final String displayName) {
+ // Replace all non-alnum chars with '_'
+ String stripped = displayName.replaceAll("[^a-zA-Z0-9_-]", "_");
+ // Replace repeated '_' with a single '_'
+ return stripped.replaceAll("(_)\\1{1,}", "$1");
+ }
+
+ private void getFileImpl(final String previousFile, final String suggestedFile,
+ final String[] result) {
+
+ final EditText input = new EditText(activity);
+ input.setHint(R.string.destination_filename);
+ input.setText(previousFile);
+ input.selectAll();
+
+ final int ok = android.R.string.ok;
+ final int cancel = android.R.string.cancel;
+ final int suggest = R.string.suggest;
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ AlertDialog dlg = builder.setIcon(R.mipmap.ic_launcher)
+ .setTitle(R.string.enter_destination_filename)
+ .setView(input)
+ .setPositiveButton(ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface iface, int id) {
+ result[0] = input.getText().toString();
+ }
+ })
+ .setNeutralButton(suggest, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface iface, int id) {
+ }
+ })
+ .setNegativeButton(cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface iface, int id) {
+ result[0] = "";
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface iface) {
+ result[0] = "";
+ }
+ })
+ .create();
+ int state = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+ dlg.getWindow().setSoftInputMode(state);
+ dlg.show();
+ // Overriding 'Suggest' here prevents it from closing the dialog
+ dlg.getButton(DialogInterface.BUTTON_NEUTRAL)
+ .setOnClickListener(new View.OnClickListener() {
+ public void onClick(View onClick) {
+ input.setText(suggestedFile);
+ input.setSelection(input.getText().length());
+ }
+ });
+ }
+
+ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) {
+ Log_OC.d(TAG, "cursor: " + DatabaseUtils.dumpCurrentRowToString(cur));
+
+ if (hasStringValue(cur, Events.ORIGINAL_ID)) {
+ // FIXME: Support these edited instances
+ Log_OC.w(TAG, "Ignoring edited instance of a recurring event");
+ return null;
+ }
+
+ PropertyList l = new PropertyList();
+ l.add(timestamp);
+ copyProperty(l, Property.UID, cur, Events.UID_2445);
+
+ String summary = copyProperty(l, Property.SUMMARY, cur, Events.TITLE);
+ String description = copyProperty(l, Property.DESCRIPTION, cur, Events.DESCRIPTION);
+
+ String organizer = getString(cur, Events.ORGANIZER);
+ if (!TextUtils.isEmpty(organizer)) {
+ // The check for mailto: here handles early versions of this code which
+ // incorrectly left it in the organizer column.
+ if (!organizer.startsWith("mailto:")) {
+ organizer = "mailto:" + organizer;
+ }
+ try {
+ l.add(new Organizer(organizer));
+ } catch (URISyntaxException ignored) {
+ if (!mFailedOrganisers.contains(organizer)) {
+ Log_OC.e(TAG, "Failed to create mailTo for organizer " + organizer);
+ mFailedOrganisers.add(organizer);
+ }
+ }
+ }
+
+ copyProperty(l, Property.LOCATION, cur, Events.EVENT_LOCATION);
+ copyEnumProperty(l, Property.STATUS, cur, Events.STATUS, STATUS_ENUM);
+
+ boolean allDay = TextUtils.equals(getString(cur, Events.ALL_DAY), "1");
+ boolean isTransparent;
+ DtEnd dtEnd = null;
+
+ if (allDay) {
+ // All day event
+ isTransparent = true;
+ Date start = getDateTime(cur, Events.DTSTART, null, null);
+ Date end = getDateTime(cur, Events.DTEND, null, null);
+ l.add(new DtStart(new Date(start)));
+
+ if (end != null) {
+ dtEnd = new DtEnd(new Date(end));
+ } else {
+ dtEnd = new DtEnd(utcDateFromMs(start.getTime() + DateUtils.DAY_IN_MILLIS));
+ }
+
+ l.add(dtEnd);
+ } else {
+ // Regular or zero-time event. Start date must be a date-time
+ Date startDate = getDateTime(cur, Events.DTSTART, Events.EVENT_TIMEZONE, cal);
+ l.add(new DtStart(startDate));
+
+ // Use duration if we have one, otherwise end date
+ if (hasStringValue(cur, Events.DURATION)) {
+ isTransparent = getString(cur, Events.DURATION).equals("PT0S");
+ if (!isTransparent) {
+ copyProperty(l, Property.DURATION, cur, Events.DURATION);
+ }
+ } else {
+ String endTz = Events.EVENT_END_TIMEZONE;
+ if (endTz == null) {
+ endTz = Events.EVENT_TIMEZONE;
+ }
+ Date end = getDateTime(cur, Events.DTEND, endTz, cal);
+ dtEnd = new DtEnd(end);
+ isTransparent = startDate.getTime() == end.getTime();
+ if (!isTransparent) {
+ l.add(dtEnd);
+ }
+ }
+ }
+
+ copyEnumProperty(l, Property.CLASS, cur, Events.ACCESS_LEVEL, CLASS_ENUM);
+
+ int availability = getInt(cur, Events.AVAILABILITY);
+ if (availability > Events.AVAILABILITY_TENTATIVE) {
+ availability = -1; // Unknown/Invalid
+ }
+
+ if (isTransparent) {
+ // This event is ordinarily transparent. If availability shows that its
+ // not free, then mark it opaque.
+ if (availability >= 0 && availability != Events.AVAILABILITY_FREE) {
+ l.add(Transp.OPAQUE);
+ }
+
+ } else if (availability > Events.AVAILABILITY_BUSY) {
+ // This event is ordinarily busy but differs, so output a FREEBUSY
+ // period covering the time of the event
+ FreeBusy fb = new FreeBusy();
+ fb.getParameters().add(new FbType(AVAIL_ENUM.get(availability)));
+ DateTime start = new DateTime(((DtStart) l.getProperty(Property.DTSTART)).getDate());
+
+ if (dtEnd != null) {
+ fb.getPeriods().add(new Period(start, new DateTime(dtEnd.getDate())));
+ } else {
+ Duration d = (Duration) l.getProperty(Property.DURATION);
+ fb.getPeriods().add(new Period(start, d.getDuration()));
+ }
+ l.add(fb);
+ }
+
+ copyProperty(l, Property.RRULE, cur, Events.RRULE);
+ copyProperty(l, Property.RDATE, cur, Events.RDATE);
+ copyProperty(l, Property.EXRULE, cur, Events.EXRULE);
+ copyProperty(l, Property.EXDATE, cur, Events.EXDATE);
+ if (TextUtils.isEmpty(getString(cur, Events.CUSTOM_APP_PACKAGE))) {
+ // Only copy URL if there is no app i.e. we probably imported it.
+ copyProperty(l, Property.URL, cur, Events.CUSTOM_APP_URI);
+ }
+
+ VEvent e = new VEvent(l);
+
+ if (getInt(cur, Events.HAS_ALARM) == 1) {
+ // Add alarms
+
+ String s = summary == null ? (description == null ? "" : description) : summary;
+ Description desc = new Description(s);
+
+ ContentResolver resolver = activity.getContentResolver();
+ long eventId = getLong(cur, Events._ID);
+ Cursor alarmCur;
+ alarmCur = Reminders.query(resolver, eventId, mAllCols ? null : REMINDER_COLS);
+ while (alarmCur.moveToNext()) {
+ int mins = getInt(alarmCur, Reminders.MINUTES);
+ if (mins == -1) {
+ mins = 60; // FIXME: Get the real default
+ }
+
+ // FIXME: We should support other types if possible
+ int method = getInt(alarmCur, Reminders.METHOD);
+ if (method == Reminders.METHOD_DEFAULT || method == Reminders.METHOD_ALERT) {
+ VAlarm alarm = new VAlarm(new Dur(0, 0, -mins, 0));
+ alarm.getProperties().add(Action.DISPLAY);
+ alarm.getProperties().add(desc);
+ e.getAlarms().add(alarm);
+ }
+ }
+ alarmCur.close();
+ }
+
+ return e;
+ }
+
+ private int getColumnIndex(Cursor cur, String dbName) {
+ return dbName == null ? -1 : cur.getColumnIndex(dbName);
+ }
+
+ private String getString(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? null : cur.getString(i);
+ }
+
+ private long getLong(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? -1 : cur.getLong(i);
+ }
+
+ private int getInt(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i == -1 ? -1 : cur.getInt(i);
+ }
+
+ private boolean hasStringValue(Cursor cur, String dbName) {
+ int i = getColumnIndex(cur, dbName);
+ return i != -1 && !TextUtils.isEmpty(cur.getString(i));
+ }
+
+ private Date utcDateFromMs(long ms) {
+ // This date will be UTC provided the default false value of the iCal4j property
+ // "net.fortuna.ical4j.timezone.date.floating" has not been changed.
+ return new Date(ms);
+ }
+
+ private boolean isUtcTimeZone(final String tz) {
+ if (TextUtils.isEmpty(tz)) {
+ return true;
+ }
+ final String utz = tz.toUpperCase(Locale.US);
+ return utz.equals("UTC") || utz.equals("UTC-0") || utz.equals("UTC+0") || utz.endsWith("/UTC");
+ }
+
+ private Date getDateTime(Cursor cur, String dbName, String dbTzName, Calendar cal) {
+ int i = getColumnIndex(cur, dbName);
+ if (i == -1 || cur.isNull(i)) {
+ Log_OC.e(TAG, "No valid " + dbName + " column found, index: " + Integer.toString(i));
+ return null;
+ }
+
+ if (cal == null) {
+ return utcDateFromMs(cur.getLong(i)); // Ignore timezone for date-only dates
+ } else if (dbTzName == null) {
+ Log_OC.e(TAG, "No valid tz " + dbName + " column given");
+ }
+
+ String tz = getString(cur, dbTzName);
+ final boolean isUtc = isUtcTimeZone(tz);
+
+ DateTime dt = new DateTime(isUtc);
+ if (dt.isUtc() != isUtc) {
+ throw new RuntimeException("UTC mismatch after construction");
+ }
+ dt.setTime(cur.getLong(i));
+ if (dt.isUtc() != isUtc) {
+ throw new RuntimeException("UTC mismatch after setTime");
+ }
+
+ if (!isUtc) {
+ if (mTzRegistry == null) {
+ mTzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry();
+ if (mTzRegistry == null) {
+ throw new RuntimeException("Failed to create TZ registry");
+ }
+ }
+ TimeZone t = mTzRegistry.getTimeZone(tz);
+ if (t == null) {
+ Log_OC.e(TAG, "Unknown TZ " + tz + ", assuming UTC");
+ } else {
+ dt.setTimeZone(t);
+ if (!mInsertedTimeZones.contains(t)) {
+ cal.getComponents().add(t.getVTimeZone());
+ mInsertedTimeZones.add(t);
+ }
+ }
+ }
+ return dt;
+ }
+
+ private String copyProperty(PropertyList l, String evName, Cursor cur, String dbName) {
+ // None of the exceptions caught below should be able to be thrown AFAICS.
+ try {
+ String value = getString(cur, dbName);
+ if (value != null) {
+ Property p = mPropertyFactory.createProperty(evName);
+ p.setValue(value);
+ l.add(p);
+ return value;
+ }
+ } catch (IOException | URISyntaxException | ParseException ignored) {
+ }
+ return null;
+ }
+
+ private void copyEnumProperty(PropertyList l, String evName, Cursor cur, String dbName,
+ List vals) {
+ // None of the exceptions caught below should be able to be thrown AFAICS.
+ try {
+ int i = getColumnIndex(cur, dbName);
+ if (i != -1 && !cur.isNull(i)) {
+ int value = (int) cur.getLong(i);
+ if (value >= 0 && value < vals.size() && vals.get(value) != null) {
+ Property p = mPropertyFactory.createProperty(evName);
+ p.setValue(vals.get(value));
+ l.add(p);
+ }
+ }
+ } catch (IOException | URISyntaxException | ParseException ignored) {
+ }
+ }
+
+ // TODO move this to some common place
+ private String generateUid() {
+ // Generated UIDs take the form -@nextcloud.com.
+ if (mUidTail == null) {
+ String uidPid = preferences.getUidPid();
+ if (uidPid.length() == 0) {
+ uidPid = UUID.randomUUID().toString().replace("-", "");
+ preferences.setUidPid(uidPid);
+ }
+ mUidTail = uidPid + "@nextcloud.com";
+ }
+
+ mUidMs = Math.max(mUidMs, System.currentTimeMillis());
+ String uid = mUidMs + mUidTail;
+ mUidMs++;
+
+ return uid;
+ }
+
+ private void upload(File file) {
+ String backupFolder = activity.getResources().getString(R.string.calendar_backup_folder)
+ + OCFile.PATH_SEPARATOR;
+
+ Request request = new UploadRequest.Builder(user, file.getAbsolutePath(), backupFolder + file.getName())
+ .setFileSize(file.length())
+ .setNameConflicPolicy(NameCollisionPolicy.RENAME)
+ .setCreateRemoteFolder(true)
+ .setTrigger(UploadTrigger.USER)
+ .setPostAction(PostUploadAction.MOVE_TO_APP)
+ .setRequireWifi(false)
+ .setRequireCharging(false)
+ .build();
+
+ TransferManagerConnection connection = new TransferManagerConnection(activity, user);
+ connection.enqueue(request);
+ }
+}
diff --git a/src/main/res/drawable/nav_contacts.xml b/src/main/res/drawable/nav_contacts.xml
deleted file mode 100644
index 3ca6e6ec7e..0000000000
--- a/src/main/res/drawable/nav_contacts.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
diff --git a/src/main/res/layout/backup_fragment.xml b/src/main/res/layout/backup_fragment.xml
new file mode 100644
index 0000000000..a441e1d552
--- /dev/null
+++ b/src/main/res/layout/backup_fragment.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/backup_list_item.xml b/src/main/res/layout/backup_list_item.xml
new file mode 100644
index 0000000000..ed1d14425f
--- /dev/null
+++ b/src/main/res/layout/backup_list_item.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/src/main/res/layout/backup_list_item_header.xml b/src/main/res/layout/backup_list_item_header.xml
new file mode 100644
index 0000000000..e81878105a
--- /dev/null
+++ b/src/main/res/layout/backup_list_item_header.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/contactlist_fragment.xml b/src/main/res/layout/backuplist_fragment.xml
similarity index 61%
rename from src/main/res/layout/contactlist_fragment.xml
rename to src/main/res/layout/backuplist_fragment.xml
index 92eae883a9..e8394f3d7d 100644
--- a/src/main/res/layout/contactlist_fragment.xml
+++ b/src/main/res/layout/backuplist_fragment.xml
@@ -18,64 +18,71 @@
License along with this program. If not, see .
-->
+ android:animateLayoutChanges="true">
-
+ android:choiceMode="multipleChoice"
+ android:scrollbarStyle="outsideOverlay"
+ android:scrollbars="vertical"
+ android:layout_above="@+id/contactlist_restore_selected_container" />
-
+
+
+ android:layout_height="@dimen/uploader_list_separator_height"
+ android:contentDescription="@null"
+ android:src="@drawable/uploader_list_separator" />
-
+ android:text="@string/restore_selected" />
-
-
-
-
-
+ android:orientation="vertical"
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/calendarlist_list_item.xml b/src/main/res/layout/calendarlist_list_item.xml
new file mode 100644
index 0000000000..1835d03b3b
--- /dev/null
+++ b/src/main/res/layout/calendarlist_list_item.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/contactlist_list_item.xml b/src/main/res/layout/contactlist_list_item.xml
index 148581c2c1..d2a32426ef 100644
--- a/src/main/res/layout/contactlist_list_item.xml
+++ b/src/main/res/layout/contactlist_list_item.xml
@@ -23,7 +23,7 @@
android:layout_height="@dimen/standard_list_item_size">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/res/menu/partial_drawer_entries.xml b/src/main/res/menu/partial_drawer_entries.xml
index 2a3c2a0f4d..b8da994af9 100644
--- a/src/main/res/menu/partial_drawer_entries.xml
+++ b/src/main/res/menu/partial_drawer_entries.xml
@@ -102,11 +102,6 @@
-
- true
true
-
- true
+
/.Contacts-Backup
-1
+ /.Calendar-Backup
true
@@ -58,7 +58,6 @@
true
false
false
- false
true
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index aff311ed0b..0f7e64aee3 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -51,7 +51,7 @@
Server address for the account could not be resolved for DAVx5 (formerly known as DAVdroid)
Neither F-Droid nor Google Play is installed
Calendar & contacts sync set up
- Daily backup of your contacts
+ Daily backup of your calendar & contacts
Manage folders for auto upload
Help
Recommend to friend
@@ -187,6 +187,22 @@
- Failed to copy %1$d file from the %2$s folder into
- Failed to copy %1$d files from the %2$s folder into
+
+ - Wrote %1$d event to %2$s
+ - Wrote %1$d events to %2$s
+
+
+ - Created %1$d fresh UID
+ - Created %1$d fresh UIDs
+
+
+ - Processed %d entry.
+ - Processed %d entries.
+
+
+ - Found %d duplicate entry.
+ - Found %d duplicate entries.
+
As of version 1.3.16, files uploaded from this device are copied into the local %1$s folder to prevent data loss when a single file is synced with multiple accounts.\n\nDue to this change, all files uploaded with earlier versions of this app were copied into the %2$s folder. However, an error prevented the completion of this operation during account synchronization. You may either leave the file(s) as is and delete the link to %3$s, or move the file(s) into the %1$s folder and retain the link to %4$s.\n\nListed below are the local file(s), and the remote file(s) in %5$s they were linked to.
The folder %1$s does not exist anymore
Move all
@@ -571,21 +587,15 @@
No events like additions, changes and shares yet.
About
- Back up contacts
- Restore contacts
+ Restore contacts & calendar
Back up now
- Automatic backup
- Last backup
- Permission to read contact list needed
- Restore selected contacts
- Choose account to import
No permission given, nothing imported.
- Choose date
- never
+ Restore backup
No file found
Could not find your last backup!
Backup scheduled and will start shortly
Import scheduled and will start shortly
+ Contacts & calendar backup
Log out
No app found to set a picture with
@@ -961,5 +971,20 @@
Please choose a template and enter a file name.
Strict mode: no HTTP connection allowed!
Fullscreen
+ Destination filename
+ Suggest
+ Enter destination filename
+ Did not check for duplicates.
+ Last backup: %1$s
+ Error choosing date
+ Restore selected
+ Calendars
+ Contacts
+ Data to back up
+ Calendar
+ Backup settings
+ Daily backup
+ %1$s\n%2$s
+ No calendar exists
More
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index 7c7d73ad41..aeec72b03b 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -68,9 +68,9 @@
android:key="calendar_contacts"
android:summary="@string/prefs_calendar_contacts_summary" />
+ android:title="@string/backup_title"
+ android:key="backup"
+ android:summary="@string/prefs_daily_backup_summary" />