From 77299a68ed9ffbf927c22dfb0874a13e48927754 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 26 Nov 2018 20:12:59 -0200 Subject: [PATCH 1/4] [android] Allow the user to save image attachment outside of Briar --- .../briar/android/activity/RequestCodes.java | 1 + .../conversation/AttachmentController.java | 24 ++-- .../android/conversation/AttachmentItem.java | 10 +- .../android/conversation/ImageActivity.java | 124 +++++++++++++++++- .../src/main/res/menu/image_actions.xml | 11 ++ briar-android/src/main/res/values/strings.xml | 4 + 6 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 briar-android/src/main/res/menu/image_actions.xml diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java index 0fc18c60b..3e1351ab3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java @@ -15,5 +15,6 @@ public interface RequestCodes { int REQUEST_UNLOCK = 11; int REQUEST_KEYGUARD_UNLOCK = 12; int REQUEST_ATTACH_IMAGE = 13; + int REQUEST_SAVE_ATTACHMENT = 14; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java index 7c077958e..b8754e755 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java @@ -127,12 +127,14 @@ class AttachmentController { } // calculate thumbnail size - Size thumbnailSize = new Size(defaultSize, defaultSize); + Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType); if (!size.error) { - thumbnailSize = getThumbnailSize(size.width, size.height); + thumbnailSize = + getThumbnailSize(size.width, size.height, size.mimeType); } return new AttachmentItem(messageId, size.width, size.height, - thumbnailSize.width, thumbnailSize.height, size.error); + size.mimeType, thumbnailSize.width, thumbnailSize.height, + size.error); } /** @@ -151,9 +153,9 @@ class AttachmentController { orientation == ORIENTATION_TRANSVERSE || orientation == ORIENTATION_TRANSPOSE) { //noinspection SuspiciousNameCombination - return new Size(height, width); + return new Size(height, width, "image/jpeg"); } - return new Size(width, height); + return new Size(width, height, "image/jpeg"); } /** @@ -165,10 +167,11 @@ class AttachmentController { BitmapFactory.decodeStream(is, null, options); if (options.outWidth < 1 || options.outHeight < 1) return new Size(); - return new Size(options.outWidth, options.outHeight); + return new Size(options.outWidth, options.outHeight, + options.outMimeType); } - private Size getThumbnailSize(int width, int height) { + private Size getThumbnailSize(int width, int height, String mimeType) { float widthPercentage = maxWidth / (float) width; float heightPercentage = maxHeight / (float) height; float scaleFactor = Math.min(widthPercentage, heightPercentage); @@ -184,24 +187,27 @@ class AttachmentController { if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth; if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight; } - return new Size(thumbnailWidth, thumbnailHeight); + return new Size(thumbnailWidth, thumbnailHeight, mimeType); } private static class Size { private final int width; private final int height; + private final String mimeType; private final boolean error; - private Size(int width, int height) { + private Size(int width, int height, String mimeType) { this.width = width; this.height = height; + this.mimeType = mimeType; this.error = false; } private Size() { this.width = 0; this.height = 0; + this.mimeType = ""; this.error = true; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java index 7f39bfefc..9e36e848a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java @@ -14,6 +14,7 @@ public class AttachmentItem implements Parcelable { private final MessageId messageId; private final int width, height; + private final String mimeType; private final int thumbnailWidth, thumbnailHeight; private final boolean hasError; @@ -30,11 +31,12 @@ public class AttachmentItem implements Parcelable { } }; - AttachmentItem(MessageId messageId, int width, int height, + AttachmentItem(MessageId messageId, int width, int height, String mimeType, int thumbnailWidth, int thumbnailHeight, boolean hasError) { this.messageId = messageId; this.width = width; this.height = height; + this.mimeType = mimeType; this.thumbnailWidth = thumbnailWidth; this.thumbnailHeight = thumbnailHeight; this.hasError = hasError; @@ -46,6 +48,7 @@ public class AttachmentItem implements Parcelable { messageId = new MessageId(messageIdByte); width = in.readInt(); height = in.readInt(); + mimeType = in.readString(); thumbnailWidth = in.readInt(); thumbnailHeight = in.readInt(); hasError = in.readByte() != 0; @@ -63,6 +66,10 @@ public class AttachmentItem implements Parcelable { return height; } + String getMimeType() { + return mimeType; + } + int getThumbnailWidth() { return thumbnailWidth; } @@ -90,6 +97,7 @@ public class AttachmentItem implements Parcelable { dest.writeByteArray(messageId.getBytes()); dest.writeInt(width); dest.writeInt(height); + dest.writeString(mimeType); dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailHeight); dest.writeByte((byte) (hasError ? 1 : 0)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index fb8e8ad4b..6b18a113d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -1,14 +1,20 @@ package org.briarproject.briar.android.conversation; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.AppBarLayout; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog.Builder; import android.support.v7.widget.Toolbar; import android.transition.Fade; import android.transition.Transition; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; @@ -20,14 +26,34 @@ import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.github.chrisbanes.photoview.PhotoView; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.view.PullDownLayout; +import org.briarproject.briar.api.messaging.Attachment; +import org.briarproject.briar.api.messaging.MessagingManager; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.content.Intent.ACTION_CREATE_DOCUMENT; +import static android.content.Intent.CATEGORY_OPENABLE; +import static android.content.Intent.EXTRA_TITLE; import static android.graphics.Color.TRANSPARENT; import static android.os.Build.VERSION.SDK_INT; +import static android.support.design.widget.Snackbar.LENGTH_LONG; import static android.view.View.GONE; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; @@ -37,18 +63,32 @@ import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static android.widget.ImageView.ScaleType.FIT_START; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.IoUtils.copyAndClose; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT; import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute; public class ImageActivity extends BriarActivity implements PullDownLayout.Callback { + private final static Logger LOG = getLogger(ImageActivity.class.getName()); + final static String ATTACHMENT = "attachment"; final static String NAME = "name"; final static String DATE = "date"; + @Inject + MessagingManager messagingManager; + @Inject + @IoExecutor + Executor ioExecutor; + private PullDownLayout layout; private AppBarLayout appBarLayout; private PhotoView photoView; + private AttachmentItem attachment; @Override public void injectActivity(ActivityComponent component) { @@ -88,7 +128,7 @@ public class ImageActivity extends BriarActivity TextView dateView = toolbar.findViewById(R.id.dateView); // Intent Extras - AttachmentItem attachment = getIntent().getParcelableExtra(ATTACHMENT); + attachment = getIntent().getParcelableExtra(ATTACHMENT); String name = getIntent().getStringExtra(NAME); long time = getIntent().getLongExtra(DATE, 0); String date = formatDateAbsolute(this, time); @@ -143,17 +183,37 @@ public class ImageActivity extends BriarActivity .into(photoView); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.image_actions, menu); + if (SDK_INT >= 19) { + menu.findItem(R.id.action_save_image).setVisible(true); + } + return super.onCreateOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: onBackPressed(); return true; + case R.id.action_save_image: + if (SDK_INT >= 19) startSaveImage(); + return true; default: return super.onOptionsItemSelected(item); } } + @Override + protected void onActivityResult(int request, int result, Intent data) { + super.onActivityResult(request, result, data); + if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK) { + saveImage(data.getData()); + } + } + @Override public void onPullStart() { appBarLayout.animate() @@ -230,4 +290,66 @@ public class ImageActivity extends BriarActivity drawableTop != appBarLayout.getTop(); } + @RequiresApi(api = 19) + private void startSaveImage() { + OnClickListener okListener = (dialog, which) -> { + Intent intent = getCreationIntent(); + startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT); + }; + Builder builder = new Builder(this, R.style.BriarDialogTheme); + builder.setTitle(getString(R.string.dialog_title_save_image)); + builder.setMessage(getString(R.string.dialog_message_save_image)); + builder.setIcon(R.drawable.emoji_google_1f6af); + builder.setPositiveButton(R.string.save_image, okListener); + builder.setNegativeButton(R.string.cancel, null); + builder.show(); + } + + @RequiresApi(api = 19) + private Intent getCreationIntent() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", + Locale.getDefault()); + String fileName = sdf.format(new Date()); + Intent intent = new Intent(ACTION_CREATE_DOCUMENT); + intent.addCategory(CATEGORY_OPENABLE); + intent.setType(attachment.getMimeType()); + intent.putExtra(EXTRA_TITLE, fileName); + return intent; + } + + private void saveImage(@Nullable Uri uri) { + if (uri == null) return; + MessageId messageId = attachment.getMessageId(); + runOnDbThread(() -> { + try { + Attachment a = messagingManager.getAttachment(messageId); + copyImageFromDb(a, uri); + } catch (DbException e) { + logException(LOG, WARNING, e); + onImageSaveError(); + } + }); + } + + private void copyImageFromDb(Attachment a, Uri uri) { + ioExecutor.execute(() -> { + try { + InputStream is = a.getStream(); + OutputStream os = getContentResolver().openOutputStream(uri); + if (os == null) throw new IOException(); + copyAndClose(is, os); + } catch (IOException e) { + logException(LOG, WARNING, e); + onImageSaveError(); + } + }); + } + + private void onImageSaveError() { + Snackbar s = + Snackbar.make(layout, R.string.save_image_error, LENGTH_LONG); + s.getView().setBackgroundResource(R.color.briar_red); + s.show(); + } + } diff --git a/briar-android/src/main/res/menu/image_actions.xml b/briar-android/src/main/res/menu/image_actions.xml new file mode 100644 index 000000000..86831dbe3 --- /dev/null +++ b/briar-android/src/main/res/menu/image_actions.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index f1e45e2b0..733cb0648 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -139,6 +139,10 @@ Contact deleted You + Save image + Save Image? + Saving this image will allow other apps to access it.\n\nAre you sure you want to save? + Could not save image Add a Contact From a6b1ad48c39b0c5d9007399047a9b1664bcd1fb4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 27 Nov 2018 16:25:33 -0200 Subject: [PATCH 2/4] [android] Add support for saving image attachments on API < 19 This is done by using the WRITE_EXTERNAL_STORAGE permission to write the file directly without using the system activity. --- briar-android/src/main/AndroidManifest.xml | 1 + .../android/conversation/ImageActivity.java | 97 ++++------ .../android/conversation/ImageViewModel.java | 168 ++++++++++++++++++ .../android/viewmodel/ViewModelModule.java | 7 + .../src/main/res/menu/image_actions.xml | 1 - briar-android/src/main/res/values/strings.xml | 1 + 6 files changed, 207 insertions(+), 68 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index b8c4a22fb..7e3f9f712 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 6b18a113d..796a1b893 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -1,10 +1,11 @@ package org.briarproject.briar.android.conversation; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -26,25 +27,15 @@ import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.github.chrisbanes.photoview.PhotoView; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.lifecycle.IoExecutor; -import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.view.PullDownLayout; -import org.briarproject.briar.api.messaging.Attachment; -import org.briarproject.briar.api.messaging.MessagingManager; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -import java.util.concurrent.Executor; -import java.util.logging.Logger; import javax.inject.Inject; @@ -63,28 +54,20 @@ import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; import static android.widget.ImageView.ScaleType.FIT_START; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static java.util.Objects.requireNonNull; -import static java.util.logging.Level.WARNING; -import static java.util.logging.Logger.getLogger; -import static org.briarproject.bramble.util.IoUtils.copyAndClose; -import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT; import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute; public class ImageActivity extends BriarActivity implements PullDownLayout.Callback { - private final static Logger LOG = getLogger(ImageActivity.class.getName()); - final static String ATTACHMENT = "attachment"; final static String NAME = "name"; final static String DATE = "date"; @Inject - MessagingManager messagingManager; - @Inject - @IoExecutor - Executor ioExecutor; + ViewModelProvider.Factory viewModelFactory; + private ImageViewModel viewModel; private PullDownLayout layout; private AppBarLayout appBarLayout; private PhotoView photoView; @@ -107,6 +90,11 @@ public class ImageActivity extends BriarActivity setSceneTransitionAnimation(transition, null, transition); } + // get View Model + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(ImageViewModel.class); + viewModel.getSaveState().observe(this, this::onImageSaveStateChanged); + // inflate layout setContentView(R.layout.activity_image); layout = findViewById(R.id.layout); @@ -186,9 +174,6 @@ public class ImageActivity extends BriarActivity @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.image_actions, menu); - if (SDK_INT >= 19) { - menu.findItem(R.id.action_save_image).setVisible(true); - } return super.onCreateOptionsMenu(menu); } @@ -199,7 +184,7 @@ public class ImageActivity extends BriarActivity onBackPressed(); return true; case R.id.action_save_image: - if (SDK_INT >= 19) startSaveImage(); + showSaveImageDialog(); return true; default: return super.onOptionsItemSelected(item); @@ -210,7 +195,7 @@ public class ImageActivity extends BriarActivity protected void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK) { - saveImage(data.getData()); + viewModel.saveImage(attachment, data.getData()); } } @@ -250,9 +235,8 @@ public class ImageActivity extends BriarActivity @RequiresApi(api = 16) private void hideSystemUi(View decorView) { - decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE - | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | SYSTEM_UI_FLAG_FULLSCREEN + decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN | + SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN ); appBarLayout.animate() .translationYBy(-1 * appBarLayout.getHeight()) @@ -264,8 +248,7 @@ public class ImageActivity extends BriarActivity @RequiresApi(api = 16) private void showSystemUi(View decorView) { decorView.setSystemUiVisibility( - SYSTEM_UI_FLAG_LAYOUT_STABLE - | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN ); appBarLayout.animate() .translationYBy(appBarLayout.getHeight()) @@ -290,11 +273,14 @@ public class ImageActivity extends BriarActivity drawableTop != appBarLayout.getTop(); } - @RequiresApi(api = 19) - private void startSaveImage() { + private void showSaveImageDialog() { OnClickListener okListener = (dialog, which) -> { - Intent intent = getCreationIntent(); - startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT); + if (SDK_INT >= 19) { + Intent intent = getCreationIntent(); + startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT); + } else { + viewModel.saveImage(attachment); + } }; Builder builder = new Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.dialog_title_save_image)); @@ -317,39 +303,16 @@ public class ImageActivity extends BriarActivity return intent; } - private void saveImage(@Nullable Uri uri) { - if (uri == null) return; - MessageId messageId = attachment.getMessageId(); - runOnDbThread(() -> { - try { - Attachment a = messagingManager.getAttachment(messageId); - copyImageFromDb(a, uri); - } catch (DbException e) { - logException(LOG, WARNING, e); - onImageSaveError(); - } - }); - } - - private void copyImageFromDb(Attachment a, Uri uri) { - ioExecutor.execute(() -> { - try { - InputStream is = a.getStream(); - OutputStream os = getContentResolver().openOutputStream(uri); - if (os == null) throw new IOException(); - copyAndClose(is, os); - } catch (IOException e) { - logException(LOG, WARNING, e); - onImageSaveError(); - } - }); - } - - private void onImageSaveError() { - Snackbar s = - Snackbar.make(layout, R.string.save_image_error, LENGTH_LONG); - s.getView().setBackgroundResource(R.color.briar_red); + private void onImageSaveStateChanged(@Nullable Boolean error) { + if (error == null) return; + int stringRes = error ? + R.string.save_image_error : R.string.save_image_success; + int colorRes = error ? + R.color.briar_red : R.color.briar_primary; + Snackbar s = Snackbar.make(layout, stringRes, LENGTH_LONG); + s.getView().setBackgroundResource(colorRes); s.show(); + viewModel.onSaveStateSeen(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java new file mode 100644 index 000000000..da1735857 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -0,0 +1,168 @@ +package org.briarproject.briar.android.conversation; + +import android.app.Application; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.messaging.Attachment; +import org.briarproject.briar.api.messaging.MessagingManager; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.media.MediaScannerConnection.scanFile; +import static android.os.Environment.DIRECTORY_PICTURES; +import static android.os.Environment.getExternalStoragePublicDirectory; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.IoUtils.copyAndClose; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +public class ImageViewModel extends AndroidViewModel { + + private static Logger LOG = getLogger(ImageViewModel.class.getName()); + + private final MessagingManager messagingManager; + @DatabaseExecutor + private final Executor dbExecutor; + @IoExecutor + private final Executor ioExecutor; + + private MutableLiveData saveState = new MutableLiveData<>(); + + @Inject + public ImageViewModel(Application application, + MessagingManager messagingManager, + @DatabaseExecutor Executor dbExecutor, + @IoExecutor Executor ioExecutor) { + super(application); + this.messagingManager = messagingManager; + this.dbExecutor = dbExecutor; + this.ioExecutor = ioExecutor; + } + + /** + * A LiveData that is true if the image was saved, + * false if there was an error and null otherwise. + * + * Call {@link #onSaveStateSeen()} after consuming an update. + */ + LiveData getSaveState() { + return saveState; + } + + @UiThread + void onSaveStateSeen() { + saveState.setValue(null); + } + + /** + * Saves the attachment to a writeable {@link Uri}. + */ + @UiThread + void saveImage(AttachmentItem attachment, @Nullable Uri uri) { + if (uri == null) { + saveState.setValue(true); + } else { + saveImage(attachment, () -> getOutputStream(uri), null); + } + } + + /** + * Saves the attachment on external storage, + * assuming the permission was granted during install time. + */ + void saveImage(AttachmentItem attachment) { + File file = getImageFile(attachment); + saveImage(attachment, () -> getOutputStream(file), () -> { + scanFile(getApplication(), new String[] {file.toString()}, null, + null); + }); + } + + private void saveImage(AttachmentItem attachment, OutputStreamProvider osp, + @Nullable Runnable afterCopy) { + MessageId messageId = attachment.getMessageId(); + dbExecutor.execute(() -> { + try { + Attachment a = messagingManager.getAttachment(messageId); + copyImageFromDb(a, osp, afterCopy); + } catch (DbException e) { + logException(LOG, WARNING, e); + saveState.postValue(true); + } + }); + } + + private void copyImageFromDb(Attachment a, OutputStreamProvider osp, + @Nullable Runnable afterCopy) { + ioExecutor.execute(() -> { + try { + InputStream is = a.getStream(); + OutputStream os = osp.getOutputStream(); + copyAndClose(is, os); + if (afterCopy != null) afterCopy.run(); + saveState.postValue(false); + } catch (IOException e) { + logException(LOG, WARNING, e); + saveState.postValue(true); + } + }); + } + + private String getFileName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", + Locale.getDefault()); + return sdf.format(new Date()); + } + + private File getImageFile(AttachmentItem attachment) { + File path = getExternalStoragePublicDirectory(DIRECTORY_PICTURES); + //noinspection ResultOfMethodCallIgnored + path.mkdirs(); + String fileName = getFileName(); + String ext = attachment.getMimeType().replaceFirst("image/", "."); + File file = new File(path, fileName + ext); + int i = 1; + while (file.exists()) { + file = new File(path, fileName + " (" + i + ")" + ext); + } + return file; + } + + private OutputStream getOutputStream(File file) throws IOException { + return new FileOutputStream(file); + } + + private OutputStream getOutputStream(Uri uri) throws IOException { + OutputStream os = + getApplication().getContentResolver().openOutputStream(uri); + if (os == null) throw new IOException(); + return os; + } + + private interface OutputStreamProvider { + OutputStream getOutputStream() throws IOException; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java index c7ded4cab..e525fd059 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/ViewModelModule.java @@ -4,6 +4,7 @@ import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProvider; import org.briarproject.briar.android.conversation.ConversationViewModel; +import org.briarproject.briar.android.conversation.ImageViewModel; import javax.inject.Singleton; @@ -20,6 +21,12 @@ public abstract class ViewModelModule { abstract ViewModel bindConversationViewModel( ConversationViewModel conversationViewModel); + @Binds + @IntoMap + @ViewModelKey(ImageViewModel.class) + abstract ViewModel bindImageViewModel( + ImageViewModel imageViewModel); + @Binds @Singleton abstract ViewModelProvider.Factory bindViewModelFactory( diff --git a/briar-android/src/main/res/menu/image_actions.xml b/briar-android/src/main/res/menu/image_actions.xml index 86831dbe3..1943f6cfd 100644 --- a/briar-android/src/main/res/menu/image_actions.xml +++ b/briar-android/src/main/res/menu/image_actions.xml @@ -6,6 +6,5 @@ diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 733cb0648..dd4b0f96a 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -142,6 +142,7 @@ Save image Save Image? Saving this image will allow other apps to access it.\n\nAre you sure you want to save? + Image was saved Could not save image From 768488eb04c2b82a43f107ae55a913dff581af21 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 7 Dec 2018 13:54:13 -0200 Subject: [PATCH 3/4] [android] Show (tinted) security icon when warning about saving attachments --- .../briar/android/conversation/ImageActivity.java | 7 ++++++- .../briar/android/conversation/ImageViewModel.java | 2 +- briar-android/src/main/res/drawable/ic_security.xml | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 briar-android/src/main/res/drawable/ic_security.xml diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 796a1b893..f36cbab0f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -11,6 +11,8 @@ import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.AppBarLayout; import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.AlertDialog.Builder; import android.support.v7.widget.Toolbar; import android.transition.Fade; @@ -285,7 +287,10 @@ public class ImageActivity extends BriarActivity Builder builder = new Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.dialog_title_save_image)); builder.setMessage(getString(R.string.dialog_message_save_image)); - builder.setIcon(R.drawable.emoji_google_1f6af); + Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_security); + DrawableCompat.setTint(requireNonNull(icon), + ContextCompat.getColor(this, R.color.color_primary)); + builder.setIcon(icon); builder.setPositiveButton(R.string.save_image, okListener); builder.setNegativeButton(R.string.cancel, null); builder.show(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index da1735857..059f657e3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -82,7 +82,7 @@ public class ImageViewModel extends AndroidViewModel { @UiThread void saveImage(AttachmentItem attachment, @Nullable Uri uri) { if (uri == null) { - saveState.setValue(true); + saveState.setValue(false); } else { saveImage(attachment, () -> getOutputStream(uri), null); } diff --git a/briar-android/src/main/res/drawable/ic_security.xml b/briar-android/src/main/res/drawable/ic_security.xml new file mode 100644 index 000000000..918642399 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_security.xml @@ -0,0 +1,9 @@ + + + From 59cd98db818435274cf6c9c09f9523cc0406e5dc Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 7 Dec 2018 15:10:29 -0200 Subject: [PATCH 4/4] [android] Get image extension from MimeTypeMap and store it in AttachmentItem --- .../android/conversation/AttachmentController.java | 9 ++++++++- .../briar/android/conversation/AttachmentItem.java | 12 ++++++++++-- .../briar/android/conversation/ImageViewModel.java | 8 +++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java index b8754e755..2d85e2668 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java @@ -4,6 +4,7 @@ import android.content.res.Resources; import android.graphics.BitmapFactory; import android.support.annotation.Nullable; import android.support.media.ExifInterface; +import android.webkit.MimeTypeMap; import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.db.DatabaseExecutor; @@ -132,8 +133,14 @@ class AttachmentController { thumbnailSize = getThumbnailSize(size.width, size.height, size.mimeType); } + // get file extension + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + String extension = mimeTypeMap.getExtensionFromMimeType(size.mimeType); + if (extension == null) { + return new AttachmentItem(messageId, 0, 0, "", "", 0, 0, true); + } return new AttachmentItem(messageId, size.width, size.height, - size.mimeType, thumbnailSize.width, thumbnailSize.height, + size.mimeType, extension, thumbnailSize.width, thumbnailSize.height, size.error); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java index 9e36e848a..bbc7c1f6b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java @@ -14,7 +14,7 @@ public class AttachmentItem implements Parcelable { private final MessageId messageId; private final int width, height; - private final String mimeType; + private final String mimeType, extension; private final int thumbnailWidth, thumbnailHeight; private final boolean hasError; @@ -32,11 +32,13 @@ public class AttachmentItem implements Parcelable { }; AttachmentItem(MessageId messageId, int width, int height, String mimeType, - int thumbnailWidth, int thumbnailHeight, boolean hasError) { + String extension, int thumbnailWidth, int thumbnailHeight, + boolean hasError) { this.messageId = messageId; this.width = width; this.height = height; this.mimeType = mimeType; + this.extension = extension; this.thumbnailWidth = thumbnailWidth; this.thumbnailHeight = thumbnailHeight; this.hasError = hasError; @@ -49,6 +51,7 @@ public class AttachmentItem implements Parcelable { width = in.readInt(); height = in.readInt(); mimeType = in.readString(); + extension = in.readString(); thumbnailWidth = in.readInt(); thumbnailHeight = in.readInt(); hasError = in.readByte() != 0; @@ -70,6 +73,10 @@ public class AttachmentItem implements Parcelable { return mimeType; } + String getExtension() { + return extension; + } + int getThumbnailWidth() { return thumbnailWidth; } @@ -98,6 +105,7 @@ public class AttachmentItem implements Parcelable { dest.writeInt(width); dest.writeInt(height); dest.writeString(mimeType); + dest.writeString(extension); dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailHeight); dest.writeByte((byte) (hasError ? 1 : 0)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index 059f657e3..694887c91 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -94,10 +94,8 @@ public class ImageViewModel extends AndroidViewModel { */ void saveImage(AttachmentItem attachment) { File file = getImageFile(attachment); - saveImage(attachment, () -> getOutputStream(file), () -> { - scanFile(getApplication(), new String[] {file.toString()}, null, - null); - }); + saveImage(attachment, () -> getOutputStream(file), () -> scanFile( + getApplication(), new String[] {file.toString()}, null, null)); } private void saveImage(AttachmentItem attachment, OutputStreamProvider osp, @@ -141,7 +139,7 @@ public class ImageViewModel extends AndroidViewModel { //noinspection ResultOfMethodCallIgnored path.mkdirs(); String fileName = getFileName(); - String ext = attachment.getMimeType().replaceFirst("image/", "."); + String ext = "." + attachment.getExtension(); File file = new File(path, fileName + ext); int i = 1; while (file.exists()) {