diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-11-04 16:08:50 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-11-04 16:08:50 +0300 |
commit | 1ab635e7ab3e103230f78aa96bc715218a78c4f4 (patch) | |
tree | 817ccf51388690c84c2d0ca54e2b7a8cde775620 /app/src/main/java/it/niedermann/nextcloud/deck/ui | |
parent | d525953eb5163d372c09bd59ca8320bc07258634 (diff) |
Created different pickers
Signed-off-by: Stefan Niedermann <info@niedermann.it>
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud/deck/ui')
11 files changed, 620 insertions, 81 deletions
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 e3395ea35..607390c12 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 @@ -30,6 +30,7 @@ import androidx.core.app.SharedElementCallback; 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; @@ -54,6 +55,9 @@ 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.GalleryAdapter; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.takephoto.TakePhotoActivity; @@ -90,16 +94,19 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme private static final int REQUEST_CODE_ADD_FILE = 1; private static final int REQUEST_CODE_ADD_FILE_PERMISSION = 2; - private static final int REQUEST_CODE_CAMERA = 3; - private static final int REQUEST_CODE_CAMERA_PERMISSION = 4; - private static final int REQUEST_CODE_PICK_CONTACT = 5; - private static final int REQUEST_CODE_PICK_CONTACT_PERMISSION = 6; + private static final int REQUEST_CODE_ADD_FILE_PICKER_PERMISSION = 3; + private static final int REQUEST_CODE_CAMERA = 4; + private static final int REQUEST_CODE_CAMERA_PERMISSION = 5; + private static final int REQUEST_CODE_GALLERY_PICKER_PERMISSION = 6; + private static final int REQUEST_CODE_PICK_CONTACT = 7; + private static final int REQUEST_CODE_PICK_CONTACT_PERMISSION = 8; + private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 9; private SyncManager syncManager; private CardAttachmentAdapter adapter; private FloatingActionButton[] brandedViews; - private GalleryAdapter galleryAdapter; + private AbstractPickerAdapter<?> pickerAdapter; private boolean pickerAnimationInProgress = false; private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) { @@ -221,20 +228,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } if (viewModel.canEdit()) { - binding.fab.setOnClickListener(v -> { - if (SDK_INT >= LOLLIPOP && galleryAdapter == null) { - galleryAdapter = new GalleryAdapter(requireContext(), uri -> { - // TODO show selected image in dialog and let it confirm first - onActivityResult(REQUEST_CODE_ADD_FILE, RESULT_OK, new Intent().setData(uri)); - }); - binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); - binding.pickerRecyclerView.setAdapter(galleryAdapter); - } - mBottomSheetBehaviour.setState(STATE_HALF_EXPANDED); - showPicker(); - backPressedCallback.setEnabled(true); - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); - }); + binding.fab.setOnClickListener(v -> showGalleryPicker()); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -286,6 +280,55 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } } + public void showContactPicker() { + if (isPermissionRequestNeeded(READ_CONTACTS)) { + requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION); + } else { + pickerAdapter = new ContactAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri)), this::pickContact); + binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.pickerRecyclerView.setAdapter(pickerAdapter); + mBottomSheetBehaviour.setState(STATE_HALF_EXPANDED); + showPicker(); + backPressedCallback.setEnabled(true); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); + } + } + + public void showGalleryPicker() { + if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE) || isPermissionRequestNeeded(CAMERA)) { + requestPermissions(new String[]{READ_EXTERNAL_STORAGE, CAMERA}, REQUEST_CODE_GALLERY_PICKER_PERMISSION); + } else { + if (SDK_INT >= LOLLIPOP) { + pickerAdapter = new GalleryAdapter(requireContext(), uri -> { + // TODO show selected image in dialog and let it confirm first + onActivityResult(REQUEST_CODE_ADD_FILE, RESULT_OK, new Intent().setData(uri)); + }, this::pickCamera, getViewLifecycleOwner()); + binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); + binding.pickerRecyclerView.setAdapter(pickerAdapter); + } + mBottomSheetBehaviour.setState(STATE_HALF_EXPANDED); + showPicker(); + backPressedCallback.setEnabled(true); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); + } + } + + public void showFilesPicker() { + if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) { + requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_ADD_FILE_PICKER_PERMISSION); + } else { + if (SDK_INT >= LOLLIPOP) { + pickerAdapter = new FileAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_ADD_FILE, RESULT_OK, new Intent().setData(uri)), this::pickFile); + binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.pickerRecyclerView.setAdapter(pickerAdapter); + } + mBottomSheetBehaviour.setState(STATE_HALF_EXPANDED); + showPicker(); + backPressedCallback.setEnabled(true); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); + } + } + public void pickContact() { if (isPermissionRequestNeeded(READ_CONTACTS)) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PERMISSION); @@ -348,8 +391,8 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme @Override public void onDestroy() { - if (this.galleryAdapter != null) { - this.galleryAdapter.onDestroy(); + if (this.pickerAdapter != null) { + this.pickerAdapter.onDestroy(); this.binding.pickerRecyclerView.setAdapter(null); } super.onDestroy(); @@ -444,6 +487,30 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } break; } + case REQUEST_CODE_ADD_FILE_PICKER_PERMISSION: { + if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) { + showFilesPicker(); + } else { + Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); + } + break; + } + case REQUEST_CODE_GALLERY_PICKER_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); } 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..e2c655916 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java @@ -0,0 +1,98 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; + +import static android.database.Cursor.FIELD_TYPE_BLOB; +import static android.database.Cursor.FIELD_TYPE_FLOAT; +import static android.database.Cursor.FIELD_TYPE_INTEGER; +import static android.database.Cursor.FIELD_TYPE_NULL; +import static android.database.Cursor.FIELD_TYPE_STRING; +import static androidx.recyclerview.widget.RecyclerView.NO_ID; + +public abstract class AbstractPickerAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> { + + protected static final int VIEW_TYPE_ITEM = 0; + protected static final int VIEW_TYPE_ITEM_PICKER = 1; + + private final int count; + protected final int columnIndex; + private final int columnIndexType; + @NonNull + protected final Consumer<Uri> onSelect; + @NonNull + protected final Runnable onSelectPicker; + @NonNull + protected final Cursor cursor; + @NonNull + protected final ContentResolver contentResolver; + + public AbstractPickerAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect, @NonNull Runnable onSelectPicker, Uri subject, String idColumn, String sortOrder) { + this(context, onSelect, onSelectPicker, subject, idColumn, new String[]{idColumn}, sortOrder); + } + + public AbstractPickerAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect, @NonNull Runnable onSelectPicker, Uri subject, String idColumn, String[] requestedColumns, String sortOrder) { + this.onSelect = onSelect; + this.onSelectPicker = onSelectPicker; + this.contentResolver = context.getContentResolver(); + this.cursor = Objects.requireNonNull(contentResolver.query(subject, requestedColumns, null, null, sortOrder)); + this.columnIndex = cursor.getColumnIndex(idColumn); + cursor.moveToFirst(); + this.columnIndexType = this.cursor.getType(columnIndex); + this.count = cursor.getCount(); + setHasStableIds(true); + } + + /** + * Moves the {@link #cursor} to the given position + */ + @Override + public long getItemId(int position) { + if (cursor.moveToPosition(position - 1)) { + switch (columnIndexType) { + case FIELD_TYPE_INTEGER: + return cursor.getLong(columnIndex); + case FIELD_TYPE_NULL: + return NO_ID; + case FIELD_TYPE_FLOAT: + return String.valueOf(cursor.getFloat(columnIndex)).hashCode(); + case FIELD_TYPE_STRING: + return cursor.getString(columnIndex).hashCode(); + case FIELD_TYPE_BLOB: + return Arrays.hashCode(cursor.getBlob(columnIndex)); + default: + throw new IllegalStateException("Unknown type for columnIndex \"" + columnIndex + "\": " + columnIndexType); + } + } else { + return NO_ID; + } + } + + @Override + public int getItemCount() { + return count; + } + + @Override + public int getItemViewType(int position) { + return position == 0 + ? VIEW_TYPE_ITEM_PICKER + : VIEW_TYPE_ITEM; + } + + /** + * Call this method when the {@link AbstractPickerAdapter} is no longe need to free resources. + */ + public void onDestroy() { + cursor.close(); + } +} 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..4c8bae92f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java @@ -0,0 +1,81 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.ContentUris; +import android.content.Context; +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.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +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.LOOKUP_KEY; +import static android.provider.ContactsContract.Contacts.SORT_KEY_PRIMARY; + +public class ContactAdapter extends AbstractPickerAdapter<RecyclerView.ViewHolder> { + + + private final int displayNameColumnIndex; + @NonNull + private final ExecutorService bitmapExecutor = Executors.newCachedThreadPool(); + + public ContactAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect, @NonNull Runnable onSelectPicker) { + super(context, onSelect, onSelectPicker, CONTENT_URI, LOOKUP_KEY, new String[]{LOOKUP_KEY, DISPLAY_NAME}, SORT_KEY_PRIMARY); + displayNameColumnIndex = cursor.getColumnIndex(DISPLAY_NAME); + notifyItemRangeInserted(0, getItemCount()); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_PICKER: + return new ContactPickerItemViewHolder(ItemFilterUserBinding.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_PICKER: { + ((ContactPickerItemViewHolder) holder).bind(onSelectPicker); + break; + } + case VIEW_TYPE_ITEM: { + bitmapExecutor.execute(() -> { + final ContactItemViewHolder viewHolder = (ContactItemViewHolder) holder; + final long id = getItemId(position); + final String name = cursor.getString(displayNameColumnIndex); + try (InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, Uri.withAppendedPath(CONTENT_LOOKUP_URI, cursor.getString(columnIndex)))) { + final Bitmap thumbnail = BitmapFactory.decodeStream(inputStream); + new Handler(Looper.getMainLooper()).post(() -> viewHolder.bind(ContentUris.withAppendedId(CONTENT_LOOKUP_URI, id), thumbnail, name, onSelect)); + } catch (IOException ignored) { + 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..272fcdca3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java @@ -0,0 +1,43 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.graphics.Bitmap; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +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, @NonNull Consumer<Uri> onSelect) { + itemView.setOnClickListener((v) -> onSelect.accept(uri)); + binding.displayName.setText(displayName); + 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/ContactPickerItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactPickerItemViewHolder.java new file mode 100644 index 000000000..7978dde93 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactPickerItemViewHolder.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding; + +public class ContactPickerItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemFilterUserBinding binding; + + public ContactPickerItemViewHolder(@NonNull ItemFilterUserBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(Runnable onOpenMajorPicker) { + Glide.with(itemView.getContext()) + .load(R.drawable.ic_baseline_account_circle_24) + .into(binding.avatar); + binding.displayName.setText("Show all 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..867081a4f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java @@ -0,0 +1,72 @@ +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.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +import static android.provider.BaseColumns._ID; +import static android.provider.MediaStore.Files.FileColumns.TITLE; +import static android.provider.MediaStore.MediaColumns.DATE_ADDED; +import static android.provider.MediaStore.MediaColumns.SIZE; + +public class FileAdapter extends AbstractPickerAdapter<RecyclerView.ViewHolder> { + + private final int displayNameColumnIndex; + private final int sizeColumnIndex; + @NonNull + private final ExecutorService bitmapExecutor = Executors.newCachedThreadPool(); + + public FileAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect, @NonNull Runnable onSelectPicker) { + super(context, onSelect, onSelectPicker, MediaStore.Files.getContentUri("external"), _ID, new String[]{_ID, TITLE, SIZE}, DATE_ADDED + " DESC"); + displayNameColumnIndex = cursor.getColumnIndex(TITLE); + sizeColumnIndex = cursor.getColumnIndex(SIZE); + notifyItemRangeInserted(0, getItemCount()); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_PICKER: + return new FilePickerItemViewHolder(ItemFilterUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + case VIEW_TYPE_ITEM: + return new FileItemViewHolder(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_PICKER: { + ((FilePickerItemViewHolder) holder).bind(onSelectPicker); + break; + } + case VIEW_TYPE_ITEM: { + bitmapExecutor.execute(() -> { + final long id = getItemId(position); + final String name = cursor.getString(displayNameColumnIndex); + final long size = cursor.getLong(sizeColumnIndex); + new Handler(Looper.getMainLooper()).post(() -> ((FileItemViewHolder) holder).bind(ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id), name, size, onSelect)); + }); + break; + } + } + } +} 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..6273ea1dc --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java @@ -0,0 +1,43 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.net.Uri; +import android.text.format.Formatter; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +public class FileItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPickerUserBinding binding; + + public FileItemViewHolder(@NonNull ItemPickerUserBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Uri uri, @NonNull String displayName, long size, @NonNull Consumer<Uri> onSelect) { + itemView.setOnClickListener((v) -> onSelect.accept(uri)); + binding.displayName.setText(displayName); + binding.contactInformation.setText(Formatter.formatFileSize(itemView.getContext(), size)); + Glide.with(itemView.getContext()) + .load(R.drawable.ic_attach_file_grey600_24dp) + .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/FilePickerItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FilePickerItemViewHolder.java new file mode 100644 index 000000000..2cf012acb --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FilePickerItemViewHolder.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding; + +public class FilePickerItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemFilterUserBinding binding; + + public FilePickerItemViewHolder(@NonNull ItemFilterUserBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(Runnable onOpenMajorPicker) { + Glide.with(itemView.getContext()) + .load(R.drawable.ic_attach_file_grey600_24dp) + .into(binding.avatar); + binding.displayName.setText("Show all files"); + 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 index 902ff2cb7..209e4af90 100644 --- 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 @@ -1,10 +1,8 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; -import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Handler; @@ -15,92 +13,89 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import androidx.recyclerview.widget.RecyclerView; import java.io.IOException; -import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; 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 androidx.recyclerview.widget.RecyclerView.NO_ID; +import static android.provider.BaseColumns._ID; +import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI; -public class GalleryAdapter extends RecyclerView.Adapter<GalleryItemViewHolder> { +public class GalleryAdapter extends AbstractPickerAdapter<RecyclerView.ViewHolder> { - private final int columnIndex; - private final int count; @NonNull - private final Consumer<Uri> onSelect; + private final ExecutorService bitmapExecutor = Executors.newCachedThreadPool(); @NonNull - private final Cursor cursor; - @NonNull - private final ContentResolver contentResolver; - @NonNull - private final ExecutorService bitmapFetcherExecutor = Executors.newCachedThreadPool(); - @NonNull - private final ExecutorService bitmapWaiterExecutor = Executors.newCachedThreadPool(); + private final LifecycleOwner lifecycleOwner; - public GalleryAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect) { - this.onSelect = onSelect; - this.contentResolver = context.getContentResolver(); - @SuppressLint("InlinedApi") final String sortOrder = (SDK_INT >= Q) - ? MediaStore.Images.Media.DATE_TAKEN - : MediaStore.Images.Media.DATE_ADDED; - cursor = Objects.requireNonNull(contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID}, null, null, sortOrder + " DESC")); - this.columnIndex = cursor.getColumnIndex(MediaStore.Images.Media._ID); - this.count = cursor.getCount(); - notifyItemRangeInserted(0, this.count); - setHasStableIds(true); - } + @SuppressLint("InlinedApi") + private static final String sortOrder = (SDK_INT >= Q) + ? MediaStore.Images.Media.DATE_TAKEN + : MediaStore.Images.Media.DATE_ADDED; - @Override - public long getItemId(int position) { - if (cursor.moveToPosition(position)) { - return cursor.getLong(columnIndex); - } else { - return NO_ID; - } + public GalleryAdapter(@NonNull Context context, @NonNull Consumer<Uri> onSelect, @NonNull Runnable onSelectPicker, @NonNull LifecycleOwner lifecycleOwner) { + super(context, onSelect, onSelectPicker, EXTERNAL_CONTENT_URI, _ID, sortOrder + " DESC"); + this.lifecycleOwner = lifecycleOwner; + notifyItemRangeInserted(0, getItemCount()); } @NonNull @Override - public GalleryItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new GalleryItemViewHolder(ItemAttachmentImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_PICKER: + return new GalleryPickerItemViewHolder(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 GalleryItemViewHolder holder, int position) { - long id = getItemId(position); - try { - Bitmap thumbnail; - if (SDK_INT >= Q) { - thumbnail = contentResolver.loadThumbnail(ContentUris.withAppendedId( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), new Size(512, 384), null); - } else { - thumbnail = MediaStore.Images.Thumbnails.getThumbnail( - contentResolver, id, - MediaStore.Images.Thumbnails.MINI_KIND, null); + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_ITEM_PICKER: { + ((GalleryPickerItemViewHolder) holder).bind(onSelectPicker, lifecycleOwner); + break; + } + case VIEW_TYPE_ITEM: { + bitmapExecutor.execute(() -> { + final long id = getItemId(position); + 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); + } + }); } - new Handler(Looper.getMainLooper()).post(() -> holder.bind(ContentUris.withAppendedId( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id), thumbnail, onSelect)); - } catch (IOException ignored) { - new Handler(Looper.getMainLooper()).post(holder::bindError); } } @Override - public int getItemCount() { - return count; - } - - /** - * Call this method when the {@link GalleryAdapter} is no longe need to free resources. - */ - public void onDestroy() { - cursor.close(); + public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) { + super.onViewDetachedFromWindow(holder); + if(holder instanceof GalleryPickerItemViewHolder) { + ((GalleryPickerItemViewHolder) holder).unbind(); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPickerItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPickerItemViewHolder.java new file mode 100644 index 000000000..80e15fa7b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPickerItemViewHolder.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 GalleryPickerItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPhotoPreviewBinding binding; + private ProcessCameraProvider cameraProvider; + + public GalleryPickerItemViewHolder(@NonNull ItemPhotoPreviewBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Runnable onSelectPicker, @NonNull LifecycleOwner lifecycleOwner) { + itemView.setOnClickListener((v) -> onSelectPicker.run()); + + ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(itemView.getContext()); + cameraProviderFuture.addListener(() -> { + try { + cameraProvider = cameraProviderFuture.get(); + Preview previewUseCase = new Preview.Builder().build(); + previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider()); + cameraProvider.bindToLifecycle(lifecycleOwner, DEFAULT_BACK_CAMERA, previewUseCase); + } catch (ExecutionException | InterruptedException 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/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 |