Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-deck.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordesperateCoder <echotodevnull@gmail.com>2020-11-14 15:50:47 +0300
committerdesperateCoder <echotodevnull@gmail.com>2020-11-14 15:50:47 +0300
commitd235ae16f1e582fc21a8631ccb3ecd840fba13b4 (patch)
treeef13a3464a6beab9b74499a7d865b448210c7b87 /app/src/main/java/it
parent58903257509071f9d245132697d20ace6be17600 (diff)
parent0f8b8d55fa6c5de2072e306ef3446dab11f155eb (diff)
Merge branch 'master' of github.com:stefan-niedermann/nextcloud-deck
Diffstat (limited to 'app/src/main/java/it')
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java31
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java91
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java452
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java104
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java66
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java85
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java88
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java51
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java102
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java182
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java57
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java35
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java79
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java42
29 files changed, 1807 insertions, 163 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
index 957743919..3ded31bb7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
@@ -83,6 +83,7 @@ public class ImportAccountActivity extends AppCompatActivity {
DeckLog.warn("=============================================================");
DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
DeckLog.logError(e);
+ binding.addButton.setEnabled(true);
} catch (AndroidGetAccountsPermissionNotGranted e) {
binding.addButton.setEnabled(true);
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
index 1af96bca5..fcc9d24e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
@@ -25,6 +25,7 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityEditBinding;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
@@ -171,12 +172,16 @@ public class EditActivity extends BrandedActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_card_save) {
- saveAndFinish();
+ saveAndRun(super::finish);
}
return super.onOptionsItemSelected(item);
}
- private void saveAndFinish() {
+ /**
+ * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable}
+ * @param runnable
+ */
+ private void saveAndRun(@NonNull Runnable runnable) {
if (!viewModel.isPendingCreation()) {
viewModel.setPendingCreation(true);
final String title = viewModel.getFullCard().getCard().getTitle();
@@ -194,9 +199,9 @@ public class EditActivity extends BrandedActivity {
.show();
} else {
if (viewModel.isCreateMode()) {
- observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
} else {
- observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
}
}
}
@@ -265,26 +270,15 @@ public class EditActivity extends BrandedActivity {
}
@Override
- public boolean onSupportNavigateUp() {
- finish(); // close this activity as oppose to navigating up
- return true;
- }
-
- @Override
- public void onBackPressed() {
- finish();
- }
-
- @Override
public void finish() {
if (!viewModel.hasChanges() && viewModel.canEdit()) {
new BrandedAlertDialogBuilder(this)
.setTitle(R.string.simple_save)
.setMessage(R.string.do_you_want_to_save_your_changes)
- .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish())
+ .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndRun(super::finish))
.setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show();
} else {
- directFinish();
+ super.finish();
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
index 74c89d830..34d2eb3f3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
@@ -19,7 +19,7 @@ import com.bumptech.glide.Glide;
import java.io.Serializable;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.DialogAssigneeBinding;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
@@ -27,10 +27,11 @@ import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+@Deprecated
public class CardAssigneeDialog extends BrandedDialogFragment {
private static final String KEY_USER = "user";
- private DialogAssigneeBinding binding;
+ private DialogPreviewBinding binding;
private EditCardViewModel viewModel;
@Nullable
@@ -63,7 +64,7 @@ public class CardAssigneeDialog extends BrandedDialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- binding = DialogAssigneeBinding.inflate(LayoutInflater.from(requireContext()));
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext());
@@ -95,7 +96,7 @@ public class CardAssigneeDialog extends BrandedDialogFragment {
.placeholder(circularProgressDrawable)
.error(R.drawable.ic_person_grey600_24dp)
.into(binding.avatar));
- binding.displayName.setText(user.getDisplayname());
+ binding.title.setText(user.getDisplayname());
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
index ea347417a..d601f6bbd 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
@@ -14,6 +14,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
@@ -28,6 +30,7 @@ import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity;
import it.niedermann.nextcloud.deck.ui.branding.Branded;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
@@ -37,6 +40,9 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
public static final int VIEW_TYPE_DEFAULT = 2;
public static final int VIEW_TYPE_IMAGE = 1;
+ @NonNull
+ private final MutableLiveData<Boolean> isEmpty = new MutableLiveData<>(true);
+ @NonNull
private final MenuInflater menuInflater;
@ColorInt
private int mainColor;
@@ -45,9 +51,9 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
private Long cardRemoteId = null;
private final long cardLocalId;
@NonNull
- FragmentManager fragmentManager;
+ private final FragmentManager fragmentManager;
@NonNull
- private List<Attachment> attachments = new ArrayList<>();
+ private final List<Attachment> attachments = new ArrayList<>();
@NonNull
private final AttachmentClickedListener attachmentClickedListener;
@@ -126,22 +132,41 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
return attachments.size();
}
+ private void updateIsEmpty() {
+ this.isEmpty.postValue(getItemCount() <= 0);
+ }
+
+ @NonNull
+ public LiveData<Boolean> isEmpty() {
+ return distinctUntilChanged(this.isEmpty);
+ }
+
public void setAttachments(@NonNull List<Attachment> attachments, @Nullable Long cardRemoteId) {
this.cardRemoteId = cardRemoteId;
this.attachments.clear();
this.attachments.addAll(attachments);
notifyDataSetChanged();
+ this.updateIsEmpty();
}
public void addAttachment(Attachment a) {
- this.attachments.add(a);
+ this.attachments.add(0, a);
notifyItemInserted(this.attachments.size());
+ this.updateIsEmpty();
}
public void removeAttachment(Attachment a) {
final int index = this.attachments.indexOf(a);
this.attachments.remove(a);
notifyItemRemoved(index);
+ this.updateIsEmpty();
+ }
+
+ public void replaceAttachment(Attachment toReplace, Attachment with) {
+ final int index = this.attachments.indexOf(toReplace);
+ this.attachments.remove(toReplace);
+ this.attachments.add(index, with);
+ notifyItemChanged(index);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
new file mode 100644
index 000000000..6b60bbffd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
@@ -0,0 +1,91 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.animation.ArgbEvaluatorCompat;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import it.niedermann.android.util.DimensionUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
+
+public class CardAttachmentsBottomsheetBehaviorCallback extends BottomSheetBehavior.BottomSheetCallback {
+ @NonNull
+ private final OnBackPressedCallback backPressedCallback;
+ @NonNull
+ private final FloatingActionButton fab;
+ @NonNull
+ private final View pickerBackdrop;
+ @NonNull
+ private final BottomNavigationView bottomNavigation;
+ @ColorInt
+ private final int backdropColorExpanded;
+ @ColorInt
+ private final int backdropColorCollapsed;
+ @Px
+ private final int bottomNavigationHeight;
+
+ private float lastOffset = -1;
+
+ public CardAttachmentsBottomsheetBehaviorCallback(@NonNull Context context,
+ @NonNull OnBackPressedCallback backPressedCallback,
+ @NonNull FloatingActionButton fab,
+ @NonNull View pickerBackdrop,
+ @NonNull BottomNavigationView bottomNavigation,
+ @ColorRes int backdropColorExpanded,
+ @ColorRes int backdropColorCollapsed,
+ @DimenRes int bottomNavigationHeight
+ ) {
+ this.backPressedCallback = backPressedCallback;
+ this.fab = fab;
+ this.pickerBackdrop = pickerBackdrop;
+ this.bottomNavigation = bottomNavigation;
+ this.backdropColorExpanded = ContextCompat.getColor(context, backdropColorExpanded);
+ this.backdropColorCollapsed = ContextCompat.getColor(context, backdropColorCollapsed);
+ this.bottomNavigationHeight = DimensionUtil.INSTANCE.dpToPx(context, bottomNavigationHeight);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == STATE_HIDDEN) {
+ backPressedCallback.setEnabled(false);
+ if (pickerBackdrop.getVisibility() != GONE) {
+ pickerBackdrop.setVisibility(GONE);
+ }
+ } else if (pickerBackdrop.getVisibility() != VISIBLE) {
+ pickerBackdrop.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ if (slideOffset <= 0) {
+ final float bottomSheetPercentageShown = slideOffset * -1;
+ pickerBackdrop.setBackgroundColor(ArgbEvaluatorCompat.getInstance().evaluate(bottomSheetPercentageShown, backdropColorExpanded, backdropColorCollapsed));
+ bottomNavigation.setTranslationY(bottomSheetPercentageShown * bottomNavigationHeight);
+ if (slideOffset <= lastOffset && slideOffset != 0) {
+ if (fab.getVisibility() == GONE) {
+ fab.show();
+ }
+ } else {
+ if (fab.getVisibility() == VISIBLE) {
+ fab.hide();
+ }
+ }
+ }
+ lastOffset = slideOffset;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
index 1c8a84103..bb42f27ef 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
@@ -1,23 +1,32 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
+import android.provider.ContactsContract;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
import android.widget.Toast;
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.SharedElementCallback;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
@@ -27,6 +36,7 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabAttachmentsBinding;
@@ -38,16 +48,36 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiv
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.AbstractPickerAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.ContactAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapterLegacy;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryItemDecoration;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialogViewModel;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.takephoto.TakePhotoActivity;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
+import it.niedermann.nextcloud.deck.util.VCardUtil;
+import static android.Manifest.permission.CAMERA;
+import static android.Manifest.permission.READ_CONTACTS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_IMAGE;
import static it.niedermann.nextcloud.deck.util.AttachmentUtil.copyContentUriToTempFile;
@@ -56,14 +86,36 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT;
public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener {
private FragmentCardEditTabAttachmentsBinding binding;
- private EditCardViewModel viewModel;
+ private EditCardViewModel editViewModel;
+ private PreviewDialogViewModel previewViewModel;
+ private BottomSheetBehavior<LinearLayout> mBottomSheetBehaviour;
- private static final int REQUEST_CODE_ADD_FILE = 1;
- private static final int REQUEST_CODE_ADD_FILE_PERMISSION = 2;
+ private RecyclerView.ItemDecoration galleryItemDecoration;
+
+ private static final int REQUEST_CODE_PICK_FILE = 1;
+ private static final int REQUEST_CODE_PICK_FILE_PERMISSION = 2;
+ private static final int REQUEST_CODE_PICK_CAMERA = 3;
+ private static final int REQUEST_CODE_PICK_GALLERY_PERMISSION = 4;
+ private static final int REQUEST_CODE_PICK_CONTACT = 5;
+ private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6;
+
+ @ColorInt
+ private int accentColor;
+ @ColorInt
+ private int primaryColor;
private SyncManager syncManager;
private CardAttachmentAdapter adapter;
+ private AbstractPickerAdapter<?> pickerAdapter;
+
+ private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ }
+ };
+
private int clickedItemPosition;
@Override
@@ -72,11 +124,24 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
Bundle savedInstanceState) {
binding = FragmentCardEditTabAttachmentsBinding.inflate(inflater, container, false);
- viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ editViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ previewViewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding.bottomNavigation.setOnNavigationItemSelectedListener(item -> {
+ if (item.getItemId() == R.id.gallery) {
+ showGalleryPicker();
+ } else if (item.getItemId() == R.id.contacts) {
+ showContactPicker();
+ } else if (item.getItemId() == R.id.files) {
+ showFilePicker();
+ }
+ return true;
+ });
+ accentColor = ContextCompat.getColor(requireContext(), R.color.accent);
+ primaryColor = ContextCompat.getColor(requireContext(), R.color.primary);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
// See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
- if (viewModel.getFullCard() == null) {
+ if (editViewModel.getFullCard() == null) {
DeckLog.logError(new IllegalStateException("Cannot populate " + CardAttachmentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null"));
return binding.getRoot();
}
@@ -86,15 +151,32 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
getChildFragmentManager(),
requireActivity().getMenuInflater(),
this,
- viewModel.getAccount(),
- viewModel.getFullCard().getLocalId());
+ editViewModel.getAccount(),
+ editViewModel.getFullCard().getLocalId());
binding.attachmentsList.setAdapter(adapter);
- updateEmptyContentView();
+ adapter.isEmpty().observe(getViewLifecycleOwner(), (isEmpty) -> {
+ if (isEmpty) {
+ this.binding.emptyContentView.setVisibility(VISIBLE);
+ this.binding.attachmentsList.setVisibility(GONE);
+ } else {
+ this.binding.emptyContentView.setVisibility(GONE);
+ this.binding.attachmentsList.setVisibility(VISIBLE);
+ }
+ });
+ galleryItemDecoration = new GalleryItemDecoration(DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1qx));
+ mBottomSheetBehaviour = BottomSheetBehavior.from(binding.bottomSheetParent);
+ mBottomSheetBehaviour.setDraggable(true);
+ mBottomSheetBehaviour.setHideable(true);
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ mBottomSheetBehaviour.addBottomSheetCallback(new CardAttachmentsBottomsheetBehaviorCallback(
+ requireContext(), backPressedCallback, binding.fab, binding.pickerBackdrop, binding.bottomNavigation,
+ R.color.mdtp_transparent_black, android.R.color.transparent, R.dimen.attachments_bottom_navigation_height));
+ binding.pickerBackdrop.setOnClickListener(v -> mBottomSheetBehaviour.setState(STATE_HIDDEN));
final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
- int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
- GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
+ final int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
+ final GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
@@ -108,7 +190,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
});
binding.attachmentsList.setLayoutManager(glm);
- if (!viewModel.isCreateMode()) {
+ if (!editViewModel.isCreateMode()) {
// https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html?m=1
// https://github.com/android/animation-samples/blob/master/GridToPager/app/src/main/java/com/google/samples/gridtopager/fragment/ImagePagerFragment.java
setExitSharedElementCallback(new SharedElementCallback() {
@@ -121,12 +203,21 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
}
});
- adapter.setAttachments(viewModel.getFullCard().getAttachments(), viewModel.getFullCard().getId());
- updateEmptyContentView();
+ adapter.setAttachments(editViewModel.getFullCard().getAttachments(), editViewModel.getFullCard().getId());
}
- if (viewModel.canEdit()) {
- binding.fab.setOnClickListener(v -> pickFile());
+ if (editViewModel.canEdit()) {
+ binding.fab.setOnClickListener(v -> {
+ if (SDK_INT < LOLLIPOP) {
+ openNativeFilePicker();
+ } else {
+ binding.bottomNavigation.setSelectedItemId(R.id.gallery);
+ showGalleryPicker();
+ mBottomSheetBehaviour.setState(STATE_COLLAPSED);
+ backPressedCallback.setEnabled(true);
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
+ }
+ });
binding.fab.show();
binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
@@ -141,113 +232,264 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
binding.fab.hide();
binding.emptyContentView.hideDescription();
}
+ @Nullable Context context = requireContext();
+ if (isBrandingEnabled(context)) {
+ applyBrand(readBrandMainColor(context));
+ }
return binding.getRoot();
}
- public void pickFile() {
- if (SDK_INT >= M && checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
- requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_ADD_FILE_PERMISSION);
+ @Override
+ public void onPause() {
+ super.onPause();
+ backPressedCallback.setEnabled(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ backPressedCallback.setEnabled(binding.bottomNavigation.getTranslationY() == 0);
+ }
+
+ private void showGalleryPicker() {
+ if (!(pickerAdapter instanceof GalleryAdapter)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE) || isPermissionRequestNeeded(CAMERA)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE, CAMERA}, REQUEST_CODE_PICK_GALLERY_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ pickerAdapter = new GalleryAdapter(requireContext(), (uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeCameraPicker, getViewLifecycleOwner());
+ if (binding.pickerRecyclerView.getItemDecorationCount() == 0) {
+ binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration);
+ }
+ binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+
+ private void showContactPicker() {
+ if (!(pickerAdapter instanceof ContactAdapter)) {
+ if (isPermissionRequestNeeded(READ_CONTACTS)) {
+ requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ pickerAdapter = new ContactAdapter(requireContext(), (uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeContactPicker);
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+
+ private void showFilePicker() {
+ if (!(pickerAdapter instanceof FileAdapter) && !(pickerAdapter instanceof FileAdapterLegacy)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_PICK_FILE_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ if (SDK_INT >= LOLLIPOP) {
+// if (SDK_INT >= Build.VERSION_CODES.Q) {
+// // TODO Only usable with Scoped Storage
+// pickerAdapter = new FileAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)), this::openNativeFilePicker);
+// } else {
+ pickerAdapter = new FileAdapterLegacy((uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeFilePicker);
+// }
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+ }
+
+ private void openNativeCameraPicker() {
+ if (SDK_INT >= LOLLIPOP) {
+ startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA);
} else {
- startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("*/*"), REQUEST_CODE_ADD_FILE);
+ ExceptionDialogFragment.newInstance(new UnsupportedOperationException("This feature requires Android 5"), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ }
+
+ private void openNativeContactPicker() {
+ final Intent intent = new Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE);
+ if (intent.resolveActivity(requireContext().getPackageManager()) != null) {
+ startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
+ }
+ }
+
+ private void openNativeFilePicker() {
+ startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*"), REQUEST_CODE_PICK_FILE);
+ }
+
+ /**
+ * Checks the current Android version and whether the permission has already been granted.
+ *
+ * @param permission see {@link android.Manifest.permission}
+ * @return whether or not requesting permission is needed
+ */
+ private boolean isPermissionRequestNeeded(@NonNull String permission) {
+ return SDK_INT >= M && checkSelfPermission(requireActivity(), permission) != PERMISSION_GRANTED;
+ }
+
+ private void unbindPickerAdapter() {
+ if (pickerAdapter != null) {
+ pickerAdapter.onDestroy();
+ }
+ }
+
+ private void removeGalleryItemDecoration() {
+ if (binding.pickerRecyclerView.getItemDecorationCount() > 0) {
+ binding.pickerRecyclerView.removeItemDecoration(galleryItemDecoration);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- //noinspection SwitchStatementWithTooFewBranches
+ super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
- case REQUEST_CODE_ADD_FILE: {
+ case REQUEST_CODE_PICK_CONTACT:
+ case REQUEST_CODE_PICK_CAMERA:
+ case REQUEST_CODE_PICK_FILE: {
if (resultCode == RESULT_OK) {
- if (data == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
- }
- final Uri sourceUri = data.getData();
- if (sourceUri == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
- }
- if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ final Uri sourceUri = requestCode == REQUEST_CODE_PICK_CONTACT
+ ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString()))
+ : data.getData();
+ try {
+ uploadNewAttachmentFromUri(sourceUri, requestCode == REQUEST_CODE_PICK_CAMERA
+ ? data.getType()
+ : requireContext().getContentResolver().getType(sourceUri));
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
+ }
+ break;
+ }
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
- DeckLog.verbose("--- found content URL " + sourceUri.getPath());
- File fileToUpload;
+ @Override
+ public void onDestroy() {
+ if (this.pickerAdapter != null) {
+ this.pickerAdapter.onDestroy();
+ this.binding.pickerRecyclerView.setAdapter(null);
+ }
+ super.onDestroy();
+ }
- try {
- DeckLog.verbose("---- so, now copy & upload: " + sourceUri.getPath());
- fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, viewModel.getAccount().getId(), viewModel.getFullCard().getCard().getLocalId());
- } catch (IllegalArgumentException | IOException e) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Could not copy content URI to temporary file", e), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException {
+ if (sourceUri == null) {
+ throw new UploadAttachmentFailedException("sourceUri is null");
+ }
+ switch (sourceUri.getScheme()) {
+ case ContentResolver.SCHEME_CONTENT:
+ case ContentResolver.SCHEME_FILE: {
+ DeckLog.verbose("--- found content URL " + sourceUri.getPath());
+ final File fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId());
+ for (Attachment existingAttachment : editViewModel.getFullCard().getAttachments()) {
+ final String existingPath = existingAttachment.getLocalPath();
+ if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
return;
}
-
- for (Attachment existingAttachment : viewModel.getFullCard().getAttachments()) {
- final String existingPath = existingAttachment.getLocalPath();
- if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- return;
- }
- }
-
- final Instant now = Instant.now();
- final Attachment a = new Attachment();
- a.setMimetype(requireContext().getContentResolver().getType(sourceUri));
- a.setData(fileToUpload.getName());
- a.setFilename(fileToUpload.getName());
- a.setBasename(fileToUpload.getName());
- a.setFilesize(fileToUpload.length());
- a.setLocalPath(fileToUpload.getAbsolutePath());
- a.setLastModifiedLocal(now);
- a.setStatusEnum(DBStatus.LOCAL_EDITED);
- a.setCreatedAt(now);
- viewModel.getFullCard().getAttachments().add(a);
- adapter.addAttachment(a);
- if (!viewModel.isCreateMode()) {
- WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
- observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
- if (liveData.hasError()) {
- Throwable t = liveData.getError();
- if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
- // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
- viewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- } else {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- } else {
- viewModel.getFullCard().getAttachments().remove(a);
+ }
+ final Instant now = Instant.now();
+ final Attachment a = new Attachment();
+ a.setMimetype(mimeType);
+ a.setData(fileToUpload.getName());
+ a.setFilename(fileToUpload.getName());
+ a.setBasename(fileToUpload.getName());
+ a.setFilesize(fileToUpload.length());
+ a.setLocalPath(fileToUpload.getAbsolutePath());
+ a.setLastModifiedLocal(now);
+ a.setCreatedAt(now);
+ a.setStatusEnum(DBStatus.LOCAL_EDITED);
+ editViewModel.getFullCard().getAttachments().add(0, a);
+ adapter.addAttachment(a);
+ if (!editViewModel.isCreateMode()) {
+ WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
+ observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
+ if (liveData.hasError()) {
+ Throwable t = liveData.getError();
+ if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
+ editViewModel.getFullCard().getAttachments().remove(a);
adapter.removeAttachment(a);
- viewModel.getFullCard().getAttachments().add(next);
- adapter.addAttachment(next);
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ } else {
+ ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
- });
- }
- updateEmptyContentView();
+ } else {
+ editViewModel.getFullCard().getAttachments().remove(a);
+ editViewModel.getFullCard().getAttachments().add(0, next);
+ adapter.replaceAttachment(a, next);
+ }
+ });
}
break;
}
default: {
- super.onActivityResult(requestCode, resultCode, data);
+ throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme());
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- //noinspection SwitchStatementWithTooFewBranches
switch (requestCode) {
- case REQUEST_CODE_ADD_FILE_PERMISSION:
+ case REQUEST_CODE_PICK_FILE_PERMISSION: {
if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
- pickFile();
+ showFilePicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case REQUEST_CODE_PICK_GALLERY_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED && checkSelfPermission(requireActivity(), CAMERA) == PERMISSION_GRANTED) {
+ showGalleryPicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) {
+ showContactPicker();
} else {
Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
}
break;
+ }
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@@ -256,16 +498,15 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
@Override
public void onAttachmentDeleted(Attachment attachment) {
adapter.removeAttachment(attachment);
- viewModel.getFullCard().getAttachments().remove(attachment);
- if (!viewModel.isCreateMode() && attachment.getLocalId() != null) {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteAttachmentOfCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), attachment.getLocalId());
+ editViewModel.getFullCard().getAttachments().remove(attachment);
+ if (!editViewModel.isCreateMode() && attachment.getLocalId() != null) {
+ final WrappedLiveData<Void> deleteLiveData = syncManager.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId());
observeOnce(deleteLiveData, this, (next) -> {
if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
- ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
}
- updateEmptyContentView();
}
@Override
@@ -273,20 +514,25 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
this.clickedItemPosition = position;
}
- private void updateEmptyContentView() {
- if (this.adapter == null || this.adapter.getItemCount() == 0) {
- this.binding.emptyContentView.setVisibility(View.VISIBLE);
- this.binding.attachmentsList.setVisibility(View.GONE);
- } else {
- this.binding.emptyContentView.setVisibility(View.GONE);
- this.binding.attachmentsList.setVisibility(View.VISIBLE);
- }
- }
-
@Override
public void applyBrand(int mainColor) {
applyBrandToFAB(mainColor, binding.fab);
adapter.applyBrand(mainColor);
+ @ColorInt final int finalMainColor = DeckColorUtil.contrastRatioIsSufficient(mainColor, primaryColor)
+ ? mainColor
+ : accentColor;
+ final ColorStateList list = new ColorStateList(
+ new int[][]{
+ new int[]{android.R.attr.state_checked},
+ new int[]{}
+ },
+ new int[]{
+ finalMainColor,
+ accentColor
+ }
+ );
+ binding.bottomNavigation.setItemIconTintList(list);
+ binding.bottomNavigation.setItemTextColor(list);
}
public static Fragment newInstance() {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
index 3f7287c24..2b5358eb9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
@@ -10,16 +10,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.util.AttachmentUtil;
import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
+
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
- private ItemAttachmentDefaultBinding binding;
+ private final ItemAttachmentDefaultBinding binding;
@SuppressWarnings("WeakerAccess")
public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) {
@@ -39,20 +39,8 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
super.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor);
-
- if (MimeTypeUtil.isAudio(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp);
- } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp);
- } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24);
- } else if (MimeTypeUtil.isContact(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24);
- } else {
- getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp);
- }
-
- itemView.setOnClickListener((event) -> AttachmentUtil.openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId()));
+ getPreview().setImageResource(getIconForMimeType(attachment.getMimetype()));
+ itemView.setOnClickListener((event) -> openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId()));
binding.filename.setText(attachment.getBasename());
binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize()));
if (attachment.getLastModifiedLocal() != null) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
new file mode 100644
index 000000000..a2ea6dd37
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+import static android.database.Cursor.FIELD_TYPE_INTEGER;
+import static android.database.Cursor.FIELD_TYPE_NULL;
+import static androidx.recyclerview.widget.RecyclerView.NO_ID;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * An {@link RecyclerView.Adapter} which provides previews of one type of files and also an option to open a native dialog.
+ * <p>
+ * Example: Previews for images of the gallery as well a one option to take a photo
+ */
+public abstract class AbstractCursorPickerAdapter<T extends RecyclerView.ViewHolder> extends AbstractPickerAdapter<T> {
+
+ private final int count;
+ protected final int columnIndex;
+ private final int columnIndexType;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+ @NonNull
+ protected final Cursor cursor;
+ @NonNull
+ protected final ContentResolver contentResolver;
+
+ /**
+ * Should be used to bind heavy operations like when dealing with {@link Bitmap}.
+ * This must only be one {@link Thread} because otherwise the cursor might change while fetching data from it.
+ */
+ @NonNull
+ protected final ExecutorService bindExecutor = Executors.newFixedThreadPool(1);
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String sortOrder) {
+ this(context, onSelect, openNativePicker, subject, idColumn, new String[]{idColumn}, sortOrder);
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String[] requestedColumns, String sortOrder) {
+ this(context, onSelect, openNativePicker, idColumn, requireNonNull(context.getContentResolver().query(subject, requestedColumns, null, null, sortOrder)));
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, String idColumn, @NonNull Cursor cursor) {
+ this.contentResolver = context.getContentResolver();
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.cursor = cursor;
+ this.cursor.moveToFirst();
+ this.columnIndex = this.cursor.getColumnIndex(idColumn);
+ this.count = cursor.getCount() + 1;
+ this.columnIndexType = (this.count > 1) ? this.cursor.getType(columnIndex) : FIELD_TYPE_NULL;
+ setHasStableIds(true);
+ }
+
+ /**
+ * Moves the {@link #cursor} to the given position
+ */
+ @Override
+ public long getItemId(int position) {
+ if (!cursor.isClosed() && cursor.moveToPosition(position - 1)) {
+ //noinspection SwitchStatementWithTooFewBranches
+ switch (columnIndexType) {
+ case FIELD_TYPE_INTEGER:
+ return cursor.getLong(columnIndex);
+ default:
+ throw new IllegalStateException("Unknown type for columnIndex \"" + columnIndex + "\": " + columnIndexType);
+ }
+ } else {
+ return NO_ID;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return count;
+ }
+
+ /**
+ * Call this method when the {@link AbstractCursorPickerAdapter} is no longer need to free resources.
+ */
+ public void onDestroy() {
+ cursor.close();
+ bindExecutor.shutdownNow();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
new file mode 100644
index 000000000..901d204cd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
@@ -0,0 +1,26 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class AbstractPickerAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
+
+ protected static final int VIEW_TYPE_NONE = -1;
+ protected static final int VIEW_TYPE_ITEM = 0;
+ protected static final int VIEW_TYPE_ITEM_NATIVE = 1;
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position > 0) {
+ return VIEW_TYPE_ITEM;
+ } else if (position == 0) {
+ return VIEW_TYPE_ITEM_NATIVE;
+ } else {
+ return VIEW_TYPE_NONE;
+ }
+ }
+
+ /**
+ * Call this method when the {@link AbstractPickerAdapter} is no longer need to free resources.
+ */
+ public abstract void onDestroy();
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
new file mode 100644
index 000000000..22ac0c694
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
@@ -0,0 +1,104 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.provider.ContactsContract.CommonDataKinds.Email.DATA;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER;
+import static android.provider.ContactsContract.Contacts.CONTENT_LOOKUP_URI;
+import static android.provider.ContactsContract.Contacts.CONTENT_URI;
+import static android.provider.ContactsContract.Contacts.DISPLAY_NAME;
+import static android.provider.ContactsContract.Contacts.SORT_KEY_PRIMARY;
+import static android.provider.ContactsContract.Contacts._ID;
+
+public class ContactAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int lookupKeyColumnIndex;
+ private final int displayNameColumnIndex;
+
+ public ContactAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, CONTENT_URI, _ID, new String[]{_ID, LOOKUP_KEY, DISPLAY_NAME}, SORT_KEY_PRIMARY);
+ lookupKeyColumnIndex = cursor.getColumnIndex(LOOKUP_KEY);
+ displayNameColumnIndex = cursor.getColumnIndex(DISPLAY_NAME);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new ContactNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new ContactItemViewHolder(ItemPickerUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((ContactNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final ContactItemViewHolder viewHolder = (ContactItemViewHolder) holder;
+ if (!cursor.isClosed()) {
+ cursor.moveToPosition(position - 1);
+ final String displayName = cursor.getString(displayNameColumnIndex);
+ final String lookupKey = cursor.getString(lookupKeyColumnIndex);
+ bindExecutor.execute(() -> {
+ try (InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey))) {
+ final Bitmap thumbnail = BitmapFactory.decodeStream(inputStream);
+ String contactInformation = "";
+ try (final Cursor phoneCursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{NUMBER}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (phoneCursor != null && phoneCursor.moveToFirst()) {
+ contactInformation = phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER));
+ }
+ }
+ if (TextUtils.isEmpty(contactInformation)) {
+ try (final Cursor emailCursor = contentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, new String[]{DATA}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (emailCursor != null && emailCursor.moveToFirst()) {
+ contactInformation = emailCursor.getString(emailCursor.getColumnIndex(DATA));
+ }
+ }
+ }
+ final String finalContactInformation = contactInformation;
+ new Handler(Looper.getMainLooper()).post(() -> viewHolder.bind(Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey), thumbnail, displayName, finalContactInformation, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ });
+ } else {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
new file mode 100644
index 000000000..f403fed21
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
@@ -0,0 +1,66 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName;
+
+public class ContactItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerUserBinding binding;
+
+ public ContactItemViewHolder(@NonNull ItemPickerUserBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull String displayName, @Nullable String contactInformation, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(displayName, image == null ? null : Glide.with(itemView.getContext()).load(image))));
+ binding.title.setText(displayName);
+ binding.contactInformation.setText(contactInformation);
+ if (image == null) {
+ binding.initials.setVisibility(VISIBLE);
+ binding.initials.setText(TextUtils.isEmpty(displayName)
+ ? null
+ : String.valueOf(displayName.charAt(0))
+ );
+ Glide.with(itemView.getContext())
+ .load(new ColorDrawable(getColorBasedOnDisplayName(itemView.getContext(), displayName)))
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ } else {
+ binding.initials.setVisibility(GONE);
+ binding.initials.setText(null);
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ }
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_person_grey600_24dp)
+ .into(binding.avatar);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
new file mode 100644
index 000000000..a1d7d5921
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class ContactNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public ContactNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_contacts);
+ binding.subtitle.setText(R.string.contacts);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
new file mode 100644
index 000000000..aa96a0e69
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
@@ -0,0 +1,85 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+import static android.provider.MediaStore.Downloads.DATE_ADDED;
+import static android.provider.MediaStore.Downloads.DATE_MODIFIED;
+import static android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI;
+import static android.provider.MediaStore.Downloads.MIME_TYPE;
+import static android.provider.MediaStore.Downloads.SIZE;
+import static android.provider.MediaStore.Downloads.TITLE;
+import static android.provider.MediaStore.Downloads._ID;
+import static java.util.Objects.requireNonNull;
+
+@RequiresApi(api = 29)
+public class FileAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int displayNameColumnIndex;
+ private final int sizeColumnIndex;
+ private final int modifiedColumnIndex;
+ private final int mimeTypeColumnIndex;
+
+ private FileAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, _ID, requireNonNull(context.getContentResolver().query(EXTERNAL_CONTENT_URI, new String[]{_ID, TITLE, SIZE, DATE_MODIFIED, MIME_TYPE}, null, null, DATE_ADDED + " DESC")));
+ displayNameColumnIndex = cursor.getColumnIndex(TITLE);
+ sizeColumnIndex = cursor.getColumnIndex(SIZE);
+ modifiedColumnIndex = cursor.getColumnIndex(DATE_MODIFIED);
+ mimeTypeColumnIndex = cursor.getColumnIndex(MIME_TYPE);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ if (!cursor.isClosed()) {
+ bindExecutor.execute(() -> {
+ final long id = getItemId(position);
+ final String name = cursor.getString(displayNameColumnIndex);
+ final String mimeType = cursor.getString(mimeTypeColumnIndex);
+ final long size = cursor.getLong(sizeColumnIndex);
+ final long modified = cursor.getLong(modifiedColumnIndex);
+ new Handler(Looper.getMainLooper()).post(() -> ((FileItemViewHolder) holder).bind(ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id), name, mimeType, size, modified, onSelect));
+ });
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
new file mode 100644
index 000000000..1ac14361a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
@@ -0,0 +1,88 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static java.util.Collections.reverseOrder;
+import static java.util.Comparator.comparingLong;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+@Deprecated
+public class FileAdapterLegacy extends AbstractPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final List<File> files;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+
+ public FileAdapterLegacy(@NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker) {
+ // TODO run in separate thread?
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.files = Arrays.stream(requireNonNull(requireNonNull(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)).listFiles()))
+ .sorted(reverseOrder(comparingLong(File::lastModified)))
+ .collect(toList());
+
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final File file = files.get(position - 1);
+ if (file.isFile()) {
+ ((FileItemViewHolder) holder).bind(Uri.fromFile(file), file.getName(), AttachmentUtil.getMimeType(file.getAbsolutePath()), file.length(), file.lastModified(), onSelect);
+ } else {
+ ((FileItemViewHolder) holder).bindError();
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return files.size();
+ }
+
+ public void onDestroy() {
+ // Let GarbageCollection do this stuff...
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
new file mode 100644
index 000000000..f7d64aca8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+
+import static android.text.format.Formatter.formatFileSize;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.DateUtil.getRelativeDateTimeString;
+
+public class FileItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentDefaultBinding binding;
+
+ public FileItemViewHolder(@NonNull ItemAttachmentDefaultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @NonNull String name, String mimeType, long size, long modified, @Nullable BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener(onSelect == null ? null : (v) -> onSelect.accept(uri, new Pair<>(name, null)));
+ binding.filename.setText(name);
+ binding.filesize.setText(formatFileSize(binding.filesize.getContext(), size));
+ binding.modified.setText(getRelativeDateTimeString(binding.modified.getContext(), modified));
+ binding.preview.setImageResource(getIconForMimeType(mimeType));
+ }
+
+ public void bindError() {
+ binding.filename.setText(R.string.simple_exception);
+ binding.filesize.setText(null);
+ binding.modified.setText(null);
+ itemView.setOnClickListener(null);
+ binding.preview.setImageResource(R.drawable.ic_attach_file_grey600_24dp);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
new file mode 100644
index 000000000..79129f26a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class FileNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public FileNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_files);
+ binding.subtitle.setText(R.string.downloads);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
new file mode 100644
index 000000000..658eb1ee3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.provider.BaseColumns._ID;
+import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+public class GalleryAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final LifecycleOwner lifecycleOwner;
+
+ @SuppressLint("InlinedApi")
+ private static final String sortOrder = (SDK_INT >= Q)
+ ? MediaStore.Images.Media.DATE_TAKEN
+ : MediaStore.Images.Media.DATE_ADDED;
+
+ public GalleryAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ super(context, onSelect, openNativePicker, EXTERNAL_CONTENT_URI, _ID, sortOrder + " DESC");
+ this.lifecycleOwner = lifecycleOwner;
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new GalleryPhotoPreviewItemViewHolder(ItemPhotoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new GalleryItemViewHolder(ItemAttachmentImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((GalleryPhotoPreviewItemViewHolder) holder).bind(openNativePicker, lifecycleOwner);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final long id = getItemId(position);
+ bindExecutor.execute(() -> {
+ try {
+ final Bitmap thumbnail;
+ if (SDK_INT >= Q) {
+ thumbnail = contentResolver.loadThumbnail(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), new Size(512, 384), null);
+ } else {
+ thumbnail = MediaStore.Images.Thumbnails.getThumbnail(
+ contentResolver, id,
+ MediaStore.Images.Thumbnails.MINI_KIND, null);
+ }
+ new Handler(Looper.getMainLooper()).post(() -> ((GalleryItemViewHolder) holder).bind(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), thumbnail, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(((GalleryItemViewHolder) holder)::bindError);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
+ super.onViewDetachedFromWindow(holder);
+ if (holder instanceof GalleryPhotoPreviewItemViewHolder) {
+ ((GalleryPhotoPreviewItemViewHolder) holder).unbind();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
new file mode 100644
index 000000000..c70dc8277
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class GalleryItemDecoration extends RecyclerView.ItemDecoration {
+
+ @Px
+ private final int gutter;
+
+ public GalleryItemDecoration(@Px int gutter) {
+ this.gutter = gutter;
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ final int position = parent.getChildAdapterPosition(view);
+ if (position >= 0) {
+ outRect.left = gutter;
+ outRect.top = gutter;
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
new file mode 100644
index 000000000..346fca9c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+
+public class GalleryItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentImageBinding binding;
+
+ public GalleryItemViewHolder(@NonNull ItemAttachmentImageBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(null, Glide.with(itemView.getContext()).load(image))));
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
new file mode 100644
index 000000000..00a833e57
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
@@ -0,0 +1,51 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+
+public class GalleryPhotoPreviewItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPhotoPreviewBinding binding;
+ private ProcessCameraProvider cameraProvider;
+
+ public GalleryPhotoPreviewItemViewHolder(@NonNull ItemPhotoPreviewBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ itemView.setOnClickListener((v) -> openNativePicker.run());
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(itemView.getContext());
+ cameraProviderFuture.addListener(() -> {
+ try {
+ unbind();
+ cameraProvider = cameraProviderFuture.get();
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ cameraProvider.bindToLifecycle(lifecycleOwner, DEFAULT_BACK_CAMERA, previewUseCase);
+ } catch (ExecutionException | InterruptedException | IllegalArgumentException e) {
+ DeckLog.logError(e);
+ }
+ }, ContextCompat.getMainExecutor(itemView.getContext()));
+ }
+
+
+ public void unbind() {
+ if (cameraProvider != null) {
+ cameraProvider.unbindAll();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
new file mode 100644
index 000000000..8ebdf1b50
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
@@ -0,0 +1,102 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
+
+import com.bumptech.glide.RequestBuilder;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+
+public class PreviewDialog extends BrandedDialogFragment {
+
+ private DialogPreviewBinding binding;
+ private PreviewDialogViewModel viewModel;
+ private LiveData<RequestBuilder<?>> imageBuilder$;
+ private LiveData<String> title$;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ viewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
+
+ final Context context = requireContext();
+
+ this.imageBuilder$ = this.viewModel.getImageBuilder();
+ this.imageBuilder$.observe(requireActivity(), builder -> {
+ if (builder == null) {
+ binding.avatar.setVisibility(GONE);
+ } else {
+ final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context);
+ circularProgressDrawable.setStrokeWidth(5f);
+ circularProgressDrawable.setCenterRadius(30f);
+ circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.start();
+ binding.avatar.setVisibility(VISIBLE);
+ binding.avatar.post(() -> builder
+ .placeholder(circularProgressDrawable)
+ .into(binding.avatar));
+ }
+ });
+ this.title$ = this.viewModel.getTitle();
+ this.title$.observe(requireActivity(), title -> {
+ if (TextUtils.isEmpty(title)) {
+ binding.title.setVisibility(GONE);
+ } else {
+ binding.title.setVisibility(VISIBLE);
+ binding.title.setText(title);
+ }
+ });
+
+ return new BrandedAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.simple_attach, (d, w) -> {
+ viewModel.setResult(true);
+ dismiss();
+ })
+ .setNeutralButton(R.string.simple_close, (d, w) -> {
+ viewModel.setResult(false);
+ dismiss();
+ })
+ .setView(binding.getRoot())
+ .create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ viewModel.setResult(false);
+ super.onCancel(dialog);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ }
+
+ @Override
+ public void onDestroy() {
+ this.imageBuilder$.removeObservers(requireActivity());
+ this.title$.removeObservers(requireActivity());
+ super.onDestroy();
+ }
+
+ public static DialogFragment newInstance() {
+ return new PreviewDialog();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
new file mode 100644
index 000000000..8ee8a0e08
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
@@ -0,0 +1,50 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.bumptech.glide.RequestBuilder;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+public class PreviewDialogViewModel extends ViewModel {
+
+ @NonNull
+ private final MutableLiveData<String> title$ = new MutableLiveData<>();
+ @NonNull
+ private final MutableLiveData<RequestBuilder<?>> imageBuilder$ = new MutableLiveData<>();
+ private MutableLiveData<Boolean> result$ = new MutableLiveData<>();
+
+ /**
+ * Call this before observing {@link #getResult()} to prepare the {@link PreviewDialog}.
+ */
+ public void prepareDialog(@Nullable String title, @Nullable RequestBuilder<?> imageBuilder) {
+ this.result$ = new MutableLiveData<>();
+ this.title$.setValue(title);
+ this.imageBuilder$.setValue(imageBuilder);
+ }
+
+ /**
+ * This will be a new instance after each call of {@link #prepareDialog(String, RequestBuilder)}.
+ *
+ * @return {@link Boolean#TRUE} if a positive action has been submitted, {@link Boolean#FALSE} if the dialog has been canceled.
+ */
+ public LiveData<Boolean> getResult() {
+ return this.result$;
+ }
+
+ protected LiveData<String> getTitle() {
+ return distinctUntilChanged(this.title$);
+ }
+
+ protected LiveData<RequestBuilder<?>> getImageBuilder() {
+ return distinctUntilChanged(this.imageBuilder$);
+ }
+
+ protected void setResult(boolean submittedPositive) {
+ result$.setValue(submittedPositive);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
index 82230060c..4b75b985f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
@@ -84,14 +84,14 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
}
void bind(@NonNull final User user) {
- binding.displayName.setText(user.getDisplayname());
+ binding.title.setText(user.getDisplayname());
ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp);
itemView.setSelected(selectedUsers.contains(user));
bindClickListener(user);
}
public void bindNotAssigned() {
- binding.displayName.setText(itemView.getContext().getString(R.string.simple_unassigned));
+ binding.title.setText(itemView.getContext().getString(R.string.simple_unassigned));
Glide.with(itemView.getContext())
.load(R.drawable.ic_baseline_block_24)
.into(binding.avatar);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
new file mode 100644
index 000000000..af17464dc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
@@ -0,0 +1,182 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Size;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Camera;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.IMAGE_JPEG;
+
+@RequiresApi(LOLLIPOP)
+public class TakePhotoActivity extends BrandedActivity {
+
+ private ActivityTakePhotoBinding binding;
+ private TakePhotoViewModel viewModel;
+
+ private View[] brandedViews;
+
+ private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
+ private OrientationEventListener orientationEventListener;
+
+ private final DateTimeFormatter fileNameFromCameraFormatter = DateTimeFormatter.ofPattern("'JPG_'yyyyMMdd'_'HHmmss'.jpg'");
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityTakePhotoBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(TakePhotoViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ final Preview previewUseCase = getPreviewUseCase();
+ final ImageCapture captureUseCase = getCaptureUseCase();
+ final Camera camera = cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+
+ viewModel.getCameraSelectorToggleButtonImageResource().observe(this, res -> binding.switchCamera.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.getTorchToggleButtonImageResource().observe(this, res -> binding.toggleTorch.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.isTorchEnabled().observe(this, enabled -> camera.getCameraControl().enableTorch(enabled));
+
+ binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
+ binding.switchCamera.setOnClickListener((v) -> {
+ viewModel.toggleCameraSelector();
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+ });
+ } catch (ExecutionException | InterruptedException e) {
+ DeckLog.logError(e);
+ finish();
+ }
+ }, ContextCompat.getMainExecutor(this));
+
+ brandedViews = new View[]{binding.takePhoto, binding.switchCamera, binding.toggleTorch};
+ }
+
+ private ImageCapture getCaptureUseCase() {
+ final ImageCapture captureUseCase = new ImageCapture.Builder().setTargetResolution(new Size(720, 1280)).build();
+
+ orientationEventListener = new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ int rotation;
+
+ // Monitors orientation values to determine the target rotation value
+ if (orientation >= 45 && orientation < 135) {
+ rotation = Surface.ROTATION_270;
+ } else if (orientation >= 135 && orientation < 225) {
+ rotation = Surface.ROTATION_180;
+ } else if (orientation >= 225 && orientation < 315) {
+ rotation = Surface.ROTATION_90;
+ } else {
+ rotation = Surface.ROTATION_0;
+ }
+
+ captureUseCase.setTargetRotation(rotation);
+ }
+ };
+ orientationEventListener.enable();
+
+ binding.takePhoto.setOnClickListener((v) -> {
+ binding.takePhoto.setEnabled(false);
+ final String photoFileName = Instant.now().atZone(ZoneId.systemDefault()).format(fileNameFromCameraFormatter);
+ try {
+ final File photoFile = AttachmentUtil.getTempCacheFile(this, "photos/" + photoFileName);
+ final ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
+ captureUseCase.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
+ final Uri savedUri = Uri.fromFile(photoFile);
+ DeckLog.info("onImageSaved - savedUri: " + savedUri.toString());
+ setResult(RESULT_OK, new Intent().setDataAndType(savedUri, IMAGE_JPEG));
+ finish();
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ e.printStackTrace();
+ //noinspection ResultOfMethodCallIgnored
+ photoFile.delete();
+ binding.takePhoto.setEnabled(true);
+ }
+ });
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+
+ return captureUseCase;
+ }
+
+ private Preview getPreviewUseCase() {
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ return previewUseCase;
+ }
+
+ @Override
+ protected void onPause() {
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.disable();
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.enable();
+ }
+ }
+
+ @RequiresApi(LOLLIPOP)
+ public static Intent createIntent(@NonNull Context context) {
+ return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ final ColorStateList colorStateList = ColorStateList.valueOf(mainColor);
+ for (View v : brandedViews) {
+ v.setBackgroundTintList(colorStateList);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
new file mode 100644
index 000000000..a71291ff2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
@@ -0,0 +1,57 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraSelector;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+
+import it.niedermann.nextcloud.deck.R;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA;
+
+public class TakePhotoViewModel extends ViewModel {
+
+ @NonNull
+ private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA;
+ @NonNull
+ private final MutableLiveData<Integer> cameraSelectorToggleButtonImageResource = new MutableLiveData<>(R.drawable.ic_baseline_camera_front_24);
+ @NonNull
+ private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false);
+
+ @NonNull
+ public CameraSelector getCameraSelector() {
+ return this.cameraSelector;
+ }
+
+ public LiveData<Integer> getCameraSelectorToggleButtonImageResource() {
+ return this.cameraSelectorToggleButtonImageResource;
+ }
+
+ public void toggleCameraSelector() {
+ if (this.cameraSelector == DEFAULT_BACK_CAMERA) {
+ this.cameraSelector = DEFAULT_FRONT_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_rear_24);
+ } else {
+ this.cameraSelector = DEFAULT_BACK_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_front_24);
+ }
+ }
+
+ public void toggleTorchEnabled() {
+ //noinspection ConstantConditions
+ this.torchEnabled.postValue(!this.torchEnabled.getValue());
+ }
+
+ public LiveData<Boolean> isTorchEnabled() {
+ return this.torchEnabled;
+ }
+
+ public LiveData<Integer> getTorchToggleButtonImageResource() {
+ return Transformations.map(isTorchEnabled(), enabled -> enabled
+ ? R.drawable.ic_baseline_flash_off_24
+ : R.drawable.ic_baseline_flash_on_24);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
new file mode 100644
index 000000000..0912a07dd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
@@ -0,0 +1,35 @@
+package it.niedermann.nextcloud.deck.ui.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+public class SquareConstraintLayout extends ConstraintLayout {
+
+ public SquareConstraintLayout(Context context) {
+ super(context);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Set a square layout.
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
index 844a301e8..2baec8e92 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
@@ -3,8 +3,11 @@ package it.niedermann.nextcloud.deck.util;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
import android.widget.Toast;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -58,34 +61,78 @@ public class AttachmentUtil {
return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId;
}
- public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localId) throws IOException, IllegalArgumentException {
- String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context);
- DeckLog.verbose("----- fullTempPath: " + fullTempPath);
- InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
+ public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localCardId) throws IOException, IllegalArgumentException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
if (inputStream == null) {
throw new IOException("Could not open input stream for " + currentUri.getPath());
}
- File cacheFile = new File(fullTempPath);
- File tempDir = cacheFile.getParentFile();
+ final File cacheFile = getTempCacheFile(context, "attachments/account-" + accountId + "/card-" + (localCardId == null ? "pending-creation" : localCardId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context));
+ final FileOutputStream outputStream = new FileOutputStream(cacheFile);
+ byte[] buffer = new byte[4096];
+
+ int count;
+ while ((count = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ }
+ DeckLog.verbose("----- wrote");
+ return cacheFile;
+ }
+
+ /**
+ * Creates a new {@link File}
+ */
+ public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException {
+ File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
+
+ DeckLog.verbose("- Full path for new cache file: " + cacheFile.getAbsolutePath());
+
+ final File tempDir = cacheFile.getParentFile();
if (tempDir == null) {
- throw new FileNotFoundException("could not cacheFile.getPranetFile()");
+ throw new FileNotFoundException("could not cacheFile.getParentFile()");
}
if (!tempDir.exists()) {
- if (!tempDir.mkdirs()) {
+ DeckLog.verbose("-- The folder in which the new file should be created does not exist yet. Trying to create it...");
+ if (tempDir.mkdirs()) {
+ DeckLog.verbose("--- Creation successful");
+ } else {
throw new IOException("Directory for temporary file does not exist and could not be created.");
}
}
- if (!cacheFile.createNewFile()) {
+
+ DeckLog.verbose("- Try to create actual cache file");
+ if (cacheFile.createNewFile()) {
+ DeckLog.verbose("-- Successfully created cache file");
+ } else {
throw new IOException("Failed to create cacheFile");
}
- FileOutputStream outputStream = new FileOutputStream(fullTempPath);
- byte[] buffer = new byte[4096];
- int count;
- while ((count = inputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, count);
- }
- DeckLog.verbose("----- wrote");
return cacheFile;
}
+
+ @DrawableRes
+ public static int getIconForMimeType(@NonNull String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ } else if (MimeTypeUtil.isAudio(mimeType)) {
+ return R.drawable.ic_music_note_grey600_24dp;
+ } else if (MimeTypeUtil.isVideo(mimeType)) {
+ return R.drawable.ic_local_movies_grey600_24dp;
+ } else if (MimeTypeUtil.isPdf(mimeType)) {
+ return R.drawable.ic_baseline_picture_as_pdf_24;
+ } else if (MimeTypeUtil.isContact(mimeType)) {
+ return R.drawable.ic_baseline_contact_mail_24;
+ } else {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ }
+ }
+
+ public static String getMimeType(@Nullable String url) {
+ String type = null;
+ String extension = MimeTypeMap.getFileExtensionFromUrl(url);
+ if (extension != null) {
+ type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+ return type;
+ }
+
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
index 0390bf96d..04694a058 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
@@ -6,6 +6,7 @@ import java.util.Locale;
public class MimeTypeUtil {
+ public static final String IMAGE_JPEG = "image/jpeg";
public static final String TEXT_PLAIN = "text/plain";
public static final String TEXT_VCARD = "text/vcard";
public static final String APPLICATION_PDF = "application/pdf";
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
new file mode 100644
index 000000000..274af332d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import it.niedermann.nextcloud.deck.R;
+
+public class VCardUtil {
+
+ private VCardUtil() {
+ // You shall not pass
+ }
+
+ public static Uri getVCardContentUri(@NonNull Context context, @NonNull Uri contactUri) throws NoSuchElementException {
+ final ContentResolver cr = context.getContentResolver();
+ try (final Cursor cursor = cr.query(contactUri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
+ return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
+ } else {
+ throw new NoSuchElementException("Cursor has zero entries");
+ }
+ }
+ }
+
+ @ColorInt
+ public static int getColorBasedOnDisplayName(@NonNull Context context, @NonNull String displayName) {
+ final String[] colors = context.getResources().getStringArray(R.array.board_default_colors);
+ final int hashCode = Objects.hashCode(displayName);
+ return Color.parseColor(colors[(hashCode < 0 ? hashCode * -1 : hashCode) % colors.length]);
+ }
+}