diff --git a/app/src/main/java/com/zulip/android/activities/ZulipActivity.java b/app/src/main/java/com/zulip/android/activities/ZulipActivity.java index cdef7f8bd..f18e0a09b 100644 --- a/app/src/main/java/com/zulip/android/activities/ZulipActivity.java +++ b/app/src/main/java/com/zulip/android/activities/ZulipActivity.java @@ -1,14 +1,6 @@ package com.zulip.android.activities; import android.Manifest; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.Callable; -import java.util.ArrayList; - import android.animation.Animator; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -34,10 +26,12 @@ import android.os.CountDownTimer; import android.os.Environment; import android.os.Handler; +import android.os.Looper; import android.provider.MediaStore; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; @@ -97,9 +91,9 @@ import com.zulip.android.models.Emoji; import com.zulip.android.models.Message; import com.zulip.android.models.MessageType; +import com.zulip.android.models.PeopleDrawerList; import com.zulip.android.models.Person; import com.zulip.android.models.Presence; -import com.zulip.android.models.PeopleDrawerList; import com.zulip.android.models.Stream; import com.zulip.android.networking.AsyncGetEvents; import com.zulip.android.networking.AsyncSend; @@ -112,6 +106,7 @@ import com.zulip.android.util.Constants; import com.zulip.android.util.FilePathHelper; import com.zulip.android.util.MutedTopics; +import com.zulip.android.util.RemoveFabOnScroll; import com.zulip.android.util.RemoveViewsOnScroll; import com.zulip.android.util.SwipeRemoveLinearLayout; import com.zulip.android.util.UrlHelper; @@ -124,9 +119,16 @@ import java.io.File; import java.io.IOException; +import java.sql.SQLException; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Callable; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -154,6 +156,7 @@ public class ZulipActivity extends BaseActivity implements private static final int HIDE_FAB_AFTER_SEC = 5; public MessageListFragment currentList; public CommonProgressDialog commonProgressDialog; + private Snackbar connectivitySnackbar; FloatingActionButton fab; NarrowFilter narrowFilter; String prevId = null; @@ -162,6 +165,7 @@ public class ZulipActivity extends BaseActivity implements private boolean logged_in = false; private boolean backPressedOnce = false; private boolean inSearch = false; + private String networkStatus = Constants.STATUS_CONNECTING; private ZulipActivity that = this; // self-ref private DrawerLayout drawerLayout; private ActionBarDrawerToggle drawerToggle; @@ -229,8 +233,8 @@ public void removeChatBox(boolean animToRight) { AnimationHelper.hideViewX(chatBox, animToRight); //show fab button CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); - RemoveViewsOnScroll removeViewsOnScroll = (RemoveViewsOnScroll) layoutParams.getBehavior(); - removeViewsOnScroll.showView(fab); + RemoveFabOnScroll removeFabOnScroll = (RemoveFabOnScroll) layoutParams.getBehavior(); + removeFabOnScroll.showView(fab); } public HashMap getGravatars() { @@ -775,6 +779,7 @@ public void afterTextChanged(Editable s) { /** * Filter'keyWords people drawer according to name + * * @param keyWords removes names which don't contain keyWords */ private void filterPeopleDrawer(String keyWords) { @@ -800,6 +805,7 @@ private void filterPeopleDrawer(String keyWords) { /** * Refreshes recyclerView of people drawer + * * @throws SQLException */ public void refreshPeopleDrawer() throws SQLException { @@ -842,6 +848,7 @@ public void refreshPeopleDrawer() throws SQLException { /** * Combine list of recent private messages persons and persons with no recent messages + * * @param drawerLists persons with whom no recent messages */ private void combineList(List drawerLists) { @@ -1751,7 +1758,7 @@ public void setLayoutBehaviour(LinearLayoutManager linearLayoutManager, Recycler appBarLayout.requestLayout(); layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); - layoutParams.setBehavior(new RemoveViewsOnScroll(linearLayoutManager, adapter)); + layoutParams.setBehavior(new RemoveFabOnScroll(linearLayoutManager, adapter)); fab.setLayoutParams(layoutParams); topSnackBar.setMessagesLayoutManager(linearLayoutManager); @@ -2424,4 +2431,51 @@ public MessageListFragment getCurrentMessageList() { public enum Flag { RESET_DATABASE, } + + /** + * This function shows the snackbar stating the connectivity status of the device and also changes the behaviour of the + * fab. + */ + public void showConnectivitySnackBar(final String networkState) { + final CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinatorLayout); + final Handler handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(android.os.Message msg) { + if (networkState.equals(Constants.STATUS_CONNECTING)) { + networkStatus = Constants.STATUS_CONNECTING; + connectivitySnackbar = Snackbar.make(coordinatorLayout, R.string.connecting, Snackbar.LENGTH_INDEFINITE); + connectivitySnackbar.show(); + + } else if (networkState.equals(Constants.STATUS_CONNECTED)) { + if (connectivitySnackbar != null) { + connectivitySnackbar.dismiss(); + } + //Starts a network request only when there is an active network connection + startRequests(); + networkStatus = Constants.STATUS_CONNECTED; + } else { + displayChatBox(false); + displayFAB(true); + //Displays old offline messages + if (!networkStatus.equals(Constants.STATUS_CONNECTED)) + onReadyToDisplay(true); + networkStatus = Constants.STATUS_NOT_CONNECTED; + connectivitySnackbar = Snackbar.make(coordinatorLayout, R.string.no_connection, Snackbar.LENGTH_INDEFINITE); + connectivitySnackbar.setAction("RETRY", new View.OnClickListener() { + @Override + public void onClick(View view) { + showConnectivitySnackBar(Constants.STATUS_CONNECTING); + startRequests(); + } + }); + connectivitySnackbar.setActionTextColor(getResources().getColor(R.color.top_snackbar_show_button_text_color)); + connectivitySnackbar.show(); + } + Log.d("NetworkStatus", networkState); + super.handleMessage(msg); + } + }; + + handler.sendEmptyMessage(0); + } } diff --git a/app/src/main/java/com/zulip/android/networking/AsyncGetEvents.java b/app/src/main/java/com/zulip/android/networking/AsyncGetEvents.java index 7c452522b..0b32f8785 100644 --- a/app/src/main/java/com/zulip/android/networking/AsyncGetEvents.java +++ b/app/src/main/java/com/zulip/android/networking/AsyncGetEvents.java @@ -26,10 +26,11 @@ import com.zulip.android.networking.response.events.GetEventResponse; import com.zulip.android.networking.response.events.MessageWrapper; import com.zulip.android.networking.response.events.MutedTopicsWrapper; -import com.zulip.android.networking.response.events.StreamWrapper; import com.zulip.android.networking.response.events.StarWrapper; +import com.zulip.android.networking.response.events.StreamWrapper; import com.zulip.android.networking.response.events.SubscriptionWrapper; import com.zulip.android.networking.response.events.UpdateMessageWrapper; +import com.zulip.android.util.Constants; import com.zulip.android.util.MutedTopics; import com.zulip.android.util.TypeSwapper; import com.zulip.android.util.ZLog; @@ -107,6 +108,10 @@ private void backoff(Exception e) { long backoff = (long) (Math.exp(failures / 2.0) * 1000); Log.e(ASYNC_GET_EVENTS, "Failure " + failures + ", sleeping for " + backoff); + //MAX_CONNECTION_FAILURE_COUNT failures represent loss in network connectivity + if (failures == Constants.MAX_CONNECTION_FAILURE_COUNT) { + mActivity.showConnectivitySnackBar(Constants.STATUS_NOT_CONNECTED); + } SystemClock.sleep(backoff); } @@ -288,6 +293,8 @@ public void run() { if (body.getEvents().size() > 0) { this.processEvents(body); app.setLastEventId(body.getEvents().get(body.getEvents().size() - 1).getId()); + //Dismiss Snackbar on Connecting + mActivity.showConnectivitySnackBar(Constants.STATUS_CONNECTED); failures = 0; } diff --git a/app/src/main/java/com/zulip/android/util/Constants.java b/app/src/main/java/com/zulip/android/util/Constants.java index 9e98e4d82..2d4b57d20 100644 --- a/app/src/main/java/com/zulip/android/util/Constants.java +++ b/app/src/main/java/com/zulip/android/util/Constants.java @@ -24,4 +24,9 @@ public class Constants { public static final int ALL_PEOPLE_ID = -1; // row number which is used to differentiate the '@-mentions' in people drawer public static int MENTIONS = -2; + //Connection States + public static final String STATUS_CONNECTED = "connected"; + public static final String STATUS_CONNECTING = "connecting"; + public static final String STATUS_NOT_CONNECTED = "no_connection"; + public static final int MAX_CONNECTION_FAILURE_COUNT = 2; } diff --git a/app/src/main/java/com/zulip/android/util/RemoveFabOnScroll.java b/app/src/main/java/com/zulip/android/util/RemoveFabOnScroll.java new file mode 100644 index 000000000..104dfe01e --- /dev/null +++ b/app/src/main/java/com/zulip/android/util/RemoveFabOnScroll.java @@ -0,0 +1,195 @@ +package com.zulip.android.util; + +import android.animation.Animator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v7.widget.LinearLayoutManager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import android.widget.LinearLayout; + +import com.zulip.android.R; +import com.zulip.android.activities.RecyclerMessageAdapter; + +import java.util.List; + +/** + * This hides the {@link android.support.design.widget.FloatingActionButton} when the + * recyclerView is scrolled, used in here {@link com.zulip.android.R.layout#main} as a behaviour. + * This also shrinks the {@link android.support.design.widget.FloatingActionButton} when the snackbar comes + * and goes in and out of view. + */ + + +public class RemoveFabOnScroll extends CoordinatorLayout.Behavior { + + private static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); + private static float toolbarHeight; + private int changeInYDir; + private boolean mIsShowing; + private boolean isViewHidden; + private View chatBox; + private LinearLayoutManager linearLayoutManager; + private RecyclerMessageAdapter adapter; + + public RemoveFabOnScroll(Context context, AttributeSet attrs) { + super(context, attrs); + TypedValue tv = new TypedValue(); + if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) + toolbarHeight = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics()); + } + + public RemoveFabOnScroll(LinearLayoutManager linearLayoutManager, RecyclerMessageAdapter adapter) { + this.linearLayoutManager = linearLayoutManager; + this.adapter = adapter; + } + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @SuppressLint("NewApi") + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dx, int dy, int[] consumed) throws NullPointerException { + //count index starts from 1 where as position starts from 0, thus difference 1 + //we have 2 loading layouts one at top and another at bottom of the messages which should be ignored + //resulting in a overall difference of 3 + if (linearLayoutManager.findLastCompletelyVisibleItemPosition() < adapter.getItemCount() - 3) { + if (dy > 0 && changeInYDir < 0 || dy < 0 && changeInYDir > 0) { + child.animate().cancel(); + changeInYDir = 0; + } + + changeInYDir += dy; + if ((changeInYDir > toolbarHeight && child.getVisibility() == View.VISIBLE) && (!isViewHidden || isTopSnackBar(child))) + hideView(child); + else if (changeInYDir < 0 && (child.getVisibility() == View.GONE && !mIsShowing) || isTopSnackBar(child)) { + if (chatBox == null) + chatBox = coordinatorLayout.findViewById(R.id.messageBoxContainer); + if (chatBox.getVisibility() == View.VISIBLE) { + return; + } + showView(child); + } + } + } + + private boolean isTopSnackBar(View child) { + return (child.getId() != R.id.appBarLayout && !(child instanceof FloatingActionButton)); + } + + @SuppressLint("NewApi") + private void hideView(final View view) { + isViewHidden = true; + int y = view.getHeight(); + ; + if (view instanceof AppBarLayout) { + y = -1 * view.getHeight(); + } else if (view instanceof LinearLayout) { + y = 0; + } + ViewPropertyAnimator animator = view.animate() + .translationY(y) + .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) + .setDuration(200); + + animator.setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationEnd(Animator animator) { + isViewHidden = false; + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animator) { + isViewHidden = false; + if (!mIsShowing) + showView(view); + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + animator.start(); + } + + @SuppressLint("NewApi") + public void showView(final View view) { + mIsShowing = true; + ViewPropertyAnimator animator = view.animate() + .translationY((view.getId() == R.id.appBarLayout || view instanceof FloatingActionButton) ? 0 : toolbarHeight) + .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) + .setDuration(200); + + animator.setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animator) { + mIsShowing = false; + } + + @Override + public void onAnimationCancel(Animator animator) { + mIsShowing = false; + if (!isViewHidden) + hideView(view); + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + animator.start(); + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) { + return dependency instanceof Snackbar.SnackbarLayout; + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) { + float translationY = getFabTranslationYForSnackbar(parent, child); + float percentComplete = -translationY / dependency.getHeight(); + float scaleFactor = 1 - percentComplete; + + child.setScaleX(scaleFactor); + child.setScaleY(scaleFactor); + return false; + } + + private float getFabTranslationYForSnackbar(CoordinatorLayout parent, + FloatingActionButton fab) { + float minOffset = 0; + final List dependencies = parent.getDependencies(fab); + for (int i = 0, z = dependencies.size(); i < z; i++) { + final View view = dependencies.get(i); + if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { + minOffset = Math.min(minOffset, + ViewCompat.getTranslationY(view) - view.getHeight()); + } + } + + return minOffset; + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dffaa22fc..4576f7c66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,4 +167,6 @@ Recent Private Messages New Private Message \@-mentions + Waiting for Connection + Connecting ...