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 18527125a..8a5c6a695 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 bbc7c1f6b..43ab2efd5 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 f5f6ff37a..5e4ef63a5 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 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 f36cbab0f..7c7a94fc5 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 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::onPhotoClicked); window.getDecorView().setSystemUiVisibility( SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } - - // Request Listener - RequestListener listener = new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, - Object model, Target target, - boolean isFirstResource) { - supportStartPostponedEnterTransition(); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target 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,21 @@ public class ImageActivity extends BriarActivity } } + @Override + public void onGlobalLayout() { + viewModel.setToolbarPosition( + appBarLayout.getTop(), appBarLayout.getBottom() + ); + if (SDK_INT >= 16) { + layout.getViewTreeObserver().removeOnGlobalLayoutListener(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 +203,13 @@ public class ImageActivity extends BriarActivity supportFinishAfterTransition(); } + private void onPhotoClicked(@Nullable Boolean clicked) { + if (clicked != null&& clicked && SDK_INT >= 16) { + toggleSystemUi(); + viewModel.onOnImageClickSeen(); + } + } + @RequiresApi(api = 16) private void toggleSystemUi() { View decorView = getWindow().getDecorView(); @@ -259,29 +244,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 +272,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 +289,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 000000000..7a95d0f55 --- /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.clickPhoto()); + + // Request Listener + RequestListener listener = new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, Target target, + boolean isFirstResource) { + if (getActivity() != null) + getActivity().supportStartPostponedEnterTransition(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target 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 d3cc62019..42e31bf15 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,9 @@ public class ImageViewModel extends AndroidViewModel { @IoExecutor private final Executor ioExecutor; + private final MutableLiveData imageClicked = new MutableLiveData<>(); private final MutableLiveData saveState = new MutableLiveData<>(); + private int toolbarTop, toolbarBottom; @Inject ImageViewModel(Application application, @@ -61,6 +65,45 @@ public class ImageViewModel extends AndroidViewModel { this.ioExecutor = ioExecutor; } + void clickPhoto() { + 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 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 photo 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 df81ce2e2..84f786f49 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"> - + tools:background="@color/briar_green_light"/> +