diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 18527125a2aba451ad3af3213a1202bab9ba03fb..8a5c6a69558acbd966b46a5122a73b0efc6e20a3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -20,6 +20,7 @@ import org.briarproject.briar.android.contact.ContactModule; import org.briarproject.briar.android.conversation.AliasDialogFragment; import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ImageActivity; +import org.briarproject.briar.android.conversation.ImageFragment; import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumListFragment; @@ -218,4 +219,6 @@ public interface ActivityComponent { void inject(AliasDialogFragment aliasDialogFragment); + void inject(ImageFragment imageFragment); + } 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 bbc7c1f6baa23648de50da83e4fe33a56cf42541..43ab2efd5f95d39302bdc872f79de1e47bd4f1e8 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 @@ -2,6 +2,7 @@ package org.briarproject.briar.android.conversation; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; @@ -111,4 +112,10 @@ public class AttachmentItem implements Parcelable { dest.writeByte((byte) (hasError ? 1 : 0)); } + @Override + public boolean equals(@Nullable Object o) { + return o instanceof AttachmentItem && + messageId.equals(((AttachmentItem) o).messageId); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index f5f6ff37aeac3afabc3bd543407b7107107e80b3..5e4ef63a5e8c3db60598e08f4c949dea451ed867 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -121,8 +121,9 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; -import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT; +import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; import static org.briarproject.briar.android.conversation.ImageActivity.DATE; +import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENTS; import static org.briarproject.briar.android.conversation.ImageActivity.NAME; import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; @@ -812,8 +813,11 @@ public class ConversationActivity extends BriarActivity } else { name = getString(R.string.you); } + ArrayList<AttachmentItem> attachments = + new ArrayList<>(messageItem.getAttachments()); Intent i = new Intent(this, ImageActivity.class); - i.putExtra(ATTACHMENT, item); + i.putParcelableArrayListExtra(ATTACHMENTS, attachments); + i.putExtra(ATTACHMENT_POSITION, attachments.indexOf(item)); i.putExtra(NAME, name); i.putExtra(DATE, messageItem.getTime()); if (SDK_INT >= 23) { 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 f36cbab0fbd4344f3f86019749185ae101803f57..811bf11e3811b544945c9d31d3747ed7c07fccd4 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 @@ -4,15 +4,18 @@ 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.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.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.view.ViewPager; import android.support.v7.app.AlertDialog.Builder; import android.support.v7.widget.Toolbar; import android.transition.Fade; @@ -20,23 +23,18 @@ import android.transition.Transition; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.Window; import android.widget.TextView; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.Target; -import com.github.chrisbanes.photoview.PhotoView; - 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 java.text.SimpleDateFormat; import java.util.Date; +import java.util.List; import java.util.Locale; import javax.inject.Inject; @@ -53,16 +51,15 @@ import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE; import static android.view.View.VISIBLE; 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 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 { + implements PullDownLayout.Callback, OnGlobalLayoutListener { - final static String ATTACHMENT = "attachment"; + final static String ATTACHMENTS = "attachments"; + final static String ATTACHMENT_POSITION = "position"; final static String NAME = "name"; final static String DATE = "date"; @@ -72,8 +69,8 @@ public class ImageActivity extends BriarActivity private ImageViewModel viewModel; private PullDownLayout layout; private AppBarLayout appBarLayout; - private PhotoView photoView; - private AttachmentItem attachment; + private ViewPager viewPager; + private List<AttachmentItem> attachments; @Override public void injectActivity(ActivityComponent component) { @@ -102,6 +99,7 @@ public class ImageActivity extends BriarActivity layout = findViewById(R.id.layout); layout.getBackground().setAlpha(255); layout.setCallback(this); + layout.getViewTreeObserver().addOnGlobalLayoutListener(this); // Status Bar if (SDK_INT >= 21) { @@ -118,59 +116,29 @@ public class ImageActivity extends BriarActivity TextView dateView = toolbar.findViewById(R.id.dateView); // Intent Extras - attachment = getIntent().getParcelableExtra(ATTACHMENT); - String name = getIntent().getStringExtra(NAME); - long time = getIntent().getLongExtra(DATE, 0); + Intent i = getIntent(); + attachments = i.getParcelableArrayListExtra(ATTACHMENTS); + int position = i.getIntExtra(ATTACHMENT_POSITION, -1); + if (position == -1) throw new IllegalStateException(); + String name = i.getStringExtra(NAME); + long time = i.getLongExtra(DATE, 0); String date = formatDateAbsolute(this, time); contactName.setText(name); dateView.setText(date); - // Image View - photoView = findViewById(R.id.photoView); + // Set up image ViewPager + viewPager = findViewById(R.id.viewPager); + ImagePagerAdapter pagerAdapter = + new ImagePagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(pagerAdapter); + viewPager.setCurrentItem(position); + if (SDK_INT >= 16) { - photoView.setOnClickListener(view -> toggleSystemUi()); + viewModel.getOnImageClicked().observe(this, this::onImageClicked); window.getDecorView().setSystemUiVisibility( SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } - - // Request Listener - RequestListener<Drawable> listener = new RequestListener<Drawable>() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, - Object model, Target<Drawable> target, - boolean isFirstResource) { - supportStartPostponedEnterTransition(); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target<Drawable> target, DataSource dataSource, - boolean isFirstResource) { - if (SDK_INT >= 21 && !(resource instanceof Animatable)) { - // set transition name only when not animatable, - // because the animation won't start otherwise - photoView.setTransitionName( - attachment.getTransitionName()); - } - // Move image to the top if overlapping toolbar - if (isOverlappingToolbar(resource)) { - photoView.setScaleType(FIT_START); - } - supportStartPostponedEnterTransition(); - return false; - } - }; - - // Load Image - GlideApp.with(this) - .load(attachment) - .diskCacheStrategy(NONE) - .error(R.drawable.ic_image_broken) - .dontTransform() - .addListener(listener) - .into(photoView); } @Override @@ -193,11 +161,23 @@ public class ImageActivity extends BriarActivity } } + @Override + public void onGlobalLayout() { + viewModel.setToolbarPosition( + appBarLayout.getTop(), appBarLayout.getBottom() + ); + if (SDK_INT >= 16) { + layout.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + layout.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + } + @Override protected void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_SAVE_ATTACHMENT && result == RESULT_OK) { - viewModel.saveImage(attachment, data.getData()); + viewModel.saveImage(getVisibleAttachment(), data.getData()); } } @@ -225,6 +205,14 @@ public class ImageActivity extends BriarActivity supportFinishAfterTransition(); } + @RequiresApi(api = 16) + private void onImageClicked(@Nullable Boolean clicked) { + if (clicked != null && clicked) { + toggleSystemUi(); + viewModel.onOnImageClickSeen(); + } + } + @RequiresApi(api = 16) private void toggleSystemUi() { View decorView = getWindow().getDecorView(); @@ -259,29 +247,13 @@ public class ImageActivity extends BriarActivity .start(); } - private boolean isOverlappingToolbar(Drawable drawable) { - int width = drawable.getIntrinsicWidth(); - int height = drawable.getIntrinsicHeight(); - float widthPercentage = photoView.getWidth() / (float) width; - float heightPercentage = photoView.getHeight() / (float) height; - float scaleFactor = Math.min(widthPercentage, heightPercentage); - int realWidth = (int) (width * scaleFactor); - int realHeight = (int) (height * scaleFactor); - // return if photo doesn't use the full width, - // because it will be moved to the right otherwise - if (realWidth < photoView.getWidth()) return false; - int drawableTop = (photoView.getHeight() - realHeight) / 2; - return drawableTop < appBarLayout.getBottom() && - drawableTop != appBarLayout.getTop(); - } - private void showSaveImageDialog() { OnClickListener okListener = (dialog, which) -> { if (SDK_INT >= 19) { Intent intent = getCreationIntent(); startActivityForResult(intent, REQUEST_SAVE_ATTACHMENT); } else { - viewModel.saveImage(attachment); + viewModel.saveImage(getVisibleAttachment()); } }; Builder builder = new Builder(this, R.style.BriarDialogTheme); @@ -303,7 +275,7 @@ public class ImageActivity extends BriarActivity String fileName = sdf.format(new Date()); Intent intent = new Intent(ACTION_CREATE_DOCUMENT); intent.addCategory(CATEGORY_OPENABLE); - intent.setType(attachment.getMimeType()); + intent.setType(getVisibleAttachment().getMimeType()); intent.putExtra(EXTRA_TITLE, fileName); return intent; } @@ -320,4 +292,26 @@ public class ImageActivity extends BriarActivity viewModel.onSaveStateSeen(); } + AttachmentItem getVisibleAttachment() { + return attachments.get(viewPager.getCurrentItem()); + } + + private class ImagePagerAdapter extends FragmentStatePagerAdapter { + + private ImagePagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + return ImageFragment.newInstance(attachments.get(position)); + } + + @Override + public int getCount() { + return attachments.size(); + } + + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..ba4bfec9c4a2afa6424f2903b0bf60165ca2e79c --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java @@ -0,0 +1,125 @@ +package org.briarproject.briar.android.conversation; + +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.github.chrisbanes.photoview.PhotoView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.conversation.glide.GlideApp; + +import javax.annotation.ParametersAreNonnullByDefault; +import javax.inject.Inject; + +import static android.os.Build.VERSION.SDK_INT; +import static android.widget.ImageView.ScaleType.FIT_START; +import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT_POSITION; + +@MethodsNotNullByDefault +@ParametersAreNonnullByDefault +public class ImageFragment extends Fragment { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private AttachmentItem attachment; + private ImageViewModel viewModel; + private PhotoView photoView; + + static ImageFragment newInstance(AttachmentItem a) { + ImageFragment f = new ImageFragment(); + Bundle args = new Bundle(); + args.putParcelable(ATTACHMENT_POSITION, a); + f.setArguments(args); + return f; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + BaseActivity a = (BaseActivity) requireNonNull(getActivity()); + a.getActivityComponent().inject(this); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = requireNonNull(getArguments()); + attachment = requireNonNull(args.getParcelable(ATTACHMENT_POSITION)); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_image, container, + false); + + viewModel = ViewModelProviders.of(requireNonNull(getActivity()), + viewModelFactory).get(ImageViewModel.class); + + photoView = v.findViewById(R.id.photoView); + photoView.setOnClickListener(view -> viewModel.clickImage()); + + // Request Listener + RequestListener<Drawable> listener = new RequestListener<Drawable>() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, Target<Drawable> target, + boolean isFirstResource) { + if (getActivity() != null) + getActivity().supportStartPostponedEnterTransition(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target<Drawable> target, DataSource dataSource, + boolean isFirstResource) { + if (SDK_INT >= 21 && !(resource instanceof Animatable)) { + // set transition name only when not animatable, + // because the animation won't start otherwise + photoView.setTransitionName( + attachment.getTransitionName()); + } + // Move image to the top if overlapping toolbar + if (viewModel.isOverlappingToolbar(photoView, resource)) { + photoView.setScaleType(FIT_START); + } + if (getActivity() != null) + getActivity().supportStartPostponedEnterTransition(); + return false; + } + }; + + // Load Image + GlideApp.with(this) + .load(attachment) + .diskCacheStrategy(NONE) + .error(R.drawable.ic_image_broken) + .dontTransform() + .addListener(listener) + .into(photoView); + + return v; + } + +} 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 d3cc620196f439edd1dfb3e576902440b8d0f5c5..fc5b6cdff0b68fce022962a0c05ad2d33263dcd4 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 @@ -4,9 +4,11 @@ import android.app.Application; import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.MutableLiveData; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.view.View; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; @@ -48,7 +50,10 @@ public class ImageViewModel extends AndroidViewModel { @IoExecutor private final Executor ioExecutor; + private final MutableLiveData<Boolean> imageClicked = + new MutableLiveData<>(); private final MutableLiveData<Boolean> saveState = new MutableLiveData<>(); + private int toolbarTop, toolbarBottom; @Inject ImageViewModel(Application application, @@ -61,6 +66,45 @@ public class ImageViewModel extends AndroidViewModel { this.ioExecutor = ioExecutor; } + void clickImage() { + imageClicked.setValue(true); + } + + /** + * A LiveData that is true if the image was clicked, + * false if it wasn't. + * + * Call {@link #onOnImageClickSeen()} after consuming an update. + */ + LiveData<Boolean> getOnImageClicked() { + return imageClicked; + } + + @UiThread + void onOnImageClickSeen() { + imageClicked.setValue(false); + } + + void setToolbarPosition(int top, int bottom) { + toolbarTop = top; + toolbarBottom = bottom; + } + + boolean isOverlappingToolbar(View screenView, Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + float widthPercentage = screenView.getWidth() / (float) width; + float heightPercentage = screenView.getHeight() / (float) height; + float scaleFactor = Math.min(widthPercentage, heightPercentage); + int realWidth = (int) (width * scaleFactor); + int realHeight = (int) (height * scaleFactor); + // return if image doesn't use the full width, + // because it will be moved to the right otherwise + if (realWidth < screenView.getWidth()) return false; + int drawableTop = (screenView.getHeight() - realHeight) / 2; + return drawableTop < toolbarBottom && drawableTop != toolbarTop; + } + /** * A LiveData that is true if the image was saved, * false if there was an error and null otherwise. diff --git a/briar-android/src/main/res/layout/activity_image.xml b/briar-android/src/main/res/layout/activity_image.xml index df81ce2e27d0552024c786a7674dcf0edd568871..84f786f493e89279326262f30b3f3fd9486c4da0 100644 --- a/briar-android/src/main/res/layout/activity_image.xml +++ b/briar-android/src/main/res/layout/activity_image.xml @@ -8,12 +8,11 @@ android:background="@color/briar_black" tools:context=".android.conversation.ImageActivity"> - <com.github.chrisbanes.photoview.PhotoView - android:id="@+id/photoView" + <android.support.v4.view.ViewPager + android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" - tools:ignore="ContentDescription" - tools:srcCompat="@tools:sample/backgrounds/scenic"/> + tools:background="@color/briar_green_light"/> <android.support.design.widget.AppBarLayout android:id="@+id/appBarLayout" diff --git a/briar-android/src/main/res/layout/fragment_image.xml b/briar-android/src/main/res/layout/fragment_image.xml new file mode 100644 index 0000000000000000000000000000000000000000..2da926ea9750e39cf5d8945893aa0e63b129feb7 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_image.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.github.chrisbanes.photoview.PhotoView + android:id="@+id/photoView" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" + tools:srcCompat="@tools:sample/backgrounds/scenic"/>