diff options
author | desperateCoder <echotodevnull@gmail.com> | 2020-11-14 15:50:47 +0300 |
---|---|---|
committer | desperateCoder <echotodevnull@gmail.com> | 2020-11-14 15:50:47 +0300 |
commit | d235ae16f1e582fc21a8631ccb3ecd840fba13b4 (patch) | |
tree | ef13a3464a6beab9b74499a7d865b448210c7b87 /app/src/main/java/it/niedermann/nextcloud/deck/ui | |
parent | 58903257509071f9d245132697d20ace6be17600 (diff) | |
parent | 0f8b8d55fa6c5de2072e306ef3446dab11f155eb (diff) |
Merge branch 'master' of github.com:stefan-niedermann/nextcloud-deck
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud/deck/ui')
26 files changed, 1701 insertions, 147 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 |