From f706cc15ab6fa14f53ec1ef40357fcb98dbe6457 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Wed, 24 Jun 2020 13:54:41 +0200 Subject: [PATCH 01/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20sources=20-=20Intro?= =?UTF-8?q?duce=20bottom=20sheet=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attachments/CardAttachmentsFragment.java | 66 ++++++++++------ .../picker/CardAttachmentPicker.java | 61 +++++++++++++++ .../picker/CardAttachmentPickerListener.java | 13 ++++ .../drawable/ic_baseline_photo_camera_24.xml | 6 ++ .../res/layout/dialog_attachment_picker.xml | 76 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 6 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPickerListener.java create mode 100644 app/src/main/res/drawable/ic_baseline_photo_camera_24.xml create mode 100644 app/src/main/res/layout/dialog_attachment_picker.xml 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 dff12272e..1e2f2b72c 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 @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.provider.MediaStore; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; @@ -41,6 +42,8 @@ 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.CardAttachmentPicker; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.CardAttachmentPickerListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -50,13 +53,14 @@ import static it.niedermann.nextcloud.deck.util.AttachmentUtil.copyContentUriToTempFile; import static java.net.HttpURLConnection.HTTP_CONFLICT; -public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener { +public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener, CardAttachmentPickerListener { private FragmentCardEditTabAttachmentsBinding binding; private EditCardViewModel viewModel; - private static final int REQUEST_CODE_ADD_ATTACHMENT = 1; - private static final int REQUEST_PERMISSION = 2; + 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_CAPTURE_IMAGE = 3; private SyncManager syncManager; private CardAttachmentAdapter adapter; @@ -123,15 +127,8 @@ public void onMapSharedElements(List names, Map sharedElem updateEmptyContentView(); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && viewModel.canEdit()) { - binding.fab.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERMISSION); - } else { - startFilePickerIntent(); - } - }); + if (viewModel.canEdit()) { + binding.fab.setOnClickListener(v -> new CardAttachmentPicker().show(getChildFragmentManager(), CardAttachmentPicker.class.getSimpleName())); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -149,18 +146,10 @@ else if (dy < 0) return binding.getRoot(); } - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private void startFilePickerIntent() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, REQUEST_CODE_ADD_ATTACHMENT); - } - @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_ADD_ATTACHMENT && resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_CODE_ADD_FILE && resultCode == Activity.RESULT_OK) { if (data == null) { ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); return; @@ -235,7 +224,7 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == REQUEST_PERMISSION) { + if (requestCode == REQUEST_CODE_ADD_FILE_PERMISSION) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { startFilePickerIntent(); } @@ -263,7 +252,6 @@ public void onAttachmentClicked(int position) { this.clickedItemPosition = position; } - private void updateEmptyContentView() { if (this.adapter == null || this.adapter.getItemCount() == 0) { this.binding.emptyContentView.setVisibility(View.VISIBLE); @@ -278,4 +266,36 @@ private void updateEmptyContentView() { public void applyBrand(int mainColor, int textColor) { applyBrandToFAB(mainColor, textColor, binding.fab); } + + @Override + public void pickCamera() { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(requireContext().getPackageManager()) != null) { + startActivityForResult(takePictureIntent, REQUEST_CODE_CAPTURE_IMAGE); + } + } + + @Override + public void pickContact() { + + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void pickFile() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_CODE_ADD_FILE_PERMISSION); + } else { + startFilePickerIntent(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private void startFilePickerIntent() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, REQUEST_CODE_ADD_FILE); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java new file mode 100644 index 000000000..fcd6c217d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -0,0 +1,61 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import it.niedermann.nextcloud.deck.databinding.DialogAttachmentPickerBinding; + +public class CardAttachmentPicker extends BottomSheetDialogFragment { + + private DialogAttachmentPickerBinding binding; + private CardAttachmentPickerListener listener; + + public CardAttachmentPicker() { + + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (getParentFragment() instanceof CardAttachmentPickerListener) { + this.listener = (CardAttachmentPickerListener) getParentFragment(); + } else { + throw new IllegalArgumentException("Caller must implement " + CardAttachmentPickerListener.class.getSimpleName()); + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = DialogAttachmentPickerBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + binding.pickCamera.setOnClickListener((v) -> listener.pickCamera()); + binding.pickContact.setOnClickListener((v) -> listener.pickContact()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + binding.pickFile.setOnClickListener((v) -> listener.pickFile()); + } else { + binding.pickFile.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPickerListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPickerListener.java new file mode 100644 index 000000000..84b292d81 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPickerListener.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +public interface CardAttachmentPickerListener { + + void pickCamera(); + void pickContact(); + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + void pickFile(); +} diff --git a/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml new file mode 100644 index 000000000..497db8383 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/dialog_attachment_picker.xml b/app/src/main/res/layout/dialog_attachment_picker.xml new file mode 100644 index 000000000..afe5f2ce3 --- /dev/null +++ b/app/src/main/res/layout/dialog_attachment_picker.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d0867b74..6f5a3c4f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -276,4 +276,7 @@ %1$d errors while uploading Report + Contact + File + Camera From 7b8f51f73cf264dfdcbddc78377ab9bb977af988 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Wed, 24 Jun 2020 17:53:32 +0200 Subject: [PATCH 02/34] First steps for retrieving contacts and camera images --- app/src/main/AndroidManifest.xml | 2 +- .../attachments/CardAttachmentsFragment.java | 181 +++++++++++------- .../picker/CardAttachmentPicker.java | 20 +- .../nextcloud/deck/util/AttachmentUtil.java | 2 +- 4 files changed, 129 insertions(+), 76 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 132beb6ce..008a5554f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + - names, Map sharedElem } if (viewModel.canEdit()) { - binding.fab.setOnClickListener(v -> new CardAttachmentPicker().show(getChildFragmentManager(), CardAttachmentPicker.class.getSimpleName())); + binding.fab.setOnClickListener(v -> CardAttachmentPicker.newInstance().show(getChildFragmentManager(), CardAttachmentPicker.class.getSimpleName())); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -149,77 +158,95 @@ else if (dy < 0) @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_ADD_FILE && resultCode == Activity.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; - } - - DeckLog.verbose("--- found content URL " + sourceUri.getPath()); - File fileToUpload; - - 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()); - return; - } + switch (requestCode) { + case REQUEST_CODE_PICK_CONTACT: { + Uri uri = (Uri) data.getData(); + try { + Cursor a = requireContext().getContentResolver().query(uri, null, null, null, null); + Objects.requireNonNull(a).moveToFirst(); + for(int i = 0; i < a.getColumnCount(); i++) { + Log.e("col:", a.getColumnName(i) + " | " + a.getString(i)); + } + } catch (Exception e) { - 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; } + break; } + case REQUEST_CODE_CAPTURE_IMAGE: + case REQUEST_CODE_ADD_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; + } + + DeckLog.verbose("--- found content URL " + sourceUri.getPath()); + File fileToUpload; - final Date now = new Date(); - 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 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); + 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()); + 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(); - } else { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + return; } - } else { - viewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - viewModel.getFullCard().getAttachments().add(next); - adapter.addAttachment(next); } - }); + + final Date now = new Date(); + 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 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); + adapter.removeAttachment(a); + viewModel.getFullCard().getAttachments().add(next); + adapter.addAttachment(next); + } + }); + } + updateEmptyContentView(); + } + break; } - updateEmptyContentView(); } - } @Override @@ -233,10 +260,6 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } } - public static Fragment newInstance() { - return new CardAttachmentsFragment(); - } - @Override public void onAttachmentDeleted(Attachment attachment) { adapter.removeAttachment(attachment); @@ -269,15 +292,27 @@ public void applyBrand(int mainColor, int textColor) { @Override public void pickCamera() { - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + final Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(requireContext().getPackageManager()) != null) { - startActivityForResult(takePictureIntent, REQUEST_CODE_CAPTURE_IMAGE); + Long localId = viewModel.getFullCard().getLocalId(); + String imageFileName = "JPEG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()) + "_"; + File tempFile = new File(requireContext().getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + viewModel.getFullCard().getAccountId() + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + imageFileName); + + Uri photoURI = FileProvider.getUriForFile(requireContext(), + BuildConfig.APPLICATION_ID + ".fileprovider", + tempFile); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(takePictureIntent, REQUEST_CODE_ADD_FILE); } } @Override public void pickContact() { - + 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); + } } @Override @@ -298,4 +333,8 @@ private void startFilePickerIntent() { intent.setType("*/*"); startActivityForResult(intent, REQUEST_CODE_ADD_FILE); } + + public static Fragment newInstance() { + return new CardAttachmentsFragment(); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index fcd6c217d..e84dd6c60 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; @@ -49,13 +50,26 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - binding.pickCamera.setOnClickListener((v) -> listener.pickCamera()); - binding.pickContact.setOnClickListener((v) -> listener.pickContact()); + binding.pickCamera.setOnClickListener((v) -> { + listener.pickCamera(); + dismiss(); + }); + binding.pickContact.setOnClickListener((v) -> { + listener.pickContact(); + dismiss(); + }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - binding.pickFile.setOnClickListener((v) -> listener.pickFile()); + binding.pickFile.setOnClickListener((v) -> { + listener.pickFile(); + dismiss(); + }); } else { binding.pickFile.setVisibility(View.GONE); } } + + public static DialogFragment newInstance() { + return new CardAttachmentPicker(); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java index 6b2d6925f..ce9e10012 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java @@ -36,7 +36,7 @@ public static File copyContentUriToTempFile(@NonNull Context context, @NonNull U File cacheFile = new File(fullTempPath); File tempDir = cacheFile.getParentFile(); if (tempDir == null) { - throw new FileNotFoundException("could not cacheFile.getPranetFile()"); + throw new FileNotFoundException("could not cacheFile.getParentFile()"); } if (!tempDir.exists()) { if (!tempDir.mkdirs()) { From a8d8a5538fb0dddb8a0f762ca3d84919fdaa4b93 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 26 Jun 2020 13:08:10 +0200 Subject: [PATCH 03/34] Refactor permissions process --- .../attachments/CardAttachmentsFragment.java | 150 +++++++++++------- 1 file changed, 92 insertions(+), 58 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 5634b71ee..cab648c6b 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 @@ -20,6 +20,7 @@ import androidx.annotation.RequiresApi; import androidx.core.app.SharedElementCallback; import androidx.core.content.FileProvider; +import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; @@ -35,7 +36,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.DeckLog; @@ -54,6 +54,7 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static android.app.Activity.RESULT_OK; +import static androidx.core.content.PermissionChecker.checkSelfPermission; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandedActivity.applyBrandToFAB; import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT; @@ -70,6 +71,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme private static final int REQUEST_CODE_ADD_FILE_PERMISSION = 2; private static final int REQUEST_CODE_CAPTURE_IMAGE = 3; private static final int REQUEST_CODE_PICK_CONTACT = 4; + private static final int REQUEST_CODE_PICK_CONTACT_PERMISSION = 5; private SyncManager syncManager; private CardAttachmentAdapter adapter; @@ -155,21 +157,92 @@ else if (dy < 0) return binding.getRoot(); } + @Override + public void pickCamera() { + final Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(requireContext().getPackageManager()) != null) { + Long localId = viewModel.getFullCard().getLocalId(); + String imageFileName = "JPEG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()) + "_"; + File tempFile = new File(requireContext().getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + viewModel.getFullCard().getAccountId() + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + imageFileName); + + Uri photoURI = FileProvider.getUriForFile(requireContext(), + BuildConfig.APPLICATION_ID + ".fileprovider", + tempFile); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(takePictureIntent, REQUEST_CODE_ADD_FILE); + } + } + + @Override + public void pickContact() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS) != PermissionChecker.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PERMISSION); + } else { + 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); + } + } + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void pickFile() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS) != PermissionChecker.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_CODE_ADD_FILE_PERMISSION); + } else { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*"); + startActivityForResult(intent, REQUEST_CODE_ADD_FILE); + } + } + @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQUEST_CODE_PICK_CONTACT: { +// Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); Uri uri = (Uri) data.getData(); - try { - Cursor a = requireContext().getContentResolver().query(uri, null, null, null, null); - Objects.requireNonNull(a).moveToFirst(); - for(int i = 0; i < a.getColumnCount(); i++) { - Log.e("col:", a.getColumnName(i) + " | " + a.getString(i)); - } - } catch (Exception e) { + ContentResolver cr = requireContext().getContentResolver(); + // Get the Cursor of all the contacts + Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); + + // Move the cursor to first. Also check whether the cursor is empty or not. + if (cursor.moveToFirst()) { + // Iterate through the cursor + // Get the contacts name + String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); + Log.e("col: ", name); + + + +// +// AssetFileDescriptor fd; +// try +// { +// fd = requireContext().getContentResolver().openAssetFileDescriptor(uri, "r"); +// FileInputStream fis = fd.createInputStream(); +// byte[] buf = new byte[(int) fd.getDeclaredLength()]; +// fis.read(buf); +// String VCard = new String(buf); +// String path = Environment.getExternalStorageDirectory().toString() + File.separator + "POContactsRestore.vcf"; +// FileOutputStream mFileOutputStream = new FileOutputStream(path, true); +// mFileOutputStream.write(VCard.toString().getBytes()); +// Log.d("Vcard", VCard); +// } +// catch (Exception e1) +// { +// // TODO Auto-generated catch block +// e1.printStackTrace(); +// } } + // Close the curosor + cursor.close(); break; } case REQUEST_CODE_CAPTURE_IMAGE: @@ -251,12 +324,17 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == REQUEST_CODE_ADD_FILE_PERMISSION) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - startFilePickerIntent(); - } - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case REQUEST_CODE_ADD_FILE_PERMISSION: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + pickFile(); + } + break; + case REQUEST_CODE_PICK_CONTACT_PERMISSION: + pickContact(); + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } @@ -290,50 +368,6 @@ public void applyBrand(int mainColor, int textColor) { applyBrandToFAB(mainColor, textColor, binding.fab); } - @Override - public void pickCamera() { - final Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - if (takePictureIntent.resolveActivity(requireContext().getPackageManager()) != null) { - Long localId = viewModel.getFullCard().getLocalId(); - String imageFileName = "JPEG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()) + "_"; - File tempFile = new File(requireContext().getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + viewModel.getFullCard().getAccountId() + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + imageFileName); - - Uri photoURI = FileProvider.getUriForFile(requireContext(), - BuildConfig.APPLICATION_ID + ".fileprovider", - tempFile); - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); - startActivityForResult(takePictureIntent, REQUEST_CODE_ADD_FILE); - } - } - - @Override - public void pickContact() { - 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); - } - } - - @Override - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - public void pickFile() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_CODE_ADD_FILE_PERMISSION); - } else { - startFilePickerIntent(); - } - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private void startFilePickerIntent() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, REQUEST_CODE_ADD_FILE); - } - public static Fragment newInstance() { return new CardAttachmentsFragment(); } From 34b73be02c22e9e8e85d88ff3869c72fa29def1d Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 29 Oct 2020 16:42:20 +0100 Subject: [PATCH 04/34] Adjust to targetSdk 30 Signed-off-by: Stefan Niedermann --- app/src/main/AndroidManifest.xml | 6 ++++++ .../deck/ui/card/attachments/CardAttachmentsFragment.java | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 609c022bb..73a1bcbe6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,12 @@ + + + + + + Date: Thu, 29 Oct 2020 16:43:37 +0100 Subject: [PATCH 05/34] Restore default path for switch-case Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentsFragment.java | 3 +++ 1 file changed, 3 insertions(+) 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 73f1474a2..6a78d9e74 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 @@ -322,6 +322,9 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d } break; } + default: { + super.onActivityResult(requestCode, resultCode, data); + } } } From 9929931ba8716cea5a84f01bc209a5083acbefea Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 29 Oct 2020 18:09:45 +0100 Subject: [PATCH 06/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make contacts upload work Signed-off-by: Stefan Niedermann --- .../attachments/CardAttachmentsFragment.java | 167 +++++++----------- .../nextcloud/deck/util/AttachmentUtil.java | 6 +- .../nextcloud/deck/util/VCardUtil.java | 32 ++++ 3 files changed, 103 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java 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 6a78d9e74..6860b70cd 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 @@ -3,14 +3,12 @@ import android.Manifest; import android.content.ContentResolver; import android.content.Intent; -import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.provider.MediaStore; import android.util.DisplayMetrics; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -53,6 +51,7 @@ import it.niedermann.nextcloud.deck.ui.card.attachments.picker.CardAttachmentPicker; import it.niedermann.nextcloud.deck.ui.card.attachments.picker.CardAttachmentPickerListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.deck.util.VCardUtil; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.app.Activity.RESULT_OK; @@ -209,43 +208,14 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQUEST_CODE_PICK_CONTACT: { -// Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); - Uri uri = (Uri) data.getData(); - - ContentResolver cr = requireContext().getContentResolver(); - // Get the Cursor of all the contacts - Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); - - // Move the cursor to first. Also check whether the cursor is empty or not. - if (cursor.moveToFirst()) { - // Iterate through the cursor - // Get the contacts name - String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); - Log.e("col: ", name); - - -// -// AssetFileDescriptor fd; -// try -// { -// fd = requireContext().getContentResolver().openAssetFileDescriptor(uri, "r"); -// FileInputStream fis = fd.createInputStream(); -// byte[] buf = new byte[(int) fd.getDeclaredLength()]; -// fis.read(buf); -// String VCard = new String(buf); -// String path = Environment.getExternalStorageDirectory().toString() + File.separator + "POContactsRestore.vcf"; -// FileOutputStream mFileOutputStream = new FileOutputStream(path, true); -// mFileOutputStream.write(VCard.toString().getBytes()); -// Log.d("Vcard", VCard); -// } -// catch (Exception e1) -// { -// // TODO Auto-generated catch block -// e1.printStackTrace(); -// } + if (resultCode == RESULT_OK) { + final Uri sourceUri = VCardUtil.getVCardContentUri(requireContext()); + try { + uploadNewAttachment(sourceUri); + } catch (UploadAttachmentFailedException | IOException e) { + ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } } - // Close the curosor - cursor.close(); break; } case REQUEST_CODE_CAPTURE_IMAGE: @@ -256,69 +226,11 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d 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; - } - - DeckLog.verbose("--- found content URL " + sourceUri.getPath()); - File fileToUpload; - 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()); - 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; - } + uploadNewAttachment(sourceUri); + } catch (UploadAttachmentFailedException | IOException e) { + ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } - - final Date now = new Date(); - 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 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); - adapter.removeAttachment(a); - viewModel.getFullCard().getAttachments().add(next); - adapter.addAttachment(next); - } - }); - } - updateEmptyContentView(); } break; } @@ -328,6 +240,63 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d } } + private void uploadNewAttachment(@NonNull Uri sourceUri) throws UploadAttachmentFailedException, IOException { + if (sourceUri == null) { + throw new UploadAttachmentFailedException("sourceUri is null"); + } + if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) { + throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()); + } + + DeckLog.verbose("--- found content URL " + sourceUri.getPath()); + + final File fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId()); + + 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 Date now = new Date(); + 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 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); + adapter.removeAttachment(a); + viewModel.getFullCard().getAttachments().add(next); + adapter.addAttachment(next); + } + }); + } + updateEmptyContentView(); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java index 3931e3323..f349561de 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java @@ -58,8 +58,8 @@ private static String getRemoteUrl(@NonNull String accountUrl, @NonNull Long car return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId; } - public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localId) throws IOException, IllegalArgumentException { - String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context); + public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localCardId) throws IOException, IllegalArgumentException { + String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localCardId == null ? "pending-creation" : localCardId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context); DeckLog.verbose("----- fullTempPath: " + fullTempPath); InputStream inputStream = context.getContentResolver().openInputStream(currentUri); if (inputStream == null) { @@ -78,7 +78,7 @@ public static File copyContentUriToTempFile(@NonNull Context context, @NonNull U if (!cacheFile.createNewFile()) { throw new IOException("Failed to create cacheFile"); } - FileOutputStream outputStream = new FileOutputStream(fullTempPath); + final FileOutputStream outputStream = new FileOutputStream(fullTempPath); byte[] buffer = new byte[4096]; int count; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java new file mode 100644 index 000000000..b54361ca3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java @@ -0,0 +1,32 @@ +package it.niedermann.nextcloud.deck.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; + +import androidx.annotation.NonNull; + +import java.util.NoSuchElementException; + +public class VCardUtil { + + private static final String TAG = VCardUtil.class.getSimpleName(); + + private VCardUtil() { + // You shall not pass + } + + public static Uri getVCardContentUri(@NonNull Context context) { + final ContentResolver cr = context.getContentResolver(); + try (final Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null)) { + if (cursor.moveToFirst()) { + final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); + return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); + } else { + throw new NoSuchElementException("Cursor has zero entries"); + } + } + } +} From d773e16a1e1c941c8445600f42adccd1213683c8 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 29 Oct 2020 18:17:36 +0100 Subject: [PATCH 07/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor clean up Signed-off-by: Stefan Niedermann --- .../ui/card/attachments/CardAttachmentsFragment.java | 10 ++++------ .../it/niedermann/nextcloud/deck/util/VCardUtil.java | 4 +--- app/src/main/res/layout/dialog_attachment_picker.xml | 6 +++--- 3 files changed, 8 insertions(+), 12 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 6860b70cd..cf4a3feaf 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 @@ -209,10 +209,9 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d switch (requestCode) { case REQUEST_CODE_PICK_CONTACT: { if (resultCode == RESULT_OK) { - final Uri sourceUri = VCardUtil.getVCardContentUri(requireContext()); try { - uploadNewAttachment(sourceUri); - } catch (UploadAttachmentFailedException | IOException e) { + uploadNewAttachment(VCardUtil.getVCardContentUri(requireContext())); + } catch (Exception e) { ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } } @@ -225,10 +224,9 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); return; } - final Uri sourceUri = data.getData(); try { - uploadNewAttachment(sourceUri); - } catch (UploadAttachmentFailedException | IOException e) { + uploadNewAttachment(data.getData()); + } catch (Exception e) { ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java index b54361ca3..4350f4874 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java @@ -12,13 +12,11 @@ public class VCardUtil { - private static final String TAG = VCardUtil.class.getSimpleName(); - private VCardUtil() { // You shall not pass } - public static Uri getVCardContentUri(@NonNull Context context) { + public static Uri getVCardContentUri(@NonNull Context context) throws NoSuchElementException { final ContentResolver cr = context.getContentResolver(); try (final Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null)) { if (cursor.moveToFirst()) { diff --git a/app/src/main/res/layout/dialog_attachment_picker.xml b/app/src/main/res/layout/dialog_attachment_picker.xml index afe5f2ce3..6b5d98524 100644 --- a/app/src/main/res/layout/dialog_attachment_picker.xml +++ b/app/src/main/res/layout/dialog_attachment_picker.xml @@ -22,7 +22,7 @@ android:layout_width="36dp" android:layout_height="36dp" android:contentDescription="@null" - android:src="@drawable/ic_baseline_photo_camera_24" /> + app:srcCompat="@drawable/ic_baseline_photo_camera_24" /> + app:srcCompat="@drawable/ic_baseline_contact_mail_24" /> + app:srcCompat="@drawable/ic_attach_file_grey600_24dp" /> Date: Thu, 29 Oct 2020 19:06:15 +0100 Subject: [PATCH 08/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix contact picker Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentsFragment.java | 6 +----- .../java/it/niedermann/nextcloud/deck/util/VCardUtil.java | 4 ++-- 2 files changed, 3 insertions(+), 7 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 cf4a3feaf..473374173 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 @@ -210,7 +210,7 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d case REQUEST_CODE_PICK_CONTACT: { if (resultCode == RESULT_OK) { try { - uploadNewAttachment(VCardUtil.getVCardContentUri(requireContext())); + uploadNewAttachment(VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString()))); } catch (Exception e) { ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } @@ -220,10 +220,6 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d case REQUEST_CODE_CAPTURE_IMAGE: case REQUEST_CODE_ADD_FILE: { if (resultCode == RESULT_OK) { - if (data == null) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; - } try { uploadNewAttachment(data.getData()); } catch (Exception e) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java index 4350f4874..b0e971082 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java @@ -16,9 +16,9 @@ private VCardUtil() { // You shall not pass } - public static Uri getVCardContentUri(@NonNull Context context) throws NoSuchElementException { + public static Uri getVCardContentUri(@NonNull Context context, @NonNull Uri contactUri) throws NoSuchElementException { final ContentResolver cr = context.getContentResolver(); - try (final Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null)) { + try (final Cursor cursor = cr.query(contactUri, null, null, null, null)) { if (cursor.moveToFirst()) { final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); From fbf96fa995e75f64e00f96422cfc885937e408a1 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 29 Oct 2020 19:25:41 +0100 Subject: [PATCH 09/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design fine tuning Signed-off-by: Stefan Niedermann --- .../picker/CardAttachmentPicker.java | 34 ++++++++++++++++- .../res/layout/dialog_attachment_picker.xml | 38 ++++++++++++++----- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index e84dd6c60..f8ff805a6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; import android.content.Context; +import android.content.res.ColorStateList; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; @@ -13,9 +14,17 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.util.stream.Stream; + import it.niedermann.nextcloud.deck.databinding.DialogAttachmentPickerBinding; +import it.niedermann.nextcloud.deck.ui.branding.Branded; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; -public class CardAttachmentPicker extends BottomSheetDialogFragment { +public class CardAttachmentPicker extends BottomSheetDialogFragment implements Branded { private DialogAttachmentPickerBinding binding; private CardAttachmentPickerListener listener; @@ -35,6 +44,16 @@ public void onAttach(@NonNull Context context) { } } + @Override + public void onStart() { + super.onStart(); + + @Nullable Context context = getContext(); + if (context != null && isBrandingEnabled(context)) { + applyBrand(readBrandMainColor(context)); + } + } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -59,7 +78,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { dismiss(); }); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (SDK_INT >= Build.VERSION_CODES.KITKAT) { binding.pickFile.setOnClickListener((v) -> { listener.pickFile(); dismiss(); @@ -72,4 +91,15 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { public static DialogFragment newInstance() { return new CardAttachmentPicker(); } + + @Override + public void applyBrand(int mainColor) { + if (SDK_INT >= LOLLIPOP) { + Stream.of( + binding.pickCameraIamge, + binding.pickContactIamge, + binding.pickFileIamge + ).forEach(image -> image.setBackgroundTintList(ColorStateList.valueOf(mainColor))); + } + } } diff --git a/app/src/main/res/layout/dialog_attachment_picker.xml b/app/src/main/res/layout/dialog_attachment_picker.xml index 6b5d98524..904c3dcfc 100644 --- a/app/src/main/res/layout/dialog_attachment_picker.xml +++ b/app/src/main/res/layout/dialog_attachment_picker.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="@dimen/spacer_3x" + android:padding="@dimen/spacer_2x" app:behavior_hideable="false" app:behavior_peekHeight="90dp" app:justifyContent="space_around" @@ -15,14 +15,20 @@ android:id="@+id/pickCamera" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center" android:orientation="vertical"> + android:padding="12dp" + app:backgroundTint="@color/grey600" + app:srcCompat="@drawable/ic_baseline_photo_camera_24" + app:tint="@color/primary" /> + android:padding="12dp" + app:backgroundTint="@color/grey600" + app:srcCompat="@drawable/ic_person_grey600_24dp" + app:tint="@color/primary" /> + android:padding="12dp" + app:backgroundTint="@color/grey600" + app:srcCompat="@drawable/ic_attach_file_grey600_24dp" + app:tint="@color/primary" /> Date: Thu, 29 Oct 2020 19:34:33 +0100 Subject: [PATCH 10/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design fine tuning Signed-off-by: Stefan Niedermann --- app/src/main/res/layout/dialog_attachment_picker.xml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_attachment_picker.xml b/app/src/main/res/layout/dialog_attachment_picker.xml index 904c3dcfc..120fd5b9e 100644 --- a/app/src/main/res/layout/dialog_attachment_picker.xml +++ b/app/src/main/res/layout/dialog_attachment_picker.xml @@ -17,7 +17,8 @@ android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center" - android:orientation="vertical"> + android:orientation="vertical" + app:layout_flexGrow="1"> + android:orientation="vertical" + app:layout_flexGrow="1"> + android:orientation="vertical" + app:layout_flexGrow="1"> From 3c7d4f6cbfacc1399a39fad8611e1c4eb819987f Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 29 Oct 2020 20:51:29 +0100 Subject: [PATCH 11/34] =?UTF-8?q?#289=20=F0=9F=93=8E=20Add=20more=20source?= =?UTF-8?q?s=20to=20upload=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design fine tuning Signed-off-by: Stefan Niedermann --- .../picker/CardAttachmentPicker.java | 67 ++++++++++++++++--- .../res/drawable/bottom_sheet_rounded.xml | 9 +++ .../res/layout/dialog_attachment_picker.xml | 11 +-- app/src/main/res/values/styles.xml | 9 +++ 4 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/bottom_sheet_rounded.xml diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index f8ff805a6..cb7660130 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -1,15 +1,23 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; +import android.app.Dialog; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.os.Bundle; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.fragment.app.DialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; @@ -18,9 +26,11 @@ import it.niedermann.nextcloud.deck.databinding.DialogAttachmentPickerBinding; import it.niedermann.nextcloud.deck.ui.branding.Branded; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -44,25 +54,31 @@ public void onAttach(@NonNull Context context) { } } + @NonNull @Override - public void onStart() { - super.onStart(); + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); - @Nullable Context context = getContext(); - if (context != null && isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); + if (SDK_INT >= Build.VERSION_CODES.O_MR1) { + if (!isDarkTheme(requireContext())) { + setWhiteNavigationBar(dialog); + } } - } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + return dialog; } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = DialogAttachmentPickerBinding.inflate(inflater, container, false); + + @Nullable Context context = getContext(); + if (context != null && isBrandingEnabled(context)) { + applyBrand(readBrandMainColor(context)); + } + return binding.getRoot(); } @@ -99,7 +115,38 @@ public void applyBrand(int mainColor) { binding.pickCameraIamge, binding.pickContactIamge, binding.pickFileIamge - ).forEach(image -> image.setBackgroundTintList(ColorStateList.valueOf(mainColor))); + ).forEach(image -> { + image.setBackgroundTintList(ColorStateList.valueOf(mainColor)); + image.setImageTintList(ColorStateList.valueOf( + DeckColorUtil.contrastRatioIsSufficient(mainColor, Color.WHITE) + ? Color.WHITE + : Color.BLACK + )); + }); } } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void setWhiteNavigationBar(@NonNull Dialog dialog) { + Window window = dialog.getWindow(); + if (window != null) { + DisplayMetrics metrics = new DisplayMetrics(); + window.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + GradientDrawable dimDrawable = new GradientDrawable(); + // ...customize your dim effect here + + GradientDrawable navigationBarDrawable = new GradientDrawable(); + navigationBarDrawable.setShape(GradientDrawable.RECTANGLE); + navigationBarDrawable.setColor(Color.WHITE); + + Drawable[] layers = {dimDrawable, navigationBarDrawable}; + + LayerDrawable windowBackground = new LayerDrawable(layers); + windowBackground.setLayerInsetTop(1, metrics.heightPixels); + + window.setBackgroundDrawable(windowBackground); + } + } + } diff --git a/app/src/main/res/drawable/bottom_sheet_rounded.xml b/app/src/main/res/drawable/bottom_sheet_rounded.xml new file mode 100644 index 000000000..ba266ed32 --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_rounded.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_attachment_picker.xml b/app/src/main/res/layout/dialog_attachment_picker.xml index 120fd5b9e..150a5a3a5 100644 --- a/app/src/main/res/layout/dialog_attachment_picker.xml +++ b/app/src/main/res/layout/dialog_attachment_picker.xml @@ -5,7 +5,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="@dimen/spacer_2x" + android:paddingStart="@dimen/spacer_2x" + android:paddingEnd="@dimen/spacer_2x" + android:paddingTop="@dimen/spacer_3x" + android:paddingBottom="@dimen/spacer_3x" app:behavior_hideable="false" app:behavior_peekHeight="90dp" app:justifyContent="space_around" @@ -27,7 +30,7 @@ android:background="@drawable/circle_grey600_36dp" android:contentDescription="@null" android:padding="12dp" - app:backgroundTint="@color/grey600" + app:backgroundTint="@color/defaultBrand" app:srcCompat="@drawable/ic_baseline_photo_camera_24" app:tint="@color/primary" /> @@ -55,7 +58,7 @@ android:background="@drawable/circle_grey600_36dp" android:contentDescription="@null" android:padding="12dp" - app:backgroundTint="@color/grey600" + app:backgroundTint="@color/defaultBrand" app:srcCompat="@drawable/ic_person_grey600_24dp" app:tint="@color/primary" /> @@ -83,7 +86,7 @@ android:background="@drawable/circle_grey600_36dp" android:contentDescription="@null" android:padding="12dp" - app:backgroundTint="@color/grey600" + app:backgroundTint="@color/defaultBrand" app:layout_flexGrow="1" app:srcCompat="@drawable/ic_attach_file_grey600_24dp" app:tint="@color/primary" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c253ae461..874d2e751 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,6 +7,15 @@ @style/toolbarStyle ?attr/colorPrimary @style/Deck.TextAppearance.Headline1 + @style/AppBottomSheetDialogTheme + + + + + + + From 572fb47a45bb6adb8bc10cf3ad4596c9921826ac Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 14:10:04 +0100 Subject: [PATCH 23/34] Lower targetSdk to 29 because of https://github.com/nextcloud/Android-SingleSignOn/issues/277 Signed-off-by: Stefan Niedermann --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0a8753360..f798e91c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 30 + compileSdkVersion 29 defaultConfig { applicationId "it.niedermann.nextcloud.deck" minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 29 versionCode 1011001 versionName "1.11.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From e646639d91f50bdff728e9c9ecb0325b299c8eaa Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 14:10:55 +0100 Subject: [PATCH 24/34] Do not continue with contacts picker if camera permissions haven't been granted Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentsFragment.java | 1 + 1 file changed, 1 insertion(+) 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 6514ad1e9..3a3346dcc 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 @@ -317,6 +317,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } else { Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); } + break; case REQUEST_CODE_PICK_CONTACT_PERMISSION: if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) { pickContact(); From 35ff744f31204f1e00bf506d293e657021bbbd44 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 15:01:21 +0100 Subject: [PATCH 25/34] Minor refactoring Signed-off-by: Stefan Niedermann --- .../attachments/CardAttachmentsFragment.java | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 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 3a3346dcc..3fcd5ade1 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 @@ -3,7 +3,6 @@ import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.util.DisplayMetrics; @@ -16,7 +15,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.SharedElementCallback; -import androidx.core.content.PermissionChecker; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -55,6 +53,7 @@ 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 androidx.core.content.PermissionChecker.PERMISSION_GRANTED; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -169,7 +168,7 @@ else if (dy < 0) @Override @RequiresApi(LOLLIPOP) public void pickCamera() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), CAMERA) != PermissionChecker.PERMISSION_GRANTED) { + if (isPermissionRequestNeeded(CAMERA)) { requestPermissions(new String[]{CAMERA}, REQUEST_CODE_CAMERA_PERMISSION); } else { startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_CAMERA); @@ -178,7 +177,7 @@ public void pickCamera() { @Override public void pickContact() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), READ_CONTACTS) != PermissionChecker.PERMISSION_GRANTED) { + if (isPermissionRequestNeeded(READ_CONTACTS)) { requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PERMISSION); } else { final Intent intent = new Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE); @@ -190,7 +189,7 @@ public void pickContact() { @Override public void pickFile() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) != PermissionChecker.PERMISSION_GRANTED) { + if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) { requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_ADD_FILE_PERMISSION); } else { Intent intent = new Intent(Intent.ACTION_GET_CONTENT) @@ -200,28 +199,29 @@ public void pickFile() { } } + /** + * 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; + } + @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { - case REQUEST_CODE_PICK_CONTACT: { - if (resultCode == RESULT_OK) { - try { - uploadNewAttachmentFromUri(VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString()))); - if (picker != null) { - picker.dismiss(); - } - } catch (Exception e) { - ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - } - break; - } + case REQUEST_CODE_PICK_CONTACT: case REQUEST_CODE_CAMERA: case REQUEST_CODE_ADD_FILE: { if (resultCode == RESULT_OK) { + final Uri sourceUri = requestCode == REQUEST_CODE_PICK_CONTACT + ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString())) + : data.getData(); try { - uploadNewAttachmentFromUri(data.getData()); + uploadNewAttachmentFromUri(sourceUri); if (picker != null) { picker.dismiss(); } @@ -300,14 +300,15 @@ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri) throws UploadAtt @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { - case REQUEST_CODE_ADD_FILE_PERMISSION: + case REQUEST_CODE_ADD_FILE_PERMISSION: { if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) { pickFile(); } else { Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); } break; - case REQUEST_CODE_CAMERA_PERMISSION: + } + case REQUEST_CODE_CAMERA_PERMISSION: { if (checkSelfPermission(requireActivity(), CAMERA) == PERMISSION_GRANTED) { if (SDK_INT >= LOLLIPOP) { pickCamera(); @@ -318,13 +319,15 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); } break; - case REQUEST_CODE_PICK_CONTACT_PERMISSION: + } + case REQUEST_CODE_PICK_CONTACT_PERMISSION: { if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) { pickContact(); } else { Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); } break; + } default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); } From f05402c7b67f8ea234f6e9ce749dfe8ba95b22af Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 15:03:49 +0100 Subject: [PATCH 26/34] Minor refactoring Signed-off-by: Stefan Niedermann --- .../attachments/CardAttachmentsFragment.java | 2 -- .../picker/CardAttachmentPicker.java | 18 ++++++++---------- 2 files changed, 8 insertions(+), 12 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 3fcd5ade1..64ab02f1d 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 @@ -65,8 +65,6 @@ public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener, CardAttachmentPickerListener { - private static final String TAG = CardAttachmentsFragment.class.getSimpleName(); - private FragmentCardEditTabAttachmentsBinding binding; private EditCardViewModel viewModel; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index 6b9acffe5..02c13a8c8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -14,6 +14,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; +import android.widget.ImageView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -23,8 +24,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import java.util.stream.Stream; - import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogAttachmentPickerBinding; import it.niedermann.nextcloud.deck.ui.branding.Branded; @@ -41,6 +40,8 @@ public class CardAttachmentPicker extends BottomSheetDialogFragment implements B private DialogAttachmentPickerBinding binding; private CardAttachmentPickerListener listener; + private ImageView[] brandedViews; + public CardAttachmentPicker() { } @@ -75,6 +76,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = DialogAttachmentPickerBinding.inflate(inflater, container, false); + brandedViews = new ImageView[]{binding.pickCameraIamge, binding.pickContactIamge, binding.pickFileIamge}; @Nullable Context context = getContext(); if (context != null && isBrandingEnabled(context)) { @@ -105,18 +107,14 @@ public static DialogFragment newInstance() { @Override public void applyBrand(int mainColor) { if (SDK_INT >= LOLLIPOP) { - Stream.of( - binding.pickCameraIamge, - binding.pickContactIamge, - binding.pickFileIamge - ).forEach(image -> { - image.setBackgroundTintList(ColorStateList.valueOf(mainColor)); - image.setImageTintList(ColorStateList.valueOf( + for (ImageView v : brandedViews) { + v.setBackgroundTintList(ColorStateList.valueOf(mainColor)); + v.setImageTintList(ColorStateList.valueOf( DeckColorUtil.contrastRatioIsSufficient(mainColor, Color.WHITE) ? Color.WHITE : Color.BLACK )); - }); + } } } From ad41b1df555d085edaaf5f00963ff9007536f88c Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 15:31:57 +0100 Subject: [PATCH 27/34] Fix crash on Android 4.4 Signed-off-by: Stefan Niedermann --- .../picker/CardAttachmentPicker.java | 62 +++--------------- .../res/drawable-v21/bottom_sheet_rounded.xml | 9 +++ .../main/res/drawable-xxxhdpi/background.png | Bin 44461 -> 0 bytes .../res/drawable/bottom_sheet_rounded.xml | 3 +- app/src/main/res/values-v21/styles.xml | 9 +++ 5 files changed, 27 insertions(+), 56 deletions(-) create mode 100644 app/src/main/res/drawable-v21/bottom_sheet_rounded.xml delete mode 100644 app/src/main/res/drawable-xxxhdpi/background.png create mode 100644 app/src/main/res/values-v21/styles.xml diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index 02c13a8c8..1d89ee0ce 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -1,25 +1,17 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; -import android.app.Dialog; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Build; import android.os.Bundle; -import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.Window; import android.widget.ImageView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.fragment.app.DialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; @@ -31,7 +23,6 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.LOLLIPOP; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -57,21 +48,6 @@ public void onAttach(@NonNull Context context) { } } - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Dialog dialog = super.onCreateDialog(savedInstanceState); - - if (SDK_INT >= Build.VERSION_CODES.O_MR1) { - if (!isDarkTheme(requireContext())) { - setWhiteNavigationBar(dialog); - } - } - - return dialog; - } - - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -107,38 +83,16 @@ public static DialogFragment newInstance() { @Override public void applyBrand(int mainColor) { if (SDK_INT >= LOLLIPOP) { + final ColorStateList backgroundColorStateList = ColorStateList.valueOf(mainColor); + final ColorStateList foregroundColorStateList = ColorStateList.valueOf( + DeckColorUtil.contrastRatioIsSufficient(mainColor, Color.WHITE) + ? Color.WHITE + : Color.BLACK + ); for (ImageView v : brandedViews) { - v.setBackgroundTintList(ColorStateList.valueOf(mainColor)); - v.setImageTintList(ColorStateList.valueOf( - DeckColorUtil.contrastRatioIsSufficient(mainColor, Color.WHITE) - ? Color.WHITE - : Color.BLACK - )); + v.setBackgroundTintList(backgroundColorStateList); + v.setImageTintList(foregroundColorStateList); } } } - - @RequiresApi(api = Build.VERSION_CODES.M) - private void setWhiteNavigationBar(@NonNull Dialog dialog) { - Window window = dialog.getWindow(); - if (window != null) { - DisplayMetrics metrics = new DisplayMetrics(); - window.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - GradientDrawable dimDrawable = new GradientDrawable(); - // ...customize your dim effect here - - GradientDrawable navigationBarDrawable = new GradientDrawable(); - navigationBarDrawable.setShape(GradientDrawable.RECTANGLE); - navigationBarDrawable.setColor(Color.WHITE); - - Drawable[] layers = {dimDrawable, navigationBarDrawable}; - - LayerDrawable windowBackground = new LayerDrawable(layers); - windowBackground.setLayerInsetTop(1, metrics.heightPixels); - - window.setBackgroundDrawable(windowBackground); - } - } - } diff --git a/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml b/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml new file mode 100644 index 000000000..ba266ed32 --- /dev/null +++ b/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xxxhdpi/background.png b/app/src/main/res/drawable-xxxhdpi/background.png deleted file mode 100644 index 90856f4c889605689ea8658893fb0d4a7c1770bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44461 zcma&OcT^Ky+ddkK6zTAYR6|EZK%^HFsx&Fmt5l^1q=OKGfMB6lr8hx(4IKiHNR{3L z5)_coi`0Z9C%*4@);a69)_2z7kIb40yUonr`@YJZcSZ(UbT>I}0ssIy9c>L`0Dx=} z03c1Kq99&zF`w=y9?1RGbWEv;f8kUvZ;9vBFSV`w0RVd9cl7-v8KU%@#Ea|!nw9}3 zJ}&}-9sQmIf`fy_-Mu~iogH637x(dV%{fr!004LZIvT2`A-Q|q+d=E{-)p+JzyC3U z#`X}Zlu*lNpI9cYBN!{c*tSqb7V|4%t*T%Fn#d@HNPx~e-hoSr(`y9dzkV}7=lDTD z1ly^3C>3wK=nml~?~@)QFyImlh%zA%Xn5~;h`*IwC6o8vA<*(Zk-`K64Dv7p7PXHS zvjmBl2cu|vKwHob_zmN#=ngoR5%dtF1;G36AYvGwNn!X)Wp`9fKxSu~?5m(7R<#gw zGy(8(ZxTkw+eNEF;OKp$4&zb_F+vxRv<0F)M-eSO3x=LQ-FVa`{#&qe2&oZ<-p z`5he?yV@m86!UrACkNi3@$+)=0Uhu4UI))W2-(`o7tR!?>W@=Bq%YlP&A}xP42BaW zPiTFB;{$h#Vf1SQMF?M7@yS!=QBF$DQh8tJTyabs0H399zq{V$`(76zJ57m~i%dD5 zulC_VQF^Wq-+<;9zRbb2zlOKqw2p)q#&8O{QIuod>hlY;_7H4$8rkyX!ivo_rS!SQ zT-NI;n?Vj7(=~$E1`j{G5qzb6_4^1}2d}cmwbcH(+-@4d*lohllewP1Lj6?)S4JhBY8clfz%$v?zv;=g-%wzJS;A6BrSRjtQ?ojtrqFh(Ip zfX=E6&ZCOvGT86|du!&)%y@}BOwAVgr893r(jC8Cv(6;JBU}ZaEgT1qs;oII$@^bP( z%k)WwVw>oxXKTTrvr&~nc!uRHHweC_btY4Qnnr)$Rn0!Qtt_Qgt&An%;v zA7zdA;HopR6x`*sk&yVWE)xxkf5tBq0RcTgWdLpk2R~CnfyL*tF1&RqCUSA@nBn8O zGaF32-9l1(aMR5*nf=b7tMuln=Lfh!G4z5sv1zvmgoXVsusgyu43)f$7Qz{$Rw~Mm zIKNF;{vilfi~N{?5=XB1o?|k5UB8RN=AQ7#(PiJP?4+;_ue&+LTqpr2IpziYt#Zy;w z@N+*q_VmkqGgeQ~qLm`aj|#zcJ>Z1p-JK;HbNFE79-9qY`Rsu+{H*g}w+ZJno7Q3` zHvZ7o#Ls^096{H$S;EafEZ3Xr0NbFrI1=1U-ucdcnMOSUB`a@-J=GD{4(!M3O$1(!oHkW z9i*lGQyn3#w%xh0J2}rYP<+Vk>apu{AGU1)r6}Je{XR{N&Yfq&=`(ws+*%z5R>hVl zjddFEx*wCk6rgu4s2=&od}~E~#dEe4q0D@sEd*QG*#MLOb1u0zK71C-wZaBapKGLO z>TXE*n_w@&u(z^(iGapI1l7cm0>Sn3`o#`dq;3Xpa0sodr;PJ&m-ry?$$X0|WyGw6 zK##f?d5(U=P~KB{{ru=yzVBj%$z2ROv36ND+j{yM=@D=g4L9 zqIs&}VcU;be2$5IhJymMpR{Fdi&_?p^ID0$cAJGg+_Bj&a8FG3bmShC##BnG^^xvI zj|QK%>YvxOm_E2_8EN~P)l+o;orV7F+d4P}x{^&ZB1f3XN)7dR)uBMZ79Uo$KQD4=%D#JUm}javZ=M zT+6iQiP%ayd>&>ZcFmtOjMTXSOd1loZ}*vUJxs)KvhIaODF#kkhH&s#&OfOrmQeo6 zs{AD0RSKkVItMWzg^a2oRCKkQF?x$TYl6WM7aUMM&Q~q7btQCT4x$Bl^SAGVoT%5b zzEQpdPvK)-o&9#EO59e(?r+jvvR1x%w6g0u2&7Q)erKt0ieng;G$?20G z_;zk&I%+LXgEF0^=wJR*=-zOu?NErL)lX_k_VD{v4j3v_h7Gsn)hSs?Gk*B zG2zx`=l#qf0jF_!^tT7^mis%9malHEv++B($a*&}Hr^_&bK7+wS!dHZFJN@OZe2g5 z5h(Y{H+wIc4`|a2W(KC@MupC6RUkAt+P`CMYf1@76J!{*#s2W*c1Z+w)|Ib_1l({l-B{hg2vo+m?COp zLLkDY?FLT0RB_dm(&vQM!r_*)vRY+yAZ9Gn4Bitj3SI->`zfUPFl8TSB`gh~Dp8(Mi0s0I+r3Rikv6<)S}3 zeIZZ`ht%e_b^ardxLxLPHnGRd zJlfLO9N56qh0rjvhcc_C_^$Wf*(8j_!98K;f-`43E3k4fZp=)PxToh%nwOPoNw z9Qv7^_eVZ`z?md3hb1D07I^2a0Tpk|9&gPL>am^eLon|RoHH76zuN#dJM9x>DJ?Df z1^7m3F3}n@c5|QE31(Q;yU)#-fRBGiJIvcDW^lYEF0=M$gJzUcoSHX`l*LPpig$yP zXL-=MCeJeUWwcWFP_TxO$74WdZZ2;OBk|k+?PH1Nl3tcKCfQ?1(s_&tJjSFzQ<`MD z|6NinhL%ae_41!>s`B4oz6yepShz2I<4yE8nR*!N0oNOC-0?es%bOFS;k z7#ce8Nx9_WgbC+d+GMocXH$ZT!||7X+In%`_2|#a;IT-~sW9}rmmYWP+0?WI$hxGZ zN4cb1jqRviQT{-*Fcmd*`Bt;Qo|<)kZZ6Bcpi10t*3duR$e-Ns_*1DE&N)02c8EEN zM`6^Tr98~FnY@h>#hxt0Gu{x|81_0~;7{9z)k)X0 zeUz*ikes+oR7hl8Y^w`Oc1y0xs_!_L_3+|$xC%tSBcRgLY|h>}#;mQ{4H=mXrA?)J zP8~>4ab*O=`uwmo>C0Fx)~L}=E&dplh?pQze#I=fbmAX<{e2@%c*^=z>~h=hy$dr1v%3x$`5!m4yaCuwrI7_P?#2mcFm( zJzfYEqYDuHq%c?@`ljKt&$-V-E5A5z0_wL|%#jx~QF)iEBHnzWFP~@0UYW_7h5duX zace;iUxwcChPr-fZC(9pT#ps5>9X0TT)`gzhC`o7Qjl#2D|=vHU*dp6bzODN%xag< z+f~E+1yhqOAC-3;7ky?4+YJwpz`#>5Z7L_e5TNzO`Z@Ks|Lx1pF@t2xg9!y;Sc+c# z>)GK5t0;{@z>yof3m0aBH%2bHCot%8IMr>)W8qPN%_+a})t6{0b|bv%#fA!}$nxk75OsqQ-l!df)Gjxr{?XlKHX$@)sFa5m3UiwAI7MY+Bg;<&vDI6%Z?M<`oEH`xVQ_`55!K0Y^ z@)1dJUb|P63v514H;3@LaRF|m0zy(;N9ivx&+7K3FXHJ zWFv3x@rcPPL;3_7)vIj+tpovWX|b7_6oB3N&z$XbnRvJW+#0tM**21(il9a|leqw1 ziY6NAiKA^b?eG9b=dRZgo(ohJILp0uS+v}~i5;lMOkYgyVKTiYLg#)n1g$2NFw4gL znI8ZaV4L@l8?GoDli5c#^WB4(a`yii@+PCnR&D(`@)Tcw5^vjn2hb*miM#ifPHc2X%A99*Lxa+5Z{Y z;LwiPFxXbFdy3!BP0IymP&?`6cn@5S%Rfc>==jl))VAc_ry4o&-H|&C_c)zvseV~P9qF-4lP~)Uk}J?8pgYE@EtXa9!(f_aczu1K zB8D?rT;(P=zN$*SS0Oy=@Z)5ZZ8zkqm{-K<<7NCC(M2RWp)6v3>w>wOzn=AliJ9I=s49$I0R+XHzc_vA7o*YDqs9H}l22FQ=l9obBR@68_44*lBFno-WZ zN2YOj-+4ZwPe~>Jp2boZMpzd&cTwNWrIKZ8HYed$G8Sgep&av(M_mW@a9xpPS%BfCM z=7Ux&PUb1wFESTa)7~{;cI;Jq2mq zZy6fq-vsZiD%|{)&U--4XCEW@*0bkyS+nBL_aF8)2x*nEZ8>|%^WuiO4LP@w)>8T) zJA0P3baGXWW!+O5>hM2v{-Q{1;?F}eD;z17=HPaMh+X_tfDun)&V-KnxjGfF5D ztQO39jCqR$y}B!!`%J0FD}L$WScQ^#V!YDlp*zkHs&@9y>3x3t8hgwg=L5Vk*8fn> zObj?BLEWo3T22WaJCvw!PKeOqcJmUQwjiGr3^ZEC=SdLq>vng0K3P_6zNFQ9jP!jF zqthlJ;E1%fSbs~?C~L}TAqra)$WhNXx%%Ab(DUsr3d6!N7Sv5VLL za1m_1nsquYQ+93!w*L|5DMiky%k~O8ygkR^-0~bY+|;8u@Pk#o!5Qed#0)qmR|<~T z*tUYTV?T^bnXT9_aoc|AFl9OB_6w?Wbcsb|r3U;b!M0LLa*m zM2ZgmpLL#gAxy&TGeDEkFz?G9kZn$03!*;1bMFw_3Yiw&AI%BCwh%<2{o)w3>@%fb zj7eXsRQBcHgp*ZM2OV%Brv35f*7oCXPlHxIpKyIkB|B6)yftWT{Fnom`X^kbC1VkB zfnnx)wW~67?y-!Bgz@QB9dfI{p)<>IdYA|i|I^OsvMTy+h;~?k1+4aC@6LMHNiK9X z=s{Dyt9^8oZ(~6C4oe-kU7&@cYTcZu*jIf>mk+iRV+;W{d;oQf)h^p}9v<4pFk-y7 zT(lYl7EP$d<@l%zTYWV*Sn5Tt`}QA}CvDjNpsrT~0*FBTaAda&V?yoa7cPIcH9}3g ze|WkRwx2W-=DBOQ=2L=fJe%z^m8yTsa7_D*sn;^Jrj?$^8Ju}$5|}Y3+j)0{P{@sD z(Aq9prC&zSo_h%gY8RqJw-mv19=7xEZkXp$%+1w!+OI@&BgF$EkrZnLXFCi)ZUJESj$u$S?{WhW8G8_uw>rL%CEE8G*bvW zn1c<|^_*NVV`bApX@ENifIvP>txSus8AW}rE~a-X*}fk=CXW!3~cX!TgRcA1Y`31(D}3# z7vOBC2R=5K-`pvzlbzr8bg4G__C)MivywcKq@b>1z$FLa>_TE8e8GwM0hLBdHIwF5gamlRRR-47}qnnVJ4hrov{B^onhV`rHL?B}pG5Jh}W` z<~X-MeJ2lwWb?Mcz?on*+;L(H2R9zRE9TzX(gvZcgxiNBsU!zrQVU&fX((CA0Z~BK zd%Z#J=noZ+9H*%2zKrng3y$9QUndLzLwc+hzq3KZBU0kYJ0U+@(6-H#O_e+m z`@qYj;n4ag13H`?1DKMhncDclZV#cr@PxCi0a+W}rp}UibUsO&%u1um&9x^`v4M~n z7ea^ZP7BONJFH{i1z7R%4q5bdee&gG8AIKlA}Imv9E*wMfX&8WhIoWWemPe(XFhzr z>$C#4hDG|39~FECYTI}I`>LQ>`_+ze$2)S)A=WVu@_qjF^31??qDXb4L9~YN& zN`bb{{?)6OlSTSRDH1~cY=k}(cU345GnW>QW7Ubq-kM(P*?HnxU*~0g%8C6UdXKrw z&I$VR;Tz~?zz?a!WW@s}W5yrhE*`eV=VCarDez+s0H3h~=z#x0R^KiyY+m&beaUm1 zLyK)7zhJWPwa>J0K;aq0gQ*_lnAiT z)8yDQ6+x-4vS&*BqQ2><*-Impa3dWfL0J@^y#Y(94m#D9`z8@E?n~Fx#tZr0juw46Q4Lo2_)aX3}dchcYVgi0v^;x^qpk?VsibrI%$SrsPio!ccJ*?{#B|%kCl8Rf zf3ali^uLIrxXj%MBP4FBoea^#X*M_g7wvD@KvzkOxn`e4jq43$e?Lez6Ve7b2@9DL zaWQ2viyBo$RP}plC3&PMeBTT&0fU9#9qR87R`AUHhfilm5WkW)`652JU-7LAzsel*C+XkI=#M1PAbA3bXAa4xq8)DS`?-tWN{?cC8*gJ`a?s>IZUMzRmPy9eFRkdm`+@=9&4Y=4gMnrc!E{bvy1pDX-rBs#H0;{ss=3( z1)zk8nZYusP|yiyw#22lXLz?o;zmUUDxRbvsqvlUGeDbK1m6XBuYXY;=->AYn^Pbd zUj(`}X%{OxRt$~`|JETv5JDytc!#5zhqW%Q;>*G#^Y`l#(hX-QBNai-9Z6OC5Mak{ z^B}|0v5EXuZ4t)4>_ou z;k1W`SDpNe~y1F-Sn-KbM`%5ccZYWTDsd$0Tc?3nR#kuWTKl z?D{Et7nu{zH;hTzLEr?oo0lz|6-G}5LA z$5I9^`)a5}2OV3JRs(6*>lG;YHagQd+afnU9=Ozx^FB`G?MZRg($f)6t_N&O-~aBhF~_0L$wAWA znx2F%;L~T^Ku(fG0tfJO`Gv!Cs?LacL|$)}Etv>Ymnmp#;u?qQlLE9^FGVLey=@e$%^iBQz4x2&oGdlPM?TbPb3paBHfuWjKh@fJ06~IC+6b6 z>c>Clb`IkB$_6N98`@o=my zS#wt(3v=U8hOPlHC@6gx=c8$%GO>Al;5j%3li)qG8dU>hCaOWWzu0)O~E5I zQpW$=3m{f-c?JK;yW<&cb}S06d8Cm0H zM!y7I`FNfMXN|g1(Y7;KT=?F&pjWm3nmkxvw1%U@9xcm+XQ6b4vK+aG{01MS=L>%9 zM4YlwAqjAdy&dTD#N>*N@#E~U4?8Npt6RyV{TUy8Ii@*#mTu4C?ZXbS1+}>o% zO*-lJ6+2hsJh79=O_Q9;^V@e?WnxZ9uK#lbLty1WwEV=!sbBB!T9ZBQ5wwEX#YmM( zi9G7h?UC5<%7XA`2Mr zri~t~b`}Qw($jL#wMpiJgza+pyv%^>lX^|?WbY%25nn1KLUt`}f>#yiSGO8q)X=N= zpMOg7f^-PG&bO2+0Z>_s;b}{z8W$3;II+b zGOp;>KBc{N7oG4HlNd&!qi;Jt&}Fru9w(i8`&T{Q85WysXM3}-;q6hB=Q81D!MerL zEwzw1pBpM>wKp)V*#E#ruAa4@`b`)?vMk{tw$Tw?0~j@mr!^8EjObl`Y_@4qNbn#E zw?z%Jc_XZgEW=h*s<*myl;!^=CjSBV{oSr&9^qkF128 zdfn@>7!@TRsc~!%q9Yo=jLj~_iKRWExpa=^=;}R)o&_n!JczAH3Juql*$B3VPJQqr zb+fOZUl@!OGNNKvMv`MSmmB&eZOA_9P^oUx=3#)yHPzZC`h#`Gs1YIGYliw8V1|L3 z1mcmQ&bXEpI;!F_M|eI9lO#7+;jp|<_Dvwhfx7mNV-t*IO((2j(WwE<2j_*5$~G-V z&*JAc*Jmv#^-h#vswm1A+JR+r_SUsp9yFP|Oll#qs|=4gu@>uEaFTh;y{WalNHSvu z_kurE(5Tj!BHSU_^kDP)6aN}buo`&$KFRgNL`TDV;iF8D*D;emmi@_zV2kB1sAW=mcA` z7fk2n;%6`u^rE_wydcZ6p!D1kroypkn6kselY!%McYsOQ1-_Ha=etq3|1g{X2I%nf z>u7WVf5uZf91*N8Y$Gyi?+vZ_C7OctnP<9$ub(l?YvZ21#qX@GdRd88I*Sz0T>ps6 z(uSbnaSs(W?-dX=X-ghB!58>?>pdvht3Zt~{=$GjRYAwiH|JYY@FTH$(tGFfpMuSq zaN2G4PBGi#c;|4;Tfym|#nmWlkrL<96L*lTyx5bjgULsI3-v0x$+ifs)Oac4^_}2E zQ$)128TMXjd}7MAk<`EwjP+a=#{1&u8*~r>icru7>&ZeAs>ssq* zW-F*l4Ba|f9*mp!SJQWWjOzj&g<%~~$hb>d#@kRzp+=-XfI1?!tGIq**1M!L_6b0S z8Q=2`~M%x=l=n}{-4w1uXS%(8oxG= z3qR`HlC+~K^^XrzAj|l#idxLyPZWBh{-|HG=fYwb2LyjHUl)+ce=E>x{tk%WWY^p( z7+^~}awkx!XeinTt_)iBiV?-hlWiLUBP`Pph2ft9KbtCuVJ!QMSymZwo5Q=?yKd>S z=e-3bV|ttGn#Mi12iYV5wk-h*$MkETtZZKQHz<+a=D=Y0cjc| z?X8Q72yggp)oq2lPi@0xLnz$s!f3+AeaoW%|wu*%F-|HI0SC*mqj0jMtha1Fk ztvy2bh8OBz@Rl4iv6O<0q*L;z4!>!o;T}7+4^YC_`x#D~CWG!`#tcAfhQJ@APSm`t ze$=wbT-y>KSTZ8d&BGLond|5`1RUD5f=^cG3D4PG;xT#vhR1)Im+Der@(9et>0)rR z*yrZ#Cc7LupU0ACuEv#glUYPz?5FM>!Mg}`ybQJf)xvy|bsMDY%@q%cYU-e1@pf@(V8HSibEM9X3KsqzI|nv2g3+20=m|Lqy}vCToGtH$_k~GI?q6 z9iIE7vy@_6eR!cbUQ-Y1CT@QJq$nn2N+6jZ5X}7DC1vAuz(JiT&h?c(M(7xroxjD~ zY2wPlJ1E*(BAPH?8V4&S0%}HTo$eK|UZ4$XUrVD=qdYZ_TR@(vyXR8PXLHR@^-pL@ zPY|u?hN5@3?GW&6_s=?yn^$-WY{}fs&YmIR9G_xFZ+HV<^0M3%DpiK@peDw@C93kC z6Qa9t!hi_*5irl$&lT~v&H#cX#zKz}r$#V3)yHe?P3;g~Udr{e`Ap^Ik`5ED84H}| z5GH6jJZfc&4s&q_dIBF3IC#`S1Vafk7Y0OS7mHTK7@feI2O@KQM2?H(h6dkz19i2j zk%ij;_o{|i&qZ+le08ixTh!`7>`xvv*U-(78>Mz0v(s z<3%5(swcC-axA^0sNv9=H8=oZgY`Lu>p=d5iP>_VYMDbn4NafF9vG~^b~U@|^#*%> za;d2??8%%zpo8QnW%@2x)@}9|8}{6=m`swq1AKMUrdkiE)$(32MR`I7KR@_InQQAs zeW|}CDM41a-7H=WXR>+koy=1=c@v?ERSjMnQ19XE1q|o?i-2ya)o<-(;R2!3buUQd z9}1MiPnUtZe~Q_Ym0wT4aeR3?d;HlPWks1aZ-#syvImyhi;|D|F_GtnguTWW3BWmU zB_o*pchzKfp}A-qTr(ygiE&}H?FF&gvW441ppApt_aO-^H5ZmpX(-pyJyut?WW#(o z&9FQr%=IqEHlHud2OAonhv!}N{8m>sZ$>Ov)xl9pOoL+SozdmBQ9nU- z3}OjILlH)ET6(#^tcK*_JqDU@Z}0Nt(sb#y+HV|O1l%1ZnnCqO%bR*!&fEG!i7wC9 zc=}sPJdf|@M1FAx7cL*TL?nID+&?EPW#9~&Q`g7l%;^(>6XeglFOY=|x>iHBe|Wy* zIXY;Ax9;pBb_r}+yB2z;HDY&j^#tfkvw6fs*uN8Z3&Vdon4NkYy~m(_HpQPlcop-d zo}vTp8sUfeJTZY=E!y3>JGT&#KB{-HI=%lKzP~_pqq?DysqB%St-r}`-OvB|4>+XFnY z?vcU3{KlVnB7y*|SM9c*vfJ07My(P*@n^*nJ)8J-C{Z%oqp8CY^+1G46;Wy{+VMOT zb9c4hj~-fRVgP0D$*s!%lQGymHJH-(7qV>4ewt8{T|HD3o_LAatv3CNlPdKuf283u zqQzxG6dlLa&UPmJ5>1#9sfv@$NvS7O=&a@`oF)2w986U}$OzQL#;7j1a5S0*LiP5MLq)KRCtyuD8@ z=D{1pu{g;FeN;i=ebt(Zq}(1_Ti`Ru=J(Z36PWl}mNO}ew(vZ7YsZa64=qI1g^4_n z3)#Irv~2DartZrAhVl2&*-ydK==}2d=KYx}`)N5Wu`4am@ZPPh`_K^AGj%GdOH8CW zO38p;t$>!)(9V5u2!c+-4RGptLRUz81hDGtS}ex#f_)L&R~=@n|A^)?^?JUJaCN#1 zyAb+f1^s#JoLolDs+Ko~;qpZa9OGeQ{k=$HLGF_I(jsA<=$2pe|L#oPg?RGUgIB@b zEmHgE)Y0sU#;3l{3G6A6u2@g;%17O*n}#>u(MR0_`}S z1H)LVWkXNJ0$=QZH7723U0*@(PMnUW?%-U7B*-rx4Ol`apZu~AGY9UlUr_m$=-1mf zS7|W|ghm0)z7j!B-xe(<@r*PqN^xOF@8u;GZtofSkpnFr=%DBD4BjUOh+$kUs#_;= zag`|1#H**CA0ANiS|V5Yy*8TwM=rCiPna!F?c}NCr8RQL_*^~sRHFR0ZHngG=x#nL|>nQ-b;6WmJ=qM=bGl|9XQVT(Pb z3iP}mDIPorM#TAVs*RvobFfRsL`^FvJmHigXCfd$`}NPsR8If`w?ZddKxgd*i$A{E zdqcMMv!pS>Pk-m`DL8qjt*mDW+T(etfe)5#91 zF}X)gbA5%McK&+%8J)FP%@-Em&y@8=ZXlQMs!hM)lxsNd1PgGfieX!4xZYRhH}*R< zet0zEc$B3*4ukvarBoGdOwbRh@f1=t zf2hHq*C~>b7t>}pd_k_-fy&IdH)zjRc<^>o@*dcIdm@Foqc~~x-FXJOj=1pc3-in! zD&T|0$6A7dx!q1_NDKH&$)EY(pR}Rs>&UAN9VeOgk?-4bQTzi=%sfWFv9elr^r3lhIVWQMNQ;I%;za=`rcry&?aFK?< zSCN?vV#i=CamTy!Dd3Uj$rMQe=#{PFfRB2?o*eTi9BTAObvfw{G=L>eR&t6f)*^{& zI|p#>eGM5@IP~xQD2-QdX08Y2HIkztIQD6!Xejv>$*@Q>%Uap`dB%}zFJb~&97X0^9C~c)EhCus1Meq~_eDsACMtWzn3JL|6n?sN z_Onfp*s8-!*Oek&54`yPc4GT+ngf}L2==-Dkbq=IKT1?U58zyy!DNxjTO}d4m}?7g z_gfQ*c@T^)c#r?G`>?cdCa;%R4>A%{BQVh1GOQuW+0g}=lI*&)Tyx|k(7oCI^4E= zb3Qo=k*%QA3#9hL(DxIdmfkJ;Jk`4IBLXOPddDF3uk~}1Pj{8HI7YLN%l6eNAHCBfK9ywfl+dEe%^my<{W&So99?O`Bo*=F50Sqy zw{Uu;8`6WM!x1wA@5yn&Lv?GR%V?3vWe&_ZxqIR)%xpM*_E5%n4fRGTv@YSv%axyp z77j?-py%e-D`Fk{1w2<&HTEDDeF>tkQh2lJ@kzBfc;mzuOT=y*y_$o9dZju>J(Zfo zj_*-rlf!=kMs7(_|B|NOq3UI@x?#rqu0pn*!Z^xbw6EncfxKmd10XISV!?~}N%=Qc zzA+DOZfR|c6(|0W!s-Y8djeM-e=Txugye^2{8dfaL^Is}$+M`?8oXq{u*{JaXh)F| zs_zGx_o=ySqYzVLhx$)&{~T+8>~}~$xqf}H@sBp*xVA@AR-k=;5f(K&*tm(xmKOPG zJ5%r(j8|D@kO7Hr(Oe|5t#kkF!Bw0Ry9qUMGBlD|GJ65!M+Hf1YL;2O=}0iQ_JWcM z8SZzJpfh-fJHKsV+Z3|r+-}wE$FknvB&$X}T=`bh;$;nd*67f^MS%%6|Rjf48mSNfa}2{%`Cak=wNQVo6&zNVTbrWku2fUzO5e#UHukqd|9VKVN;c z6mG;g>ll!%8MSbE9#wmj_Erc*rfDo?Ke0O4e&yQr!C#-*-e^#3B%{*)PgH(?O*Z{}p6@5-iVKYdlf`ASooHn;)fKJn;3bDdO~Dv=dP)&DrY+A9n5ybNO%hRI~oFkG1Wud3b%HI!w08TeP>9 z^W}B(Lh$}Szg!=sJy&-p(m=#jxi{OvGis7yF*;5#a$!ZYkua-U=UkYzBf1`PyiBVaJ%BfcIy_K$epdB$L`$MN zl|NdDNtP*PL@o{9sZ>MyuQbNJ3>jqK-Uh#caVTt}GA!Y3L9y{^c6- z;l{Ny@b4zr@>ZsAUZ8@hm+Vx>nEa@aZd{#;n1xj!g^o@WUS#RId*L1$3!={9fWJ1y zXS)3S#=H_sj6xX-vj7q7JHhW<5Q0{mag6s#y5Zrh^>ckC>h_#3!Cw&{)YD>QSoAZ= z9qvP%w3XGsQ*y-MQ|Ok5Qv>XNL<37j$@BuPX#IJKh|u{HRb?&yyZxOG@!tc%mZBLS9yC zpQzhG`f|#5iMRjs#U7mYtrzWLMU@dLcpR^+Nn7h=~PVu(^;te2qE54e>9tD{Ho0WU_p^a!LdAc2Tu{PuKH zs#MV%v-nR%0=<(>$ZaufQt}95DB_s9wO)9sE`ONHT|FV`(wWxb*X6QdE?C5h%EL*t-DuJ3rMZh~)d~FaI;9OaC{2 zptsFKh-{-6yV$%nKqaZ?J@YOgRlP%Sswd{{z4=oO`ESMNpEA}1_+5(L3!5fGKVOjB^gIrW3pmZnq>iNY zN$vQ}wP!`;xHC)i4-8_QJ!K`;8w<3unf{hd_DS$-@Q81o954|bddBqkjW9@I9*LB$ zlD=`or1sIWUqAu(I$-5P276>drhv`(%kKUy_EpIDs&yvQ z8h_VsZh6+K{H{(b=lD2)cyf3^+K|K%dz@cJu23m!H)9y5HRF#dMA}zlmbF;ahCs&kmd_0uo#QREX&{^us_)eYAKOrg6z}4kcm(CZ z0!!N(=1zL4CII=2TT@&}V$egq207rfqLY9lthR?9?E;3cHeQ_56S>-EewCfwJ68{h zciJK|)CRFsk=!{^MRdkx*vetR)tDH@RQ`dGxGu0yc=KikA94G0_5Qg`{R;9f_&I5_ zXgkI9_=?i-Q>rD7zh)bqwTxeiV=^3yfll$|e<{ZDRF&YA-BN35#n#D;M?B+wj?vE> z*UVF2oSoHoQ#i~a)akJroreJjD-aGY4(azQ0k!D%aSi zX+sAm{?NdGe-~iLd{Rj8wjG_xi$knOo7Sf}fDd2SF(`DRRRXJJCt7(Tx2t% zz61U$15TYApm=EB6`k1MO*A&2HdpagxuqdlE_>+f;G>;mkHb(RJ4iz+UOTR@va+(4 z!W6_|_#_VxgMMv~hqO98J&adM7;32}XR@=2!mb82Y+L2mrFkA4F5|@?9g@F3?@6Vm z+7~5z22F7Ovjd#=WmbDt*gxn?!^gYP=@%K1NEo|%T9-tBcY>elFK^UYyN|^!=)d|r zu{3*(HTd4KcM8%Avmf@ETCpjvt3V)$Pip!Gl#w@Paa%NV{OT#j?P17$x|&_C_MVB4 z%cZW0&d9>$R-9eXtAfwnbsMd22i(;?XS9B5R95N`pcWM`R@)vgaeH)1E3MC&*xoot zeildLzh&bx>WVYtqh(FHe_K86+WPD<@#;LE`jJU~Fvt{||3;r`o2*X4>SbyYs+)W=P#arHX@jhWW(v5iy6P)M6%bwd(_z6L5qIly{*`kq zoLhL-`u;rFaLs@6Stl`6mE(F$|ZM2xeO_}-;3ODx#qH2rIcKPJg)x_ryO{|yHsgFY z^*g`c)#uGK|gd-`VtSQd#+hybp5c(jMVEmcrEAs2!)U3(9joH`&(M18lA;Uafw)yl^ zlcF$p4>ftGsHtZgwt&I{q&O>s@?KaafuNQU*XTccMR^9NvYmC*YD#SiBl6jN?P;Qz zAs<1+iLautPZu9O@-GDJP`f0@t3MyJjT>cPZ?B{VDcJI*llfCLm#*2P>@tEt+1 z(Z}F#Q_c$$eh75L-`qajcm>XCd7+almgb}IU`G^AmjkM_?0!H26;2~;P89RS2iDCw z9eqMM8#rC72j>21=S!qRc|!NV!@a^}-}t6{TGYDWNLYhx%Yd-pb!FJ>(?lGsuK;?{ zUU-D#+^lNm z9t900l=~C^hSe`|4*3tF4}iLEq7`Qfd+m%dnXk6Pw8^s|wGfQ@F*{mZtN z&A!n?qA&+Ub_Xvb>{omg4e~kZP$#=!cV-QcUaT*~c(GT+#O(7zsxUxukd=NPQPKr>atZrR zN}apfUb z;_dfgL}W=aoG8imSNR#bb(tNtZrN{2l5^qK>|PN4P}N@KJ#<9+Cd8hO9i#P0RVV*F zu>CQ@6TQ$MxKQNEowZKw2d7D^3s^}3Vtpl<;S2yebi4@UD)~(Z$i$uB4Ph*-7uODuFeV1}f)b&@{-sQ4MY8>mY@E1@n;3kBxUm&y-DBY*px*h)}?hTY5hbwQZ zX@O%qte!dmlB#hq!8Fm4s6F}9( zQsCRVQ2ZgLs}IoL($51fZ_xV-XWz59_=5 z^VD*1SMc64w)2-82E_jDmed}=Nw2NLZVoCte7>WhPYiPiPy|8Apxet7$2Pu3E4B#; z;RRDQ1p`?Sh_7^u#?^SL(S0859Q_B>cp3&EY@aafG%QZd^se$+^ZAHiAjA*4 zT~(~k1EJgmu5mahTHST!z;e44&s9agsD$s=#HV8D_u@qU9ry)6Zg@B21!2ucn+C-$ z55r4A*8VQ&=SOXeabO7O@quMNjhDCS?x0z_eD(dy$2H7(E4|}I&Q<_fP!+dN0h|72 z!Twj#er_RJ_1W~@P5Vrl;#z!hKo2g_I;pyCd)ls9A!7MMe;`mB4c89fQjw-t&lSNJ zPfZFNVcZ{bjH?q#FR$2Uhx%rJO|1JbO1)xa8zgA;q|#b5Sp{$z2|Yh~8Mr6OE6{xB zXk*EY#*n19MEkm*EW6Bu=vkb5JZ>;uU+nLpwTxvMq-x(0@mjh42#cBxum<~@>+M|r znGbe3v=)~@)#jjc=g@gB(XMXYl04?+*;zF%dhg%Ch+ZzjIzL4(JH z8FA00V2*=rnZw`zwx>l02pjp;Uu#uf?0pdcuqY8cx?SwOJ zJB3mJO?D31k8XaK0ZFx$w4MQSzV;|+D%ov*LwT&c9*=57dAh)@dC*Zv zLf8V1{s;8i&w=YG(B@|2f9DXf)mfA1{@J-jZ1udF*#=OWe<@5V_5UA8{!-@oG&WzY z6;X_0t6)B3xDn}$I8T#l< z8v;^%K>(;8Tfolj520~hQ2~sy-M))I=$5gJU;UY1V>%|fgj=tRT|G5<_ZIzLFm|gw z9v|#fM|8Zj{ud0`EaK$9^0O%1wRJb(KU1MwZiWxF!~d5$c2r~FgFzX~aFPdSlEkze z*?m$%fCTxs1{6&A>wsi1LDfxxgTBG2UIt**%#NDhK`B}0OB+Xi)<*cQTsP!1IY6! zonOP;MG}Z;v*H2Rnvy zi#v+4|Fy$g1~0Ei*69|7jN(x7@!U`0Km~L(2zcYHxOGvb`>?jAFJdZrTgb z$$lM%;Mxb1L3|HMRD@lDRRK5ZrJ;6RFl1xHGKXlEMs}EH7_y-Qq_4^L2@VA55S;XW zXL5-y+xa4fl()Vyo1M&1Q3V1@m{GR#icR3BylPiESw~!&@bo+2+dbN3 zB}``=*mC|wZLEI}9JK3bXYi7q~# zanAGWKG;6k6|;SUxLC( zy*E7(9LP^Y+rV_)pD-@_@cJ^vTEHpYD`ExY3CGB~&P|I(EPFj3V>c2|uwI?rXG9a= z8D#3IDI}gX8ST(HVO=cgy-s>K@ZWN$+5}p#Y`S;dUnfd z4Uocw5$`+6bopa6?-R*2i^;8#p~c}Z$CvhF(BRdi+bH znT!~A&9`+QXhdf`Z<-4=#iL`zEtQD1Ra}dkYPej||EQfdU*(&5klmxQ$T$xkUF2HQ z;0~0uVMu2WOVH%Zk)CVXJ;K=QI2#`+%26Zk#JIMjvPxH<c^eDr@=8U^z(~r+M1b< zL)~_t0=xe{$nXIpxXowJNa z?rN*Bbk@#9pMW?w;#ywALIdCgSror<=u&tP+YJcuU}$! zBRq3H0lK(%nXs%w%Bk~+gZsE02tE4yRh!m3qw}8B zX)sams*5lCI$xuq!wMJ6)>E~wR(l@zh&<++QoSy~Cr>TJkC%l@QWy4`ne>WacqKl-D$Ji;ey&CHY^IHFCKel7XSwfs8Oz_+bR8AN!>eqb)wA2 z(q7LUWf%{sEjFQ77$&o&IMnO$JY2#;4vzPc+^LKke5LbnwQz{KW8%ngCKMa z`sq8m7s3vXvx&a4z978(Rt;WzY7)3W%oJniymQ!f*atr?tI<7;#|WJ7e{?fM+b=Kn zPl4wSGMRHoz!5WhGW>SPjQKfY;3GqY%CI|pgW|jCAexZGH87@lW0@vUVyC9?+T9xn`H2o>+YK_1F=A?GAkw709gA zMBC|(@b{CQfrb*y6GEr_pVEA*!2edv+%9haXn>^6!{YZg@=#5vXv*fl^+rwdo# zX_%{>Rj>Pe`@tRI0mGX>26MReMl>tJW^c33x;DrUF$oD7Qau*{jjqu9Sb{g(EM4dZ z>2DO?&=7ICy{xEXo1FAf70_i$D#DI1admAoypl8H-aE5$x#rdxBT=3YE|e z8hazcZvCoE|GM=giT#7^TF}`|CUZiY`h6Rbc#02mkQoj)Px}{>> z+tCA-biZe+&7h?UTvqrV8^(1;mTQy<*ZIlyT*1+-7{^_XP1E;^4v4uhwhuJLVXWWFjLc%)BV)MHgB_h*+>nmMSpuL zTYe%^bEhvK{i|iK_G|?9Aj^xZqwogd`||U<3hasr0GyKs>=|33cH;U6Qu+%GNbZ5h zKG?}23-Dq2?V7NqqD6ih%ecM>$Zfu;&Cay61Sk`wtA1UzGCW)~$WM28*5G`#D+`DV z03_X#Y)?NmlxtGbz>1etZEv$YW8iCiU?&-{F;lQv1bQ@=E zdnTEYA_RECPXdDsE|X?tQq#LxK-iRq7r^~GHuA}C-h|U~ z@j)Xsbsf<1m}>-r6M~qOZ-jRr@%=eSnb7*#2{%%jaUaFaYKfxe0EFaB`UEsKNh()g zjf)EQLu!$KcgGBDoam_*odc}kGQf}^!^Y?AoUQSG*+$w!TOE25h+($ zwqf2mB$MEDZFM(tPPB2rbT6J8C^WUHer7KKxZW8%waEnp2DlpaW>Oot_+q*QJ;Z0? zeiFaQkuzg(MB3+k^OH^+UI(b#wu1U~` z91T7HvQfi9gQsU%RHjVhG}!J9n(>ZLd&KOW0Fr;y6@R>4Ey$3Ql@Beu=IAVV#hRJxmaz|OKt3yrGHK_rd(Nm@ zD_po}ecuQ3lBAYBIyg8z++pg%F^dQtB~5M+J8(NW$$cOdL+iOzug*Rj?qj$H@S2GE-92gN0FFrEf2eiUw9{F<}0WywyFh|oTQp6Fh6(?0b?|Fikw-cE+B z1L3mekr=9GyI|WjaCVzPM(3j0Lpq5n^Mzt0!03!RaMD&EW5A(&e+h~>-hgYG%-UXH z3$db4+Cy|}{3+#8)c9pdT{BiJ+J~|Vr)v69AUVs!Cy~PQeWRt~gsRa@6EX|Xkv&^H z$N?-^a(*-TxA$}6MiS-qfYcFVv*4OP$N_Z6M?WiT@j%k$_a_3g4%;w?4&ga+C2}9j z%*{{noeycj=$F5usJUx=BuT8g#EA3J&FVjsQ8t>{%fYDc_Paw|63&k3?YYMti+J3K zEK&ETRpo5=WMN(V%@oxci)qM*!_J-!P|bIJiMeV5L1hP`aqHfC^pE|hZ%=>CMIt#F zt)5+C7y9UoP(D?o7RX9r%pI0)%vF|6xQ|7q8PM;q6RbRyM>=M#?l7mh|1R*(-Grw! z#pa6uS^ebf0Y)bSFZVANq}1=Nf+QI$!RO#weWaZ24;>z2q9Ib3Lk|vwpZ?4|ti!@w zWi}X_iVrC#JTq0_S-80QAj9przAM@z0cm4E2ma_9cNkJJ&_`k{szDr^$J72}*9( z1@v3x{A#=FWK8`(KO51PrH%y@SL}t0N^r})m7Vu1kStD4{-AW%Qlv8>>7(WQ9c-Kj zX=a#Nx!sQ`zOWkmOwh(&z*)z%DiI89NQsCL0=}}(P4`1hqw3KjvD@J4c#A+H| zV)zRpa@*<1zgz%2CwZjm%i1)Arfw)$aO#(uCMK0y-k^o|BW7=Pqlsp`_mNbNC8*fE zrR#U4->n@>?y-q+&fs)WTASb$Brx@`i8@xbbB9j?BAYCmBzv{o6+1cQaP|`9K=PWK zQ@Ps09$@GVr3);f6DV0nsTd4Dfg(K*W-0hJ_gE96n>t19XXzO27Ph&xk6_}b8|Soc zY`&^5YMsoRGGsIHX*~fKo~z#7Z9KoFFNVAN7!stO;GcaQmliEFiuDhi0)>mrhjl)( zO+=FNDEms$-dzZD zIr4Z3Dm1U{YLyg$*3-Q%ab!u4jCP02e*65pmlBrIAg9RPb?DvNqy_DzOurz`(QDP~84oC3pC_9f-aVMG z1mFAReGtbOZ~Uz<1roHQCmP33kl;>1e1pRzbT2Ae%bo2pmfwf-ozxIp&=YlxY;fnG zIX@>G`ptP4h8CNDaQ~dwr-zxb{Ip!_lc61x4JOzf_s_CEIbf^cU3)FL%4Dcgp)KwZ zV?`TTzGEADM=Y{LS}I}|GcL*d)+~OoPuQ%;g37)F&!_z6^<*h_IbMIm>W#X~u+y_U zu#OwSZ>V}waz%GPdmEUG*s}t?*bT>6UiCL=tU?>hqqoRx z>>@S4;UP~Wd2yrv6ue8MGb9nL_{YJ)bXf)*Zz*lMtO;8*uq%Kz9E zUIEd}*x?BvjGtOLmObdD>`$Ky%%Q28 z4M-He)wDNw7arvy>qrlq)Kaz_a2>rSkUhg2*~Nfgn4#;*p?q0;byPG)a!-fN{ZU%u z+Qc-K!a9(duX5P1qxi~5%209EQsJz1UT1onmaFz=sP?E?U)+n8$R^w&!>@kj3R?bF zx~WkxfZai14?kWu<0`Z3xUAHsXsY6^!M%FC7q_j)!wRx~U# z^^x@c*-Xg;LHeZ2Xb$x?iI2VM7J1cgymz%)+T>toufrEaX4Lb=0>m&XRv73Ul!?=M z683OuJLtopK1RLw?)dY%ndO{;QbQG!gXqfcH4aO4^%m&)d20e5S^v&$>ke1L2>lKw zC^5jRLzsRUvVU-tB5dPI=j|&koEv#uN?n3po=?Md|1!svrh?v={>eee(1T-aT>EMQ z1{lW;>z11BCb^kIM7hc`g+~L}PbtDf5}) zXa)r3{iYIHcZ%@#>F96q7^Pl6-EuX%K{;c3&h1!kdc*!o@lNT-3z!?_zVC0*a{3gP zHOd%6l@)uQ>j9LM?ZY9jlT#8^lwWHsL9=8-LJ;`S8z)(O`_?@uX7QR^4Vda+;VmZK zu_>lm>nQb;ZVXF!+k+>5BqRwwsK*CYR3LE=BEoLN_A*vLoLM>$(*9`51^CbZ@|b^q z+&c6Tqt~^bz~7PaCrgIe=6A>FPU%4D1RxlUEXAU(Su)rT)YAy4I!T_k!lS7}73=4M z)61r@$%eGUL~*hzHj`o!j-7tk$UzQ-hNOQiBam0GoGe#JAaqlU8I(4@`g*U2==0P} zr*$JEl~VRV(RoLM`cH^R$r}Y98-xse=?k$>Y?77vB@<6xuS$%|gY$QKsMymF$fS8Z zg1q(5*s)0e@keaFwIjogz?ja)*2w5d)|OtFL{9wdj+XX|QMxgva{I6r+A~*H$(9fP zlq@{o)uEL`4zl5orz6R~c9LT{7QGG;1axcIf!|Chr$tdi!9;Z?=JG$(Vr*BL;EFvi z9-663$6!GoUDZwDTHz4ig;U;p^>l8Y733CG`ztE}?hf2D+)18`TiUYEbSX|(> zKiRd?V)u*1z|~Q)O$hSZ%lfVz9EroxTdj*0aT&A6Ci9D;sJ+rw_ja<%wx9o=u!P61 z2#5kRg-OgTU32T=j)XU0w;JqtTSndqyJ^hU%%+>EtyReBSc=&Yl?kSQj82qofDLA<9_Q+E_d zU?jU!hUF^pznEHH_#KOI?{O*yTOG-mw#_{my#TvEPa1oo=Tc#3!5db_JddoI^i`W5 zDB`hZWBie2A{91@@(Li<43uZY;wjMD-1X29CVC3$*&MM^56LW9HCZ|j^k}QCY$}Y7SqbhZbRAkhQ?rd6Ca%2<#W0* zf0+&O#*D?ZU6*jo(_s#q@Vx1x=G;#+4=z*%eD?5z0K4-0(*vg3Ny-kCzO1AtC(DhJ zwU5K)PRxM(%*-ij3iUzD)XeYo3}AFJ<#HqKM*@nlfQh_GSfLc=24l>{d|XOdFFix> zOyb@}vwhlsIy{OWvq7cHvzD*=0~WrMjIR2LGxOo52P>ZSUn|ezh*yb%6q&am3rJvM zTm%4rnZe9CSY2{%z)&RWk00)fIev;LAGD)<2Y|JE)WdGJuNT1te22UVuLUxNV@r}8 zlwM2HtLdYfwV}lV)FMJ2sJQ|>1R!I0!wm>hHgW5f4+E|}(C<0JFQJq$)}oUyG^=X` zbg5&;>HvNxYAqMy8{R*|e8jxmw8qG8{9FJ?iLgTLi3EGM>_Qs(HaRW=oclgn%Zyea<@209HejV* z(v1z1`C~)PlBcg>(RWf^YpwB~57)X9v|yoU&oej@xa6Ul_QZ${diFumsu8{Vx3>^e zGHd!xq0C^}$y98Zu zxvo3$Bo$hqGLvES&aRFh0&Y2D%h5jzCbUZg<#q@g2*tRS&ZkcDiAe}onQI}jX5+KB zKhn2KOCeJCuK6=fQGxCex)fazQ}?ckeuv9(gsy`L>W_)8MTCQ&YTNpGe1dHhzA~b* zUuE^7ION18)F&BaCOTa@9yB-=oB3nUQW2%k*!9WABUgsZI;;w z9Jp4Vmx^P|9~B2E;zu$gwUbsat%UdSe@U$aQ@w?HzR(IVvTG>maA&7%4$r126}^dE z`(z#&THf+0?afo3lwU0!OJKKIDy~L5L9L{=6=E4i6v1U)Jc%H1Pi(l1wl=k|1 zy*v$^4T(w8G87b?#wtc9-?8nJ3!I&Y4BE?2t3rd(5NJSqrsR(!8*T~u5?b3uKQZ_JjV zS57UJoK-(dppYpHmKuL{Q?uiK#^>7Bg{#&!i=r_Lp1y@u$+r^w9FNy#k5qqJ3Z}5# z8|R(+Fyc?Y#=dVHsLc~5>osjO-TJ|_&rmw8mnI&4a~94HIOci3LCaJ6dzxSXLFbiq zEsk(z)cX%=(}?!kqxB$r@-%#Z0sfOG%Ca^6_g7*ae3K@h*UFq{LUalEYB}QJe>ApXu zmSQrnK5k@INzNIhkbGlnDi~bZctD(e@a>E4T$?jScS;M+8n{uurSa&NxF=kDY5nYD zDakkhf8Wa~o>sTcSD$*Cg<+))-tUE}%9SU-=)SmcvTmS%c*A0J?_W7%^Yql+WAV@i(L z7Bz>u47YUrjEYM|K*81Sg5TcTBZ%thb7u)|F)HNzW%T)J&^PjwNv>|E(JLsD@5gWB@Prk1qf z4F!<-!cPc0ITj2{!dORPhGyrD#XGJyp~yby7_V9x-?{F@JCTvv)USrM_WTsqTT1bNd&-O1qSE;Gas}ddY z(_oqwjIaDJ&$6P0Ei293^;OOfy%s0<&r0|EyVTbIu6Ad1d9xf5sWu>I8}fGJcEgP@ z>@b7(K&J+7w9n?jV@K)9DF)!zvv)$02z-)sN&$w7HO!I2-}Bn3MsU-|wo{B|zj5E+ zbpTYx*-L#vgm-xGO1#g@2scX&X=pXZcnScvxgSMS_nMp6PLUDq+w4BZ2xT+|q{WZ% zS7vCjq7s^>!UY9Uiq<V`{(DbGh0jY*ni=iMh-{uab5Xy=j(EzQGyAkmVEE zQ4mJ2vkhmMX8z#8@)j*%!UdksRGR4Kme>xBBm9}iDCU-AvXCkuJ{^ z4#4Z(pn2mO?_)HRnvS>JlH_gEKEJ>Eb6 ztcN0hANxXN7aI6Lsu9 zox&uA4;Ed$p-~d9OrmEt{U0B8QI+1G&j4OrZ_eaZI0H68jkUOW7;&i?T&ktsOTzyU zju8fQ_Gv*J!$bmYSO#9TpY2`n1`~p+PV3bWlnT?aN>WV}dm&rLGk21mTc|KBa2IV}0q>h#};@MR#52v*75CMMl)^o&Ne$kG`pc6z`mZTuwi`)kTgIvN>$N1Ne`_5C2@!Zw#bBba(P1 zjdtY9E!sbA!~M*6^A*~Xe1~bf-AkpNAeUCj`uLauUkurL5>(YprAQp7o9N1r_m{Hg;|IoHwi7dnMU(}p}B zhBBsUFFuu07x%pO`F?3XdUe|Y{Yd0a(T8pQC#&UeIT5wA6wHO+z3YI>c)s(qLXv2G z4PB=|^8r%U1j1L(R4&R6jnKia2J=5V6PmpLgyxUXjW9o8irN0`h;PXnxD;gwI-uHK$Vjk^eDEwfq36iv@m zcZwi+G}`(A;euOe&NcZamAA4JKO7iyzXJqI$e~>!t@m1ka<-oswJg;`Gv>pxWPb(Q zc`Gt?VMeq6`Me=+>2;k1Sa*IRfVBGcG4t((KqdQGY;C5fM1SR$4I|Rh(qXHX<;i#n zD}8v8HITn#J9kE|j7J0xO&JOsG-cvDO4s(byCG(zBW!Nc!uICYO;?EYJzD{Nj9>DP zsz^BYDh*jNVBS}R$dn^In7!3zNTyL;@98?oq$Lb61>WI{&R>4$&(`O}IROjKPW3o- zAMg=du`km20)!K=ZZ}u^K#^VaYHvk3=kMLkD zF7rvP0Jx?v^G3iQWKpE#=EXQ^JFwD15p-IA_7!3nQ%qRO0~2>9%|pYzwj+Ne@7w`= zvtEEL-?j3}+plEf(k9@~rHU|iqcPBj;-?$#O21Sls;WlIR0#o0e+^e@KRIhNY&R?7 zn%E(%5j7SotUPrC@0}KVf_)I~pTWAMtptfvs;x+|^s-3J2nefFkP|cioQ4d&9fv-kGV_A>NFU>|ehx2s=Id>}g8fY$ADh zzYguxoXTMpa2UTR5X;d+oz<{&53K43ewI}`lWEK5n~?PbzCwZGk3If&GU>w$5jZ=p zgPQGDNAgQ2tLkHr>k4(Z_<)`pk%j4>_|jdil?AIk(GB~Rm`yS7T)(0;=Lj2~0d8@t zy}Lo6F)KTn7I9sf_Gv@L)AykdoTSK|4B-splq(pKN`2;bKz|t5Z^q$X#I2+r z4^x#dZ`Sz~>7UNHPG@l4C1G<&iiMaqM z$m?%YMt|aD>r|rXcdBWcW9q(q$!qGB_{$EuJ7H^cOv}XozEde2e880C8yMA>Srpb- zQ3>6yBfxwJ+rhbh?k%V~uc22MtI73*5O!N3)dNObkvJtf` zbw;jflso4>XVSMmfGtFKwOXSM2`Fb`t({KIVtE4)jS&E+l=OY?fNEU}1mYE!&!Wn` z!`i=6z@3CdgM=nI?+vC46sE!!T0qtd3g`l4QgOm`ND*m?20mZws8|NLCURupeiy$! zADEg?KQee|^Jd1S^cG0Yu?@2v=VpsEUdFD7eW5*pX13BW&OaAw8)`|qRFQdwvd!{? zxnH~$20me6qzp_x&3~4a3D%%P!4X^5qFa*AV?S1YWq;(S2-`CWQZ0SUW4*y>DP88< zX-2pI@!5zn&t>Y<%ky7o83@hkhc!Y5ogJaY-qqU~7~bJ~Oe+mE{>z6VsGAIrHC2bp zCav{6H1G(*case(uDTdBGF6VSgG_<*CQpKWcnuThXJ1+ z?ptL-)1R1Hfs+I3Mw(xe*npqh1}Nujm8_?7n~)6;|BR(Xc_aJoUu}YrE7~78R})C4 zOVA7RA3T+JXJU_i{?s>;t}%S9j0uXhUT9zRz}xCdm&J&u%R%iXd8+|=gEqx5sP+D@;{vrtf5Sw8H_pneUy@r{OYRA&fL1a z`OH!t{I<~9OC@-+*!(R_dCI3)W~<{ZrwPWGc&I?%dtF{V0D%eG6`p6NZbx$KHpABw zednybXPrM6r5~GF3cnVn2KPr7*>Nz&PyJG>dsQZla))pSb1mm<`%eR5)zl#etOG|$ z_v%}MjZV#-RgVR;96qI`$#=l5cKf}Y#!i;y^o1_3s`4@^fEiagrH!=0uG7xx}(;rjurDx~NhzQEm77cnIWrY4CD7^A%;2MdP znWZJX$w?#+Kgt!M9@G_vePMCX;G*YH@Dfw^hNVv}&BBrW{1Ju;8pep32ac%qeHiB@ zTW*!mf&VKZI1{p>#Bdn1(9_4u0j)xoK6-G+fBN$xgR#p+MgGrxD|qsWxa6l`#+ zBKL|ohu?ul?ZWv>3LMdrQtGp|1cf;M@OLUTc%_4OE4NZEFb>0hcIu6_j3Bn}XyMB7 z`G)-$;&9ha8xy0aQ5-KC8nCzDXTp0%+TDkkT1`5!X#4bhZv3>Gwu!obgeg()b^j6; zOToQby~kCSmw1HH3KnO42AMO{(U-IIrgCg`!;?lP*9ZRIy2*+XuP@{WT7Uu#9xE0! zL1^!%X}vNp+~e?pUNow$@}s}tPd&uw=lZAj51kTg(XwIdg|erHB5~{FocYkeq0&+k zxp&r?Yv>YJS@Up38PmozK4r2fBUc_1*OqEwCj+th3cB_ob(MQ_8}dH0It8+}d0M6#WWg6paz9Y@hD64&{B2g(_)z7j5{lh+rY*EC* zyCYG=bs@U3+p_Wei_li(ZES}n6UZ7-?JVngzq=jq(>+6`L4{r{K9a6hPKWWL{4cbf zP7q@Ou!i*ovH+Gt_-%BOnEvHQjn`l{a7RSX-8CXpl2nh z`i(!59Q+6=>ub|jFb=R@L!!JkW~XB@I}^PfQ=6$<)0cXBVizjx0A|-wGPT4HCTM}} z+%n0XU)R#)4^V(`IvO=T^t;km`}-@SyeFcSlv@}?pi(X1c}>sO;Yio58C`k%qUm2Q z0LrKSO)2FQLNMn85uUr-_nhvpxf!@~&fRgz_DCXvZ$wHs-3`(MH3>n|zh&x`-e6)e zETfwC3BxvmMKXQNjpHNq>zHbc{#gBw zaMRRQ<*Vh79(JC|N%?x00^}cr6a&Go3qaPuCM8)dRV;m{H&XWUrO1VQfXnwApvlGq z!FM1}L0X5vSj>1jBY0Gl)cF$*y`^pJXTPO~j{K2d>&ck_VZbzgZQ#?VE`5xlOeiGN z^had?LKaf+aCS47$tCH>4`YpZhqg1c#RR>Kd4OyorAU-v_h_6D#4MCU_05{Y@KqNm zYCDqGN%ZC!5AZdnfL(mg_dswWWI_jX>%)@n80vLs_(h{+i_US{eVyI=Pgm)=B0)cin4JH0#%-7EBn-G`xvI@ zGW+zK5#HDVmk}u}Q7NSU&aVa=U(0Gdssq7MnJpw61odQE;9;;ImVI)m8F5QSRZ|L? zH;9zssAC@chV8h@&|ADr(<0C)`RGVO3Kih+A?L$b+h+|*(lrMw1w)S6U=R%wjO|;# zUvrt#SK*ym9;QxpcXeJ?g<7ja|@2c6xVei;a)T4c&V7~Q3=swc}X)t*oM36`lnBe&1PjJLXh zSfa}u`HGg9fN%rn0MTs=m|fb?Ys75RC_ietZYofv!fTe{>flqqb8w^-{0-mjgpybt zWeoFyI|!e=ra4csj>X57DItQ9-eJSF)vuSGh zq-&oU-vr1GNQ+C9FKt(O>z}M`oh_&Llvb!LLvT1Ac5&(r~wE6g61;&Q=6eW zGTd+PnYzVz`y;?D+6t^n9pzM%O?1sL}jHUXQS>!CnLj^a-x+) z&~ZNTn4eNO^<6#WI_IA3$wM+ZcX8d@56Yz?ceNGOw(Q9~u;BYpaJ9RnYayVNdIrn< zNA18q?a#?ajGb!~Siz0aLab(k-JD7Vh6{;WOcYptI9*xfY4*$kPuz9z>Wi9}rh-Ej z-3va2)v4n_5X=>!b4m3|L)b=(Neu&t9Dp=*3v&&>Y-OsYyE z0qOVQc#@{?Mojb@z_ZmpN&#wIjnTx=OLgSK{W?BvAZcc8;k2E0v9-`9y&Sahw)g7duT@WxpR3-ZHSwfpqud_hbzL&9;!UKnjA&(Xw@}L7nxa1+bIaaeI~!pS{*X`H<7?1aiNX~%yPlaYQQYzS@oZIR8ZT(6&NsHJv^PABZ_{$O!Hiz_ z6m@cPNQ}YMAZXWZ%xpGn)=w#P=})Kc<$N;UkiWz0+MG%)`8d*HEA(8+a#wLztLH;N z#kPx=Mo-4rN^Kcko~v%d?c`IPKV5V>nY!)iM2QOGQc1D6yL_!&`-k!&{zC-3sI*3d z#Oqv!*l8SJ3F$P0-@fK?HPJs)TcYZ=EjP^wlQJ+{UASILi${>oxOrWDyJqO05Ik?r z1DBNNBbc~4ZdM~^z;u}$zY4GG)in*Zncy1JZ;BUox7~LHVuyK!QEJIOUMDilTWmJH zE+AggJEBMPYiYRak-PdD@LBlO`xmR$r|+od99!ebA@z_`mTv(P9w`% zOacO(o5zobA^}d3bm>mWH8YEo4&M)CuBfwaMekz3#M0q8JR?6yL8btbxl~}V?oHiD|0e{bzWyfB1oJrA4zY^`u(M?q z5d+n!B_S}n>4f`ZfsNCf*@ z^8<`7kv3i#J8dkIMNNwrkwk;CvPI@^D+0e@Tp4ayK$ij& zr4|s60Qa!E@ql8PG>{v(uURSHzu&i*C#Ng&_p$A*ofqD?p1ao>t82Xdh@z**V{s?aM5xKygaM2$P$FJ~FErW|I7F@Ty@BGz*lNh?U- zL2W}{5Dhugd+(kSrT5%m-)ZiEL}vq8OI}y6gIJ@Q?uOarks0ezM!w>7?CY2$C1lpg zaOaPA$tu31r;t3i0O1BJN&Ysk>*IHi`iC#$)BF+-D^?u{EPjc4v9UFYE}6WaPS9D_ zs14&si>m2u)RjCFwA-0e9di8RTOlvp{kUqZdy^#SJMuDy5~Oy!6+h6Ffgp^d%nn*x zZNCGSQ$}9loNI3yT|cq)DD&H0*Xu{H-QZ!nm2~$e^6yiSeIH7fG^RHTiPUw)Zy&DY z&z9LKVZxG>gX9%W!&6qje3Y51`}QbySn2c1Y;*hrCfqDOz$H{$K%x(wZ+80Bhh^08=T)JvFyS}wYlSb`!m(tG zgPg~T^&Z7`*!D}<-|RjnKlk`ZD$={(l5k|;#pZS8R9Pa{zm7L;Az+gR$94N{C>>?^ zI?4|HEIcIL{go(Ect(OxTfU8DF(+&Ld&dQQsL|cpILIH7kWz_Ix$<0ER=2xMJ7q#d zj_5sHN(yzqSC2zURnX5yZg&aqPUSlPeaPhz03+^|ZHKi`hcxd)qz3aUi0#dnHaP|t z?@p&*_rzAY`&bX0;_6%kBII8UB|bv3wsT*3FM70jmYyYMzOxv<9M7|_`v?==l)rXl z9OvYdoJTn>l>eW)t~)Bp|KHoRtkkA`9hsRqO37U1h{{~3<;*>b+~UZArh?k=Tc)Oo z=FSvzLb(+c%Wtky3lR%b@@qe-CGbiOuXKo9({aL?PA@UiMrhO zRc0Ahys()=aU>Q>Ey7=QHwiH~;mkX%g%5(!_$m z=4xN5Z&*kDeSyvbJSJ=BQ3X{bf4xJo134%X{>nCR{YY?-;=YtzQBLNvj5<~ zH4!CYlk-#Y4WHVEkjy$cm0CE^a3>Z$8<)Fi?`j<5HE+grAbk1wZp2cT2!0x=@6GFU z7dVFot#zqcSII@i&OhHM7sNT8dw!6lR6nd}R&KiffZzNhcVHY#P?Xpg>p^ z;;&rrKLeNSfZPp`+5J!SV9`ZAvgD#}>qL%=SeyO4>3s1MY1Q-Gt~<43;^OuT$DJdy zq8RRXR%QXo9um6Q(upi4%F2!`gRRTHLMJZWH|EDUQP`` z>x;+ukY_?F{|@ih(~+2in>d|dkah!v9pz70_GgpcVO(n_Cr40IC#G0OSk*0mQ*-g^ zQ+88vyj{Zh6!G3~143O1w7*ekc~`sPRWf-ELi3hCqCM{tpw1+n^>V=n7SVw&7A^j_ z@f-1tKKXSXPW~rsJChXE&i^u;vW8`J^DNry z64FN}uLIK9La`pKsyc7^g(7e*HI;EX#2ZV6B9Ab0r>>eK<7_vJgFN zEe-HrmUMZwJ&YPt-)KE=p0XA$bV5aRl^J;cy7e+)BV&W!Plgg@F=*Bd7X%0KFPD^n zc@=Y%>A0+rJ5SZ?kP7pzL)!wFj)KNUKIQ|6#Kp{IIy{SN!I7czxZMsg zHkCiiiJJ%N19qc6JSbf#(Ie)vLZ6229X}xaIZs)kF^0QCLLds~3>CEniJ;v^<4Ot_ z$MI(>k&e!S*4O-~;y{7Y8S6LcQwRN&iX<)BWy6Z>WgRs}uuJ$GMyP_IB{jWCir>0& z7el$BOx!Jm9lCRKOH?&@rCnIgXNHf`bfa%c){@3-CNYU@XOA5Bs9fLwEo)iJfp^hY zdhxVT27bYrXOt74Ex49j#BvtISOYX~bjW>u1z|3}lpwk5!L#*ko#A2&J7th8Y?YUm zwXUn+MLpl5oUgpUi1#`e4-5QlK=hJ&5)=&3s;+ft%cMd{~Dn`owL8|d%i?b!z;uvey+FI&?rv+b$VWb;L4;Gm)Yp6w)y~f8)@8zF9N-N? z<+zZ8zAU;B?&WoI!a7V6EV?h8EkPbbGW& zLqO3~9znWuOyRiTkJt9+@56l|Z-2JY%x_xqY^`mEbV9R-WBsKz&+8wI)JT8ldv&OXkL9b*UDu03^rc*m~Wpcyf>q{F4=m zQS@)-pLhlcH9RUGn=h=tXTmpNBVcUEMifeAOda0&{N+-8g>KrS(Cw)t!+~_Mn$!$nNvfx!(-dvbt)f4YbjBfphlVazLR>)!lNwkDXyC z=4uzGxo|ituP1mwQzQGG)958KK#3E`fYI^BM(z*2(9tv~Ckq2c32_uA+}3k&dL>0S z#Y?pa_u*U4iP%U^9EH8yxSk)TZsF8sUpl0R@|y9d&*mxERzx!Ild}zyqQ&NkI)P(Ju(+)J&x1kvx9S0- zHtg$TMgZUQL>ms=EH24*QELpDGSeJXXrOncNK5!%zmMd4n3$Ly4>DtxcURs$rmB%& zUo2+^%&J82ZX)we$Puf3R8`SRvg*&zF8g%sYgd*(CMwWc9*mN!rc3o#xn^GOP7b*5 z%NTZ#*1pYDU$h19Wm8}_`}~!{XlJLp-=P;8W|r`tY_L#Uu7&EzHluUphg(LM1i$xt-2=9 z)I*m$kZ<)tq{%H*i=}+nKjHbgM5lc=-7X>b?>`&AqX*T4wdr_O2Q?p#tg_QqbVo!ld zWFSsFL=RIbH>D@qk^fY4c)G5RZxyvAlt8@jbYjw}yFgp^RWnc#$Ybs^PFkta=~tOe zJ`I<68bw+kbc>NuIKf2ViY*={Xr2+IZv4tb5Gdv2t9O&08(xDPlBY_*xbaOG=$l_9 z*P|!%Wakgb$sb)W84YD6JRb(oiq({fbp1-(6^mz3W&EEoj# zCIuNjBK>#Gy8J7%x~;qW&d?-GWYswp(zbl?)H9U3|fv5$4>7jKZG_7IaJbb2h#8N_lbMe zC@m**ONS+eP3+R#4fBFmm z4Nh+P)~!T^T%R|BJB7iN)e82X&5bPRL6##b?^R_e3CWo2N?3zv_@!_3GXI?AN>vDQ}S*YC8%%NMCkniT7i*wtLOA+NQ$Cs+LrN6M#`VCH}^qy|Q8Z2fV zJE8b037UVBrpRKnfBO|y7wOd;YuCP0r%08if4-c!8@MmTfNg@&Zyh7sh41J4`R+p~gw_4u)CjGxX=xEJ01+cvUg?ZUZ=eUTxTWl zBFG7{GE4@n6eo>{@fjTqHsH#uTNU42TsgoKT^@w~UO${_*ZLd=1^mz6@VY|`5-`PJ z_hm6{kY1|ab;j^o4Y}%dl%vN{$((z6X=;HK{|KDmi#q`}5CzyOEc71Fe#S2dr`)Y#ag5X>O zRlNq#lGu7LXXZ^FCU)HrqnlV~vGC@8HVkK=_B}BGFxlIKF^uP{r4IpwtnJ^8#oU?G z1C5LvIyw16p8c6Cx`Vv}p-I40znQ!%JLy3USW;q#Hdpdn;QoRNm%TC*<(nP~a7OTR zyk9_T4xuw%?i;6IQ1mH4*wYj-!~>X3;A6=X<8VZTb4c(D+d+TIPKmC#l;{%PWWpY4 zsqI1$M-kR*D>t>dOE2&=p1}dB###V4eAt0Y16JcoWY-YWyw0jl% z=jBWA~G;0ocE3nS#j&w(Usa9*GMz^#*urs32=2U z1@_oRQZ(?i7KMX*6A;n7b$FFLkI~fUUqk8RHEF(}?*EPH+W@r$!9DT^fwf%v>WG!8 zW%V)@Xz;HspiE|~ML7?L;HLojZ34xR^4wYTwwGBlUeCBI(L$HMC=dpacZ>(AFntkl z$d(Q{%4bz1!8{F!8HFX6Y1F3tAmivC>-WjF4PKtNYAi)B(=-nW(wo>lMVrOk+uK*y z(IYW$F6=Ncw%Hx61~(dtU8wq^)98iSu$p<(M;?AI{TpSFG_5KP56|Q5(`US$%dp<3!H86bM84rq{FIt*ud&vRUOZKvkc&5!|I;B zJG6pmCu44;>_=#&ZeRS=wpnMH-5-k`b;VW>{h1bouv2pGxmuVf*k1M@wns577Ye@W zj+Iz)9c_?y&XfqAza%29S$QO@a{ham1w4rxF$O=<I$5jf-4}BqXWPn$8Yefs!e~?_O z+sm-Hdf9=lx9T3CwqURA&`88TjqIr4Lyy5GT^WcMEz1o@4@IJFX~T-lSdaPlt1f5W zYG37zaju_CyIYBGaX_~562|m4I68n`LS(WEdxBx~5AHbC)L|5$pluifWLuFT532#^ zexUuPCub??jzqd&ONRB>vRDSB*T%j_@|w_c9-@7 z@T0s0_BxQn3d2UEJEeaM;yNMz?`tnSutK^0#`XMvJie#Ss0*+4*T#SkBH&S_BHE|Y zDgZ{UBDj8;Db1l@`41mimz{gbwH9h9SSZ!R$|oqzxB85>5nM+yjASFc0{*HV?-~&h z4mfp=k5B{OTw~u6y$ULlSnZIt)OQHPTjmQjSj|syOjX`__w^XsTHo}walzi-DnH&# zZT(RL2oXA24irG?+g2$c^tpCk=qOYlTX9F82q3bhacge?-xcyg!^e|pzcm5rC`#lM zIxg~-?=y|4lxikJ8Q>~|p91`8Gs}Ip*-K@zB+gR46C!PSckxczawoEw211q$v$qvV zVj?Ash~JD?1+ZMmia=i2OS<(=g0d0cFl%*l5Jh_l19!ojjLasFSs(&1D1CWSY-XgN zZ^Iyfx$v6^q(UP%*`uQ5Wu)-U%P%qU)n0~KS z6|d=X>k7B6XFAS0F8c~^BpK?VHVIBEu;BXm&SX9OL5oB0n{G_mQ0X1|&;}SQhZdV& z@GoPKuLEQ@nwRb);VUvtDSN6Ax7FP7Kg?oHcyID1b4Zk^9b39LxRNwqYS@a$zAeWK zs9cjRP2ahYDn+Glw$FOTC18x0KTxT|#bvTjyJ#>6{D4P*@>@v&KjLMy`?P#&3Ioiv zDHIo*L7A%04UB9;&;ETb_YP9Z$}YB zr5)GG5L1Xst}@_No+SKqIw|rT*xv#3lD%>WQ2P#Uq3oe~sUUz5IdMHGz1_~x-7=J_ zk{xg-5pR2D9YTmI9+W-j&9jV>KWY?;BLRxDdZ6MkCEI;h`Kr6evmy`IVSmpr+i~66 zo-~^z;ng>nq92TysPMt)e<)SA6I>b^y*qw^lEW>z8erpU#ZGUxIpYo*$EOFkYSTL$ zR62f0I!Xh4YD6TaepwL}b&Kr@79gh^U(e~29F*bWSMSYLlOl-I{M}8b;o?L-{>Pb5 zt6?v`M+vFk;FSC@+j=j02CI~|vj14~6v2`LDVkdvy+Lt+OUQ{DT)5$7dN@i_7EQk+ ze|4qqLhsGDy8?$+LuIOZuK3b8Kh^@v{o87Sey8ZfT67hD{77*TCZxsg)mMpDa$Gd6@~gDN)!Ob76IU znb{-lYaG11nkzm0nczykM;v)a7YOJ@F#8F8$p;x%N;{4Wb_xU%Q>ur=w&Y7qO1S1~ z@B0j?w-+t;tB}|KVix2w{Im8pXy*o{=q*bmvDn3Wja@k6khDDdRJ7&LoF>cGa=kdh z4g4=E2>O#R-;V4Ze#*lit||XiO>tX4x&clnhTAwUw|?&^Wv9~iOD!-Bns|z<+)A{3 z-wR6q)qbUzBh`90i~pn$u-5Ld6CtX!adP@AK?*T6wyS*IC^`eVcLQVYJ|9zU$ZVs| zkbd>*M6SnK@4AW6f2O}j;r7s@v1E)+6(}7V$h41Kh)a)6eZXcGJbzQ$w$o{S+k({C z<&oFcHE2NuIpHUDIFu(hP{iz#*gUrVl2>AESJgfm-v55CS>ZlajF(qL{ajM@?KX~0 z*)4IHx_;CdxuCriF{Wf&p|CyU7uNcdIe{4KZY?kjc41FGK$ML1>DEb&Vb6gLdTnYD zA$zSIOT6=4k^_VGdM0chdQ2+{nRS%@=RlX#0un^T=OR(jE~WT!=X3uA?Nplod;sLh zGegxK8g}&k&UEz6^7Ty6II~fS-`Wx4cGq;xq6(HWl5> z!se}KeKB|`yYhu)*-OT-l=`A_e?oha*dM-A7H7IFTnE7$N+BJ*o)XS ze%$f6@w*g=M~mH7tvFVMw1_Ad0(LD<(dz&jxD^{{^_mavOb!}$U$TO9jZx@_ z4pM)YC3}pgyn669n8;7(CSXc}^(^Mq*5S;E;N7R3&XwCCT}zQv=T6VDky8e)c%kD` z#0cu+cMT}0E8Ln%-Nk^?@UK-}P2sm-wWM6SHInkCeaH{OPQaO9(ODM|{?-@&IeJ~G zh&J;3%H-ovw$bZf5xxSF)M7r%dd)5%I0On%T+Jo5A?&lFx2u{c=gja03m#|GL6 zO3?c1<=DKZt=}En*rd8e0e$jo1A(?j-J)ydrN9L{l6zA9tWee{a@O^sZui>#zi@k2 zZ~Q^Be(isoU~y^Phxe%5XP3gQO>Rbnvuh$Rtd=F#n?xA?@gVCBB4qMkp(+?S!vFn< z7upT@$25B%5&CYPg80}Uk?)KL8qGzJFr2@r`c_{@UHlZ$%J)N`GaS9xg#dJ!uPueg z^BUMV{}n{Z*D%W6FRN?CqeV*OMGxc*k1GbJpbMibLFjX~agKw*=zkoY<^n&LaY{XD z4%f50>MuTH5u6M-IxEJb8Ci3iU-Y+ieEWg3cKL#mR)qrDHp0g_{G_7V}>qFoMz~%$TqkJk6oSP@0PfC=ERNvzl9?fM{Kyd!mSYR)= zm#jE=Bj;6Hxt~Rb`grE&&~zla$hcFQh_(~x71-mjevq9pO#&$hZ`o!7?0Xmgoa1JN zTDU*n9DZWny($#3?H+(&h~RHNn~OjXjBwo*)U2%@p8{+4?isBWvmmrwOxxG38=Qb|74y0PukL`Xz7vFT znEv8T*J?F5jj3;mmeV&GFdoH|O0eU#4K|PQ3vjL8m|E7}^_$00o-=LO&l*1kD)Som zmmDeuE>!}fi*SwkirccW|MqRL58WU!C#)hV4_l)jY&~uUJJ%mK#T~Eiv@*?d zu1*tlLld34%suZ>J8~|vfW4D7RvF=`U9@AB{J)O2Bg^B1>qg8qF2Xk}=hKCcP1J8) zRB83_t-)o{!8@o1jmAb$+)JgM8ErBRh_K*$A)RJznnbSK!Xqq4`XK`u@PUmb5{_M^ zgl!&_h-$)qPH3UQT=RFRChH5g9}3@_FyUaZCg*VW=s83N$D2OU88F6F?-Fb|xW)Wh UhOTSZM%J+~xpozE#XI@G0JfdYX#fBK diff --git a/app/src/main/res/drawable/bottom_sheet_rounded.xml b/app/src/main/res/drawable/bottom_sheet_rounded.xml index ba266ed32..cef4c2314 100644 --- a/app/src/main/res/drawable/bottom_sheet_rounded.xml +++ b/app/src/main/res/drawable/bottom_sheet_rounded.xml @@ -1,9 +1,8 @@ - + - \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml new file mode 100644 index 000000000..61e9c4157 --- /dev/null +++ b/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file From 1ebcfa93a6d242406ea58af46e5127efff845099 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 15:37:04 +0100 Subject: [PATCH 28/34] Hide camera button on Android < 5 Signed-off-by: Stefan Niedermann --- .../ui/card/attachments/picker/CardAttachmentPicker.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index 1d89ee0ce..c515a7c3d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -23,6 +23,7 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.view.View.GONE; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -54,6 +55,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c binding = DialogAttachmentPickerBinding.inflate(inflater, container, false); brandedViews = new ImageView[]{binding.pickCameraIamge, binding.pickContactIamge, binding.pickFileIamge}; + if (SDK_INT < LOLLIPOP) { + binding.pickCamera.setVisibility(GONE); + } + @Nullable Context context = getContext(); if (context != null && isBrandingEnabled(context)) { applyBrand(readBrandMainColor(context)); From 68d56e60450216789f8cf52f5ac1290008a10837 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 15:45:56 +0100 Subject: [PATCH 29/34] Make sure that without branding the icons are still white Signed-off-by: Stefan Niedermann --- .../attachments/picker/CardAttachmentPicker.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java index c515a7c3d..bdc620f38 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/CardAttachmentPicker.java @@ -60,8 +60,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } @Nullable Context context = getContext(); - if (context != null && isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); + if (context != null) { + if (isBrandingEnabled(context)) { + applyBrand(readBrandMainColor(context)); + } else { // Make sure that without branding the icons are still white + if (SDK_INT >= LOLLIPOP) { + final ColorStateList colorStateList = ColorStateList.valueOf(Color.WHITE); + for (ImageView v : brandedViews) { + v.setImageTintList(colorStateList); + } + } + } } return binding.getRoot(); From d68058cb1a5691f2b20135b808b017a0b15e2295 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 19:14:41 +0100 Subject: [PATCH 30/34] Remove not needed dependency Signed-off-by: Stefan Niedermann --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f798e91c5..06c527b4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,7 +70,6 @@ dependencies { implementation "androidx.camera:camera-camera2:1.0.0-beta11" implementation "androidx.camera:camera-lifecycle:1.0.0-beta11" implementation "androidx.camera:camera-view:1.0.0-alpha18" - implementation "androidx.exifinterface:exifinterface:1.3.1" // Markdown implementation 'com.yydcdut:markdown-processor:0.1.3' From 611bbce5c2f27cd977f31bb8889505a880065f9b Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 19:38:12 +0100 Subject: [PATCH 31/34] Correctly recognize mime type of captured image Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentsFragment.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 64ab02f1d..5589b97db 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 @@ -219,7 +219,9 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString())) : data.getData(); try { - uploadNewAttachmentFromUri(sourceUri); + uploadNewAttachmentFromUri(sourceUri, requestCode == REQUEST_CODE_CAMERA + ? data.getType() + : requireContext().getContentResolver().getType(sourceUri)); if (picker != null) { picker.dismiss(); } @@ -235,7 +237,7 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d } } - private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri) throws UploadAttachmentFailedException, IOException { + private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException { if (sourceUri == null) { throw new UploadAttachmentFailedException("sourceUri is null"); } @@ -254,7 +256,7 @@ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri) throws UploadAtt final Instant now = Instant.now(); final Attachment a = new Attachment(); - a.setMimetype(requireContext().getContentResolver().getType(sourceUri)); + a.setMimetype(mimeType); a.setData(fileToUpload.getName()); a.setFilename(fileToUpload.getName()); a.setBasename(fileToUpload.getName()); From c36ef845f023b993acfec0d4877ca96134b299dc Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 19:50:35 +0100 Subject: [PATCH 32/34] Fix position of added attachments Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentAdapter.java | 9 ++++++++- .../ui/card/attachments/CardAttachmentsFragment.java | 9 ++++----- 2 files changed, 12 insertions(+), 6 deletions(-) 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..0e69ce4f1 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 @@ -134,7 +134,7 @@ public void setAttachments(@NonNull List attachments, @Nullable Long } public void addAttachment(Attachment a) { - this.attachments.add(a); + this.attachments.add(0, a); notifyItemInserted(this.attachments.size()); } @@ -144,6 +144,13 @@ public void removeAttachment(Attachment a) { notifyItemRemoved(index); } + 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 public void applyBrand(@ColorInt int mainColor) { this.mainColor = mainColor; 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 5589b97db..d3441b998 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 @@ -263,9 +263,9 @@ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) a.setFilesize(fileToUpload.length()); a.setLocalPath(fileToUpload.getAbsolutePath()); a.setLastModifiedLocal(now); - a.setStatusEnum(DBStatus.LOCAL_EDITED); a.setCreatedAt(now); - viewModel.getFullCard().getAttachments().add(a); + a.setStatusEnum(DBStatus.LOCAL_EDITED); + viewModel.getFullCard().getAttachments().add(0, a); adapter.addAttachment(a); if (!viewModel.isCreateMode()) { WrappedLiveData liveData = syncManager.addAttachmentToCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload); @@ -282,9 +282,8 @@ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) } } else { viewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - viewModel.getFullCard().getAttachments().add(next); - adapter.addAttachment(next); + viewModel.getFullCard().getAttachments().add(0, next); + adapter.replaceAttachment(a, next); } }); } From 7eb093cd7386f060fd41ec8746b875ae572a63e7 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 19:52:15 +0100 Subject: [PATCH 33/34] Minor stuff Signed-off-by: Stefan Niedermann --- .../deck/ui/card/attachments/CardAttachmentAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0e69ce4f1..cfab75a32 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 @@ -45,9 +45,9 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter attachments = new ArrayList<>(); + private final List attachments = new ArrayList<>(); @NonNull private final AttachmentClickedListener attachmentClickedListener; From ea7895cac790add89a0ab13c26eba0c725b8e5dc Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sun, 1 Nov 2020 20:04:51 +0100 Subject: [PATCH 34/34] Use LiveData for empty content view Signed-off-by: Stefan Niedermann --- .../attachments/CardAttachmentAdapter.java | 18 +++++++++++++ .../attachments/CardAttachmentsFragment.java | 25 ++++++++----------- 2 files changed, 29 insertions(+), 14 deletions(-) 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 cfab75a32..d062bc827 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.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.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 isEmpty = new MutableLiveData<>(true); + @NonNull private final MenuInflater menuInflater; @ColorInt private int mainColor; @@ -126,22 +132,34 @@ public int getItemCount() { return attachments.size(); } + private void updateIsEmpty() { + this.isEmpty.postValue(getItemCount() <= 0); + } + + @NonNull + public LiveData isEmpty() { + return distinctUntilChanged(this.isEmpty); + } + public void setAttachments(@NonNull List 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(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) { 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 d3441b998..d528dc903 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 @@ -54,6 +54,8 @@ 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 it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -106,7 +108,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, viewModel.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); + } + }); final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column)); @@ -138,7 +148,6 @@ public void onMapSharedElements(List names, Map sharedElem } }); adapter.setAttachments(viewModel.getFullCard().getAttachments(), viewModel.getFullCard().getId()); - updateEmptyContentView(); } if (viewModel.canEdit()) { @@ -287,7 +296,6 @@ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) } }); } - updateEmptyContentView(); break; } default: { @@ -344,7 +352,6 @@ public void onAttachmentDeleted(Attachment attachment) { } }); } - updateEmptyContentView(); } @Override @@ -352,16 +359,6 @@ public void onAttachmentClicked(int position) { 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);