From 1665262fdf9f1ef9fa4732ab6510242f00bcd6d9 Mon Sep 17 00:00:00 2001 From: stamatiap Date: Sun, 9 May 2021 13:40:05 +0300 Subject: [PATCH 01/24] fix highlight title - issue #1075 --- .../awais/instagrabber/fragments/StoryViewerFragment.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index a170a8a0..4d1a69cc 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -118,6 +118,7 @@ public class StoryViewerFragment extends Fragment { private View root; private FragmentStoryViewerBinding binding; private String currentStoryUsername; + private String highlightTitle; private StoriesAdapter storiesAdapter; private SwipeEvent swipeEvent; private GestureDetectorCompat gestureDetector; @@ -720,7 +721,7 @@ public class StoryViewerFragment extends Fragment { final HighlightModel model = models.get(currentFeedStoryIndex); currentStoryMediaId = model.getId(); fetchOptions = StoryViewerOptions.forHighlight(model.getId()); - currentStoryUsername = model.getTitle(); + highlightTitle = model.getTitle(); break; } case FEED_STORY_POSITION: { @@ -820,8 +821,8 @@ public class StoryViewerFragment extends Fragment { if (type == Type.HIGHLIGHT) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null) { - actionBarTitle = options.getName(); - actionBar.setTitle(options.getName()); + actionBarTitle = highlightTitle; + actionBar.setTitle(highlightTitle); } } else if (hasUsername) { currentStoryUsername = currentStoryUsername.replace("@", ""); From 4d46ae8fc0ae259b755b3f9164533aa7f97bd65a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 9 May 2021 16:24:40 +0000 Subject: [PATCH 02/24] Update actions/setup-java action to v2 --- .github/workflows/github_nightly_release.yml | 2 +- .github/workflows/github_pre_release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_nightly_release.yml b/.github/workflows/github_nightly_release.yml index 8edb6a63..3f580f91 100644 --- a/.github/workflows/github_nightly_release.yml +++ b/.github/workflows/github_nightly_release.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: java-version: 1.8 diff --git a/.github/workflows/github_pre_release.yml b/.github/workflows/github_pre_release.yml index 9700fdfc..b1d6351a 100644 --- a/.github/workflows/github_pre_release.yml +++ b/.github/workflows/github_pre_release.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: java-version: 1.8 From 413f12c3c23a87dfba1241a1ba5952fdcbe33f64 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Tue, 11 May 2021 19:45:28 +0900 Subject: [PATCH 03/24] parse locationId to long before setting to bundle. Fixes austinhuang0131/barinsta#1235 --- .../instagrabber/fragments/FavoritesFragment.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index 975357e8..a7804de0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -2,6 +2,7 @@ package awais.instagrabber.fragments; import android.content.Context; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -102,8 +103,13 @@ public class FavoritesFragment extends Fragment { // Log.d(TAG, "locationId: " + locationId); final NavController navController = NavHostFragment.findNavController(this); final Bundle bundle = new Bundle(); - bundle.putString("locationId", locationId); - navController.navigate(R.id.action_global_locationFragment, bundle); + try { + bundle.putLong("locationId", Long.parseLong(locationId)); + navController.navigate(R.id.action_global_locationFragment, bundle); + } catch (Exception e) { + Log.e(TAG, "init: ", e); + return; + } break; } case HASHTAG: { From 1ede8ad4bff3eac0cf0eac3b49bb4ed9c1736333 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Tue, 11 May 2021 20:07:10 +0900 Subject: [PATCH 04/24] Fix some bottom nav bar related issues. Check description. 1. Fixed inconsistent bottom bar hiding. Since currently bottom bar cannot hide with motionlayout, keep bottom bar visible. 2. Remove unnecessary padding in location and hashtag fragment. 3. Fix the last item in more preference screen hidden under bottom bar. --- .../settings/MorePreferencesFragment.java | 18 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 4 ++-- app/src/main/res/layout/fragment_discover.xml | 2 +- app/src/main/res/layout/fragment_hashtag.xml | 1 - app/src/main/res/layout/fragment_location.xml | 1 - 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index a022d9ee..e610fe56 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -4,8 +4,11 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.Bundle; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; @@ -19,6 +22,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.RecyclerView; import java.util.List; @@ -53,6 +57,20 @@ public class MorePreferencesFragment extends BasePreferencesFragment { public MorePreferencesFragment() { } + @Override + public RecyclerView onCreateRecyclerView(final LayoutInflater inflater, final ViewGroup parent, final Bundle savedInstanceState) { + final RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState); + final Context context = getContext(); + if (recyclerView != null && context != null) { + recyclerView.setClipToPadding(false); + recyclerView.setPadding(recyclerView.getPaddingLeft(), + recyclerView.getPaddingTop(), + recyclerView.getPaddingRight(), + Utils.getActionBarHeight(context)); + } + return recyclerView; + } + @Override void setupPreferenceScreen(final PreferenceScreen screen) { final String cookie = settingsHelper.getString(Constants.COOKIE); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1e46b341..05485643 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -64,11 +64,11 @@ android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + app:labelVisibilityMode="auto" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_discover.xml b/app/src/main/res/layout/fragment_discover.xml index e18824c7..ff29514e 100644 --- a/app/src/main/res/layout/fragment_discover.xml +++ b/app/src/main/res/layout/fragment_discover.xml @@ -11,7 +11,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior"> diff --git a/app/src/main/res/layout/fragment_location.xml b/app/src/main/res/layout/fragment_location.xml index 9293dc2a..724e5814 100644 --- a/app/src/main/res/layout/fragment_location.xml +++ b/app/src/main/res/layout/fragment_location.xml @@ -19,7 +19,6 @@ android:layout_width="match_parent" android:layout_height="0dp" android:clipToPadding="false" - android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/header"> From cf71ca682e4b8ad11c25bedf5d011ec81c819029 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Fri, 14 May 2021 00:53:23 +0900 Subject: [PATCH 05/24] Update keyboard/emojipicker visiblity logic. Fixes austinhuang0131/barinsta#1181. Also check description. This commits adds some special handling for Android 11+ users regarding keyboard visibility. Check https://github.com/android/user-interface-samples/tree/master/WindowInsetsAnimation. --- app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 3 +- .../instagrabber/activities/MainActivity.java | 14 +- .../InsetsAnimationLinearLayout.java | 246 ++++++++ .../InsetsNotifyingCoordinatorLayout.java | 33 + .../InsetsNotifyingLinearLayout.java | 35 ++ .../ControlFocusInsetsAnimationCallback.java | 87 +++ .../EmojiPickerInsetsAnimationCallback.java | 117 ++++ .../RootViewDeferringInsetsCallback.java | 139 +++++ .../helpers/SimpleImeAnimationController.java | 443 ++++++++++++++ ...slateDeferringInsetsAnimationCallback.java | 128 ++++ .../DirectMessageThreadFragment.java | 396 ++++++------ .../java/awais/instagrabber/utils/Utils.java | 37 ++ .../awais/instagrabber/utils/ViewUtils.java | 51 ++ app/src/main/res/layout/activity_main.xml | 5 +- .../fragment_direct_messages_thread.xml | 563 +++++++++--------- 16 files changed, 1830 insertions(+), 474 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java diff --git a/app/build.gradle b/app/build.gradle index d8ee6ee6..9a4fe62b 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ def getGitHash = { -> } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId 'me.austinhuang.instagrabber' @@ -165,8 +165,6 @@ dependencies { implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version" - implementation "androidx.appcompat:appcompat:$appcompat_version" - implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.recyclerview:recyclerview:1.2.0" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -180,6 +178,9 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-android' + def core_version = "1.6.0-alpha03" + implementation "androidx.core:core:$core_version" + // Room def room_version = "2.2.6" implementation "androidx.room:room-runtime:$room_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ed170657..f90a4aea 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,8 +27,7 @@ + android:taskAffinity=".Main"> diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 9b052c8c..00bc847d 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -31,6 +31,9 @@ import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.NotificationManagerCompat; import androidx.core.provider.FontRequest; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.emoji.text.EmojiCompat; import androidx.emoji.text.FontRequestEmojiCompatConfig; import androidx.fragment.app.FragmentManager; @@ -61,6 +64,7 @@ import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.customviews.emoji.EmojiVariantManager; +import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.ActivityMainBinding; import awais.instagrabber.fragments.PostViewV2Fragment; @@ -137,11 +141,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage instance = this; binding = ActivityMainBinding.inflate(getLayoutInflater()); setupCookie(); - if (settingsHelper.getBoolean(Constants.FLAG_SECURE)) + if (settingsHelper.getBoolean(Constants.FLAG_SECURE)) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } setContentView(binding.getRoot()); final Toolbar toolbar = binding.toolbar; setSupportActionBar(toolbar); + final RootViewDeferringInsetsCallback deferringInsetsCallback = new RootViewDeferringInsetsCallback( + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime() + ); + ViewCompat.setWindowInsetsAnimationCallback(binding.getRoot(), deferringInsetsCallback); + ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), deferringInsetsCallback); + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); createNotificationChannels(); try { final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams(); diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java new file mode 100644 index 00000000..3e08924c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java @@ -0,0 +1,246 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; +import android.view.WindowInsetsAnimation; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.NestedScrollingParent3; +import androidx.core.view.NestedScrollingParentHelper; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.Arrays; + +import awais.instagrabber.customviews.helpers.SimpleImeAnimationController; +import awais.instagrabber.utils.ViewUtils; + +import static androidx.core.view.ViewCompat.TYPE_TOUCH; + +public final class InsetsAnimationLinearLayout extends LinearLayout implements NestedScrollingParent3 { + private final NestedScrollingParentHelper nestedScrollingParentHelper = new NestedScrollingParentHelper(this); + private final SimpleImeAnimationController imeAnimController = new SimpleImeAnimationController(); + private final int[] tempIntArray2 = new int[2]; + private final int[] startViewLocation = new int[2]; + + private View currentNestedScrollingChild; + private int dropNextY; + private boolean scrollImeOffScreenWhenVisible = true; + private boolean scrollImeOnScreenWhenNotVisible = true; + private boolean scrollImeOffScreenWhenVisibleOnFling = false; + private boolean scrollImeOnScreenWhenNotVisibleOnFling = false; + + public InsetsAnimationLinearLayout(final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public InsetsAnimationLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public final boolean getScrollImeOffScreenWhenVisible() { + return scrollImeOffScreenWhenVisible; + } + + public final void setScrollImeOffScreenWhenVisible(boolean scrollImeOffScreenWhenVisible) { + this.scrollImeOffScreenWhenVisible = scrollImeOffScreenWhenVisible; + } + + public final boolean getScrollImeOnScreenWhenNotVisible() { + return scrollImeOnScreenWhenNotVisible; + } + + public final void setScrollImeOnScreenWhenNotVisible(boolean scrollImeOnScreenWhenNotVisible) { + this.scrollImeOnScreenWhenNotVisible = scrollImeOnScreenWhenNotVisible; + } + + public boolean getScrollImeOffScreenWhenVisibleOnFling() { + return scrollImeOffScreenWhenVisibleOnFling; + } + + public void setScrollImeOffScreenWhenVisibleOnFling(final boolean scrollImeOffScreenWhenVisibleOnFling) { + this.scrollImeOffScreenWhenVisibleOnFling = scrollImeOffScreenWhenVisibleOnFling; + } + + public boolean getScrollImeOnScreenWhenNotVisibleOnFling() { + return scrollImeOnScreenWhenNotVisibleOnFling; + } + + public void setScrollImeOnScreenWhenNotVisibleOnFling(final boolean scrollImeOnScreenWhenNotVisibleOnFling) { + this.scrollImeOnScreenWhenNotVisibleOnFling = scrollImeOnScreenWhenNotVisibleOnFling; + } + + public SimpleImeAnimationController getImeAnimController() { + return imeAnimController; + } + + @Override + public boolean onStartNestedScroll(@NonNull final View child, + @NonNull final View target, + final int axes, + final int type) { + return (axes & SCROLL_AXIS_VERTICAL) != 0 && type == TYPE_TOUCH; + } + + @Override + public void onNestedScrollAccepted(@NonNull final View child, + @NonNull final View target, + final int axes, + final int type) { + nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type); + currentNestedScrollingChild = child; + } + + @Override + public void onNestedPreScroll(@NonNull final View target, + final int dx, + final int dy, + @NonNull final int[] consumed, + final int type) { + if (imeAnimController.isInsetAnimationRequestPending()) { + consumed[0] = dx; + consumed[1] = dy; + } else { + int deltaY = dy; + if (dropNextY != 0) { + consumed[1] = dropNextY; + deltaY = dy - dropNextY; + dropNextY = 0; + } + + if (deltaY < 0) { + if (imeAnimController.isInsetAnimationInProgress()) { + consumed[1] -= imeAnimController.insetBy(-deltaY); + } else if (scrollImeOffScreenWhenVisible && !imeAnimController.isInsetAnimationRequestPending()) { + WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); + if (rootWindowInsets != null) { + if (rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { + startControlRequest(); + consumed[1] = deltaY; + } + } + } + } + + } + } + + @Override + public void onNestedScroll(@NonNull final View target, + final int dxConsumed, + final int dyConsumed, + final int dxUnconsumed, + final int dyUnconsumed, + final int type, + @NonNull final int[] consumed) { + if (dyUnconsumed > 0) { + if (imeAnimController.isInsetAnimationInProgress()) { + consumed[1] = -imeAnimController.insetBy(-dyUnconsumed); + } else if (scrollImeOnScreenWhenNotVisible && !imeAnimController.isInsetAnimationRequestPending()) { + WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); + if (rootWindowInsets != null) { + if (!rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { + startControlRequest(); + consumed[1] = dyUnconsumed; + } + } + } + } + + } + + @Override + public boolean onNestedFling(@NonNull final View target, + final float velocityX, + final float velocityY, + final boolean consumed) { + if (imeAnimController.isInsetAnimationInProgress()) { + imeAnimController.animateToFinish(velocityY); + return true; + } else { + boolean imeVisible = false; + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); + if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { + imeVisible = true; + } + if (velocityY > 0 && scrollImeOnScreenWhenNotVisibleOnFling && !imeVisible) { + imeAnimController.startAndFling(this, velocityY); + return true; + } else if (velocityY < 0 && scrollImeOffScreenWhenVisibleOnFling && imeVisible) { + imeAnimController.startAndFling(this, velocityY); + return true; + } else { + return false; + } + } + } + + @Override + public void onStopNestedScroll(@NonNull final View target, final int type) { + nestedScrollingParentHelper.onStopNestedScroll(target, type); + if (imeAnimController.isInsetAnimationInProgress() && !imeAnimController.isInsetAnimationFinishing()) { + imeAnimController.animateToFinish(null); + } + reset(); + } + + @Override + public void dispatchWindowInsetsAnimationPrepare(@NonNull final WindowInsetsAnimation animation) { + super.dispatchWindowInsetsAnimationPrepare(animation); + ViewUtils.suppressLayoutCompat(this, false); + } + + private void startControlRequest() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + ViewUtils.suppressLayoutCompat(this, true); + if (currentNestedScrollingChild != null) { + currentNestedScrollingChild.getLocationInWindow(startViewLocation); + } + imeAnimController.startControlRequest(this, windowInsetsAnimationControllerCompat -> onControllerReady()); + } + + private void onControllerReady() { + if (currentNestedScrollingChild != null) { + imeAnimController.insetBy(0); + int[] location = tempIntArray2; + currentNestedScrollingChild.getLocationInWindow(location); + dropNextY = location[1] - startViewLocation[1]; + } + + } + + private void reset() { + dropNextY = 0; + Arrays.fill(startViewLocation, 0); + ViewUtils.suppressLayoutCompat(this, false); + } + + @Override + public void onNestedScrollAccepted(@NonNull final View child, + @NonNull final View target, + final int axes) { + onNestedScrollAccepted(child, target, axes, TYPE_TOUCH); + } + + @Override + public void onNestedScroll(@NonNull final View target, + final int dxConsumed, + final int dyConsumed, + final int dxUnconsumed, + final int dyUnconsumed, + final int type) { + onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, tempIntArray2); + } + + @Override + public void onStopNestedScroll(@NonNull final View target) { + onStopNestedScroll(target, TYPE_TOUCH); + } +} + diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java new file mode 100644 index 00000000..13a93e43 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java @@ -0,0 +1,33 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.WindowInsets; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +public class InsetsNotifyingCoordinatorLayout extends CoordinatorLayout { + + public InsetsNotifyingCoordinatorLayout(@NonNull final Context context) { + super(context); + } + + public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + getChildAt(index).dispatchApplyWindowInsets(insets); + } + return insets; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java new file mode 100644 index 00000000..b2faa4e1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java @@ -0,0 +1,35 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.WindowInsets; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +public class InsetsNotifyingLinearLayout extends LinearLayout { + public InsetsNotifyingLinearLayout(final Context context) { + super(context); + } + + public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public InsetsNotifyingLinearLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + getChildAt(index).dispatchApplyWindowInsets(insets); + } + return insets; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java new file mode 100644 index 00000000..e1fda461 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package awais.instagrabber.customviews.helpers; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.List; + +/** + * A [WindowInsetsAnimationCompat.Callback] which will request and clear focus on the given view, + * depending on the [WindowInsetsCompat.Type.ime] visibility state when an IME + * [WindowInsetsAnimationCompat] has finished. + *

