Skip to content

Commit

Permalink
Merge pull request #218 from NordicSemiconductor/feature/save-file
Browse files Browse the repository at this point in the history
[App] New feature: Saving locally downloaded file
  • Loading branch information
philips77 authored Jan 21, 2025
2 parents 20833d6 + b9514d2 commit 1764643
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,15 +25,22 @@
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;
import androidx.core.content.ContextCompat;
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;
Expand Down Expand Up @@ -59,12 +69,62 @@ public class FilesDownloadFragment extends Fragment implements Injectable {
private InputMethodManager imm;
private String partition;

private ActivityResultLauncher<FileData> 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, "*/*");
}
}

/**
* A custom Activity result contract to create a new document.
* <p>
* The one form {@link androidx.activity.result.contract.ActivityResultContracts} requires
* setting the MIME TYPE at the time of registration and cannot be changed later.
* <p>
* This contract allows to set the MIME TYPE when the file name is known.
* @see FileData
*/
static class CreateDocument extends ActivityResultContract<FileData, Uri> {

@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<Uri> 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
Expand Down Expand Up @@ -127,6 +187,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
Expand All @@ -139,11 +203,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);
Expand All @@ -160,13 +242,15 @@ 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(
getString(R.string.files_download_file, path, data.length, content));
spannable.setSpan(new StyleSpan(Typeface.BOLD),
0, path.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
binding.fileResult.setText(spannable);
binding.actionSave.setEnabled(true);
}
}
}
Expand All @@ -175,6 +259,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) {
Expand Down
29 changes: 25 additions & 4 deletions sample/src/main/res/layout/fragment_card_files_download.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:paddingBottom="8dp">
android:animateLayoutChanges="true">

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
Expand Down Expand Up @@ -126,16 +125,38 @@
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="16dp"
android:importantForAccessibility="no"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/file_result"
tools:visibility="visible"/>

<View
android:id="@+id/divider2"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="@color/colorDivider"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"
tools:visibility="visible"/>

<com.google.android.material.button.MaterialButton
android:id="@+id/action_save"
style="@style/Widget.ActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:enabled="false"
android:text="@string/files_download_action_save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider2"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
2 changes: 2 additions & 0 deletions sample/src/main/res/values/strings_files_download.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
<resources>
<string name="files_download_title">Download</string>
<string name="files_download_action">Download</string>
<string name="files_download_action_save">Save</string>
<string name="files_download_hint">File name</string>
<string name="files_download_recent_files">Recent files</string>
<string name="files_download_recent_files_empty">No recent files</string>
<string name="files_download_empty">File name cannot be empty.</string>
<string name="files_download_saved">File saved successfully.</string>

<string name="files_download_error_file_not_found"><i>File not found</i></string>
<string name="files_download_file_empty"><i>File empty</i></string>
Expand Down

0 comments on commit 1764643

Please sign in to comment.