diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 87748420d..c52aced57 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -123,6 +123,7 @@ dependencies { exclude group: 'com.android.support' exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it } + implementation 'com.github.chrisbanes:PhotoView:2.1.4' // later versions already use androidx annotationProcessor 'com.google.dagger:dagger-compiler:2.19' annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index e9300a7fd..b8c4a22fb 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -113,6 +113,15 @@ /> + + + + new activity. + * @param returnTransition used when window is closing, because the activity is finishing. + */ + @RequiresApi(api = 21) + public void setSceneTransitionAnimation( + @Nullable Transition enterTransition, + @Nullable Transition exitTransition, + @Nullable Transition returnTransition) { // workaround for #1007 if (isSamsung7()) { return; } - Transition slide = new Slide(Gravity.RIGHT); - slide.excludeTarget(android.R.id.statusBarBackground, true); - slide.excludeTarget(android.R.id.navigationBarBackground, true); + if (enterTransition != null) excludeSystemUi(enterTransition); + if (exitTransition != null) excludeSystemUi(exitTransition); + if (returnTransition != null) excludeSystemUi(returnTransition); Window window = getWindow(); - window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS); - window.setExitTransition(slide); - window.setEnterTransition(slide); - window.setTransitionBackgroundFadeDuration(getResources() - .getInteger(android.R.integer.config_longAnimTime)); + window.setEnterTransition(enterTransition); + window.setExitTransition(exitTransition); + window.setReturnTransition(returnTransition); } /** diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java index 7877cbc3b..d45b10789 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogActivity.java @@ -18,7 +18,6 @@ public class ReblogActivity extends BriarActivity implements @Override public void onCreate(Bundle savedInstanceState) { - setSceneTransitionAnimation(); super.onCreate(savedInstanceState); Intent intent = getIntent(); 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 5a5efb657..7f39bfefc 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 @@ -1,5 +1,8 @@ package org.briarproject.briar.android.conversation; +import android.os.Parcel; +import android.os.Parcelable; + import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; @@ -7,13 +10,26 @@ import javax.annotation.concurrent.Immutable; @Immutable @NotNullByDefault -public class AttachmentItem { +public class AttachmentItem implements Parcelable { private final MessageId messageId; private final int width, height; private final int thumbnailWidth, thumbnailHeight; private final boolean hasError; + public static final Creator CREATOR = + new Creator() { + @Override + public AttachmentItem createFromParcel(Parcel in) { + return new AttachmentItem(in); + } + + @Override + public AttachmentItem[] newArray(int size) { + return new AttachmentItem[size]; + } + }; + AttachmentItem(MessageId messageId, int width, int height, int thumbnailWidth, int thumbnailHeight, boolean hasError) { this.messageId = messageId; @@ -24,6 +40,17 @@ public class AttachmentItem { this.hasError = hasError; } + protected AttachmentItem(Parcel in) { + byte[] messageIdByte = new byte[MessageId.LENGTH]; + in.readByteArray(messageIdByte); + messageId = new MessageId(messageIdByte); + width = in.readInt(); + height = in.readInt(); + thumbnailWidth = in.readInt(); + thumbnailHeight = in.readInt(); + hasError = in.readByte() != 0; + } + public MessageId getMessageId() { return messageId; } @@ -48,4 +75,24 @@ public class AttachmentItem { return hasError; } + // TODO use counter instead, because in theory one attachment can appear in more than one messages + String getTransitionName() { + return String.valueOf(messageId.hashCode()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByteArray(messageId.getBytes()); + dest.writeInt(width); + dest.writeInt(height); + 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/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 754e8f06a..e17ad5451 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 @@ -9,11 +9,15 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.Toolbar; +import android.transition.Slide; +import android.transition.Transition; import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; @@ -96,8 +100,11 @@ import im.delight.android.identicons.IdenticonDrawable; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener; +import static android.os.Build.VERSION.SDK_INT; +import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static android.support.v4.view.ViewCompat.setTransitionName; import static android.support.v7.util.SortedList.INVALID_POSITION; +import static android.view.Gravity.END; import static android.widget.Toast.LENGTH_SHORT; import static java.util.Collections.emptyList; import static java.util.Collections.sort; @@ -108,6 +115,9 @@ import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; 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.DATE; +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; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; @@ -186,7 +196,10 @@ public class ConversationActivity extends BriarActivity @Override public void onCreate(@Nullable Bundle state) { - setSceneTransitionAnimation(); + if (SDK_INT >= 21) { + Transition slide = new Slide(END); + setSceneTransitionAnimation(slide, null, slide); + } super.onCreate(state); Intent i = getIntent(); @@ -802,6 +815,31 @@ public class ConversationActivity extends BriarActivity startActivity(i); } + @Override + public void onAttachmentClicked(View view, + ConversationMessageItem messageItem, AttachmentItem item) { + String name; + if (messageItem.isIncoming()) { + // must be available when items are being displayed + name = viewModel.getContactDisplayName().getValue(); + } else { + name = getString(R.string.you); + } + Intent i = new Intent(this, ImageActivity.class); + i.putExtra(ATTACHMENT, item); + i.putExtra(NAME, name); + i.putExtra(DATE, messageItem.getTime()); + if (SDK_INT >= 23) { + String transitionName = item.getTransitionName(); + ActivityOptionsCompat options = + makeSceneTransitionAnimation(this, view, transitionName); + ActivityCompat.startActivity(this, i, options.toBundle()); + } else { + // work-around for android bug #224270 + startActivity(i); + } + } + @DatabaseExecutor private void respondToIntroductionRequest(SessionId sessionId, boolean accept, long time) throws DbException { @@ -845,4 +883,5 @@ public class ConversationActivity extends BriarActivity } return attachments; } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java index 0b56878dc..9c8db9538 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.UiThread; +import android.view.View; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @@ -14,4 +15,7 @@ interface ConversationListener { void openRequestedShareable(ConversationRequestItem item); + void onAttachmentClicked(View view, ConversationMessageItem messageItem, + AttachmentItem attachmentItem); + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 91977da2c..abef0bffd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -85,7 +85,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { if (item.getAttachments().isEmpty()) { bindTextItem(); } else { - bindImageItem(item); + bindImageItem(item, listener); } } @@ -98,7 +98,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { textConstraints.applyTo(layout); } - private void bindImageItem(ConversationMessageItem item) { + private void bindImageItem(ConversationMessageItem item, + ConversationListener listener) { // TODO show more than just the first image AttachmentItem attachment = item.getAttachments().get(0); @@ -127,17 +128,18 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { clearImage(); imageView.setImageResource(ERROR_RES); } else { - loadImage(item, attachment); + loadImage(item, attachment, listener); } } private void clearImage() { GlideApp.with(imageView) .clear(imageView); + imageView.setOnClickListener(null); } private void loadImage(ConversationMessageItem item, - AttachmentItem attachment) { + AttachmentItem attachment, ConversationListener listener) { boolean leftCornerSmall = (isIncoming() && !isRtl) || (!isIncoming() && isRtl); boolean bottomRound = item.getText() == null; @@ -152,6 +154,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { .transition(withCrossFade()) .into(imageView) .waitForLayout(); + imageView.setOnClickListener( + view -> listener.onAttachmentClicked(view, item, attachment)); } } 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 new file mode 100644 index 000000000..fb8e8ad4b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -0,0 +1,233 @@ +package org.briarproject.briar.android.conversation; + +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.v7.widget.Toolbar; +import android.transition.Fade; +import android.transition.Transition; +import android.view.MenuItem; +import android.view.View; +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 static android.graphics.Color.TRANSPARENT; +import static android.os.Build.VERSION.SDK_INT; +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; +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.util.UiUtils.formatDateAbsolute; + +public class ImageActivity extends BriarActivity + implements PullDownLayout.Callback { + + final static String ATTACHMENT = "attachment"; + final static String NAME = "name"; + final static String DATE = "date"; + + private PullDownLayout layout; + private AppBarLayout appBarLayout; + private PhotoView photoView; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + // Transitions + supportPostponeEnterTransition(); + Window window = getWindow(); + if (SDK_INT >= 21) { + Transition transition = new Fade(); + setSceneTransitionAnimation(transition, null, transition); + } + + // inflate layout + setContentView(R.layout.activity_image); + layout = findViewById(R.id.layout); + layout.getBackground().setAlpha(255); + layout.setCallback(this); + + // Status Bar + if (SDK_INT >= 21) { + window.setStatusBarColor(TRANSPARENT); + } else if (SDK_INT >= 19) { + // we can't make the status bar transparent, but translucent + window.addFlags(FLAG_TRANSLUCENT_STATUS); + } + + // Toolbar + appBarLayout = findViewById(R.id.appBarLayout); + Toolbar toolbar = requireNonNull(setUpCustomToolbar(true)); + TextView contactName = toolbar.findViewById(R.id.contactName); + TextView dateView = toolbar.findViewById(R.id.dateView); + + // Intent Extras + AttachmentItem attachment = getIntent().getParcelableExtra(ATTACHMENT); + String name = getIntent().getStringExtra(NAME); + long time = getIntent().getLongExtra(DATE, 0); + String date = formatDateAbsolute(this, time); + contactName.setText(name); + dateView.setText(date); + + // Image View + photoView = findViewById(R.id.photoView); + if (SDK_INT >= 16) { + photoView.setOnClickListener(view -> toggleSystemUi()); + 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 + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onPullStart() { + appBarLayout.animate() + .alpha(0f) + .start(); + } + + @Override + public void onPull(float progress) { + layout.getBackground().setAlpha(Math.round((1 - progress) * 255)); + } + + @Override + public void onPullCancel() { + appBarLayout.animate() + .alpha(1f) + .start(); + } + + @Override + public void onPullComplete() { + supportFinishAfterTransition(); + } + + @RequiresApi(api = 16) + private void toggleSystemUi() { + View decorView = getWindow().getDecorView(); + if (appBarLayout.getVisibility() == VISIBLE) { + hideSystemUi(decorView); + } else { + showSystemUi(decorView); + } + } + + @RequiresApi(api = 16) + private void hideSystemUi(View decorView) { + decorView.setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE + | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | SYSTEM_UI_FLAG_FULLSCREEN + ); + appBarLayout.animate() + .translationYBy(-1 * appBarLayout.getHeight()) + .alpha(0f) + .withEndAction(() -> appBarLayout.setVisibility(GONE)) + .start(); + } + + @RequiresApi(api = 16) + private void showSystemUi(View decorView) { + decorView.setSystemUiVisibility( + SYSTEM_UI_FLAG_LAYOUT_STABLE + | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + ); + appBarLayout.animate() + .translationYBy(appBarLayout.getHeight()) + .alpha(1f) + .withStartAction(() -> appBarLayout.setVisibility(VISIBLE)) + .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(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index 5f59251f9..c73214fb9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -16,6 +16,7 @@ import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.MainThread; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.design.widget.TextInputLayout; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; @@ -31,6 +32,7 @@ import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.URLSpan; +import android.transition.Transition; import android.util.TypedValue; import android.view.KeyEvent; import android.view.View; @@ -60,12 +62,16 @@ import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_NO; import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_YES; import static android.support.v7.app.AppCompatDelegate.setDefaultNightMode; import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.FORMAT_ABBREV_ALL; import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE; import static android.text.format.DateUtils.FORMAT_ABBREV_TIME; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; +import static android.text.format.DateUtils.FORMAT_SHOW_TIME; +import static android.text.format.DateUtils.FORMAT_SHOW_YEAR; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; +import static android.text.format.DateUtils.YEAR_IN_MILLIS; import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.inputmethod.EditorInfo.IME_NULL; @@ -117,6 +123,13 @@ public class UiUtils { MIN_DATE_RESOLUTION, flags).toString(); } + public static String formatDateAbsolute(Context ctx, long time) { + int flags = FORMAT_SHOW_TIME | FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL; + long diff = System.currentTimeMillis() - time; + if (diff >= YEAR_IN_MILLIS) flags |= FORMAT_SHOW_YEAR; + return DateUtils.formatDateTime(ctx, time, flags); + } + public static int getDaysUntilExpiry() { long now = System.currentTimeMillis(); long daysBeforeExpiry = (EXPIRY_DATE - now) / 1000 / 60 / 60 / 24; @@ -318,6 +331,12 @@ public class UiUtils { keyEvent.getKeyCode() == KEYCODE_ENTER; } + @RequiresApi(api = 21) + public static void excludeSystemUi(Transition transition) { + transition.excludeTarget(android.R.id.statusBarBackground, true); + transition.excludeTarget(android.R.id.navigationBarBackground, true); + } + /** * Observes the given {@link LiveData} until the first change. * If the LiveData's value is available, the {@link Observer} will be diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/PullDownLayout.java b/briar-android/src/main/java/org/briarproject/briar/android/view/PullDownLayout.java new file mode 100644 index 000000000..a32659a07 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/PullDownLayout.java @@ -0,0 +1,161 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 XiNGRZ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.briarproject.briar.android.view; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.FrameLayout; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public class PullDownLayout extends FrameLayout { + + private final ViewDragHelper dragger; + + private final int minimumFlingVelocity; + + @Nullable + private Callback callback; + + public PullDownLayout(Context context) { + this(context, null); + } + + public PullDownLayout(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public PullDownLayout(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + dragger = ViewDragHelper.create(this, 1f / 8f, new ViewDragCallback()); + minimumFlingVelocity = + ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); + } + + public void setCallback(@Nullable Callback callback) { + this.callback = callback; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return dragger.shouldInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + dragger.processTouchEvent(event); + return true; + } + + @Override + public void computeScroll() { + if (dragger.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + public interface Callback { + + void onPullStart(); + + void onPull(float progress); + + void onPullCancel(); + + void onPullComplete(); + + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + return true; + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return Math.max(0, top); + } + + @Override + public int getViewHorizontalDragRange(View child) { + return 0; + } + + @Override + public int getViewVerticalDragRange(View child) { + return getHeight(); + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + if (callback != null) { + callback.onPullStart(); + } + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, + int dx, int dy) { + if (callback != null) { + callback.onPull((float) top / (float) getHeight()); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + int slop = yvel > minimumFlingVelocity ? getHeight() / 6 : + getHeight() / 3; + if (releasedChild.getTop() > slop) { + if (callback != null) { + callback.onPullComplete(); + } + } else { + if (callback != null) { + callback.onPullCancel(); + } + + dragger.settleCapturedViewAt(0, 0); + invalidate(); + } + } + + } + +} diff --git a/briar-android/src/main/res/layout/activity_image.xml b/briar-android/src/main/res/layout/activity_image.xml new file mode 100644 index 000000000..df81ce2e2 --- /dev/null +++ b/briar-android/src/main/res/layout/activity_image.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index a7ee15079..2f87d2a26 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -134,6 +134,8 @@ Confirm Contact Deletion Are you sure that you want to remove this contact and all messages exchanged with this contact? Contact deleted + + You Add a Contact diff --git a/briar-android/src/main/res/values/themes.xml b/briar-android/src/main/res/values/themes.xml index 806783e3a..8f54152ce 100644 --- a/briar-android/src/main/res/values/themes.xml +++ b/briar-android/src/main/res/values/themes.xml @@ -18,6 +18,12 @@ @style/BriarToolbar + +