+ * This is primarily used when animating the [WindowInsetsCompat.Type.ime], so that the + * appropriate view is focused for accepting input from the IME. + */ +public class ControlFocusInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { + + private final View view; + + public ControlFocusInsetsAnimationCallback(@NonNull final View view) { + this(view, DISPATCH_MODE_STOP); + } + + /** + * @param view the view to request/clear focus + * @param dispatchMode The dispatch mode for this callback. + * @see WindowInsetsAnimationCompat.Callback.DispatchMode + */ + public ControlFocusInsetsAnimationCallback(@NonNull final View view, final int dispatchMode) { + super(dispatchMode); + this.view = view; + } + + @NonNull + @Override + public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, + @NonNull final List runningAnimations) { + // no-op and return the insets + return insets; + } + + @Override + public void onEnd(final WindowInsetsAnimationCompat animation) { + if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) { + // The animation has now finished, so we can check the view's focus state. + // We post the check because the rootWindowInsets has not yet been updated, but will + // be in the next message traversal + view.post(this::checkFocus); + } + } + + private void checkFocus() { + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); + boolean imeVisible = false; + if (rootWindowInsets != null) { + imeVisible = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + } + if (imeVisible && view.getRootView().findFocus() == null) { + // If the IME will be visible, and there is not a currently focused view in + // the hierarchy, request focus on our view + view.requestFocus(); + } else if (!imeVisible && view.isFocused()) { + // If the IME will not be visible and our view is currently focused, clear the focus + view.clearFocus(); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java new file mode 100644 index 00000000..125b65c1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java @@ -0,0 +1,117 @@ +package awais.instagrabber.customviews.helpers; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.List; + +/** + * A customized {@link TranslateDeferringInsetsAnimationCallback} for the emoji picker + */ +public class EmojiPickerInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { + private static final String TAG = EmojiPickerInsetsAnimationCallback.class.getSimpleName(); + + private final View view; + private final int persistentInsetTypes; + private final int deferredInsetTypes; + + private int kbHeight; + private onKbVisibilityChangeListener listener; + private boolean shouldTranslate; + + public EmojiPickerInsetsAnimationCallback(final View view, + final int persistentInsetTypes, + final int deferredInsetTypes) { + this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP); + } + + public EmojiPickerInsetsAnimationCallback(final View view, + final int persistentInsetTypes, + final int deferredInsetTypes, + final int dispatchMode) { + super(dispatchMode); + if ((persistentInsetTypes & deferredInsetTypes) != 0) { + throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " + + "any of same WindowInsetsCompat.Type values"); + } + this.view = view; + this.persistentInsetTypes = persistentInsetTypes; + this.deferredInsetTypes = deferredInsetTypes; + } + + @NonNull + @Override + public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, + @NonNull final List runningAnimations) { + // onProgress() is called when any of the running animations progress... + + // First we get the insets which are potentially deferred + final Insets typesInset = insets.getInsets(deferredInsetTypes); + // Then we get the persistent inset types which are applied as padding during layout + final Insets otherInset = insets.getInsets(persistentInsetTypes); + + // Now that we subtract the two insets, to calculate the difference. We also coerce + // the insets to be >= 0, to make sure we don't use negative insets. + final Insets subtract = Insets.subtract(typesInset, otherInset); + final Insets diff = Insets.max(subtract, Insets.NONE); + + // The resulting `diff` insets contain the values for us to apply as a translation + // to the view + view.setTranslationX(diff.left - diff.right); + view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight); + + return insets; + } + + @Override + public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { + try { + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); + if (kbHeight == 0) { + if (rootWindowInsets == null) return; + final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime()); + final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()); + kbHeight = imeInsets.bottom - navBarInsets.bottom; + final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + if (layoutParams != null) { + layoutParams.height = kbHeight; + layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, layoutParams.rightMargin, -kbHeight); + } + } + view.setTranslationX(0f); + final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + float translationY = 0; + if (!shouldTranslate) { + translationY = -kbHeight; + if (visible) { + translationY = 0; + } + } + view.setTranslationY(translationY); + + if (listener != null && rootWindowInsets != null) { + listener.onChange(visible); + } + } finally { + shouldTranslate = true; + } + } + + public void setShouldTranslate(final boolean shouldTranslate) { + this.shouldTranslate = shouldTranslate; + } + + public void setKbVisibilityListener(final onKbVisibilityChangeListener listener) { + this.listener = listener; + } + + public interface onKbVisibilityChangeListener { + void onChange(boolean isVisible); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java new file mode 100644 index 00000000..f58be88a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java @@ -0,0 +1,139 @@ +package awais.instagrabber.customviews.helpers;/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.OnApplyWindowInsetsListener; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.List; + +/** + * A class which extends/implements both [WindowInsetsAnimationCompat.Callback] and + * [View.OnApplyWindowInsetsListener], which should be set on the root view in your layout. + *

+ * This class enables the root view is selectively defer handling any insets which match + * [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s. + *

+ * An example is the following: when a [WindowInsetsAnimationCompat] is started, the system will dispatch + * a [WindowInsetsCompat] instance which contains the end state of the animation. For the scenario of + * the IME being animated in, that means that the insets contains the IME height. If the view's + * [View.OnApplyWindowInsetsListener] simply always applied the combination of + * [WindowInsetsCompat.Type.ime] and [WindowInsetsCompat.Type.systemBars] using padding, the viewport of any + * child views would then be smaller. This results in us animating a smaller (padded-in) view into + * a larger viewport. Visually, this results in the views looking clipped. + *

+ * This class allows us to implement a different strategy for the above scenario, by selectively + * deferring the [WindowInsetsCompat.Type.ime] insets until the [WindowInsetsAnimationCompat] is ended. + * For the above example, you would create a [RootViewDeferringInsetsCallback] like so: + *

+ * ``` + * val callback = RootViewDeferringInsetsCallback( + * persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), + * deferredInsetTypes = WindowInsetsCompat.Type.ime() + * ) + * ``` + *

+ * This class is not limited to just IME animations, and can work with any [WindowInsetsCompat.Type]s. + */ +public class RootViewDeferringInsetsCallback extends WindowInsetsAnimationCompat.Callback implements OnApplyWindowInsetsListener { + + private final int persistentInsetTypes; + private final int deferredInsetTypes; + @Nullable + private View view = null; + @Nullable + private WindowInsetsCompat lastWindowInsets = null; + private boolean deferredInsets = false; + + /** + * @param persistentInsetTypes the bitmask of any inset types which should always be handled + * through padding the attached view + * @param deferredInsetTypes the bitmask of insets types which should be deferred until after + * any related [WindowInsetsAnimationCompat]s have ended + */ + public RootViewDeferringInsetsCallback(final int persistentInsetTypes, final int deferredInsetTypes) { + super(DISPATCH_MODE_CONTINUE_ON_SUBTREE); + if ((persistentInsetTypes & deferredInsetTypes) != 0) { + throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " + + "any of same WindowInsetsCompat.Type values"); + } + this.persistentInsetTypes = persistentInsetTypes; + this.deferredInsetTypes = deferredInsetTypes; + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(@NonNull final View v, @NonNull final WindowInsetsCompat windowInsets) { + // Store the view and insets for us in onEnd() below + view = v; + lastWindowInsets = windowInsets; + + final int types = deferredInsets + // When the deferred flag is enabled, we only use the systemBars() insets + ? persistentInsetTypes + // Otherwise we handle the combination of the the systemBars() and ime() insets + : persistentInsetTypes | deferredInsetTypes; + + // Finally we apply the resolved insets by setting them as padding + final Insets typeInsets = windowInsets.getInsets(types); + v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom); + + // We return the new WindowInsetsCompat.CONSUMED to stop the insets being dispatched any + // further into the view hierarchy. This replaces the deprecated + // WindowInsetsCompat.consumeSystemWindowInsets() and related functions. + return WindowInsetsCompat.CONSUMED; + } + + @Override + public void onPrepare(WindowInsetsAnimationCompat animation) { + if ((animation.getTypeMask() & deferredInsetTypes) != 0) { + // We defer the WindowInsetsCompat.Type.ime() insets if the IME is currently not visible. + // This results in only the WindowInsetsCompat.Type.systemBars() being applied, allowing + // the scrolling view to remain at it's larger size. + deferredInsets = true; + } + } + + @NonNull + @Override + public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, + @NonNull final List runningAnims) { + // This is a no-op. We don't actually want to handle any WindowInsetsAnimations + return insets; + } + + @Override + public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { + if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { + // If we deferred the IME insets and an IME animation has finished, we need to reset + // the flag + deferredInsets = false; + + // And finally dispatch the deferred insets to the view now. + // Ideally we would just call view.requestApplyInsets() and let the normal dispatch + // cycle happen, but this happens too late resulting in a visual flicker. + // Instead we manually dispatch the most recent WindowInsets to the view. + if (lastWindowInsets != null && view != null) { + ViewCompat.dispatchApplyWindowInsets(view, lastWindowInsets); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java new file mode 100644 index 00000000..9bcc24d8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java @@ -0,0 +1,443 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package awais.instagrabber.customviews.helpers; + +import android.os.CancellationSignal; +import android.util.Log; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationControlListenerCompat; +import androidx.core.view.WindowInsetsAnimationControllerCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import awais.instagrabber.utils.ViewUtils; + +/** + * A wrapper around the [WindowInsetsAnimationControllerCompat] APIs in AndroidX Core, to simplify + * the implementation of common use-cases around the IME. + *

+ * See [InsetsAnimationLinearLayout] and [InsetsAnimationTouchListener] for examples of how + * to use this class. + */ +public class SimpleImeAnimationController { + private static final String TAG = SimpleImeAnimationController.class.getSimpleName(); + /** + * Scroll threshold for determining whether to animating to the end state, or to the start state. + * Currently 15% of the total swipe distance distance + */ + private static final float SCROLL_THRESHOLD = 0.15f; + + @Nullable + private WindowInsetsAnimationControllerCompat insetsAnimationController = null; + @Nullable + private CancellationSignal pendingRequestCancellationSignal = null; + @Nullable + private OnRequestReadyListener pendingRequestOnReadyListener; + /** + * True if the IME was shown at the start of the current animation. + */ + private boolean isImeShownAtStart = false; + @Nullable + private SpringAnimation currentSpringAnimation = null; + private WindowInsetsAnimationControlListenerCompat fwdListener; + + /** + * A LinearInterpolator instance we can re-use across listeners. + */ + private final LinearInterpolator linearInterpolator = new LinearInterpolator(); + /* To take control of the an WindowInsetsAnimation, we need to pass in a listener to + controlWindowInsetsAnimation() in startControlRequest(). The listener created here + keeps track of the current WindowInsetsAnimationController and resets our state. */ + private final WindowInsetsAnimationControlListenerCompat animationControlListener = new WindowInsetsAnimationControlListenerCompat() { + /** + * Once the request is ready, call our [onRequestReady] function + */ + @Override + public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) { + onRequestReady(controller); + if (fwdListener != null) { + fwdListener.onReady(controller, types); + } + } + + /** + * If the request is finished, we should reset our internal state + */ + @Override + public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) { + reset(); + if (fwdListener != null) { + fwdListener.onFinished(controller); + } + } + + /** + * If the request is cancelled, we should reset our internal state + */ + @Override + public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) { + reset(); + if (fwdListener != null) { + fwdListener.onCancelled(controller); + } + } + }; + + /** + * Start a control request to the [view]s [android.view.WindowInsetsController]. This should + * be called once the view is in a position to take control over the position of the IME. + * + * @param view The view which is triggering this request + * @param onRequestReadyListener optional listener which will be called when the request is ready and + * the animation can proceed + */ + public void startControlRequest(@NonNull final View view, + @Nullable final OnRequestReadyListener onRequestReadyListener) { + if (isInsetAnimationInProgress()) { + Log.w(TAG, "startControlRequest: Animation in progress. Can not start a new request to controlWindowInsetsAnimation()"); + return; + } + + // Keep track of the IME insets, and the IME visibility, at the start of the request + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); + if (rootWindowInsets != null) { + isImeShownAtStart = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + } + + // Create a cancellation signal, which we pass to controlWindowInsetsAnimation() below + pendingRequestCancellationSignal = new CancellationSignal(); + // Keep reference to the onReady callback + pendingRequestOnReadyListener = onRequestReadyListener; + + // Finally we make a controlWindowInsetsAnimation() request: + final WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(view); + if (windowInsetsController != null) { + windowInsetsController.controlWindowInsetsAnimation( + // We're only catering for IME animations in this listener + WindowInsetsCompat.Type.ime(), + // Animation duration. This is not used by the system, and is only passed to any + // WindowInsetsAnimation.Callback set on views. We pass in -1 to indicate that we're + // not starting a finite animation, and that this is completely controlled by + // the user's touch. + -1, + // The time interpolator used in calculating the animation progress. The fraction value + // we passed into setInsetsAndAlpha() which be passed into this interpolator before + // being used by the system to inset the IME. LinearInterpolator is a good type + // to use for scrolling gestures. + linearInterpolator, + // A cancellation signal, which allows us to cancel the request to control + pendingRequestCancellationSignal, + // The WindowInsetsAnimationControlListener + animationControlListener + ); + } + } + + /** + * Start a control request to the [view]s [android.view.WindowInsetsController], similar to + * [startControlRequest], but immediately fling to a finish using [velocityY] once ready. + *

+ * This function is useful for fire-and-forget operations to animate the IME. + * + * @param view The view which is triggering this request + * @param velocityY the velocity of the touch gesture which caused this call + */ + public void startAndFling(@NonNull final View view, final float velocityY) { + startControlRequest(view, null); + animateToFinish(velocityY); + } + + /** + * Update the inset position of the IME by the given [dy] value. This value will be coerced + * into the hidden and shown inset values. + *

+ * This function should only be called if [isInsetAnimationInProgress] returns true. + * + * @return the amount of [dy] consumed by the inset animation, in pixels + */ + public int insetBy(final int dy) { + if (insetsAnimationController == null) { + throw new IllegalStateException("Current WindowInsetsAnimationController is null." + + "This should only be called if isAnimationInProgress() returns true"); + } + final WindowInsetsAnimationControllerCompat controller = insetsAnimationController; + + // Call updateInsetTo() with the new inset value + return insetTo(controller.getCurrentInsets().bottom - dy); + } + + /** + * Update the inset position of the IME to be the given [inset] value. This value will be + * coerced into the hidden and shown inset values. + *

+ * This function should only be called if [isInsetAnimationInProgress] returns true. + * + * @return the distance moved by the inset animation, in pixels + */ + public int insetTo(final int inset) { + if (insetsAnimationController == null) { + throw new IllegalStateException("Current WindowInsetsAnimationController is null." + + "This should only be called if isAnimationInProgress() returns true"); + } + final WindowInsetsAnimationControllerCompat controller = insetsAnimationController; + + final int hiddenBottom = controller.getHiddenStateInsets().bottom; + final int shownBottom = controller.getShownStateInsets().bottom; + final int startBottom = isImeShownAtStart ? shownBottom : hiddenBottom; + final int endBottom = isImeShownAtStart ? hiddenBottom : shownBottom; + + // We coerce the given inset within the limits of the hidden and shown insets + final int coercedBottom = coerceIn(inset, hiddenBottom, shownBottom); + + final int consumedDy = controller.getCurrentInsets().bottom - coercedBottom; + + // Finally update the insets in the WindowInsetsAnimationController using + // setInsetsAndAlpha(). + controller.setInsetsAndAlpha( + // Here we update the animating insets. This is what controls where the IME is displayed. + // It is also passed through to views via their WindowInsetsAnimation.Callback. + Insets.of(0, 0, 0, coercedBottom), + // This controls the alpha value. We don't want to alter the alpha so use 1f + 1f, + // Finally we calculate the animation progress fraction. This value is passed through + // to any WindowInsetsAnimation.Callbacks, but it is not used by the system. + (coercedBottom - startBottom) / (float) (endBottom - startBottom) + ); + + return consumedDy; + } + + /** + * Return `true` if an inset animation is in progress. + */ + public boolean isInsetAnimationInProgress() { + return insetsAnimationController != null; + } + + /** + * Return `true` if an inset animation is currently finishing. + */ + public boolean isInsetAnimationFinishing() { + return currentSpringAnimation != null; + } + + /** + * Return `true` if a request to control an inset animation is in progress. + */ + public boolean isInsetAnimationRequestPending() { + return pendingRequestCancellationSignal != null; + } + + /** + * Cancel the current [WindowInsetsAnimationControllerCompat]. We immediately finish + * the animation, reverting back to the state at the start of the gesture. + */ + public void cancel() { + if (insetsAnimationController != null) { + insetsAnimationController.finish(isImeShownAtStart); + } + if (pendingRequestCancellationSignal != null) { + pendingRequestCancellationSignal.cancel(); + } + if (currentSpringAnimation != null) { + // Cancel the current spring animation + currentSpringAnimation.cancel(); + } + reset(); + } + + /** + * Finish the current [WindowInsetsAnimationControllerCompat] immediately. + */ + public void finish() { + final WindowInsetsAnimationControllerCompat controller = insetsAnimationController; + + if (controller == null) { + // If we don't currently have a controller, cancel any pending request and return + if (pendingRequestCancellationSignal != null) { + pendingRequestCancellationSignal.cancel(); + } + return; + } + + final int current = controller.getCurrentInsets().bottom; + final int shown = controller.getShownStateInsets().bottom; + final int hidden = controller.getHiddenStateInsets().bottom; + + // The current inset matches either the shown/hidden inset, finish() immediately + if (current == shown) { + controller.finish(true); + } else if (current == hidden) { + controller.finish(false); + } else { + // Otherwise, we'll look at the current position... + if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) { + // If the IME is past the 'threshold' we snap to the toggled state + controller.finish(!isImeShownAtStart); + } else { + // ...otherwise, we snap back to the original visibility + controller.finish(isImeShownAtStart); + } + } + } + + /** + * Finish the current [WindowInsetsAnimationControllerCompat]. We finish the animation, + * animating to the end state if necessary. + * + * @param velocityY the velocity of the touch gesture which caused this call to [animateToFinish]. + * Can be `null` if velocity is not available. + */ + public void animateToFinish(@Nullable final Float velocityY) { + final WindowInsetsAnimationControllerCompat controller = insetsAnimationController; + + if (controller == null) { + // If we don't currently have a controller, cancel any pending request and return + if (pendingRequestCancellationSignal != null) { + pendingRequestCancellationSignal.cancel(); + } + return; + } + + final int current = controller.getCurrentInsets().bottom; + final int shown = controller.getShownStateInsets().bottom; + final int hidden = controller.getHiddenStateInsets().bottom; + + if (velocityY != null) { + // If we have a velocity, we can use it's direction to determine + // the visibility. Upwards == visible + animateImeToVisibility(velocityY > 0, velocityY); + } else if (current == shown) { + // The current inset matches either the shown/hidden inset, finish() immediately + controller.finish(true); + } else if (current == hidden) { + controller.finish(false); + } else { + // Otherwise, we'll look at the current position... + if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) { + // If the IME is past the 'threshold' we animate it to the toggled state + animateImeToVisibility(!isImeShownAtStart, null); + } else { + // ...otherwise, we animate it back to the original visibility + animateImeToVisibility(isImeShownAtStart, null); + } + } + } + + private void onRequestReady(@NonNull final WindowInsetsAnimationControllerCompat controller) { + // The request is ready, so clear out the pending cancellation signal + pendingRequestCancellationSignal = null; + // Store the current WindowInsetsAnimationController + insetsAnimationController = controller; + + // Call any pending callback + if (pendingRequestOnReadyListener != null) { + pendingRequestOnReadyListener.onRequestReady(controller); + } + pendingRequestOnReadyListener = null; + } + + /** + * Resets all of our internal state. + */ + private void reset() { + // Clear all of our internal state + insetsAnimationController = null; + pendingRequestCancellationSignal = null; + isImeShownAtStart = false; + if (currentSpringAnimation != null) { + currentSpringAnimation.cancel(); + } + currentSpringAnimation = null; + pendingRequestOnReadyListener = null; + } + + /** + * Animate the IME to a given visibility. + * + * @param visible `true` to animate the IME to it's fully shown state, `false` to it's + * fully hidden state. + * @param velocityY the velocity of the touch gesture which caused this call. Can be `null` + * if velocity is not available. + */ + private void animateImeToVisibility(final boolean visible, @Nullable final Float velocityY) { + if (insetsAnimationController == null) { + throw new IllegalStateException("Controller should not be null"); + } + final WindowInsetsAnimationControllerCompat controller = insetsAnimationController; + + final FloatPropertyCompat property = new FloatPropertyCompat("property") { + @Override + public float getValue(final Object object) { + return controller.getCurrentInsets().bottom; + } + + @Override + public void setValue(final Object object, final float value) { + if (insetsAnimationController == null) { + return; + } + insetTo((int) value); + } + }; + final float finalPosition = visible ? controller.getShownStateInsets().bottom + : controller.getHiddenStateInsets().bottom; + final SpringForce force = new SpringForce(finalPosition) + // Tweak the damping value, to remove any bounciness. + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) + // The stiffness value controls the strength of the spring animation, which + // controls the speed. Medium (the default) is a good value, but feel free to + // play around with this value. + .setStiffness(SpringForce.STIFFNESS_MEDIUM); + ViewUtils.springAnimationOf(this, property, finalPosition) + .setSpring(force) + .setStartVelocity(velocityY != null ? velocityY : 0) + .addEndListener((animation, canceled, value, velocity) -> { + if (animation == currentSpringAnimation) { + currentSpringAnimation = null; + } + // Once the animation has ended, finish the controller + finish(); + }).start(); + } + + private int coerceIn(final int v, final int min, final int max) { + if (v >= min && v <= max) { + return v; + } + if (v < min) { + return min; + } + return max; + } + + public void setAnimationControlListener(final WindowInsetsAnimationControlListenerCompat listener) { + fwdListener = listener; + } + + public interface OnRequestReadyListener { + void onRequestReady(WindowInsetsAnimationControllerCompat windowInsetsAnimationControllerCompat); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java b/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java new file mode 100644 index 00000000..e10105ea --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package awais.instagrabber.customviews.helpers; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; + +import java.util.List; + +/** + * A [WindowInsetsAnimationCompat.Callback] which will translate/move the given view during any + * inset animations of the given inset type. + *

+ * This class works in tandem with [RootViewDeferringInsetsCallback] to support the deferring of + * certain [WindowInsetsCompat.Type] values during a [WindowInsetsAnimationCompat], provided in + * [deferredInsetTypes]. The values passed into this constructor should match those which + * the [RootViewDeferringInsetsCallback] is created with. + */ +public class TranslateDeferringInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { + private final View view; + private final int persistentInsetTypes; + private final int deferredInsetTypes; + + private boolean shouldTranslate = true; + private int kbHeight; + + public TranslateDeferringInsetsAnimationCallback(final View view, + final int persistentInsetTypes, + final int deferredInsetTypes) { + this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP); + } + + /** + * @param view the view to translate from it's start to end state + * @param persistentInsetTypes the bitmask of any inset types which were handled as part of the + * layout + * @param deferredInsetTypes the bitmask of insets types which should be deferred until after + * any [WindowInsetsAnimationCompat]s have ended + * @param dispatchMode The dispatch mode for this callback. + * See [WindowInsetsAnimationCompat.Callback.getDispatchMode]. + */ + public TranslateDeferringInsetsAnimationCallback(final View view, + final int persistentInsetTypes, + final int deferredInsetTypes, + final int dispatchMode) { + super(dispatchMode); + if ((persistentInsetTypes & deferredInsetTypes) != 0) { + throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " + + "any of same WindowInsetsCompat.Type values"); + } + this.view = view; + this.persistentInsetTypes = persistentInsetTypes; + this.deferredInsetTypes = deferredInsetTypes; + } + + @NonNull + @Override + public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, + @NonNull final List runningAnimations) { + // onProgress() is called when any of the running animations progress... + + // First we get the insets which are potentially deferred + final Insets typesInset = insets.getInsets(deferredInsetTypes); + // Then we get the persistent inset types which are applied as padding during layout + final Insets otherInset = insets.getInsets(persistentInsetTypes); + + // Now that we subtract the two insets, to calculate the difference. We also coerce + // the insets to be >= 0, to make sure we don't use negative insets. + final Insets subtract = Insets.subtract(typesInset, otherInset); + final Insets diff = Insets.max(subtract, Insets.NONE); + + // The resulting `diff` insets contain the values for us to apply as a translation + // to the view + view.setTranslationX(diff.left - diff.right); + view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight); + + return insets; + } + + @Override + public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { + try { + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); + if (kbHeight == 0) { + if (rootWindowInsets == null) return; + final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime()); + final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()); + kbHeight = imeInsets.bottom - navBarInsets.bottom; + } + // Once the animation has ended, reset the translation values + view.setTranslationX(0f); + final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + float translationY = 0; + if (!shouldTranslate) { + translationY = -kbHeight; + if (visible) { + translationY = 0; + } + } + view.setTranslationY(translationY); + } finally { + shouldTranslate = true; + } + } + + public void setShouldTranslate(final boolean shouldTranslate) { + this.shouldTranslate = shouldTranslate; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 1d6b8f6e..99d72a57 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -25,11 +26,17 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; +import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsAnimationControlListenerCompat; +import androidx.core.view.WindowInsetsAnimationControllerCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -70,16 +77,21 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader; import awais.instagrabber.adapters.DirectReactionsAdapter; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; import awais.instagrabber.animations.CubicBezierInterpolator; +import awais.instagrabber.customviews.InsetsAnimationLinearLayout; +import awais.instagrabber.customviews.KeyNotifyingEmojiEditText; import awais.instagrabber.customviews.RecordView; import awais.instagrabber.customviews.Tooltip; import awais.instagrabber.customviews.emoji.Emoji; import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog; import awais.instagrabber.customviews.emoji.EmojiPicker; +import awais.instagrabber.customviews.helpers.ControlFocusInsetsAnimationCallback; +import awais.instagrabber.customviews.helpers.EmojiPickerInsetsAnimationCallback; import awais.instagrabber.customviews.helpers.HeaderItemDecoration; -import awais.instagrabber.customviews.helpers.HeightProvider; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; +import awais.instagrabber.customviews.helpers.SimpleImeAnimationController; import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.customviews.helpers.TranslateDeferringInsetsAnimationCallback; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; import awais.instagrabber.dialogs.GifPickerBottomDialogFragment; @@ -111,9 +123,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.DirectThreadViewModel; import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory; -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; -import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; - public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener, EmojiPicker.OnEmojiClickListener { private static final String TAG = DirectMessageThreadFragment.class.getSimpleName(); @@ -125,7 +134,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private DirectItemsAdapter itemsAdapter; private MainActivity fragmentActivity; private DirectThreadViewModel viewModel; - private ConstraintLayout root; + private InsetsAnimationLinearLayout root; private boolean shouldRefresh = true; private List itemOrHeaders; private List users; @@ -135,14 +144,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private ActionBar actionBar; private AppStateViewModel appStateViewModel; private Runnable prevTitleRunnable; - private int originalSoftInputMode; private AnimatorSet animatorSet; - private boolean isEmojiPickerShown; - private boolean isKbShown; - private HeightProvider heightProvider; private boolean isRecording; - private boolean wasKbShowing; - private int keyboardHeight = Utils.convertDpToPx(250); private DirectItemReactionDialogFragment reactionDialogFragment; private DirectItem itemToForward; private MutableLiveData backStackSavedStateResultLiveData; @@ -163,6 +166,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private MenuItem markAsSeenMenuItem; private Media tempMedia; private DirectItem addReactionItem; + private TranslateDeferringInsetsAnimationCallback inputHolderAnimationCallback; + private TranslateDeferringInsetsAnimationCallback chatsAnimationCallback; + private EmojiPickerInsetsAnimationCallback emojiPickerAnimationCallback; + private boolean hasKbOpenedOnce; + private boolean wasToggled; private final AppExecutors appExecutors = AppExecutors.getInstance(); private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @@ -304,7 +312,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG); } }; - private final DirectItemLongClickListener directItemLongClickListener = position -> { // viewModel.setSelectedPosition(position); }; @@ -333,6 +340,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact backStackSavedStateResultLiveData.postValue(null); }; private final MutableLiveData inputLength = new MutableLiveData<>(0); + private final MutableLiveData emojiPickerVisible = new MutableLiveData<>(false); + private final MutableLiveData kbVisible = new MutableLiveData<>(false); + private final OnBackPressedCallback onEmojiPickerBackPressedCallback = new OnBackPressedCallback(false) { + @Override + public void handleOnBackPressed() { + emojiPickerVisible.postValue(false); + } + }; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -371,13 +386,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact return root; } tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black)); - originalSoftInputMode = fragmentActivity.getWindow().getAttributes().softInputMode; // todo check has camera and remove view return root; } @Override public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + // WindowCompat.setDecorFitsSystemWindows(fragmentActivity.getWindow(), false); if (!shouldRefresh) return; init(); binding.send.post(() -> initialSendX = binding.send.getX()); @@ -490,10 +505,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (isRecording) { binding.recordView.cancelRecording(binding.send); } - if (isKbShown) { - wasKbShowing = true; - binding.emojiPicker.setAlpha(0); - } + emojiPickerVisible.postValue(false); + kbVisible.postValue(false); + binding.inputHolder.setTranslationY(0); + binding.chats.setTranslationY(0); + binding.emojiPicker.setTranslationY(0); removeObservers(); super.onPause(); } @@ -501,16 +517,12 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact @Override public void onResume() { super.onResume(); - fragmentActivity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_NOTHING | SOFT_INPUT_STATE_HIDDEN); - if (wasKbShowing) { - binding.input.requestFocus(); - binding.input.post(this::showKeyboard); - wasKbShowing = false; - } if (initialSendX != 0) { binding.send.setX(initialSendX); } binding.send.stopScale(); + final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); + onBackPressedDispatcher.addCallback(onEmojiPickerBackPressedCallback); setupBackStackResultObserver(); setObservers(); // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); @@ -533,13 +545,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (prevTitleRunnable != null) { appExecutors.mainThread().cancel(prevTitleRunnable); } - if (heightProvider != null) { - // need to close the height provider popup before navigating back to prevent leak - heightProvider.dismiss(); - } - if (originalSoftInputMode != 0) { - fragmentActivity.getWindow().setSoftInputMode(originalSoftInputMode); - } for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) { final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i)); if (holder == null) continue; @@ -561,37 +566,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact actionBar = fragmentActivity.getSupportActionBar(); setupList(); root.post(this::setupInput); - // root.post(this::getInitialData); } - // private void getInitialData() { - // final Bundle arguments = getArguments(); - // if (arguments == null) return; - // final DirectMessageThreadFragmentArgs args = DirectMessageThreadFragmentArgs.fromBundle(arguments); - // final boolean pending = args.getPending(); - // final NavController navController = NavHostFragment.findNavController(this); - // final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); - // final List threads; - // if (!pending) { - // final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); - // threads = threadListViewModel.getThreads().getValue(); - // } else { - // final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class); - // threads = threadListViewModel.getThreads().getValue(); - // } - // final Optional first = threads != null - // ? threads.stream() - // .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId())) - // .findFirst() - // : Optional.empty(); - // if (first.isPresent()) { - // final DirectThread thread = first.get(); - // viewModel.setThread(thread); - // return; - // } - // viewModel.fetchChats(); - // } - private void setupList() { final Context context = getContext(); if (context == null) return; @@ -1100,23 +1076,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { final int length = s.length(); inputLength.postValue(length); - // boolean showExtraInputOptionsChanged = false; - // if (prevLength != 0 && length == 0) { - // inputLength.postValue(true); - // showExtraInputOptionsChanged = true; - // binding.send.setListenForRecord(true); - // startIconAnimation(); - // } - // if (prevLength == 0 && length != 0) { - // inputLength.postValue(false); - // showExtraInputOptionsChanged = true; - // binding.send.setListenForRecord(false); - // startIconAnimation(); - // } - // if (!showExtraInputOptionsChanged) { - // showExtraInputOptions.postValue(length == 0); - // } - // prevLength = length; } }); binding.send.setOnRecordClickListener(v -> { @@ -1131,30 +1090,15 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact Log.d(TAG, "setOnRecordLongClickListener"); return true; }); - binding.input.setShowSoftInputOnFocus(false); - binding.input.requestFocus(); - binding.input.setOnKeyEventListener((keyCode, keyEvent) -> { - if (keyCode != KeyEvent.KEYCODE_BACK) return false; - // We'll close the keyboard/emoji picker only when user releases the back button - // return true so that system doesn't handle the event - if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true; - if (!isKbShown && !isEmojiPickerShown) { - // if both keyboard and emoji picker are hidden, navigate back - if (heightProvider != null) { - // need to close the height provider popup before navigating back to prevent leak - heightProvider.dismiss(); - } - NavHostFragment.findNavController(this).navigateUp(); - return true; - } - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - hideKeyboard(true); - return true; - }); - binding.input.setOnClickListener(v -> { - if (isKbShown) return; - showKeyboard(); + binding.input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) return; + final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); + if (emojiPickerVisibleValue == null || !emojiPickerVisibleValue) return; + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); }); + setupInsetsCallback(); setupEmojiPicker(); binding.gallery.setOnClickListener(v -> { final MediaPickerBottomDialogFragment mediaPicker = MediaPickerBottomDialogFragment.newInstance(); @@ -1166,7 +1110,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } }); mediaPicker.show(getChildFragmentManager(), "MediaPicker"); - hideKeyboard(true); }); binding.gif.setOnClickListener(v -> { final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance(); @@ -1176,7 +1119,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact handleSentMessage(viewModel.sendAnimatedMedia(giphyGif)); }); gifPicker.show(getChildFragmentManager(), "GifPicker"); - hideKeyboard(true); }); binding.camera.setOnClickListener(v -> { final Intent intent = new Intent(context, CameraActivity.class); @@ -1184,6 +1126,73 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact }); } + private void setupInsetsCallback() { + inputHolderAnimationCallback = new TranslateDeferringInsetsAnimationCallback( + binding.inputHolder, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime(), + WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE + ); + ViewCompat.setWindowInsetsAnimationCallback(binding.inputHolder, inputHolderAnimationCallback); + chatsAnimationCallback = new TranslateDeferringInsetsAnimationCallback( + binding.chats, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime() + ); + ViewCompat.setWindowInsetsAnimationCallback(binding.chats, chatsAnimationCallback); + emojiPickerAnimationCallback = new EmojiPickerInsetsAnimationCallback( + binding.emojiPicker, + WindowInsetsCompat.Type.systemBars(), + WindowInsetsCompat.Type.ime() + ); + emojiPickerAnimationCallback.setKbVisibilityListener(this::onKbVisibilityChange); + ViewCompat.setWindowInsetsAnimationCallback(binding.emojiPicker, emojiPickerAnimationCallback); + ViewCompat.setWindowInsetsAnimationCallback( + binding.input, + new ControlFocusInsetsAnimationCallback(binding.input) + ); + final SimpleImeAnimationController imeAnimController = root.getImeAnimController(); + if (imeAnimController != null) { + imeAnimController.setAnimationControlListener(new WindowInsetsAnimationControlListenerCompat() { + @Override + public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {} + + @Override + public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) { + checkKbVisibility(); + } + + @Override + public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) { + checkKbVisibility(); + } + + private void checkKbVisibility() { + final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(binding.getRoot()); + final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); + onKbVisibilityChange(visible); + } + }); + } + } + + private void onKbVisibilityChange(final boolean kbVisible) { + this.kbVisible.postValue(kbVisible); + if (wasToggled) { + emojiPickerVisible.postValue(!kbVisible); + wasToggled = false; + return; + } + final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); + if (kbVisible && emojiPickerVisibleValue != null && emojiPickerVisibleValue) { + emojiPickerVisible.postValue(false); + return; + } + if (!kbVisible) { + emojiPickerVisible.postValue(false); + } + } + private void startIconAnimation() { final Drawable icon = binding.send.getIcon(); if (icon instanceof Animatable) { @@ -1230,15 +1239,87 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void setupEmojiPicker() { root.post(() -> binding.emojiPicker.init( root, - (view, emoji) -> binding.input.append(emoji.getUnicode()), + (view, emoji) -> { + final KeyNotifyingEmojiEditText input = binding.input; + final int start = input.getSelectionStart(); + final int end = input.getSelectionEnd(); + if (start < 0) { + input.append(emoji.getUnicode()); + return; + } + input.getText().replace( + Math.min(start, end), + Math.max(start, end), + emoji.getUnicode(), + 0, + emoji.getUnicode().length() + ); + }, () -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) )); - setupKbHeightProvider(); - if (keyboardHeight == 0) { - keyboardHeight = Utils.convertDpToPx(250); - } - setEmojiPickerBounds(); - binding.emojiToggle.setOnClickListener(v -> toggleEmojiPicker()); + binding.emojiToggle.setOnClickListener(v -> { + Boolean isEmojiPickerVisible = emojiPickerVisible.getValue(); + if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; + Boolean isKbVisible = kbVisible.getValue(); + if (isKbVisible == null) isKbVisible = false; + wasToggled = isEmojiPickerVisible || isKbVisible; + + if (isEmojiPickerVisible) { + if (hasKbOpenedOnce && binding.emojiPicker.getTranslationY() != 0) { + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); + } + // trigger ime. + // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here + showKeyboard(); + return; + } + + if (isKbVisible) { + // hide the keyboard, but don't translate the views + // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here + inputHolderAnimationCallback.setShouldTranslate(false); + chatsAnimationCallback.setShouldTranslate(false); + emojiPickerAnimationCallback.setShouldTranslate(false); + hideKeyboard(); + } + emojiPickerVisible.postValue(true); + }); + final LiveData> emojiKbVisibilityLD = Utils.zipLiveData(emojiPickerVisible, kbVisible); + emojiKbVisibilityLD.observe(getViewLifecycleOwner(), pair -> { + Boolean isEmojiPickerVisible = pair.first; + Boolean isKbVisible = pair.second; + if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; + if (isKbVisible == null) isKbVisible = false; + root.setScrollImeOffScreenWhenVisible(!isEmojiPickerVisible); + root.setScrollImeOnScreenWhenNotVisible(!isEmojiPickerVisible); + onEmojiPickerBackPressedCallback.setEnabled(isEmojiPickerVisible && !isKbVisible); + if (isEmojiPickerVisible && !isKbVisible) { + animatePan(binding.emojiPicker.getMeasuredHeight(), unused -> { + binding.emojiPicker.setAlpha(1); + binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); + return null; + }, null); + return; + } + if (!isEmojiPickerVisible && !isKbVisible) { + animatePan(0, null, unused -> { + binding.emojiPicker.setAlpha(0); + binding.emojiToggle.setIconResource(R.drawable.ic_face_24); + return null; + }); + return; + } + // isKbVisible will always be true going forward + hasKbOpenedOnce = true; + if (!isEmojiPickerVisible) { + binding.emojiToggle.setIconResource(R.drawable.ic_face_24); + binding.emojiPicker.setAlpha(0); + return; + } + binding.emojiPicker.setAlpha(1); + }); } public void showKeyboard() { @@ -1246,67 +1327,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; - if (!isEmojiPickerShown) { - binding.emojiPicker.setAlpha(0); + if (!binding.input.isFocused()) { + binding.input.requestFocus(); } final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT); if (!shown) { Log.e(TAG, "showKeyboard: System did not display the keyboard"); } - if (!isEmojiPickerShown) { - animatePan(keyboardHeight); - } - isKbShown = true; } - public void hideKeyboard(final boolean shouldPan) { + public void hideKeyboard() { final Context context = getContext(); if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; - if (shouldPan) { - binding.emojiPicker.setAlpha(0); - } imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); - if (shouldPan) { - animatePan(0); - isEmojiPickerShown = false; - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - } - isKbShown = false; - } - - /** - * Toggle between emoji picker and keyboard - * If both are hidden, the emoji picker is shown first - */ - private void toggleEmojiPicker() { - if (isKbShown) { - binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); - hideKeyboard(false); - return; - } - if (isEmojiPickerShown) { - binding.emojiToggle.setIconResource(R.drawable.ic_face_24); - showKeyboard(); - return; - } - binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); - animatePan(keyboardHeight); - isEmojiPickerShown = true; - } - - /** - * Set height of the emoji picker - */ - private void setEmojiPickerBounds() { - final ViewGroup.LayoutParams layoutParams = binding.emojiPicker.getLayoutParams(); - layoutParams.height = keyboardHeight; - if (!isEmojiPickerShown) { - // If emoji picker is hidden reset the translationY so that it doesn't peek from bottom - binding.emojiPicker.setTranslationY(keyboardHeight); - } - binding.emojiPicker.requestLayout(); } private void setSendToMicIcon() { @@ -1375,40 +1410,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact return null; } - private void setupKbHeightProvider() { - if (heightProvider != null) return; - heightProvider = new HeightProvider(fragmentActivity).init().setHeightListener(height -> { - if (height > 100 && keyboardHeight != height) { - // save the current keyboard height to settings to use later - keyboardHeight = height; - setEmojiPickerBounds(); - animatePan(keyboardHeight); - } - }); - } - // Sets the translationY of views to height with animation - private void animatePan(final int height) { + private void animatePan(final int height, + @Nullable final Function onAnimationStart, + @Nullable final Function onAnimationEnd) { if (animatorSet != null && animatorSet.isStarted()) { animatorSet.cancel(); } final ImmutableList.Builder builder = ImmutableList.builder(); builder.add( ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.input, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.inputBg, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.recordView, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.emojiToggle, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.gif, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.gallery, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.camera, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.send, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyBg, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyInfo, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyCancel, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyPreviewImage, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.replyPreviewText, TRANSLATION_Y, -height), - ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, keyboardHeight - height) + ObjectAnimator.ofFloat(binding.inputHolder, TRANSLATION_Y, -height), + ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, -height) ); // if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) { // builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height)); @@ -1418,10 +1431,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact animatorSet.setDuration(200); animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN); animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(final Animator animation) { + super.onAnimationStart(animation); + if (onAnimationStart != null) { + onAnimationStart.apply(null); + } + } + @Override public void onAnimationEnd(final Animator animation) { - binding.emojiPicker.setAlpha(1); + super.onAnimationEnd(animation); animatorSet = null; + if (onAnimationEnd != null) { + onAnimationEnd.apply(null); + } } }); animatorSet.start(); diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index a9424b33..3a157f1a 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -38,6 +38,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.exoplayer2.database.ExoDatabaseProvider; @@ -562,4 +564,39 @@ public final class Utils { display.getRealSize(size); return size; } + + public static LiveData> zipLiveData(@NonNull final LiveData firstLiveData, + @NonNull final LiveData secondLiveData) { + final ZippedLiveData zippedLiveData = new ZippedLiveData<>(); + zippedLiveData.addFirstSource(firstLiveData); + zippedLiveData.addSecondSource(secondLiveData); + return zippedLiveData; + } + + public static class ZippedLiveData extends MediatorLiveData> { + private F lastF; + private S lastS; + + private void update() { + F localLastF = lastF; + S localLastS = lastS; + if (localLastF != null && localLastS != null) { + setValue(new Pair<>(localLastF, localLastS)); + } + } + + public void addFirstSource(@NonNull final LiveData firstLiveData) { + addSource(firstLiveData, f -> { + lastF = f; + update(); + }); + } + + public void addSecondSource(@NonNull final LiveData secondLiveData) { + addSource(secondLiveData, s -> { + lastS = s; + update(); + }); + } + } } diff --git a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java index bf39d7a4..3e371045 100644 --- a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java @@ -1,18 +1,28 @@ package awais.instagrabber.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; +import android.os.Build; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.core.util.Pair; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; + +import org.jetbrains.annotations.NotNull; + +import kotlin.jvm.internal.Intrinsics; public final class ViewUtils { @@ -69,4 +79,45 @@ public final class ViewUtils { public static float getTextViewValueWidth(final TextView textView, final String text) { return textView.getPaint().measureText(text); } + + /** + * Creates [SpringAnimation] for object. + * If finalPosition is not [Float.NaN] then create [SpringAnimation] with + * [SpringForce.mFinalPosition]. + * + * @param object Object + * @param property object's property to be animated. + * @param finalPosition [SpringForce.mFinalPosition] Final position of spring. + * @return [SpringAnimation] + */ + @NonNull + public static SpringAnimation springAnimationOf(final Object object, + final FloatPropertyCompat property, + @Nullable final Float finalPosition) { + return finalPosition == null ? new SpringAnimation(object, property) : new SpringAnimation(object, property, finalPosition); + } + + public static void suppressLayoutCompat(@NotNull ViewGroup $this$suppressLayoutCompat, boolean suppress) { + Intrinsics.checkNotNullParameter($this$suppressLayoutCompat, "$this$suppressLayoutCompat"); + if (Build.VERSION.SDK_INT >= 29) { + $this$suppressLayoutCompat.suppressLayout(suppress); + } else { + hiddenSuppressLayout($this$suppressLayoutCompat, suppress); + } + + } + + private static boolean tryHiddenSuppressLayout = true; + + @SuppressLint({"NewApi"}) + private static void hiddenSuppressLayout(ViewGroup group, boolean suppress) { + if (tryHiddenSuppressLayout) { + try { + group.suppressLayout(suppress); + } catch (NoSuchMethodError var3) { + tryHiddenSuppressLayout = false; + } + } + + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 05485643..9d1e880a 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - @@ -71,4 +72,4 @@ android:layout_height="wrap_content" android:layout_gravity="bottom" app:labelVisibilityMode="auto" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_direct_messages_thread.xml b/app/src/main/res/layout/fragment_direct_messages_thread.xml index 96b0bc34..50120027 100644 --- a/app/src/main/res/layout/fragment_direct_messages_thread.xml +++ b/app/src/main/res/layout/fragment_direct_messages_thread.xml @@ -1,312 +1,315 @@ - + android:clipToPadding="false" + android:orientation="vertical"> - + - + - + - + - + - + - - - - - - - + - + + + + + + + - + - + - + - + - + - + - + + + + + + + + + + - - - - - - - \ No newline at end of file + app:layout_constraintStart_toStartOf="parent" /> + \ No newline at end of file From 1eac6399f0443865d5ac5a87cfb3d43258572581 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 May 2021 14:56:30 +0000 Subject: [PATCH 06/24] Update dependency gradle to v7 --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6e61ea74..e1e2fd2c 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9af5c8e7e2cd1a3b0f694a4ac262b9f38c75262e74a9e8b5101af302a6beadd7 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip +distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2b4cdeaabeb72e6ff8bcd0c51f02c68bd93ea1d1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 May 2021 17:27:17 +0000 Subject: [PATCH 07/24] Update dependency com.android.tools.build:gradle to v4.2.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cf769fff..afb10764 100755 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:4.2.1' def nav_version = "2.3.5" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } From 429bcc4e918bb38540f4095e94d6a187c94caac6 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Fri, 14 May 2021 15:00:31 -0400 Subject: [PATCH 08/24] fix #1255 --- .../fragments/main/ProfileFragment.java | 22 ++++++++++++------- app/src/main/res/layout/fragment_profile.xml | 5 ++++- app/src/main/res/xml/header_list_scene.xml | 7 ++++++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 17dfa964..e9870987 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -662,17 +662,21 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); return; } - if (!postsSetupDone) { - setupPosts(); - } else { - binding.postsRecyclerView.refresh(); + final long profileId = profileModel.getPk(); + if (!isReallyPrivate()) { + if (!postsSetupDone) { + setupPosts(); + } + else { + binding.postsRecyclerView.refresh(); + } + if (isLoggedIn) { + fetchStoryAndHighlights(profileId); + } } profileDetailsBinding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); profileDetailsBinding.isPrivate.setVisibility(profileModel.isPrivate() ? View.VISIBLE : View.GONE); - final long profileId = profileModel.getPk(); - if (isLoggedIn) { - fetchStoryAndHighlights(profileId); - } + setupButtons(profileId); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { @@ -905,6 +909,8 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.privatePage1.setImageResource(R.drawable.lock); binding.privatePage2.setText(R.string.priv_acc); binding.privatePage.setVisibility(View.VISIBLE); + binding.privatePage1.setVisibility(View.VISIBLE); + binding.privatePage2.setVisibility(View.VISIBLE); binding.postsRecyclerView.setVisibility(View.GONE); binding.swipeRefreshLayout.setRefreshing(false); } diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 051dd0aa..48b36bfd 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -38,7 +38,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" - android:layout_marginTop="@dimen/private_page_margins" android:gravity="center" android:orientation="vertical" android:visibility="gone" @@ -48,6 +47,8 @@ android:id="@+id/privatePage1" android:layout_width="@dimen/private_page_size" android:layout_height="@dimen/private_page_size" + android:visibility="gone" + tools:visibility="visible" app:srcCompat="@drawable/lock" /> diff --git a/app/src/main/res/xml/header_list_scene.xml b/app/src/main/res/xml/header_list_scene.xml index 389b7cb1..522990ee 100644 --- a/app/src/main/res/xml/header_list_scene.xml +++ b/app/src/main/res/xml/header_list_scene.xml @@ -9,6 +9,13 @@ motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> + Date: Fri, 14 May 2021 15:08:00 -0400 Subject: [PATCH 09/24] readme chore --- .all-contributorsrc | 19 +++++++++++++++++++ README.md | 28 ++++++++++++++++------------ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index cd865d08..7a9958b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -79,6 +79,25 @@ "code" ] }, + { + "login": "stamatiap", + "name": "Stamatia Papageorgiou", + "avatar_url": "https://avatars.githubusercontent.com/u/57223967?v=4", + "profile": "https://github.com/stamatiap", + "contributions": [ + "code", + "translation" + ] + }, + { + "login": "The-EDev", + "name": "Farook Al-Sammarraie", + "avatar_url": "https://avatars.githubusercontent.com/u/60552923?v=4", + "profile": "https://github.com/The-EDev", + "contributions": [ + "code" + ] + }, { "login": "Zopieux", "name": "Alexandre Macabies", diff --git a/README.md b/README.md index d1301419..15999d16 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE) [![GitHub stars](https://img.shields.io/github/stars/austinhuang0131/instagrabber.svg?style=social&label=Star)](https://GitHub.com/austinhuang0131/barinsta/stargazers/) -[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg)](#contributors) Instagram client; previously known as InstaGrabber. @@ -63,49 +63,53 @@ Prominent contributors are listed here in the [all-contributors](https://allcont
Pablo Rodríguez

💻 +
Stamatia Papageorgiou

💻 🌍 +
Farook Al-Sammarraie

💻
Alexandre Macabies

💻
Stefan Najdovski

🎨 🌍
CrazyMarvin

💵 -
Kevin Thomas

💵 -
Shadowspear123

📝 🐛 🤔 💬 +
Kevin Thomas

💵 +
Shadowspear123

📝 🐛 🤔 💬
Ricardo

🐛 🌍
Akrai

🤔 🌍
avtkal

🌍
Cézar Augusto

🌍 -
Dimitris T

🌍 -
farzadx

🌍 +
Dimitris T

🌍 +
farzadx

🌍
Fatih Aydın

🌍
fouze555

🌍
Galang23

🌍
Initdebugs

🌍 -
Jakub Janek

🌍 -
GenosseFlosse

🌍 +
Jakub Janek

🌍 +
GenosseFlosse

🌍
kernoeb

🌍
MoaufmKlo

🌍
nalinalini

🌍
peterge1998

🌍 -
PierreM0

🌍 -
Pyrobauve

🌍 +
PierreM0

🌍 +
Pyrobauve

🌍
RAMAR-RAR

🌍
rohang02

🌍
retiolus

🌍
rikishi0071

🌍 -
Alexey Peschany

🌍 -
Sitavi

🌍 +
Alexey Peschany

🌍 +
Sitavi

🌍
Still Hsu

🌍
Ten_Lego

🌍
wagnim

🌍
wokija

🌍 + +
ysakamoto

🌍
ZDVokoun

🌍 @@ -121,7 +125,7 @@ This app's predecessor, InstaGrabber, was originally made by [@AwaisKing](https: Barinsta Copyright (C) 2020-2021 Austin Huang - Ammar Githam + Ammar Githam This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 106a8ec4061964860c7bc7dbb685711a633b8771 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Fri, 14 May 2021 15:10:53 -0400 Subject: [PATCH 10/24] #976 prep --- .github/workflows/github_nightly_release.yml | 5 +++-- .github/workflows/github_pre_release.yml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github_nightly_release.yml b/.github/workflows/github_nightly_release.yml index 8edb6a63..033ec209 100644 --- a/.github/workflows/github_nightly_release.yml +++ b/.github/workflows/github_nightly_release.yml @@ -17,8 +17,9 @@ jobs: - name: set up JDK 1.8 uses: actions/setup-java@v1 with: - java-version: 1.8 - + distribution: 'zulu' + java-version: '8' + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/github_pre_release.yml b/.github/workflows/github_pre_release.yml index 9700fdfc..cde75f36 100644 --- a/.github/workflows/github_pre_release.yml +++ b/.github/workflows/github_pre_release.yml @@ -18,7 +18,8 @@ jobs: - name: set up JDK 1.8 uses: actions/setup-java@v1 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '8' - name: Grant execute permission for gradlew run: chmod +x gradlew From 78cfa32a83332be7c4b6c3f6805306bb32b0b144 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 06:51:33 +0900 Subject: [PATCH 11/24] Fix navigating away from post view causing app resize --- .../awais/instagrabber/fragments/PostViewV2Fragment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 65315e35..f3dd3cc8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -18,6 +18,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.widget.Toast; import androidx.annotation.NonNull; @@ -29,6 +30,7 @@ import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.PermissionChecker; +import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LiveData; @@ -1427,8 +1429,10 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme if (toolbar != null) { toolbar.setVisibility(View.VISIBLE); } - final View decorView = activity.getWindow().getDecorView(); + final Window window = activity.getWindow(); + final View decorView = window.getDecorView(); decorView.setSystemUiVisibility(originalSystemUi); + WindowCompat.setDecorFitsSystemWindows(window, false); isInFullScreenMode = false; } From 988033f5fd0a7e593ba31dc5290bb56ae6ee0c9c Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 07:07:27 +0900 Subject: [PATCH 12/24] Fix verified, private icons in profile details shifting when scrolling --- .../fragments/main/ProfileFragment.java | 4 +- .../res/layout/layout_profile_details.xml | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index e9870987..da752a26 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -527,7 +527,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { - profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.GONE); + profileDetailsBinding.countsDivider.getRoot().setVisibility(View.GONE); profileDetailsBinding.mainProfileImage.setVisibility(View.INVISIBLE); fetchProfileDetails(); } @@ -748,7 +748,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getProfilePicUrl()); profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE); - profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.VISIBLE); + profileDetailsBinding.countsDivider.getRoot().setVisibility(View.VISIBLE); final long followersCount = profileModel.getFollowerCount(); final long followingCount = profileModel.getFollowingCount(); diff --git a/app/src/main/res/layout/layout_profile_details.xml b/app/src/main/res/layout/layout_profile_details.xml index d7f5bdd8..61cec0ff 100644 --- a/app/src/main/res/layout/layout_profile_details.xml +++ b/app/src/main/res/layout/layout_profile_details.xml @@ -15,10 +15,11 @@ android:transitionName="profile_pic" android:visibility="invisible" app:actualImageScaleType="centerCrop" - app:layout_constraintBottom_toBottomOf="@id/btnTagged" + app:layout_constraintBottom_toTopOf="@id/top_barrier" app:layout_constraintEnd_toStartOf="@id/btnFollow" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" tools:foreground="@mipmap/ic_launcher" tools:visibility="visible" /> @@ -112,7 +113,7 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_outline_person_pin_24" app:chipIconTint="@color/deep_orange_800" - app:layout_constraintBottom_toTopOf="@+id/mainFullName" + app:layout_constraintBottom_toTopOf="@+id/top_barrier" app:layout_constraintStart_toEndOf="@id/mainProfileImage" app:layout_constraintTop_toBottomOf="@id/fav_chip" app:rippleColor="@color/deep_orange_400" @@ -128,12 +129,18 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_round_send_24" app:chipIconTint="@color/green" - app:layout_constraintBottom_toTopOf="@+id/mainFullName" + app:layout_constraintBottom_toTopOf="@+id/top_barrier" app:layout_constraintStart_toEndOf="@id/btnTagged" app:layout_constraintTop_toBottomOf="@id/fav_chip" app:rippleColor="@color/green" tools:visibility="visible" /> + + @@ -233,6 +237,7 @@ android:paddingBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/profileContext" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/mainBiography" @@ -254,7 +259,7 @@ android:textSize="12sp" android:textStyle="italic" android:visibility="gone" - app:layout_constraintBottom_toTopOf="@id/counts_barrier" + app:layout_constraintBottom_toTopOf="@id/counts_divider" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/mainUrl" @@ -262,11 +267,12 @@ tools:visibility="visible" /> Date: Sat, 15 May 2021 07:38:00 +0900 Subject: [PATCH 13/24] BottomNavBar: update deprecated method calls --- .../java/awais/instagrabber/utils/NavigationExtensions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java index a6bccbfd..bc0f3cb4 100644 --- a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java +++ b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java @@ -63,7 +63,7 @@ public class NavigationExtensions { selectedItemTag = graphIdToTagMap.get(bottomNavigationView.getSelectedItemId()); final String firstFragmentTag = graphIdToTagMap.get(firstFragmentGraphId); isOnFirstFragment = selectedItemTag != null && selectedItemTag.equals(firstFragmentTag); - bottomNavigationView.setOnNavigationItemSelectedListener(item -> { + bottomNavigationView.setOnItemSelectedListener(item -> { if (fragmentManager.isStateSaved()) { return false; } @@ -169,7 +169,7 @@ public class NavigationExtensions { private static void setupItemReselected(final BottomNavigationView bottomNavigationView, final SparseArray graphIdToTagMap, final FragmentManager fragmentManager) { - bottomNavigationView.setOnNavigationItemReselectedListener(item -> { + bottomNavigationView.setOnItemReselectedListener(item -> { final String newlySelectedItemTag = graphIdToTagMap.get(item.getItemId()); final Fragment fragmentByTag = fragmentManager.findFragmentByTag(newlySelectedItemTag); if (fragmentByTag == null) { From 1e94c73e100598db7c66d79bb4967a284cd6ed3b Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 08:21:38 +0900 Subject: [PATCH 14/24] Fix feed stories not rendered if app goes in background immediately after startup. Fixes austinhuang0131/barinsta#1258 --- .../awais/instagrabber/fragments/main/FeedFragment.java | 9 ++++++++- app/src/main/res/xml/header_list_scene.xml | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index ea54ceca..f068f069 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -337,6 +337,12 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre return super.onOptionsItemSelected(item); } + @Override + public void onResume() { + super.onResume(); + binding.getRoot().postDelayed(feedStoriesAdapter::notifyDataSetChanged, 1000); + } + @Override public void onRefresh() { binding.feedRecyclerView.refresh(); @@ -418,15 +424,16 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void fetchStories() { + if (storiesFetching) return; // final String cookie = settingsHelper.getString(Constants.COOKIE); storiesFetching = true; updateSwipeRefreshState(); storiesService.getFeedStories(new ServiceCallback>() { @Override public void onSuccess(final List result) { + storiesFetching = false; feedStoriesViewModel.getList().postValue(result); feedStoriesAdapter.submitList(result); - storiesFetching = false; if (storyListMenu != null) storyListMenu.setVisible(true); updateSwipeRefreshState(); } diff --git a/app/src/main/res/xml/header_list_scene.xml b/app/src/main/res/xml/header_list_scene.xml index 522990ee..f4a76d88 100644 --- a/app/src/main/res/xml/header_list_scene.xml +++ b/app/src/main/res/xml/header_list_scene.xml @@ -10,11 +10,11 @@ motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" />
From eb1e55470a27bf267cc1d7a64213cda879408a7d Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 11:46:15 +0900 Subject: [PATCH 15/24] Fix null menuItemView warnings --- .../directmessages/DirectMessageInboxFragment.java | 12 ++++++++++-- .../DirectMessageThreadFragment.java | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java index 60a7482e..d8a98087 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java @@ -16,6 +16,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; @@ -27,6 +28,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; +import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.snackbar.Snackbar; import java.util.List; @@ -102,7 +104,9 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh super.onPause(); unregisterReceiver(); isPendingRequestTotalBadgeAttached = false; - if (pendingRequestTotalBadgeDrawable != null) { + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + if (pendingRequestTotalBadgeDrawable != null && menuItemView != null) { BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); pendingRequestTotalBadgeDrawable = null; } @@ -217,7 +221,11 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh pendingRequestTotalBadgeDrawable = BadgeDrawable.create(context); } if (count == null || count == 0) { - BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId()); + } isPendingRequestTotalBadgeAttached = false; pendingRequestTotalBadgeDrawable.setNumber(0); pendingRequestsMenuItem.setVisible(false); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 99d72a57..d4db3192 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -31,6 +31,7 @@ import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; @@ -56,6 +57,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; +import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; @@ -554,7 +556,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } isPendingRequestCountBadgeAttached = false; if (pendingRequestCountBadgeDrawable != null) { - BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + } pendingRequestCountBadgeDrawable = null; } } @@ -838,7 +844,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact pendingRequestCountBadgeDrawable = BadgeDrawable.create(context); } if (count == null || count == 0) { - BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils + .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); + if (menuItemView != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + } isPendingRequestCountBadgeAttached = false; pendingRequestCountBadgeDrawable.setNumber(0); return; From 5daec513baa351dc6ee5b219f1170f5c33dcfeb3 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 11:47:30 +0900 Subject: [PATCH 16/24] Fix post view button colors wrong for some themes --- .../fragments/PostViewV2Fragment.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index f3dd3cc8..35249824 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -102,7 +102,6 @@ import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; -import static awais.instagrabber.utils.Utils.getAttrValue; import static awais.instagrabber.utils.Utils.settingsHelper; public class PostViewV2Fragment extends Fragment implements EditTextDialogFragment.EditTextDialogFragmentCallback { @@ -133,6 +132,9 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private boolean isInFullScreenMode; private StyledPlayerView playerView; private int playerViewOriginalHeight; + private Drawable originalRootBackground; + private ColorStateList originalLikeColorStateList; + private ColorStateList originalSaveColorStateList; private final Observer backStackSavedStateObserver = result -> { if (result == null) return; @@ -143,7 +145,6 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme // clear result backStackSavedStateResultLiveData.postValue(null); }; - private Drawable originalRootBackground; public void setOnDeleteListener(final OnDeleteListener onDeleteListener) { if (onDeleteListener == null) return; @@ -443,6 +444,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme } private void setupLike() { + originalLikeColorStateList = bottom.like.getIconTint(); final boolean likableMedia = viewModel.hasPk() /*&& viewModel.getMedia().isCommentLikesEnabled()*/; if (!likableMedia) { bottom.like.setVisibility(View.GONE); @@ -505,25 +507,25 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private void setLikedResources(final boolean liked) { final int iconResource; - final int tintResource; + final ColorStateList tintColorStateList; final Context context = getContext(); if (context == null) return; final Resources resources = context.getResources(); if (resources == null) return; if (liked) { iconResource = R.drawable.ic_like; - tintResource = resources.getColor(R.color.red_600); - // textResId = R.string.unlike_without_count; + tintColorStateList = ColorStateList.valueOf(resources.getColor(R.color.red_600)); } else { iconResource = R.drawable.ic_not_liked; - tintResource = getAttrValue(context, R.attr.colorPrimary); - // textResId = R.string.like_without_count; + tintColorStateList = originalLikeColorStateList != null ? originalLikeColorStateList + : ColorStateList.valueOf(resources.getColor(R.color.white)); } bottom.like.setIconResource(iconResource); - bottom.like.setIconTint(ColorStateList.valueOf(tintResource)); + bottom.like.setIconTint(tintColorStateList); } private void setupSave() { + originalSaveColorStateList = bottom.save.getIconTint(); if (!viewModel.isLoggedIn() || !viewModel.hasPk() || !viewModel.getMedia().canViewerSave()) { bottom.save.setVisibility(View.GONE); return; @@ -576,22 +578,21 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private void setSavedResources(final boolean saved) { final int iconResource; - final int tintResource; + final ColorStateList tintColorStateList; final Context context = getContext(); if (context == null) return; final Resources resources = context.getResources(); if (resources == null) return; if (saved) { iconResource = R.drawable.ic_bookmark; - tintResource = resources.getColor(R.color.blue_700); - // textResId = R.string.saved; + tintColorStateList = ColorStateList.valueOf(resources.getColor(R.color.blue_700)); } else { iconResource = R.drawable.ic_round_bookmark_border_24; - tintResource = getAttrValue(context, R.attr.colorPrimary); - // textResId = R.string.save; + tintColorStateList = originalSaveColorStateList != null ? originalSaveColorStateList + : ColorStateList.valueOf(resources.getColor(R.color.white)); } bottom.save.setIconResource(iconResource); - bottom.save.setIconTint(ColorStateList.valueOf(tintResource)); + bottom.save.setIconTint(tintColorStateList); } private void setupProfilePic(final User user) { From 2f44255584d566fa3586670728a1822584c47cd5 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 11:47:43 +0900 Subject: [PATCH 17/24] Some refactoring --- app/src/main/java/awais/instagrabber/utils/ViewUtils.java | 2 -- app/src/main/res/values/color.xml | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java index 3e371045..c2c91e61 100644 --- a/app/src/main/java/awais/instagrabber/utils/ViewUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ViewUtils.java @@ -104,7 +104,6 @@ public final class ViewUtils { } else { hiddenSuppressLayout($this$suppressLayoutCompat, suppress); } - } private static boolean tryHiddenSuppressLayout = true; @@ -118,6 +117,5 @@ public final class ViewUtils { tryHiddenSuppressLayout = false; } } - } } diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index a4ea3e6f..5059c946 100755 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -19,9 +19,9 @@ @color/text_color_light #FF5500 - #FFFFFFFF + @color/white #FFBB00 - #FF000000 + @color/black #efefef From e1057c87815ed6147102db9dcf3d4e134e280517 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 12:44:03 +0900 Subject: [PATCH 18/24] Fix sending videos in DM. Fixes austinhuang0131/barinsta#1217 --- .../fragments/directmessages/DirectMessageThreadFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index d4db3192..eb97b60f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -1117,7 +1117,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (!isAdded()) return; if (!entry.isVideo) { navigateToImageEditFragment(entry.path); + return; } + handleSentMessage(viewModel.sendUri(entry)); }); mediaPicker.show(getChildFragmentManager(), "MediaPicker"); }); From e4c4f099e5cbf0554d7dab09b2124f4a74e1e092 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 12:47:22 +0900 Subject: [PATCH 19/24] Make location single line with ellipsis. Fixes austinhuang0131/barinsta#1212 --- app/src/main/res/layout/item_feed_top.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/item_feed_top.xml b/app/src/main/res/layout/item_feed_top.xml index 7aac41b5..61b49c31 100755 --- a/app/src/main/res/layout/item_feed_top.xml +++ b/app/src/main/res/layout/item_feed_top.xml @@ -43,7 +43,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/title" + android:ellipsize="end" android:gravity="center_vertical" + android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" android:textSize="15sp" android:visibility="visible" From b3cd83ad31ce174f84258889abf4a28392e0a570 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 13:21:58 +0900 Subject: [PATCH 20/24] Fix clicking story from story list after searching opens wrong story. Fixes austinhuang0131/barinsta#1189 --- .../adapters/FeedStoriesListAdapter.java | 40 ++++++++++++------- .../viewholder/StoryListViewHolder.java | 3 +- .../fragments/StoryListViewerFragment.java | 14 ++++--- .../instagrabber/models/FeedStoryModel.java | 24 ++++------- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java index 9d7401e0..146904d0 100755 --- a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java @@ -23,23 +23,29 @@ public final class FeedStoriesListAdapter extends ListAdapter list; private final Filter filter = new Filter() { - @Nullable + @NonNull @Override protected FilterResults performFiltering(final CharSequence filter) { - final boolean isFilterEmpty = TextUtils.isEmpty(filter); - final String query = isFilterEmpty ? null : filter.toString().toLowerCase(); - - for (FeedStoryModel item : list) { - if (isFilterEmpty) item.setShown(true); - else item.setShown(item.getProfileModel().getUsername().toLowerCase().contains(query)); + final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase(); + List filteredList = list; + if (list != null && query != null) { + filteredList = list.stream() + .filter(feedStoryModel -> feedStoryModel.getProfileModel() + .getUsername() + .toLowerCase() + .contains(query)) + .collect(Collectors.toList()); } - return null; + final FilterResults filterResults = new FilterResults(); + filterResults.count = filteredList != null ? filteredList.size() : 0; + filterResults.values = filteredList; + return filterResults; } @Override protected void publishResults(final CharSequence constraint, final FilterResults results) { - submitList(list); - notifyDataSetChanged(); + //noinspection unchecked + submitList((List) results.values, true); } }; @@ -65,10 +71,16 @@ public final class FeedStoriesListAdapter extends ListAdapter list, final boolean isFiltered) { + if (!isFiltered) { + this.list = list; + } + super.submitList(list); + } + @Override public void submitList(final List list) { - super.submitList(list.stream().filter(i -> i.isShown()).collect(Collectors.toList())); - this.list = list; + submitList(list, false); } @NonNull @@ -82,11 +94,11 @@ public final class FeedStoriesListAdapter extends ListAdapter { if (notificationClickListener == null) return; - notificationClickListener.onFeedStoryClick(model, position); + notificationClickListener.onFeedStoryClick(model); }); } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java index f4bf129a..f8bdfb81 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java @@ -23,9 +23,12 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.common.collect.Iterables; + import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import awais.instagrabber.R; import awais.instagrabber.adapters.FeedStoriesListAdapter; @@ -58,15 +61,17 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr private StoriesService storiesService; private Context context; private String type; - private String currentQuery; private String endCursor = null; private FeedStoriesListAdapter adapter; - private MenuItem menuSearch; private final OnFeedStoryClickListener clickListener = new OnFeedStoryClickListener() { @Override - public void onFeedStoryClick(final FeedStoryModel model, final int position) { + public void onFeedStoryClick(final FeedStoryModel model) { if (model == null) return; + final List feedStoryModels = feedStoriesViewModel.getList().getValue(); + if (feedStoryModels == null) return; + final int position = Iterables.indexOf(feedStoryModels, feedStoryModel -> feedStoryModel != null + && Objects.equals(feedStoryModel.getStoryMediaId(), model.getStoryMediaId())); final NavDirections action = StoryListViewerFragmentDirections .actionStoryListFragmentToStoryViewerFragment(StoryViewerOptions.forFeedStoryPosition(position)); NavHostFragment.findNavController(StoryListViewerFragment.this).navigate(action); @@ -153,7 +158,7 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr @Override public void onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.search, menu); - menuSearch = menu.findItem(R.id.action_search); + final MenuItem menuSearch = menu.findItem(R.id.action_search); final SearchView searchView = (SearchView) menuSearch.getActionView(); searchView.setQueryHint(getResources().getString(R.string.action_search)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @@ -166,7 +171,6 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr @Override public boolean onQueryTextChange(final String query) { if (adapter != null) { - currentQuery = query; adapter.getFilter().filter(query); } return true; diff --git a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java index 04595093..b87091e5 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java @@ -16,11 +16,15 @@ public final class FeedStoryModel implements Serializable { private final boolean isLive, isBestie; private final long timestamp; private final int mediaCount; - private boolean isShown = true; - public FeedStoryModel(final String storyMediaId, final User profileModel, final boolean fullyRead, - final long timestamp, final StoryModel firstStoryModel, final int mediaCount, - final boolean isLive, final boolean isBestie) { + public FeedStoryModel(final String storyMediaId, + final User profileModel, + final boolean fullyRead, + final long timestamp, + final StoryModel firstStoryModel, + final int mediaCount, + final boolean isLive, + final boolean isBestie) { this.storyMediaId = storyMediaId; this.profileModel = profileModel; this.fullyRead = fullyRead; @@ -52,10 +56,6 @@ public final class FeedStoryModel implements Serializable { return profileModel; } - // public void setFirstStoryModel(final StoryModel firstStoryModel) { - // this.firstStoryModel = firstStoryModel; - // } - public StoryModel getFirstStoryModel() { return firstStoryModel; } @@ -75,12 +75,4 @@ public final class FeedStoryModel implements Serializable { public boolean isBestie() { return isBestie; } - - public boolean isShown() { - return isShown; - } - - public void setShown(final boolean shown) { - isShown = shown; - } } \ No newline at end of file From 37edc0171e5e8d05bfdd78d53380ee1ef4087973 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 15 May 2021 13:27:51 +0900 Subject: [PATCH 21/24] Pause story player in onPause instead of releasing it. Fixes austinhuang0131/barinsta#1060 --- .../awais/instagrabber/fragments/StoryViewerFragment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index 5ef039c7..5cc6887b 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -275,7 +275,9 @@ public class StoryViewerFragment extends Fragment { @Override public void onPause() { super.onPause(); - releasePlayer(); + if (player != null) { + player.pause(); + } } @Override From f89f0ef542f9dd9970f0ce638f4989adf62395e8 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 15 May 2021 12:52:31 -0400 Subject: [PATCH 22/24] add a simple user constructor --- .../repositories/responses/User.java | 55 +++++++++++++-- .../responses/search/SearchItem.java | 8 +-- .../instagrabber/utils/ResponseBodyUtils.java | 6 +- .../viewmodels/CommentsViewerViewModel.java | 6 +- .../webservices/GraphQLService.java | 35 +--------- .../webservices/StoriesService.java | 68 +------------------ 6 files changed, 57 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.java b/app/src/main/java/awais/instagrabber/repositories/responses/User.java index b2f2a0b2..716735e3 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.java @@ -28,11 +28,10 @@ public class User implements Serializable { private final long usertagsCount; private final String publicEmail; private final HdProfilePicUrlInfo hdProfilePicUrlInfo; - private final String profileContext; - private final List profileContextLinksWithUserIds; - private final String socialContext; - // if a DM member is a Facebook user, this is present - private final String interopMessagingUserFbid; + private final String profileContext; // "also followed by" your friends + private final List profileContextLinksWithUserIds; // ^ + private final String socialContext; // AYML + private final String interopMessagingUserFbid; // in DMs only: Facebook user ID public User(final long pk, final String username, @@ -90,6 +89,52 @@ public class User implements Serializable { this.interopMessagingUserFbid = interopMessagingUserFbid; } + public User(final long pk, + final String username, + final String fullName, + final boolean isPrivate, + final String profilePicUrl, + final boolean isVerified) { + this.pk = pk; + this.username = username; + this.fullName = fullName; + this.isPrivate = isPrivate; + this.profilePicUrl = profilePicUrl; + this.profilePicId = null; + this.friendshipStatus = new FriendshipStatus( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ); + this.isVerified = isVerified; + this.hasAnonymousProfilePicture = false; + this.isUnpublished = false; + this.isFavorite = false; + this.isDirectappInstalled = false; + this.reelAutoArchive = null; + this.allowedCommenterType = null; + this.mediaCount = 0; + this.followerCount = 0; + this.followingCount = 0; + this.followingTagCount = 0; + this.biography = null; + this.externalUrl = null; + this.usertagsCount = 0; + this.publicEmail = null; + this.hdProfilePicUrlInfo = null; + this.profileContext = null; + this.profileContextLinksWithUserIds = null; + this.socialContext = null; + this.interopMessagingUserFbid = null; + } + public long getPk() { return pk; } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java index f99c7dfd..9f56b765 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java @@ -191,9 +191,7 @@ public class SearchItem { recentSearch.getName(), false, recentSearch.getPicUrl(), - null, null, false, false, false, false, false, - null, null, 0, 0, 0, 0, null, null, - 0, null, null, null, null, null, null + false ); } @@ -205,9 +203,7 @@ public class SearchItem { favorite.getDisplayName(), false, favorite.getPicUrl(), - null, null, false, false, false, false, false, - null, null, 0, 0, 0, 0, null, null, - 0, null, null, null, null, null, null + false ); } diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index b3968fdd..f205b04f 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -767,11 +767,7 @@ public final class ResponseBodyUtils { owner.optString("full_name"), false, owner.optString("profile_pic_url"), - null, - friendshipStatus, - owner.optBoolean("is_verified"), - false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, - null, null, null, null); + owner.optBoolean("is_verified")); } final String id = feedItem.getString(Constants.EXTRAS_ID); VideoVersion videoVersion = null; diff --git a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java index 109b6fea..ab42e84d 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java @@ -238,11 +238,7 @@ public class CommentsViewerViewModel extends ViewModel { null, false, owner.getString("profile_pic_url"), - null, - new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), - owner.optBoolean("is_verified"), - false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null, - null, null); + owner.optBoolean("is_verified")); final JSONObject likedBy = commentJsonObject.optJSONObject("edge_liked_by"); final String commentId = commentJsonObject.getString("id"); final JSONObject childCommentsJsonObject = commentJsonObject.optJSONObject("edge_threaded_comments"); diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java index eef7af9a..20cf6918 100644 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -34,7 +34,6 @@ import retrofit2.Response; public class GraphQLService extends BaseService { private static final String TAG = "GraphQLService"; - // private static final boolean loadFromMock = false; private final GraphQLRepository repository; @@ -230,39 +229,7 @@ public class GraphQLService extends BaseService { userObject.optString("full_name"), userObject.optBoolean("is_private"), userObject.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userObject.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userObject.optBoolean("is_verified") )); // userModels.add(new ProfileModel(userObject.optBoolean("is_private"), // false, diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index 169f630a..96e9bc7d 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -143,39 +143,7 @@ public class StoriesService extends BaseService { userJson.optString("full_name"), userJson.optBoolean("is_private"), userJson.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userJson.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userJson.optBoolean("is_verified") ); final long timestamp = node.getLong("latest_reel_media"); final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; @@ -210,39 +178,7 @@ public class StoriesService extends BaseService { userJson.optString("full_name"), userJson.optBoolean("is_private"), userJson.getString("profile_pic_url"), - null, - new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ), - userJson.optBoolean("is_verified"), - false, - false, - false, - false, - null, - null, - 0, - 0, - 0, - 0, - null, - null, - 0, - null, - null, - null, - null, - null, - null + userJson.optBoolean("is_verified") ); feedStoryModels.add(new FeedStoryModel( node.getString("id"), From fc70129c969ca525550c4247d6c49f87db3639b8 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 15 May 2021 12:53:00 -0400 Subject: [PATCH 23/24] close #1040 (probably) --- .../instagrabber/fragments/main/ProfileFragment.java | 4 ++-- .../instagrabber/repositories/responses/User.java | 12 ++++++++++-- .../instagrabber/webservices/GraphQLService.java | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index da752a26..cc646855 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -407,7 +407,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } chainingMenuItem = menu.findItem(R.id.chaining); if (chainingMenuItem != null) { - chainingMenuItem.setVisible(isNotMe); + chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining()); } } @@ -976,7 +976,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().isMuting() ? R.string.unmute_posts : R.string.mute_posts); } if (chainingMenuItem != null) { - chainingMenuItem.setVisible(true); + chainingMenuItem.setVisible(profileModel.hasChaining()); } } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.java b/app/src/main/java/awais/instagrabber/repositories/responses/User.java index 716735e3..0d707f72 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.java @@ -17,6 +17,7 @@ public class User implements Serializable { private final boolean isUnpublished; private final boolean isFavorite; private final boolean isDirectappInstalled; + private final boolean hasChaining; private final String reelAutoArchive; private final String allowedCommenterType; private final long mediaCount; @@ -45,6 +46,7 @@ public class User implements Serializable { final boolean isUnpublished, final boolean isFavorite, final boolean isDirectappInstalled, + final boolean hasChaining, final String reelAutoArchive, final String allowedCommenterType, final long mediaCount, @@ -72,6 +74,7 @@ public class User implements Serializable { this.isUnpublished = isUnpublished; this.isFavorite = isFavorite; this.isDirectappInstalled = isDirectappInstalled; + this.hasChaining = hasChaining; this.reelAutoArchive = reelAutoArchive; this.allowedCommenterType = allowedCommenterType; this.mediaCount = mediaCount; @@ -118,6 +121,7 @@ public class User implements Serializable { this.isUnpublished = false; this.isFavorite = false; this.isDirectappInstalled = false; + this.hasChaining = false; this.reelAutoArchive = null; this.allowedCommenterType = null; this.mediaCount = 0; @@ -194,6 +198,10 @@ public class User implements Serializable { return isDirectappInstalled; } + public boolean hasChaining() { + return hasChaining; + } + public String getReelAutoArchive() { return reelAutoArchive; } @@ -282,7 +290,7 @@ public class User implements Serializable { @Override public int hashCode() { return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, - isUnpublished, isFavorite, isDirectappInstalled, reelAutoArchive, allowedCommenterType, mediaCount, followerCount, - followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); + isUnpublished, isFavorite, isDirectappInstalled, hasChaining, reelAutoArchive, allowedCommenterType, mediaCount, + followerCount, followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); } } diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java index 20cf6918..ddb6da6b 100644 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -324,6 +324,7 @@ public class GraphQLService extends BaseService { false, false, false, + false, null, null, timelineMedia.getLong("count"), From 23b711984690ee33ca52d1a9183d434972aba01f Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 15 May 2021 20:40:02 -0400 Subject: [PATCH 24/24] null check to avoid launch crash immediately hitting the feed tab after launch will produce the following crash, so this resolves it by a null check: ``` java.lang.NullPointerException: Attempt to read from field 'awais.instagrabber.customviews.PostsRecyclerView awais.instagrabber.databinding.FragmentFeedBinding.feedRecyclerView' on a null object reference at awais.instagrabber.fragments.main.FeedFragment.scrollToTop(FeedFragment.java:461) at awais.instagrabber.utils.NavigationExtensions.lambda$setupItemReselected$2(NavigationExtensions.java:190) at awais.instagrabber.utils.-$$Lambda$NavigationExtensions$C3II1R-NOFB80ERAxio06uf3Qto.onNavigationItemReselected(Unknown Source:4) ... ``` --- .../awais/instagrabber/fragments/main/FeedFragment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index f068f069..d22b5a51 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -458,8 +458,10 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } public void scrollToTop() { - binding.feedRecyclerView.smoothScrollToPosition(0); - // binding.storiesContainer.setExpanded(true); + if (binding != null) { + binding.feedRecyclerView.smoothScrollToPosition(0); + // binding.storiesContainer.setExpanded(true); + } } private boolean isSafeToNavigate(final NavController navController) {