From 755bd6108c8ce0e7fbc27bb86bdd5b87a4a57de0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Tue, 21 Jan 2025 10:54:19 +0100 Subject: [PATCH 1/3] Android Studio Ladybug FD 2024.2.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 034ed3f..22edef4 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.7.3' + classpath 'com.android.tools.build:gradle:8.8.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.github.gradle-nexus:publish-plugin:$gradle_nexus_publish_plugin" } From 100319c83f8afcf6263d712cdb9adf2dc2969eee Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Tue, 21 Jan 2025 12:11:10 +0100 Subject: [PATCH 2/3] Option to locally save the file downloaded using FS manager --- .../mcumgr/FilesDownloadFragment.java | 77 +++++++++++++++++++ .../layout/fragment_card_files_download.xml | 29 ++++++- .../res/values/strings_files_download.xml | 2 + 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java index c22a423..7c4bf43 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java @@ -6,9 +6,12 @@ package io.runtime.mcumgr.sample.fragment.mcumgr; +import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Typeface; +import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.text.SpannableString; @@ -22,7 +25,11 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; @@ -30,7 +37,10 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import java.io.IOException; +import java.io.OutputStream; import java.util.Arrays; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; @@ -59,12 +69,53 @@ public class FilesDownloadFragment extends Fragment implements Injectable { private InputMethodManager imm; private String partition; + private ActivityResultLauncher saveFileLauncher; + + static class FileData { + private final String fileName; + private final String mimeType; + + public FileData(String fileName) { + this.fileName = fileName; + + final String ext = MimeTypeMap.getFileExtensionFromUrl(fileName); + final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + this.mimeType = Objects.requireNonNullElse(mimeType, "*/*"); + } + } + + static class CreateDocument extends ActivityResultContract { + + @NonNull + @Override + public Intent createIntent(@NonNull Context context, FileData input) { + return new Intent(Intent.ACTION_CREATE_DOCUMENT) + .setType(input.mimeType) + .putExtra(Intent.EXTRA_TITLE, input.fileName); + } + + @Nullable + @Override + public SynchronousResult getSynchronousResult(@NonNull Context context, FileData input) { + return null; + } + + @Override + public Uri parseResult(int resultCode, @Nullable Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) + return intent.getData(); + return null; + } + } + @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this, viewModelFactory) .get(FilesDownloadViewModel.class); imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + saveFileLauncher = registerForActivityResult(new CreateDocument(), this::save); } @Nullable @@ -127,6 +178,10 @@ public void onTextChanged(final CharSequence s, viewModel.download(binding.filePath.getText().toString()); } }); + binding.actionSave.setOnClickListener(v -> { + final String fileName = binding.fileName.getText().toString(); + saveFileLauncher.launch(new FileData(fileName)); + }); } @Override @@ -139,11 +194,29 @@ private void hideKeyboard() { imm.hideSoftInputFromWindow(binding.fileName.getWindowToken(), 0); } + /** + * Saves the downloaded file to the selected location. + * @param uri the URI of the file to save to. + */ + private void save(final @Nullable Uri uri) { + final byte[] data = viewModel.getResponse().getValue(); + if (uri == null || data == null) + return; + try (final OutputStream os = requireContext().getContentResolver().openOutputStream(uri)) { + os.write(data); + os.flush(); + Toast.makeText(requireContext(), R.string.files_download_saved, Toast.LENGTH_SHORT).show(); + } catch (final IOException e) { + printError(new McuMgrException(e)); + } + } + private void printContent(@Nullable final byte[] data) { binding.divider.setVisibility(View.VISIBLE); binding.fileResult.setVisibility(View.VISIBLE); binding.image.setVisibility(View.VISIBLE); binding.image.setImageDrawable(null); + binding.actionSave.setEnabled(false); if (data == null) { binding.fileResult.setText(R.string.files_download_error_file_not_found); @@ -160,6 +233,7 @@ private void printContent(@Nullable final byte[] data) { 0, path.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); binding.fileResult.setText(spannable); binding.image.setImageBitmap(bitmap); + binding.actionSave.setEnabled(true); } else { final String content = new String(data); final SpannableString spannable = new SpannableString( @@ -167,6 +241,7 @@ private void printContent(@Nullable final byte[] data) { spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, path.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); binding.fileResult.setText(spannable); + binding.actionSave.setEnabled(true); } } } @@ -175,6 +250,8 @@ private void printContent(@Nullable final byte[] data) { private void printError(@Nullable final McuMgrException error) { binding.divider.setVisibility(View.VISIBLE); binding.fileResult.setVisibility(View.VISIBLE); + binding.actionSave.setEnabled(false); + binding.image.setImageDrawable(null); String message = StringUtils.toString(requireContext(), error); if (error instanceof McuMgrErrorException e) { diff --git a/sample/src/main/res/layout/fragment_card_files_download.xml b/sample/src/main/res/layout/fragment_card_files_download.xml index 1bc6a02..a89fde2 100644 --- a/sample/src/main/res/layout/fragment_card_files_download.xml +++ b/sample/src/main/res/layout/fragment_card_files_download.xml @@ -16,8 +16,7 @@ + android:animateLayoutChanges="true"> + + + + diff --git a/sample/src/main/res/values/strings_files_download.xml b/sample/src/main/res/values/strings_files_download.xml index 811eb97..e32ee49 100644 --- a/sample/src/main/res/values/strings_files_download.xml +++ b/sample/src/main/res/values/strings_files_download.xml @@ -8,10 +8,12 @@ Download Download + Save File name Recent files No recent files File name cannot be empty. + File saved successfully. File not found File empty From b9514d2641f355310c7ada55306c66968e9a0c81 Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Tue, 21 Jan 2025 12:15:54 +0100 Subject: [PATCH 3/3] Comment --- .../sample/fragment/mcumgr/FilesDownloadFragment.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java index 7c4bf43..e25cab0 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/FilesDownloadFragment.java @@ -84,6 +84,15 @@ public FileData(String fileName) { } } + /** + * A custom Activity result contract to create a new document. + *

+ * The one form {@link androidx.activity.result.contract.ActivityResultContracts} requires + * setting the MIME TYPE at the time of registration and cannot be changed later. + *

+ * This contract allows to set the MIME TYPE when the file name is known. + * @see FileData + */ static class CreateDocument extends ActivityResultContract { @NonNull