From 6bf59e83ad09beb9f0619d21f47d93ff7ddcdd6d Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 17 Oct 2020 19:07:03 +0900 Subject: [PATCH 01/35] Initial commit for new posts layout --- app/build.gradle | 24 +- .../instagrabber/InstaGrabberApplication.java | 4 + .../awais/instagrabber/activities/Login.java | 12 +- .../instagrabber/activities/MainActivity.java | 26 +- .../instagrabber/adapters/FeedAdapter.java | 2 +- .../instagrabber/adapters/FeedAdapterV2.java | 169 +++ .../adapters/MultiSelectListAdapter.java | 4 +- .../adapters/PostViewAdapter.java | 152 +- .../adapters/PostViewerChildAdapter.java | 9 +- .../adapters/SliderCallbackAdapter.java | 15 + .../adapters/SliderItemsAdapter.java | 153 ++ .../viewholder/FeedGridItemViewHolder.java | 147 ++ .../viewholder/PostMediaViewHolder.java | 2 +- .../viewholder/PostViewerViewHolder.java | 479 +++---- .../viewholder/SliderItemViewHolder.java | 21 + .../viewholder/SliderPhotoViewHolder.java | 122 ++ .../viewholder/SliderVideoViewHolder.java | 171 +++ .../viewholder/feed/FeedItemViewHolder.java | 36 +- .../viewholder/feed/FeedPhotoViewHolder.java | 87 +- .../viewholder/feed/FeedSliderViewHolder.java | 300 ++-- .../viewholder/feed/FeedVideoViewHolder.java | 158 +-- .../animations/ResizeAnimation.java | 45 + .../instagrabber/asyncs/DownloadAsync.java | 7 +- .../asyncs/DownloadV2AsyncTask.java | 310 ++++ .../instagrabber/asyncs/FeedFetcher.java | 181 ++- .../asyncs/FeedPostFetchService.java | 54 + .../instagrabber/asyncs/PostFetcher.java | 129 +- .../instagrabber/asyncs/PostsFetcher.java | 4 +- .../instagrabber/asyncs/i/iLikedFetcher.java | 5 +- .../instagrabber/asyncs/i/iPostFetcher.java | 122 +- .../customviews/CircularImageView.java | 87 +- .../customviews/PostsRecyclerView.java | 259 ++++ .../customviews/ProfilePicView.java | 141 ++ .../customviews/RamboTextView.java | 21 +- ...SharedElementTransitionDialogFragment.java | 302 ++++ .../customviews/VerticalDragHelper.java | 203 +++ .../VideoPlayerCallbackAdapter.java | 18 + .../customviews/VideoPlayerViewHelper.java | 373 +++++ .../drawee/DefaultZoomableController.java | 20 +- .../drawee/DraggableZoomableDraweeView.java | 98 ++ .../MultiZoomableControllerListener.java | 7 + .../drawee/ZoomableController.java | 2 + .../drawee/ZoomableDraweeView.java | 44 +- .../helpers/GridSpacingItemDecoration.java | 19 +- .../customviews/helpers/PostFetcher.java | 50 + .../helpers/RecyclerLazyLoader.java | 58 +- .../helpers/RecyclerLazyLoaderAtBottom.java | 51 + .../helpers/VideoAwareRecyclerScroller.java | 206 +-- .../PostsLayoutPreferencesDialogFragment.java | 219 +++ .../directdownload/DirectDownload.java | 19 +- .../directdownload/MultiDirectDialog.java | 27 +- .../fragments/LocationFragment.java | 4 +- .../fragments/PostViewFragment.java | 682 +++++---- .../fragments/PostViewV2Fragment.java | 1245 +++++++++++++++++ .../fragments/StoryViewerFragment.java | 24 +- .../fragments/main/FeedFragment.java | 512 +++---- .../fragments/main/ProfileFragment.java | 66 +- .../interfaces/FetchListener.java | 5 +- .../instagrabber/models/BasePostModel.java | 7 +- .../awais/instagrabber/models/FeedModel.java | 235 +++- .../awais/instagrabber/models/PostChild.java | 121 ++ .../awais/instagrabber/models/PostModel.java | 14 +- .../models/PostsLayoutPreferences.java | 204 +++ .../instagrabber/models/ViewerPostModel.java | 177 ++- .../models/ViewerPostModelWrapper.java | 10 +- .../repositories/FeedRepository.java | 12 + .../responses/FeedFetchResponse.java | 29 + .../services/ActivityCheckerService.java | 5 +- .../awais/instagrabber/utils/Constants.java | 6 +- .../instagrabber/utils/DownloadUtils.java | 263 +++- .../utils/NavigationExtensions.java | 1 + .../awais/instagrabber/utils/NumberUtils.java | 4 + .../instagrabber/utils/SettingsHelper.java | 3 +- .../awais/instagrabber/utils/TextUtils.java | 20 + .../java/awais/instagrabber/utils/Utils.java | 12 + .../instagrabber/webservices/FeedService.java | 307 ++++ .../webservices/StoriesService.java | 115 +- .../instagrabber/workers/DownloadWorker.java | 382 +++++ app/src/main/res/anim/dialog_anim_in.xml | 18 + app/src/main/res/anim/dialog_anim_out.xml | 19 + .../res/drawable/background_grey_ripple.xml | 3 + .../drawable/ic_border_style_flipped_24.xml | 10 + .../drawable/ic_checkbox_multiple_blank.xml | 11 + .../ic_checkbox_multiple_blank_stroke.xml | 13 + app/src/main/res/drawable/ic_class_24.xml | 10 + app/src/main/res/drawable/ic_dashboard_24.xml | 10 + app/src/main/res/drawable/ic_forward_5_24.xml | 13 + app/src/main/res/drawable/ic_notes_24.xml | 10 + app/src/main/res/drawable/ic_pause_24.xml | 10 + .../main/res/drawable/ic_play_arrow_24.xml | 10 + .../drawable/ic_play_circle_outline_24.xml | 10 + .../{ic_profile.xml => ic_profile_24.xml} | 0 app/src/main/res/drawable/ic_profile_40.xml | 10 + app/src/main/res/drawable/ic_profile_48.xml | 10 + app/src/main/res/drawable/ic_replay_5_24.xml | 13 + .../res/drawable/ic_rounded_corner_24.xml | 10 + .../main/res/drawable/ic_view_agenda_24.xml | 10 + app/src/main/res/drawable/ic_view_grid_24.xml | 11 + .../drawable/popup_background_exoplayer.xml | 8 + .../main/res/drawable/rounder_corner_bg.xml | 5 + .../drawable/rounder_corner_semi_black_bg.xml | 9 +- .../main/res/drawable/shape_oval_light.xml | 5 + .../layout/dialog_post_layout_preferences.xml | 252 ++++ app/src/main/res/layout/dialog_post_view.xml | 322 +++++ app/src/main/res/layout/fragment_feed.xml | 9 +- app/src/main/res/layout/fragment_profile.xml | 5 +- app/src/main/res/layout/item_child_post.xml | 3 +- app/src/main/res/layout/item_feed_grid.xml | 108 ++ app/src/main/res/layout/item_feed_photo.xml | 15 +- app/src/main/res/layout/item_feed_slider.xml | 12 +- app/src/main/res/layout/item_feed_top.xml | 4 +- app/src/main/res/layout/item_feed_video.xml | 45 +- app/src/main/res/layout/item_slider_photo.xml | 9 + app/src/main/res/layout/layout_dm_base.xml | 7 +- app/src/main/res/layout/layout_dm_media.xml | 3 +- .../res/layout/layout_exo_custom_controls.xml | 120 ++ .../layout_video_player_with_thumbnail.xml | 35 + app/src/main/res/menu/feed_menu.xml | 8 + .../logged_out_bottom_navigation_menu.xml | 2 +- .../res/menu/main_bottom_navigation_menu.xml | 2 +- app/src/main/res/menu/speed_menu.xml | 27 + app/src/main/res/values-land/dimens.xml | 1 - app/src/main/res/values/attrs.xml | 11 + app/src/main/res/values/color.xml | 1 + app/src/main/res/values/dimens.xml | 8 +- app/src/main/res/values/strings.xml | 49 +- app/src/main/res/values/styles.xml | 53 + app/src/main/resources/feed_response.json | 1 + app/src/main/resources/stories_response.json | 1 + .../instagrabber/utils/DownloadUtilsTest.java | 31 + .../instagrabber/utils/TextUtilsTest.java | 18 + build.gradle | 3 +- 132 files changed, 9030 insertions(+), 2012 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/SliderItemViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java create mode 100644 app/src/main/java/awais/instagrabber/asyncs/DownloadV2AsyncTask.java create mode 100644 app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/SharedElementTransitionDialogFragment.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/VerticalDragHelper.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/DraggableZoomableDraweeView.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java create mode 100644 app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java create mode 100644 app/src/main/java/awais/instagrabber/models/PostChild.java create mode 100644 app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/FeedRepository.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java create mode 100644 app/src/main/java/awais/instagrabber/webservices/FeedService.java create mode 100644 app/src/main/java/awais/instagrabber/workers/DownloadWorker.java create mode 100644 app/src/main/res/anim/dialog_anim_in.xml create mode 100644 app/src/main/res/anim/dialog_anim_out.xml create mode 100644 app/src/main/res/drawable/background_grey_ripple.xml create mode 100644 app/src/main/res/drawable/ic_border_style_flipped_24.xml create mode 100644 app/src/main/res/drawable/ic_checkbox_multiple_blank.xml create mode 100644 app/src/main/res/drawable/ic_checkbox_multiple_blank_stroke.xml create mode 100644 app/src/main/res/drawable/ic_class_24.xml create mode 100644 app/src/main/res/drawable/ic_dashboard_24.xml create mode 100644 app/src/main/res/drawable/ic_forward_5_24.xml create mode 100644 app/src/main/res/drawable/ic_notes_24.xml create mode 100644 app/src/main/res/drawable/ic_pause_24.xml create mode 100644 app/src/main/res/drawable/ic_play_arrow_24.xml create mode 100644 app/src/main/res/drawable/ic_play_circle_outline_24.xml rename app/src/main/res/drawable/{ic_profile.xml => ic_profile_24.xml} (100%) mode change 100755 => 100644 create mode 100644 app/src/main/res/drawable/ic_profile_40.xml create mode 100644 app/src/main/res/drawable/ic_profile_48.xml create mode 100644 app/src/main/res/drawable/ic_replay_5_24.xml create mode 100644 app/src/main/res/drawable/ic_rounded_corner_24.xml create mode 100644 app/src/main/res/drawable/ic_view_agenda_24.xml create mode 100644 app/src/main/res/drawable/ic_view_grid_24.xml create mode 100644 app/src/main/res/drawable/popup_background_exoplayer.xml create mode 100644 app/src/main/res/drawable/rounder_corner_bg.xml create mode 100644 app/src/main/res/drawable/shape_oval_light.xml create mode 100644 app/src/main/res/layout/dialog_post_layout_preferences.xml create mode 100644 app/src/main/res/layout/dialog_post_view.xml create mode 100644 app/src/main/res/layout/item_feed_grid.xml create mode 100644 app/src/main/res/layout/item_slider_photo.xml create mode 100644 app/src/main/res/layout/layout_exo_custom_controls.xml create mode 100644 app/src/main/res/layout/layout_video_player_with_thumbnail.xml create mode 100644 app/src/main/res/menu/feed_menu.xml create mode 100644 app/src/main/res/menu/speed_menu.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/resources/feed_response.json create mode 100644 app/src/main/resources/stories_response.json create mode 100644 app/src/test/java/awais/instagrabber/utils/DownloadUtilsTest.java create mode 100644 app/src/test/java/awais/instagrabber/utils/TextUtilsTest.java diff --git a/app/build.gradle b/app/build.gradle index 3fa4bc3f..ec04f426 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,6 +20,9 @@ android { } compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } @@ -37,24 +40,29 @@ android { } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' + def appcompat_version = "1.2.0" def nav_version = "2.3.0" - def preference_version = "1.1.1" - implementation 'com.google.android.material:material:1.3.0-alpha02' - implementation 'com.google.android.exoplayer:exoplayer:2.11.1' + implementation 'com.google.android.material:material:1.3.0-alpha03' + implementation 'com.google.android.exoplayer:exoplayer:2.12.0' implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" - implementation "androidx.recyclerview:recyclerview:1.2.0-alpha05" + implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" - implementation "androidx.constraintlayout:constraintlayout:2.0.1" - implementation "androidx.preference:preference:$preference_version" + implementation "androidx.constraintlayout:constraintlayout:2.0.2" + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.work:work-runtime:2.4.0" + + implementation 'com.google.guava:guava:27.0.1-android' // implementation 'com.github.hendrawd:StorageUtil:1.1.0' + implementation 'com.github.armcha:AutoLinkTextViewV2:2.1.1' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.facebook.fresco:fresco:2.3.0' @@ -63,5 +71,7 @@ dependencies { implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0' } diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java index 827db5ea..5f87ed0e 100644 --- a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java +++ b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java @@ -32,12 +32,16 @@ public final class InstaGrabberApplication extends Application { @Override public void onCreate() { super.onCreate(); + // final Set requestListeners = new HashSet<>(); + // requestListeners.add(new RequestLoggingListener()); final ImagePipelineConfig imagePipelineConfig = ImagePipelineConfig .newBuilder(this) // .setMainDiskCacheConfig(diskCacheConfig) + // .setRequestListeners(requestListeners) .setDownsampleEnabled(true) .build(); Fresco.initialize(this, imagePipelineConfig); + // FLog.setMinimumLoggingLevel(FLog.VERBOSE); if (BuildConfig.DEBUG) { try { diff --git a/app/src/main/java/awais/instagrabber/activities/Login.java b/app/src/main/java/awais/instagrabber/activities/Login.java index a7f06b67..37083794 100755 --- a/app/src/main/java/awais/instagrabber/activities/Login.java +++ b/app/src/main/java/awais/instagrabber/activities/Login.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; +import android.view.LayoutInflater; import android.view.View; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; @@ -59,7 +60,7 @@ public final class Login extends BaseLanguageActivity implements View.OnClickLis @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - loginBinding = ActivityLoginBinding.inflate(getLayoutInflater()); + loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(getApplicationContext())); setContentView(loginBinding.getRoot()); initWebView(); @@ -117,12 +118,9 @@ public final class Login extends BaseLanguageActivity implements View.OnClickLis webSettings.setDisplayZoomControls(false); webSettings.setLoadWithOverviewMode(true); webSettings.setUseWideViewPort(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - webSettings.setAllowFileAccessFromFileURLs(true); - webSettings.setAllowUniversalAccessFromFileURLs(true); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + webSettings.setAllowFileAccessFromFileURLs(true); + webSettings.setAllowUniversalAccessFromFileURLs(true); + webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 79439bdc..05faef57 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.res.TypedArray; import android.database.MatrixCursor; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -94,7 +95,9 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage R.id.notificationsViewer, R.id.themePreferencesFragment, R.id.favoritesFragment, - R.id.backupPreferencesFragment); + R.id.backupPreferencesFragment + // , R.id.postViewV2Fragment + ); private static final Map NAV_TO_MENU_ID_MAP = new HashMap<>(); private static final List REMOVE_COLLAPSING_TOOLBAR_SCROLL_DESTINATIONS = Collections.singletonList(R.id.commentsViewerFragment); private static final String FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex"; @@ -157,10 +160,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage bindActivityCheckerService(); } getSupportFragmentManager().addOnBackStackChangedListener(this); - - // Log.d("austin_debug", "dir: "+Arrays.toString(StorageUtil.getStorageDirectories(getApplicationContext()))); - // final File sdcard = new File(StorageUtil.getStorageDirectories(getApplicationContext())[0]); - // Log.d("austin_debug", "files: "+Arrays.toString(sdcard.listFiles())); } @Override @@ -222,7 +221,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage @Override public void onBackPressed() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTaskRoot() && isBackStackEmpty) { + if (isTaskRoot() && isBackStackEmpty) { finishAfterTransition(); } else { super.onBackPressed(); @@ -626,4 +625,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage public BottomNavigationView getBottomNavView() { return binding.bottomNavView; } + + public void fitSystemWindows(final boolean fit) { + binding.appBarLayout.setBackground(null); + binding.appBarLayout.setFitsSystemWindows(fit); + binding.collapsingToolbarLayout.setBackground(null); + binding.collapsingToolbarLayout.setFitsSystemWindows(fit); + final Drawable toolbarBackground = binding.toolbar.getBackground(); + binding.toolbar.setFitsSystemWindows(fit); + binding.toolbar.setBackground(null); + binding.toolbar.setClickable(false); + } + + public int getNavHostContainerId() { + return binding.mainNavHost.getId(); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java index 09623f3f..af8e8476 100755 --- a/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java @@ -83,7 +83,7 @@ public final class FeedAdapter extends ListAdapter {}); } @Override diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java b/app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java new file mode 100644 index 00000000..a9e78333 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java @@ -0,0 +1,169 @@ +package awais.instagrabber.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.adapters.viewholder.FeedGridItemViewHolder; +import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder; +import awais.instagrabber.adapters.viewholder.feed.FeedPhotoViewHolder; +import awais.instagrabber.adapters.viewholder.feed.FeedSliderViewHolder; +import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder; +import awais.instagrabber.customviews.RamboTextView; +import awais.instagrabber.databinding.ItemFeedGridBinding; +import awais.instagrabber.databinding.ItemFeedPhotoBinding; +import awais.instagrabber.databinding.ItemFeedSliderBinding; +import awais.instagrabber.databinding.ItemFeedVideoBinding; +import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostsLayoutPreferences; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.utils.Utils; + +public final class FeedAdapterV2 extends ListAdapter { + private static final String TAG = "FeedAdapterV2"; + + private PostsLayoutPreferences layoutPreferences; + private OnPostClickListener postClickListener; + private int lastAnimatedPosition; + + private final View.OnClickListener clickListener; + private final MentionClickListener mentionClickListener; + private final View.OnLongClickListener longClickListener = v -> { + final Object tag; + if (v instanceof RamboTextView && (tag = v.getTag()) instanceof FeedModel) + Utils.copyText(v.getContext(), ((FeedModel) tag).getPostCaption()); + return true; + }; + + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final FeedModel oldItem, @NonNull final FeedModel newItem) { + return oldItem.getPostId().equals(newItem.getPostId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final FeedModel oldItem, @NonNull final FeedModel newItem) { + return oldItem.getPostId().equals(newItem.getPostId()); + } + }; + + public FeedAdapterV2(@NonNull final PostsLayoutPreferences layoutPreferences, + final View.OnClickListener clickListener, + final MentionClickListener mentionClickListener, + final OnPostClickListener postClickListener) { + super(DIFF_CALLBACK); + this.layoutPreferences = layoutPreferences; + this.clickListener = clickListener; + this.mentionClickListener = mentionClickListener; + this.postClickListener = postClickListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final Context context = parent.getContext(); + final LayoutInflater layoutInflater = LayoutInflater.from(context); + switch (layoutPreferences.getType()) { + case LINEAR: + return getLinearViewHolder(parent, layoutInflater, viewType); + case GRID: + case STAGGERED_GRID: + default: + final ItemFeedGridBinding binding = ItemFeedGridBinding.inflate(layoutInflater, parent, false); + return new FeedGridItemViewHolder(binding); + } + } + + @NonNull + private RecyclerView.ViewHolder getLinearViewHolder(@NonNull final ViewGroup parent, + final LayoutInflater layoutInflater, + final int viewType) { + switch (MediaItemType.valueOf(viewType)) { + case MEDIA_TYPE_VIDEO: { + final ItemFeedVideoBinding binding = ItemFeedVideoBinding.inflate(layoutInflater, parent, false); + return new FeedVideoViewHolder(binding, mentionClickListener, clickListener, longClickListener); + } + case MEDIA_TYPE_SLIDER: { + final ItemFeedSliderBinding binding = ItemFeedSliderBinding.inflate(layoutInflater, parent, false); + return new FeedSliderViewHolder(binding, mentionClickListener, clickListener, longClickListener); + } + case MEDIA_TYPE_IMAGE: + default: { + final ItemFeedPhotoBinding binding = ItemFeedPhotoBinding.inflate(layoutInflater, parent, false); + return new FeedPhotoViewHolder(binding, mentionClickListener, clickListener, longClickListener); + } + } + } + + @Override + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int position) { + final FeedModel feedModel = getItem(position); + if (feedModel == null) return; + feedModel.setPosition(position); + switch (layoutPreferences.getType()) { + case LINEAR: + ((FeedItemViewHolder) viewHolder).bind(feedModel, postClickListener); + break; + case GRID: + case STAGGERED_GRID: + default: + final boolean animate = position > lastAnimatedPosition; + ((FeedGridItemViewHolder) viewHolder).bind(feedModel, layoutPreferences, postClickListener, false); + } + lastAnimatedPosition = position; + } + + @Override + public int getItemViewType(final int position) { + return getItem(position).getItemType().getId(); + } + + @Override + public void onViewDetachedFromWindow(@NonNull final RecyclerView.ViewHolder viewHolder) { + switch (layoutPreferences.getType()) { + case LINEAR: + ((FeedItemViewHolder) viewHolder).clearAnimation(); + break; + case GRID: + case STAGGERED_GRID: + default: + ((FeedGridItemViewHolder) viewHolder).clearAnimation(); + } + } + + public void setLayoutPreferences(@NonNull final PostsLayoutPreferences layoutPreferences) { + this.layoutPreferences = layoutPreferences; + } + + // @Override + // public void onViewAttachedToWindow(@NonNull final FeedItemViewHolder holder) { + // super.onViewAttachedToWindow(holder); + // // Log.d(TAG, "attached holder: " + holder); + // if (!(holder instanceof FeedSliderViewHolder)) return; + // final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder; + // feedSliderViewHolder.startPlayingVideo(); + // } + // + // @Override + // public void onViewDetachedFromWindow(@NonNull final FeedItemViewHolder holder) { + // super.onViewDetachedFromWindow(holder); + // // Log.d(TAG, "detached holder: " + holder); + // if (!(holder instanceof FeedSliderViewHolder)) return; + // final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder; + // feedSliderViewHolder.stopPlayingVideo(); + // } + + public interface OnPostClickListener { + void onPostClick(final FeedModel feedModel, + final View profilePicView, + final View mainPostImage); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/MultiSelectListAdapter.java b/app/src/main/java/awais/instagrabber/adapters/MultiSelectListAdapter.java index d196b2c9..9b8fe04b 100644 --- a/app/src/main/java/awais/instagrabber/adapters/MultiSelectListAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/MultiSelectListAdapter.java @@ -12,8 +12,8 @@ public abstract class MultiSelectListAdapter { private boolean isSelecting; - private final OnItemClickListener internalOnItemClickListener; - private final OnItemLongClickListener internalOnLongItemClickListener; + private OnItemClickListener internalOnItemClickListener; + private OnItemLongClickListener internalOnLongItemClickListener; private final List selectedItems = new ArrayList<>(); diff --git a/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java b/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java index 305f04cd..f49b8974 100644 --- a/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java @@ -1,77 +1,75 @@ -package awais.instagrabber.adapters; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; - -import java.util.Arrays; - -import awais.instagrabber.adapters.viewholder.PostViewerViewHolder; -import awais.instagrabber.databinding.ItemFullPostViewBinding; -import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.ViewerPostModelWrapper; - -public class PostViewAdapter extends ListAdapter { - private final OnPostViewChildViewClickListener clickListener; - private final OnPostCaptionLongClickListener longClickListener; - private final MentionClickListener mentionClickListener; - - private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull final ViewerPostModelWrapper oldItem, - @NonNull final ViewerPostModelWrapper newItem) { - return oldItem.getPosition() == newItem.getPosition(); - } - - @Override - public boolean areContentsTheSame(@NonNull final ViewerPostModelWrapper oldItem, - @NonNull final ViewerPostModelWrapper newItem) { - return Arrays.equals(oldItem.getViewerPostModels(), newItem.getViewerPostModels()); - } - }; - - public PostViewAdapter(final OnPostViewChildViewClickListener clickListener, - final OnPostCaptionLongClickListener longClickListener, - final MentionClickListener mentionClickListener) { - super(diffCallback); - this.clickListener = clickListener; - this.longClickListener = longClickListener; - this.mentionClickListener = mentionClickListener; - } - - @Override - public void onViewDetachedFromWindow(@NonNull final PostViewerViewHolder holder) { - holder.stopPlayingVideo(); - } - - @NonNull - @Override - public PostViewerViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, - final int viewType) { - final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); - final ItemFullPostViewBinding binding = ItemFullPostViewBinding - .inflate(layoutInflater, parent, false); - return new PostViewerViewHolder(binding); - } - - @Override - public void onBindViewHolder(@NonNull final PostViewerViewHolder holder, final int position) { - final ViewerPostModelWrapper item = getItem(position); - holder.bind(item, position, clickListener, longClickListener, mentionClickListener); - } - - public interface OnPostViewChildViewClickListener { - void onClick(View v, - ViewerPostModelWrapper viewerPostModelWrapper, - int postPosition, - int childPosition); - } - - public interface OnPostCaptionLongClickListener { - void onLongClick(String text); - } -} +// package awais.instagrabber.adapters; +// +// import android.view.LayoutInflater; +// import android.view.View; +// import android.view.ViewGroup; +// +// import androidx.annotation.NonNull; +// import androidx.recyclerview.widget.DiffUtil; +// import androidx.recyclerview.widget.ListAdapter; +// +// import awais.instagrabber.adapters.viewholder.PostViewerViewHolder; +// import awais.instagrabber.databinding.ItemFullPostViewBinding; +// import awais.instagrabber.interfaces.MentionClickListener; +// import awais.instagrabber.models.ViewerPostModelWrapper; +// +// public class PostViewAdapter extends ListAdapter { +// private final OnPostViewChildViewClickListener clickListener; +// private final OnPostCaptionLongClickListener longClickListener; +// private final MentionClickListener mentionClickListener; +// +// private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { +// @Override +// public boolean areItemsTheSame(@NonNull final ViewerPostModelWrapper oldItem, +// @NonNull final ViewerPostModelWrapper newItem) { +// return oldItem.getPosition() == newItem.getPosition(); +// } +// +// @Override +// public boolean areContentsTheSame(@NonNull final ViewerPostModelWrapper oldItem, +// @NonNull final ViewerPostModelWrapper newItem) { +// return oldItem.getViewerPostModels().equals(newItem.getViewerPostModels()); +// } +// }; +// +// public PostViewAdapter(final OnPostViewChildViewClickListener clickListener, +// final OnPostCaptionLongClickListener longClickListener, +// final MentionClickListener mentionClickListener) { +// super(diffCallback); +// this.clickListener = clickListener; +// this.longClickListener = longClickListener; +// this.mentionClickListener = mentionClickListener; +// } +// +// @Override +// public void onViewDetachedFromWindow(@NonNull final PostViewerViewHolder holder) { +// holder.stopPlayingVideo(); +// } +// +// @NonNull +// @Override +// public PostViewerViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, +// final int viewType) { +// final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); +// final ItemFullPostViewBinding binding = ItemFullPostViewBinding +// .inflate(layoutInflater, parent, false); +// return new PostViewerViewHolder(binding); +// } +// +// @Override +// public void onBindViewHolder(@NonNull final PostViewerViewHolder holder, final int position) { +// final ViewerPostModelWrapper item = getItem(position); +// holder.bind(item, position, clickListener, longClickListener, mentionClickListener); +// } +// +// public interface OnPostViewChildViewClickListener { +// void onClick(View v, +// ViewerPostModelWrapper viewerPostModelWrapper, +// int postPosition, +// int childPosition); +// } +// +// public interface OnPostCaptionLongClickListener { +// void onLongClick(String text); +// } +// } diff --git a/app/src/main/java/awais/instagrabber/adapters/PostViewerChildAdapter.java b/app/src/main/java/awais/instagrabber/adapters/PostViewerChildAdapter.java index ca3e59e0..d94f9ff3 100644 --- a/app/src/main/java/awais/instagrabber/adapters/PostViewerChildAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/PostViewerChildAdapter.java @@ -15,6 +15,7 @@ import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -171,8 +172,9 @@ public class PostViewerChildAdapter extends ListAdapter { + + private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener; + private final SliderCallback sliderCallback; + private final awais.instagrabber.databinding.LayoutExoCustomControlsBinding controlsBinding; + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final PostChild oldItem, @NonNull final PostChild newItem) { + return oldItem.getPostId().equals(newItem.getPostId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final PostChild oldItem, @NonNull final PostChild newItem) { + return oldItem.getPostId().equals(newItem.getPostId()); + } + }; + + public SliderItemsAdapter() { + this(null, null, null); + } + + public SliderItemsAdapter(final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener, + final LayoutExoCustomControlsBinding controlsBinding, + final SliderCallback sliderCallback) { + super(DIFF_CALLBACK); + this.onVerticalDragListener = onVerticalDragListener; + this.sliderCallback = sliderCallback; + this.controlsBinding = controlsBinding; + } + + @NonNull + @Override + public SliderItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final MediaItemType mediaItemType = MediaItemType.valueOf(viewType); + switch (mediaItemType) { + case MEDIA_TYPE_VIDEO: { + final LayoutVideoPlayerWithThumbnailBinding binding = LayoutVideoPlayerWithThumbnailBinding.inflate(inflater, parent, false); + return new SliderVideoViewHolder(binding, onVerticalDragListener, controlsBinding); + } + case MEDIA_TYPE_IMAGE: + default: + final ItemSliderPhotoBinding binding = ItemSliderPhotoBinding.inflate(inflater, parent, false); + return new SliderPhotoViewHolder(binding, onVerticalDragListener); + } + } + + @Override + public void onBindViewHolder(@NonNull final SliderItemViewHolder holder, final int position) { + final PostChild model = getItem(position); + holder.bind(model, position, sliderCallback); + } + + @Override + public int getItemViewType(final int position) { + final PostChild viewerPostModel = getItem(position); + return viewerPostModel.getItemType().getId(); + } + + // @NonNull + // @Override + // public Object instantiateItem(@NonNull final ViewGroup container, final int position) { + // final Context context = container.getContext(); + // final ViewerPostModel sliderItem = sliderItems.get(position); + // + // if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + // final ViewSwitcher viewSwitcher = createViewSwitcher(context, position, sliderItem.getThumbnailUrl(), sliderItem.getDisplayUrl()); + // container.addView(viewSwitcher); + // return viewSwitcher; + // } + // final GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(container.getResources()) + // .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) + // .build(); + // final SimpleDraweeView photoView = new SimpleDraweeView(context, hierarchy); + // photoView.setLayoutParams(layoutParams); + // final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(sliderItem.getDisplayUrl())) + // .setLocalThumbnailPreviewsEnabled(true) + // .setProgressiveRenderingEnabled(true) + // .build(); + // photoView.setImageRequest(imageRequest); + // container.addView(photoView); + // return photoView; + // } + + // @NonNull + // private ViewSwitcher createViewSwitcher(final Context context, + // final int position, + // final String thumbnailUrl, + // final String displayUrl) { + // + // final ViewSwitcher viewSwitcher = new ViewSwitcher(context); + // viewSwitcher.setLayoutParams(layoutParams); + // + // final FrameLayout frameLayout = new FrameLayout(context); + // frameLayout.setLayoutParams(layoutParams); + // + // final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(context.getResources()) + // .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) + // .build(); + // final SimpleDraweeView simpleDraweeView = new SimpleDraweeView(context, hierarchy); + // simpleDraweeView.setLayoutParams(layoutParams); + // simpleDraweeView.setImageURI(thumbnailUrl); + // frameLayout.addView(simpleDraweeView); + // + // final AppCompatImageView imageView = new AppCompatImageView(context); + // final int px = Utils.convertDpToPx(50); + // final FrameLayout.LayoutParams playButtonLayoutParams = new FrameLayout.LayoutParams(px, px); + // playButtonLayoutParams.gravity = Gravity.CENTER; + // imageView.setLayoutParams(playButtonLayoutParams); + // imageView.setImageResource(R.drawable.exo_icon_play); + // frameLayout.addView(imageView); + // + // viewSwitcher.addView(frameLayout); + // + // final PlayerView playerView = new PlayerView(context); + // viewSwitcher.addView(playerView); + // if (shouldAutoPlay && position == 0) { + // loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener); + // } else + // frameLayout.setOnClickListener(v -> loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener)); + // return viewSwitcher; + // } + + public interface SliderCallback { + void onThumbnailLoaded(int position); + + void onItemClicked(int position); + + void onPlayerPlay(int position); + + void onPlayerPause(int position); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java new file mode 100644 index 00000000..2e7d72b4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java @@ -0,0 +1,147 @@ +package awais.instagrabber.adapters.viewholder; + +import android.graphics.drawable.Animatable; +import android.net.Uri; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import androidx.annotation.DimenRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.databinding.ItemFeedGridBinding; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.models.PostsLayoutPreferences; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.models.PostsLayoutPreferences.PostsLayoutType.STAGGERED_GRID; + +public class FeedGridItemViewHolder extends RecyclerView.ViewHolder { + private final ItemFeedGridBinding binding; + + public FeedGridItemViewHolder(@NonNull final ItemFeedGridBinding binding) { + super(binding.getRoot()); + this.binding = binding; + // for rounded borders (clip view to background shape) + // + } + + public void bind(@NonNull final FeedModel feedModel, + @NonNull final PostsLayoutPreferences layoutPreferences, + final FeedAdapterV2.OnPostClickListener postClickListener, + final boolean animate) { + if (postClickListener != null) { + itemView.setOnClickListener(v -> postClickListener.onPostClick(feedModel, binding.profilePic, binding.postImage)); + } + itemView.setClipToOutline(layoutPreferences.getHasRoundedCorners()); + if (layoutPreferences.getType() == STAGGERED_GRID) { + final float aspectRatio = (float) feedModel.getImageWidth() / feedModel.getImageHeight(); + binding.postImage.setAspectRatio(aspectRatio); + } else { + binding.postImage.setAspectRatio(1); + } + if (layoutPreferences.isAvatarVisible()) { + binding.profilePic.setVisibility(View.VISIBLE); + binding.profilePic.setImageURI(feedModel.getProfileModel().getSdProfilePic()); + final ViewGroup.LayoutParams layoutParams = binding.profilePic.getLayoutParams(); + @DimenRes final int dimenRes; + switch (layoutPreferences.getProfilePicSize()) { + case SMALL: + dimenRes = R.dimen.profile_pic_size_small; + break; + case TINY: + dimenRes = R.dimen.profile_pic_size_tiny; + break; + default: + case REGULAR: + dimenRes = R.dimen.profile_pic_size_regular; + break; + } + final int dimensionPixelSize = itemView.getResources().getDimensionPixelSize(dimenRes); + layoutParams.width = dimensionPixelSize; + layoutParams.height = dimensionPixelSize; + binding.profilePic.requestLayout(); + } else { + binding.profilePic.setVisibility(View.GONE); + } + if (layoutPreferences.isNameVisible()) { + binding.name.setVisibility(View.VISIBLE); + binding.name.setText(feedModel.getProfileModel().getName()); + } else { + binding.name.setVisibility(View.GONE); + } + String thumbnailUrl = null; + final int typeIconRes; + switch (feedModel.getItemType()) { + case MEDIA_TYPE_IMAGE: + typeIconRes = -1; + thumbnailUrl = feedModel.getThumbnailUrl(); + break; + case MEDIA_TYPE_VIDEO: + thumbnailUrl = feedModel.getThumbnailUrl(); + typeIconRes = R.drawable.exo_icon_play; + break; + case MEDIA_TYPE_SLIDER: + final List sliderItems = feedModel.getSliderItems(); + if (sliderItems != null) { + thumbnailUrl = sliderItems.get(0).getThumbnailUrl(); + } + typeIconRes = R.drawable.ic_checkbox_multiple_blank_stroke; + break; + default: + typeIconRes = -1; + thumbnailUrl = null; + } + if (TextUtils.isEmpty(thumbnailUrl)) { + binding.postImage.setController(null); + return; + } + if (typeIconRes <= 0) { + binding.typeIcon.setVisibility(View.GONE); + } else { + binding.typeIcon.setVisibility(View.VISIBLE); + binding.typeIcon.setImageResource(typeIconRes); + } + final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)) + .setLocalThumbnailPreviewsEnabled(true) + .setProgressiveRenderingEnabled(true) + .build(); + final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder() + .setImageRequest(requestBuilder) + .setOldController(binding.postImage.getController()); + if (animate) { + final BaseControllerListener imageListener = new BaseControllerListener() { + @Override + public void onFinalImageSet(final String id, final ImageInfo imageInfo, final Animatable animatable) { + setAnimation(binding.getRoot()); + } + }; + builder.setControllerListener(imageListener); + } + binding.postImage.setController(builder.build()); + } + + private void setAnimation(View viewToAnimate) { + final Animation animation = AnimationUtils.loadAnimation(viewToAnimate.getContext(), android.R.anim.fade_in); + animation.setDuration(300); + viewToAnimate.startAnimation(animation); + } + + public void clearAnimation() { + binding.getRoot().clearAnimation(); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java index 2c81475a..8d247dcf 100755 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostMediaViewHolder.java @@ -24,6 +24,6 @@ public final class PostMediaViewHolder extends RecyclerView.ViewHolder { itemView.setOnClickListener(clickListener); binding.selectedView.setVisibility(model.isCurrentSlide() ? View.VISIBLE : View.GONE); binding.isDownloaded.setVisibility(model.isDownloaded() ? View.VISIBLE : View.GONE); - binding.icon.setImageURI(model.getSliderDisplayUrl()); + binding.icon.setImageURI(model.getDisplayUrl()); } } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java index 7bc44b71..dcb2f9fc 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java @@ -1,239 +1,240 @@ -package awais.instagrabber.adapters.viewholder; - -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.util.Log; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ui.PlayerView; - -import java.util.ArrayList; -import java.util.List; - -import awais.instagrabber.R; -import awais.instagrabber.adapters.PostViewAdapter.OnPostCaptionLongClickListener; -import awais.instagrabber.adapters.PostViewAdapter.OnPostViewChildViewClickListener; -import awais.instagrabber.adapters.PostViewerChildAdapter; -import awais.instagrabber.databinding.ItemFullPostViewBinding; -import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.models.ViewerPostModel; -import awais.instagrabber.models.ViewerPostModelWrapper; -import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; - -public class PostViewerViewHolder extends RecyclerView.ViewHolder { - private static final String TAG = "PostViewerViewHolder"; - - private final ItemFullPostViewBinding binding; - private int currentChildPosition; - - public PostViewerViewHolder(@NonNull final ItemFullPostViewBinding binding) { - super(binding.getRoot()); - this.binding = binding; - binding.topPanel.viewStoryPost.setVisibility(View.GONE); - } - - public void bind(final ViewerPostModelWrapper wrapper, - final int position, - final OnPostViewChildViewClickListener clickListener, - final OnPostCaptionLongClickListener longClickListener, - final MentionClickListener mentionClickListener) { - if (wrapper == null) return; - final ViewerPostModel[] items = wrapper.getViewerPostModels(); - if (items == null || items.length <= 0) return; - if (items[0] == null) return; - final PostViewerChildAdapter adapter = new PostViewerChildAdapter(); - binding.mediaViewPager.setAdapter(adapter); - final ViewerPostModel firstPost = items[0]; - setPostInfo(firstPost, mentionClickListener); - setMediaItems(items, adapter); - setupListeners(wrapper, - position, - clickListener, - longClickListener, - mentionClickListener, - firstPost.getLocation()); - } - - private void setPostInfo(final ViewerPostModel firstPost, - final MentionClickListener mentionClickListener) { - final ProfileModel profileModel = firstPost.getProfileModel(); - if (profileModel == null) return; - binding.topPanel.title.setText(profileModel.getUsername()); - final String locationName = firstPost.getLocationName(); - if (!TextUtils.isEmpty(locationName)) { - binding.topPanel.location.setVisibility(View.VISIBLE); - binding.topPanel.location.setText(locationName); - } else binding.topPanel.location.setVisibility(View.GONE); - binding.topPanel.ivProfilePic.setImageURI(profileModel.getSdProfilePic()); - binding.bottomPanel.commentsCount.setText(String.valueOf(firstPost.getCommentsCount())); - final CharSequence postCaption = firstPost.getPostCaption(); - if (TextUtils.hasMentions(postCaption)) { - binding.bottomPanel.viewerCaption.setMentionClickListener(mentionClickListener); - binding.bottomPanel.viewerCaption - .setText(TextUtils.getMentionText(postCaption), TextView.BufferType.SPANNABLE); - } else { - binding.bottomPanel.viewerCaption.setMentionClickListener(null); - binding.bottomPanel.viewerCaption.setText(postCaption); - } - binding.bottomPanel.tvPostDate.setText(firstPost.getPostDate()); - setupLikes(firstPost); - setupSave(firstPost); - } - - private void setupLikes(final ViewerPostModel firstPost) { - final boolean liked = firstPost.getLike(); - final long likeCount = firstPost.getLikes(); - final Resources resources = itemView.getContext().getResources(); - if (liked) { - final String unlikeString = resources.getString(R.string.unlike, String.valueOf(likeCount)); - binding.btnLike.setText(unlikeString); - ViewCompat.setBackgroundTintList(binding.btnLike, - ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_pink_background))); - } else { - final String likeString = resources.getString(R.string.like, String.valueOf(likeCount)); - binding.btnLike.setText(likeString); - ViewCompat.setBackgroundTintList(binding.btnLike, - ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightpink_background))); - } - } - - private void setupSave(final ViewerPostModel firstPost) { - final boolean saved = firstPost.getBookmark(); - if (saved) { - binding.btnBookmark.setText(R.string.unbookmark); - ViewCompat.setBackgroundTintList(binding.btnBookmark, - ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_orange_background))); - } else { - binding.btnBookmark.setText(R.string.bookmark); - ViewCompat.setBackgroundTintList( - binding.btnBookmark, - ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightorange_background))); - } - } - - private void setupListeners(final ViewerPostModelWrapper wrapper, - final int position, - final OnPostViewChildViewClickListener clickListener, - final OnPostCaptionLongClickListener longClickListener, - final MentionClickListener mentionClickListener, - final String location) { - final View.OnClickListener onClickListener = v -> clickListener - .onClick(v, wrapper, position, currentChildPosition); - binding.bottomPanel.btnComments.setOnClickListener(onClickListener); - binding.topPanel.title.setOnClickListener(onClickListener); - binding.topPanel.ivProfilePic.setOnClickListener(onClickListener); - binding.bottomPanel.btnDownload.setOnClickListener(onClickListener); - binding.bottomPanel.viewerCaption.setOnClickListener(onClickListener); - binding.btnLike.setOnClickListener(onClickListener); - binding.btnBookmark.setOnClickListener(onClickListener); - binding.bottomPanel.viewerCaption.setOnLongClickListener(v -> { - longClickListener.onLongClick(binding.bottomPanel.viewerCaption.getText().toString()); - return true; - }); - if (!TextUtils.isEmpty(location)) { - binding.topPanel.location.setOnClickListener(v -> mentionClickListener - .onClick(binding.topPanel.location, location, false, true)); - } - } - - private void setMediaItems(final ViewerPostModel[] items, - final PostViewerChildAdapter adapter) { - final List filteredList = new ArrayList<>(); - for (final ViewerPostModel model : items) { - final MediaItemType itemType = model.getItemType(); - if (itemType == MediaItemType.MEDIA_TYPE_VIDEO || itemType == MediaItemType.MEDIA_TYPE_IMAGE) { - filteredList.add(model); - } - } - binding.mediaCounter.setVisibility(filteredList.size() > 1 ? View.VISIBLE : View.GONE); - final String counter = "1/" + filteredList.size(); - binding.mediaCounter.setText(counter); - adapter.submitList(filteredList); - binding.mediaViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(final int position) { - if (filteredList.size() <= 0 || position >= filteredList.size()) return; - currentChildPosition = position; - final String counter = (position + 1) + "/" + filteredList.size(); - binding.mediaCounter.setText(counter); - final ViewerPostModel viewerPostModel = filteredList.get(position); - if (viewerPostModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { - setVideoDetails(viewerPostModel); - setVolumeListener(position); - return; - } - setImageDetails(); - } - }); - } - - private void setVolumeListener(final int position) { - binding.bottomPanel.btnMute.setOnClickListener(v -> { - try { - final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager - .getChildAt(0)).findViewHolderForAdapterPosition(position); - if (viewHolder != null) { - final View itemView = viewHolder.itemView; - if (itemView instanceof PlayerView) { - final SimpleExoPlayer player = (SimpleExoPlayer) ((PlayerView) itemView) - .getPlayer(); - if (player == null) return; - final float vol = player.getVolume() == 0f ? 1f : 0f; - player.setVolume(vol); - binding.bottomPanel.btnMute.setImageResource(vol == 0f ? R.drawable.ic_volume_up_24 - : R.drawable.ic_volume_off_24); - Utils.sessionVolumeFull = vol == 1f; - } - } - } catch (Exception e) { - Log.e(TAG, "Error", e); - } - }); - } - - private void setImageDetails() { - binding.bottomPanel.btnMute.setVisibility(View.GONE); - binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); - } - - private void setVideoDetails(final ViewerPostModel viewerPostModel) { - binding.bottomPanel.btnMute.setVisibility(View.VISIBLE); - final long videoViews = viewerPostModel.getVideoViews(); - if (videoViews < 0) { - binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); - return; - } - binding.bottomPanel.tvVideoViews.setText(String.valueOf(videoViews)); - binding.bottomPanel.videoViewsContainer.setVisibility(View.VISIBLE); - } - - public void stopPlayingVideo() { - try { - final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager - .getChildAt(0)).findViewHolderForAdapterPosition(currentChildPosition); - if (viewHolder != null) { - final View itemView = viewHolder.itemView; - if (itemView instanceof PlayerView) { - final Player player = ((PlayerView) itemView).getPlayer(); - if (player != null) { - player.setPlayWhenReady(false); - } - } - } - } catch (Exception e) { - Log.e(TAG, "Error", e); - } - } -} +// package awais.instagrabber.adapters.viewholder; +// +// import android.content.res.ColorStateList; +// import android.content.res.Resources; +// import android.util.Log; +// import android.view.View; +// import android.widget.TextView; +// +// import androidx.annotation.NonNull; +// import androidx.core.content.ContextCompat; +// import androidx.core.view.ViewCompat; +// import androidx.recyclerview.widget.RecyclerView; +// import androidx.viewpager2.widget.ViewPager2; +// +// import com.google.android.exoplayer2.Player; +// import com.google.android.exoplayer2.SimpleExoPlayer; +// import com.google.android.exoplayer2.ui.PlayerView; +// +// import java.util.ArrayList; +// import java.util.List; +// +// import awais.instagrabber.R; +// import awais.instagrabber.adapters.PostViewAdapter.OnPostCaptionLongClickListener; +// import awais.instagrabber.adapters.PostViewAdapter.OnPostViewChildViewClickListener; +// import awais.instagrabber.adapters.PostViewerChildAdapter; +// import awais.instagrabber.databinding.ItemFullPostViewBinding; +// import awais.instagrabber.interfaces.MentionClickListener; +// import awais.instagrabber.models.PostChild; +// import awais.instagrabber.models.ProfileModel; +// import awais.instagrabber.models.ViewerPostModel; +// import awais.instagrabber.models.ViewerPostModelWrapper; +// import awais.instagrabber.models.enums.MediaItemType; +// import awais.instagrabber.utils.TextUtils; +// import awais.instagrabber.utils.Utils; +// +// public class PostViewerViewHolder extends RecyclerView.ViewHolder { +// private static final String TAG = "PostViewerViewHolder"; +// +// private final ItemFullPostViewBinding binding; +// private int currentChildPosition; +// +// public PostViewerViewHolder(@NonNull final ItemFullPostViewBinding binding) { +// super(binding.getRoot()); +// this.binding = binding; +// binding.topPanel.viewStoryPost.setVisibility(View.GONE); +// } +// +// public void bind(final ViewerPostModelWrapper wrapper, +// final int position, +// final OnPostViewChildViewClickListener clickListener, +// final OnPostCaptionLongClickListener longClickListener, +// final MentionClickListener mentionClickListener) { +// if (wrapper == null) return; +// final List items = wrapper.getViewerPostModels(); +// if (items == null || items.size() == 0) return; +// if (items.get(0) == null) return; +// final PostViewerChildAdapter adapter = new PostViewerChildAdapter(); +// binding.mediaViewPager.setAdapter(adapter); +// final PostChild firstPost = items.get(0); +// setPostInfo(firstPost, mentionClickListener); +// setMediaItems(items, adapter); +// setupListeners(wrapper, +// position, +// clickListener, +// longClickListener, +// mentionClickListener, +// firstPost.getLocation()); +// } +// +// private void setPostInfo(final PostChild firstPost, +// final MentionClickListener mentionClickListener) { +// final ProfileModel profileModel = firstPost.getProfileModel(); +// if (profileModel == null) return; +// binding.topPanel.title.setText(profileModel.getUsername()); +// final String locationName = firstPost.getLocationName(); +// if (!TextUtils.isEmpty(locationName)) { +// binding.topPanel.location.setVisibility(View.VISIBLE); +// binding.topPanel.location.setText(locationName); +// } else binding.topPanel.location.setVisibility(View.GONE); +// binding.topPanel.ivProfilePic.setImageURI(profileModel.getSdProfilePic()); +// binding.bottomPanel.commentsCount.setText(String.valueOf(firstPost.getCommentsCount())); +// final CharSequence postCaption = firstPost.getPostCaption(); +// if (TextUtils.hasMentions(postCaption)) { +// binding.bottomPanel.viewerCaption.setMentionClickListener(mentionClickListener); +// binding.bottomPanel.viewerCaption +// .setText(TextUtils.getMentionText(postCaption), TextView.BufferType.SPANNABLE); +// } else { +// binding.bottomPanel.viewerCaption.setMentionClickListener(null); +// binding.bottomPanel.viewerCaption.setText(postCaption); +// } +// binding.bottomPanel.tvPostDate.setText(firstPost.getPostDate()); +// setupLikes(firstPost); +// setupSave(firstPost); +// } +// +// private void setupLikes(final ViewerPostModel firstPost) { +// final boolean liked = firstPost.getLike(); +// final long likeCount = firstPost.getLikes(); +// final Resources resources = itemView.getContext().getResources(); +// if (liked) { +// final String unlikeString = resources.getString(R.string.unlike, String.valueOf(likeCount)); +// binding.btnLike.setText(unlikeString); +// ViewCompat.setBackgroundTintList(binding.btnLike, +// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_pink_background))); +// } else { +// final String likeString = resources.getString(R.string.like, String.valueOf(likeCount)); +// binding.btnLike.setText(likeString); +// ViewCompat.setBackgroundTintList(binding.btnLike, +// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightpink_background))); +// } +// } +// +// private void setupSave(final ViewerPostModel firstPost) { +// final boolean saved = firstPost.isSaved(); +// if (saved) { +// binding.btnBookmark.setText(R.string.unbookmark); +// ViewCompat.setBackgroundTintList(binding.btnBookmark, +// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_orange_background))); +// } else { +// binding.btnBookmark.setText(R.string.bookmark); +// ViewCompat.setBackgroundTintList( +// binding.btnBookmark, +// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightorange_background))); +// } +// } +// +// private void setupListeners(final ViewerPostModelWrapper wrapper, +// final int position, +// final OnPostViewChildViewClickListener clickListener, +// final OnPostCaptionLongClickListener longClickListener, +// final MentionClickListener mentionClickListener, +// final String location) { +// final View.OnClickListener onClickListener = v -> clickListener +// .onClick(v, wrapper, position, currentChildPosition); +// binding.bottomPanel.btnComments.setOnClickListener(onClickListener); +// binding.topPanel.title.setOnClickListener(onClickListener); +// binding.topPanel.ivProfilePic.setOnClickListener(onClickListener); +// binding.bottomPanel.btnDownload.setOnClickListener(onClickListener); +// binding.bottomPanel.viewerCaption.setOnClickListener(onClickListener); +// binding.btnLike.setOnClickListener(onClickListener); +// binding.btnBookmark.setOnClickListener(onClickListener); +// binding.bottomPanel.viewerCaption.setOnLongClickListener(v -> { +// longClickListener.onLongClick(binding.bottomPanel.viewerCaption.getText().toString()); +// return true; +// }); +// if (!TextUtils.isEmpty(location)) { +// binding.topPanel.location.setOnClickListener(v -> mentionClickListener +// .onClick(binding.topPanel.location, location, false, true)); +// } +// } +// +// private void setMediaItems(final List items, +// final PostViewerChildAdapter adapter) { +// final List filteredList = new ArrayList<>(); +// for (final ViewerPostModel model : items) { +// final MediaItemType itemType = model.getItemType(); +// if (itemType == MediaItemType.MEDIA_TYPE_VIDEO || itemType == MediaItemType.MEDIA_TYPE_IMAGE) { +// filteredList.add(model); +// } +// } +// binding.mediaCounter.setVisibility(filteredList.size() > 1 ? View.VISIBLE : View.GONE); +// final String counter = "1/" + filteredList.size(); +// binding.mediaCounter.setText(counter); +// adapter.submitList(filteredList); +// binding.mediaViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { +// @Override +// public void onPageSelected(final int position) { +// if (filteredList.size() <= 0 || position >= filteredList.size()) return; +// currentChildPosition = position; +// final String counter = (position + 1) + "/" + filteredList.size(); +// binding.mediaCounter.setText(counter); +// final ViewerPostModel viewerPostModel = filteredList.get(position); +// if (viewerPostModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { +// setVideoDetails(viewerPostModel); +// setVolumeListener(position); +// return; +// } +// setImageDetails(); +// } +// }); +// } +// +// private void setVolumeListener(final int position) { +// binding.bottomPanel.btnMute.setOnClickListener(v -> { +// try { +// final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager +// .getChildAt(0)).findViewHolderForAdapterPosition(position); +// if (viewHolder != null) { +// final View itemView = viewHolder.itemView; +// if (itemView instanceof PlayerView) { +// final SimpleExoPlayer player = (SimpleExoPlayer) ((PlayerView) itemView) +// .getPlayer(); +// if (player == null) return; +// final float vol = player.getVolume() == 0f ? 1f : 0f; +// player.setVolume(vol); +// binding.bottomPanel.btnMute.setImageResource(vol == 0f ? R.drawable.ic_volume_up_24 +// : R.drawable.ic_volume_off_24); +// Utils.sessionVolumeFull = vol == 1f; +// } +// } +// } catch (Exception e) { +// Log.e(TAG, "Error", e); +// } +// }); +// } +// +// private void setImageDetails() { +// binding.bottomPanel.btnMute.setVisibility(View.GONE); +// binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); +// } +// +// private void setVideoDetails(final ViewerPostModel viewerPostModel) { +// binding.bottomPanel.btnMute.setVisibility(View.VISIBLE); +// final long videoViews = viewerPostModel.getVideoViews(); +// if (videoViews < 0) { +// binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); +// return; +// } +// binding.bottomPanel.tvVideoViews.setText(String.valueOf(videoViews)); +// binding.bottomPanel.videoViewsContainer.setVisibility(View.VISIBLE); +// } +// +// public void stopPlayingVideo() { +// try { +// final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager +// .getChildAt(0)).findViewHolderForAdapterPosition(currentChildPosition); +// if (viewHolder != null) { +// final View itemView = viewHolder.itemView; +// if (itemView instanceof PlayerView) { +// final Player player = ((PlayerView) itemView).getPlayer(); +// if (player != null) { +// player.setPlayWhenReady(false); +// } +// } +// } +// } catch (Exception e) { +// Log.e(TAG, "Error", e); +// } +// } +// } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderItemViewHolder.java new file mode 100644 index 00000000..64b00fb6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderItemViewHolder.java @@ -0,0 +1,21 @@ +package awais.instagrabber.adapters.viewholder; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.adapters.SliderItemsAdapter; +import awais.instagrabber.models.PostChild; + +public abstract class SliderItemViewHolder extends RecyclerView.ViewHolder { + private static final String TAG = "FeedSliderItemViewHolder"; + + public SliderItemViewHolder(@NonNull final View itemView) { + super(itemView); + } + + public abstract void bind(final PostChild model, + final int position, + final SliderItemsAdapter.SliderCallback sliderCallback); +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java new file mode 100644 index 00000000..4788edc0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java @@ -0,0 +1,122 @@ +package awais.instagrabber.adapters.viewholder; + +import android.graphics.drawable.Animatable; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import awais.instagrabber.adapters.SliderItemsAdapter; +import awais.instagrabber.customviews.VerticalDragHelper; +import awais.instagrabber.customviews.drawee.AnimatedZoomableController; +import awais.instagrabber.databinding.ItemSliderPhotoBinding; +import awais.instagrabber.models.PostChild; + +public class SliderPhotoViewHolder extends SliderItemViewHolder { + private static final String TAG = "FeedSliderPhotoViewHolder"; + + private final ItemSliderPhotoBinding binding; + private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener; + + public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding, + final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener) { + super(binding.getRoot()); + this.binding = binding; + this.onVerticalDragListener = onVerticalDragListener; + } + + public void bind(@NonNull final PostChild model, + final int position, + final SliderItemsAdapter.SliderCallback sliderCallback) { + final ImageRequest requestBuilder = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(model.getDisplayUrl())) + .setLocalThumbnailPreviewsEnabled(true) + .build(); + binding.getRoot() + .setController(Fresco.newDraweeControllerBuilder() + .setImageRequest(requestBuilder) + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + if (sliderCallback != null) { + sliderCallback.onThumbnailLoaded(position); + } + } + + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + if (sliderCallback != null) { + sliderCallback.onThumbnailLoaded(position); + } + } + }) + .setLowResImageRequest(ImageRequest.fromUri(model.getThumbnailUrl())) + .build()); + binding.getRoot().setOnClickListener(v -> { + if (sliderCallback != null) { + sliderCallback.onItemClicked(position); + } + }); + final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance(); + zoomableController.setMaxScaleFactor(3f); + binding.getRoot().setZoomableController(zoomableController); + if (onVerticalDragListener != null) { + binding.getRoot().setOnVerticalDragListener(onVerticalDragListener); + } + } + + // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) { + // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); + // final int deviceWidth = Utils.displayMetrics.widthPixels; + // final int spanWidth = deviceWidth / spanCount; + // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + // final int width = spanWidth == 0 ? deviceWidth : spanWidth; + // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight; + // if (animate) { + // Animation animation = AnimationUtils.expand( + // binding.imageViewer, + // layoutParams.width, + // layoutParams.height, + // width, + // height, + // new Animation.AnimationListener() { + // @Override + // public void onAnimationStart(final Animation animation) { + // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationEnd(final Animation animation) { + // // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationRepeat(final Animation animation) { + // + // } + // }); + // binding.imageViewer.startAnimation(animation); + // } else { + // layoutParams.width = width; + // layoutParams.height = height; + // binding.imageViewer.requestLayout(); + // } + // } + // + // private void showOrHideDetails(final int spanCount) { + // if (spanCount == 1) { + // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE); + // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE); + // } else { + // binding.itemFeedTop.getRoot().setVisibility(View.GONE); + // binding.itemFeedBottom.getRoot().setVisibility(View.GONE); + // } + // } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java new file mode 100644 index 00000000..00c70ee6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java @@ -0,0 +1,171 @@ +package awais.instagrabber.adapters.viewholder; + +import android.annotation.SuppressLint; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import awais.instagrabber.adapters.SliderItemsAdapter; +import awais.instagrabber.customviews.VerticalDragHelper; +import awais.instagrabber.customviews.VideoPlayerCallbackAdapter; +import awais.instagrabber.customviews.VideoPlayerViewHelper; +import awais.instagrabber.databinding.LayoutExoCustomControlsBinding; +import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.NumberUtils; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class SliderVideoViewHolder extends SliderItemViewHolder { + private static final String TAG = "SliderVideoViewHolder"; + + private final LayoutVideoPlayerWithThumbnailBinding binding; + private final awais.instagrabber.databinding.LayoutExoCustomControlsBinding controlsBinding; + private VideoPlayerViewHelper videoPlayerViewHelper; + + @SuppressLint("ClickableViewAccessibility") + public SliderVideoViewHolder(@NonNull final LayoutVideoPlayerWithThumbnailBinding binding, + final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener, + final LayoutExoCustomControlsBinding controlsBinding) { + super(binding.getRoot()); + this.binding = binding; + this.controlsBinding = controlsBinding; + final VerticalDragHelper thumbnailVerticalDragHelper = new VerticalDragHelper(binding.thumbnailParent); + final VerticalDragHelper playerVerticalDragHelper = new VerticalDragHelper(binding.playerView); + thumbnailVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener); + playerVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener); + binding.thumbnailParent.setOnTouchListener((v, event) -> { + final boolean onDragTouch = thumbnailVerticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + return thumbnailVerticalDragHelper.onGestureTouchEvent(event); + }); + binding.playerView.setOnTouchListener((v, event) -> { + final boolean onDragTouch = playerVerticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + return playerVerticalDragHelper.onGestureTouchEvent(event); + }); + } + + public void bind(@NonNull final PostChild model, + final int position, + final SliderItemsAdapter.SliderCallback sliderCallback) { + final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; + final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() { + @Override + public void onThumbnailLoaded() { + if (sliderCallback != null) { + sliderCallback.onThumbnailLoaded(position); + } + } + + @Override + public void onPlayerViewLoaded() { + // binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); + final ViewGroup.LayoutParams layoutParams = binding.playerView.getLayoutParams(); + final int requiredWidth = Utils.displayMetrics.widthPixels; + final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, model.getHeight(), model.getWidth()); + layoutParams.width = requiredWidth; + layoutParams.height = resultingHeight; + binding.playerView.requestLayout(); + // setMuteIcon(vol == 0f && Utils.sessionVolumeFull ? 1f : vol); + } + + @Override + public void onPlay() { + if (sliderCallback != null) { + sliderCallback.onPlayerPlay(position); + } + } + + @Override + public void onPause() { + if (sliderCallback != null) { + sliderCallback.onPlayerPause(position); + } + } + }; + final float aspectRatio = (float) model.getWidth() / model.getHeight(); + videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(), + binding, + model.getDisplayUrl(), + vol, + aspectRatio, + model.getThumbnailUrl(), + controlsBinding, + videoPlayerCallback); + // binding.itemFeedBottom.btnMute.setOnClickListener(v -> { + // final float newVol = videoPlayerViewHelper.toggleMute(); + // setMuteIcon(newVol); + // Utils.sessionVolumeFull = newVol == 1f; + // }); + binding.playerView.setOnClickListener(v -> { + if (sliderCallback != null) { + sliderCallback.onItemClicked(position); + } + }); + } + + public void pause() { + if (videoPlayerViewHelper == null) return; + videoPlayerViewHelper.pause(); + } + + public void releasePlayer() { + if (videoPlayerViewHelper == null) return; + videoPlayerViewHelper.releasePlayer(); + } + + // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) { + // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); + // final int deviceWidth = Utils.displayMetrics.widthPixels; + // final int spanWidth = deviceWidth / spanCount; + // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + // final int width = spanWidth == 0 ? deviceWidth : spanWidth; + // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight; + // if (animate) { + // Animation animation = AnimationUtils.expand( + // binding.imageViewer, + // layoutParams.width, + // layoutParams.height, + // width, + // height, + // new Animation.AnimationListener() { + // @Override + // public void onAnimationStart(final Animation animation) { + // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationEnd(final Animation animation) { + // // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationRepeat(final Animation animation) { + // + // } + // }); + // binding.imageViewer.startAnimation(animation); + // } else { + // layoutParams.width = width; + // layoutParams.height = height; + // binding.imageViewer.requestLayout(); + // } + // } + // + // private void showOrHideDetails(final int spanCount) { + // if (spanCount == 1) { + // binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE); + // binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE); + // } else { + // binding.itemFeedTop.getRoot().setVisibility(View.GONE); + // binding.itemFeedBottom.getRoot().setVisibility(View.GONE); + // } + // } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java index ce28419f..9fe7efab 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java @@ -9,6 +9,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.customviews.CommentMentionClickSpan; import awais.instagrabber.customviews.RamboTextView; import awais.instagrabber.databinding.ItemFeedBottomBinding; @@ -34,17 +35,19 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { this.topBinding = topBinding; this.bottomBinding = bottomBinding; this.mentionClickListener = mentionClickListener; + // itemView.setOnClickListener(clickListener); // topBinding.title.setMovementMethod(new LinkMovementMethod()); - bottomBinding.btnComments.setOnClickListener(clickListener); - topBinding.viewStoryPost.setOnClickListener(clickListener); - topBinding.ivProfilePic.setOnClickListener(clickListener); - bottomBinding.btnDownload.setOnClickListener(clickListener); - bottomBinding.viewerCaption.setOnClickListener(clickListener); - bottomBinding.viewerCaption.setOnLongClickListener(longClickListener); - bottomBinding.viewerCaption.setMentionClickListener(mentionClickListener); + // bottomBinding.btnComments.setOnClickListener(clickListener); + // topBinding.viewStoryPost.setOnClickListener(clickListener); + // topBinding.ivProfilePic.setOnClickListener(clickListener); + // bottomBinding.btnDownload.setOnClickListener(clickListener); + // bottomBinding.viewerCaption.setOnClickListener(clickListener); + // bottomBinding.viewerCaption.setOnLongClickListener(longClickListener); + // bottomBinding.viewerCaption.setMentionClickListener(mentionClickListener); } - public void bind(final FeedModel feedModel) { + public void bind(final FeedModel feedModel, + final FeedAdapterV2.OnPostClickListener postClickListener) { if (feedModel == null) { return; } @@ -83,7 +86,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { } } expandCollapseTextView(bottomBinding.viewerCaption, feedModel.getPostCaption()); - bindItem(feedModel); + bindItem(feedModel, postClickListener); } private void setLocation(final String locationName, final String locationId) { @@ -106,7 +109,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { * expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation] * * @param textView the {@link RamboTextView} view, to expand and collapse - * @param caption caption + * @param caption caption * @return isExpanded */ public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, final CharSequence caption) { @@ -116,7 +119,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { if (textView.isCaptionExpanded()) { textView.setText(caption, bufferType); - textView.setCaptionIsExpanded(false); + // textView.setCaptionIsExpanded(false); return true; } int i = TextUtils.indexOfChar(caption, '\r', 0); @@ -129,10 +132,15 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { if (TextUtils.hasMentions(caption)) textView.setText(TextUtils.getMentionText(caption), TextView.BufferType.SPANNABLE); - textView.setCaptionIsExpandable(true); - textView.setCaptionIsExpanded(true); + // textView.setCaptionIsExpandable(true); + // textView.setCaptionIsExpanded(true); return true; } - public abstract void bindItem(final FeedModel feedModel); + public abstract void bindItem(final FeedModel feedModel, + final FeedAdapterV2.OnPostClickListener postClickListener); + + public void clearAnimation() { + itemView.clearAnimation(); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java index f89a2624..c67a4808 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java @@ -1,8 +1,9 @@ package awais.instagrabber.adapters.viewholder.feed; import android.net.Uri; +import android.view.GestureDetector; +import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -13,16 +14,17 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.databinding.ItemFeedPhotoBinding; import awais.instagrabber.interfaces.MentionClickListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; public class FeedPhotoViewHolder extends FeedItemViewHolder { private static final String TAG = "FeedPhotoViewHolder"; private final ItemFeedPhotoBinding binding; + // private final long animationDuration; public FeedPhotoViewHolder(@NonNull final ItemFeedPhotoBinding binding, final MentionClickListener mentionClickListener, @@ -30,6 +32,7 @@ public class FeedPhotoViewHolder extends FeedItemViewHolder { final View.OnLongClickListener longClickListener) { super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); this.binding = binding; + // this.animationDuration = animationDuration; binding.itemFeedBottom.videoViewsContainer.setVisibility(View.GONE); binding.itemFeedBottom.btnMute.setVisibility(View.GONE); binding.imageViewer.setAllowTouchInterceptionWhileZoomed(false); @@ -40,15 +43,13 @@ public class FeedPhotoViewHolder extends FeedItemViewHolder { } @Override - public void bindItem(final FeedModel feedModel) { + public void bindItem(final FeedModel feedModel, + final FeedAdapterV2.OnPostClickListener postClickListener) { if (feedModel == null) { return; } - final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); - final int requiredWidth = Utils.displayMetrics.widthPixels; - layoutParams.width = feedModel.getImageWidth() == 0 ? requiredWidth : feedModel.getImageWidth(); - layoutParams.height = feedModel.getImageHeight() == 0 ? requiredWidth + 1 : feedModel.getImageHeight(); - binding.imageViewer.requestLayout(); + setDimensions(feedModel); + showOrHideDetails(false); final String thumbnailUrl = feedModel.getThumbnailUrl(); String url = feedModel.getDisplayUrl(); if (TextUtils.isEmpty(url)) url = thumbnailUrl; @@ -61,16 +62,66 @@ public class FeedPhotoViewHolder extends FeedItemViewHolder { .setOldController(binding.imageViewer.getController()) .setLowResImageRequest(ImageRequest.fromUri(thumbnailUrl)) .build()); - // binding.imageViewer.setImageURI(url); - // final RequestBuilder thumbnailRequestBuilder = glide - // .asBitmap() - // .load(thumbnailUrl) - // .diskCacheStrategy(DiskCacheStrategy.ALL); - // glide.asBitmap() - // .load(url) - // .thumbnail(thumbnailRequestBuilder) - // .diskCacheStrategy(DiskCacheStrategy.ALL) - // .into(customTarget); + binding.imageViewer.setTapListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (postClickListener != null) { + postClickListener.onPostClick(feedModel, binding.itemFeedTop.ivProfilePic, binding.imageViewer); + return true; + } + return false; + } + }); + } + private void setDimensions(final FeedModel feedModel) { + // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); + // final int deviceWidth = Utils.displayMetrics.widthPixels; + // final int spanWidth = deviceWidth / spanCount; + // final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + // final int width = spanWidth == 0 ? deviceWidth : spanWidth; + // final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight; + final float aspectRatio = (float) feedModel.getImageWidth() / feedModel.getImageHeight(); + binding.imageViewer.setAspectRatio(aspectRatio); + // Log.d(TAG, "setDimensions: aspectRatio:" + aspectRatio); + // if (animate) { + // Animation animation = AnimationUtils.expand( + // binding.imageViewer, + // layoutParams.width, + // layoutParams.height, + // width, + // height, + // new Animation.AnimationListener() { + // @Override + // public void onAnimationStart(final Animation animation) { + // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationEnd(final Animation animation) { + // // showOrHideDetails(spanCount); + // } + // + // @Override + // public void onAnimationRepeat(final Animation animation) { + // + // } + // }); + // binding.imageViewer.startAnimation(animation); + // } else { + // layoutParams.width = width; + // layoutParams.height = height; + // binding.imageViewer.requestLayout(); + // } + } + + private void showOrHideDetails(final boolean show) { + if (show) { + binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE); + binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE); + } else { + binding.itemFeedTop.getRoot().setVisibility(View.GONE); + binding.itemFeedBottom.getRoot().setVisibility(View.GONE); + } } } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java index 9fa5fcf5..12cde610 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java @@ -1,24 +1,15 @@ package awais.instagrabber.adapters.viewholder.feed; import android.content.Context; -import android.net.Uri; -import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.view.ViewTreeObserver; import android.widget.ViewSwitcher; import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; -import com.facebook.drawee.drawable.ScalingUtils; -import com.facebook.drawee.generic.GenericDraweeHierarchy; -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; -import com.facebook.drawee.view.SimpleDraweeView; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -28,13 +19,18 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import java.util.List; + import awais.instagrabber.R; +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.adapters.SliderItemsAdapter; import awais.instagrabber.databinding.ItemFeedSliderBinding; import awais.instagrabber.interfaces.MentionClickListener; import awais.instagrabber.models.FeedModel; -import awais.instagrabber.models.ViewerPostModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.NumberUtils; import awais.instagrabber.utils.Utils; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -75,69 +71,69 @@ public class FeedSliderViewHolder extends FeedItemViewHolder { } @Override - public void bindItem(final FeedModel feedModel) { - final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); - final int sliderItemLen = sliderItems != null ? sliderItems.length : 0; - if (sliderItemLen <= 0) { - return; - } + public void bindItem(final FeedModel feedModel, + final FeedAdapterV2.OnPostClickListener postClickListener) { + final List sliderItems = feedModel.getSliderItems(); + final int sliderItemLen = sliderItems != null ? sliderItems.size() : 0; + if (sliderItemLen <= 0) return; final String text = "1/" + sliderItemLen; binding.mediaCounter.setText(text); - binding.mediaList.setOffscreenPageLimit(Math.min(5, sliderItemLen)); - - final PagerAdapter adapter = binding.mediaList.getAdapter(); - if (adapter != null) { - final int count = adapter.getCount(); - for (int i = 0; i < count; i++) { - adapter.destroyItem(binding.mediaList, i, binding.mediaList.getChildAt(i)); - } + binding.mediaList.setOffscreenPageLimit(1); + SliderItemsAdapter adapter = (SliderItemsAdapter) binding.mediaList.getAdapter(); + if (adapter == null) { + adapter = new SliderItemsAdapter(); } - final ChildMediaItemsAdapter itemsAdapter = new ChildMediaItemsAdapter(sliderItems, - cacheDataSourceFactory != null - ? cacheDataSourceFactory - : dataSourceFactory, - playerChangeListener); - binding.mediaList.setAdapter(itemsAdapter); - - //noinspection deprecation - binding.mediaList.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - private int prevPos = 0; - + // adapter.setSpanCount(spanCount); + binding.mediaList.setAdapter(adapter); + binding.mediaList.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(final int position) { - ViewerPostModel sliderItem = sliderItems[prevPos]; - if (sliderItem != null) { - sliderItem.setSelected(false); - if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { - // stop playing prev video - final ViewSwitcher prevChild = (ViewSwitcher) binding.mediaList.getChildAt(prevPos); - if (prevChild == null || prevChild.getTag() == null || !(prevChild.getTag() instanceof SimpleExoPlayer)) { - return; - } - ((SimpleExoPlayer) prevChild.getTag()).setPlayWhenReady(false); - } - } - sliderItem = sliderItems[position]; - if (sliderItem == null) return; - sliderItem.setSelected(true); - final String text = (position + 1) + "/" + sliderItemLen; - binding.mediaCounter.setText(text); - prevPos = position; - if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { - binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); - if (shouldAutoPlay) { - autoPlay(position); - } - } else binding.itemFeedBottom.btnMute.setVisibility(View.GONE); + if (position >= sliderItemLen) return; + setDimensions(binding.mediaList, sliderItems.get(position)); } }); + setDimensions(binding.mediaList, sliderItems.get(0)); + + //noinspection deprecation + // binding.mediaList.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + // private int prevPos = 0; + // + // @Override + // public void onPageSelected(final int position) { + // ViewerPostModel sliderItem = sliderItems.get(prevPos); + // if (sliderItem != null) { + // sliderItem.setSelected(false); + // if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + // // stop playing prev video + // final ViewSwitcher prevChild = (ViewSwitcher) binding.mediaList.getChildAt(prevPos); + // if (prevChild == null || prevChild.getTag() == null || !(prevChild.getTag() instanceof SimpleExoPlayer)) { + // return; + // } + // ((SimpleExoPlayer) prevChild.getTag()).setPlayWhenReady(false); + // } + // } + // sliderItem = sliderItems.get(position); + // if (sliderItem == null) return; + // sliderItem.setSelected(true); + // final String text = (position + 1) + "/" + sliderItemLen; + // binding.mediaCounter.setText(text); + // prevPos = position; + // if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + // binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); + // if (shouldAutoPlay) { + // autoPlay(position); + // } + // } else binding.itemFeedBottom.btnMute.setVisibility(View.GONE); + // } + // }); + adapter.submitList(sliderItems); final View.OnClickListener muteClickListener = v -> { final int currentItem = binding.mediaList.getCurrentItem(); if (currentItem < 0 || currentItem >= binding.mediaList.getChildCount()) { return; } - final ViewerPostModel sliderItem = sliderItems[currentItem]; + final PostChild sliderItem = sliderItems.get(currentItem); if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { return; } @@ -156,7 +152,7 @@ public class FeedSliderViewHolder extends FeedItemViewHolder { binding.itemFeedBottom.btnMute.setImageResource(intVol == 0f ? R.drawable.ic_volume_up_24 : R.drawable.ic_volume_off_24); Utils.sessionVolumeFull = intVol == 1f; }; - final ViewerPostModel firstItem = sliderItems[0]; + final PostChild firstItem = sliderItems.get(0); if (firstItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); } @@ -164,24 +160,50 @@ public class FeedSliderViewHolder extends FeedItemViewHolder { binding.itemFeedBottom.btnMute.setOnClickListener(muteClickListener); } + private void setDimensions(final View view, final PostChild model) { + final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams(); + int requiredWidth = layoutParams.width; + if (requiredWidth <= 0) { + final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + view.getViewTreeObserver().removeOnPreDrawListener(this); + setLayoutParamDimens(binding.mediaList, model); + return true; + } + }; + view.getViewTreeObserver().addOnPreDrawListener(preDrawListener); + return; + } + setLayoutParamDimens(binding.mediaList, model); + } + + private void setLayoutParamDimens(final View view, final PostChild model) { + final int requiredWidth = view.getMeasuredWidth(); + final ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + final int spanHeight = NumberUtils.getResultingHeight(requiredWidth, model.getHeight(), model.getWidth()); + layoutParams.height = spanHeight == 0 ? requiredWidth + 1 : spanHeight; + view.requestLayout(); + } + private void autoPlay(final int position) { - if (!shouldAutoPlay) { - return; - } - final ChildMediaItemsAdapter adapter = (ChildMediaItemsAdapter) binding.mediaList.getAdapter(); - if (adapter == null) { - return; - } - final ViewerPostModel sliderItem = adapter.getItemAtPosition(position); - if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { - return; - } - final ViewSwitcher viewSwitcher = (ViewSwitcher) binding.mediaList.getChildAt(position); - loadPlayer(binding.getRoot().getContext(), - position, sliderItem.getDisplayUrl(), - viewSwitcher, - cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory, - playerChangeListener); + // if (!shouldAutoPlay) { + // return; + // } + // final ChildMediaItemsAdapter adapter = (ChildMediaItemsAdapter) binding.mediaList.getAdapter(); + // if (adapter == null) { + // return; + // } + // final ViewerPostModel sliderItem = adapter.getItemAtPosition(position); + // if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { + // return; + // } + // final ViewSwitcher viewSwitcher = (ViewSwitcher) binding.mediaList.getChildAt(position); + // loadPlayer(binding.getRoot().getContext(), + // position, sliderItem.getDisplayUrl(), + // viewSwitcher, + // cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory, + // playerChangeListener); } public void startPlayingVideo() { @@ -200,7 +222,8 @@ public class FeedSliderViewHolder extends FeedItemViewHolder { } private static void loadPlayer(final Context context, - final int position, final String displayUrl, + final int position, + final String displayUrl, final ViewSwitcher viewSwitcher, final DataSource.Factory factory, final PlayerChangeListener playerChangeListener) { @@ -223,119 +246,14 @@ public class FeedSliderViewHolder extends FeedItemViewHolder { if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; player.setVolume(vol); player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); - final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(displayUrl)); + final MediaItem mediaItem = MediaItem.fromUri(displayUrl); + final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem); player.setRepeatMode(Player.REPEAT_MODE_ALL); - player.prepare(mediaSource); + player.setMediaSource(mediaSource); + player.prepare(); player.setVolume(vol); playerChangeListener.playerChanged(position, player); viewSwitcher.setTag(player); } - private static final class ChildMediaItemsAdapter extends PagerAdapter { - // private static final String TAG = "ChildMediaItemsAdapter"; - - private final ViewerPostModel[] sliderItems; - private final DataSource.Factory factory; - private final PlayerChangeListener playerChangeListener; - private final ViewGroup.LayoutParams layoutParams; - - private ChildMediaItemsAdapter(final ViewerPostModel[] sliderItems, - final DataSource.Factory factory, - final PlayerChangeListener playerChangeListener) { - this.sliderItems = sliderItems; - this.factory = factory; - this.playerChangeListener = playerChangeListener; - layoutParams = new ViewGroup.LayoutParams(Utils.displayMetrics.widthPixels, Utils.displayMetrics.widthPixels + 1); - } - - @NonNull - @Override - public Object instantiateItem(@NonNull final ViewGroup container, final int position) { - final Context context = container.getContext(); - final ViewerPostModel sliderItem = sliderItems[position]; - - final String displayUrl = sliderItem.getDisplayUrl(); - if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { - final ViewSwitcher viewSwitcher = createViewSwitcher(context, position, sliderItem.getSliderDisplayUrl(), displayUrl); - container.addView(viewSwitcher); - return viewSwitcher; - } - final GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(container.getResources()) - .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - .build(); - final SimpleDraweeView photoView = new SimpleDraweeView(context, hierarchy); - photoView.setLayoutParams(layoutParams); - final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(displayUrl)) - .setLocalThumbnailPreviewsEnabled(true) - .setProgressiveRenderingEnabled(true) - .build(); - photoView.setImageRequest(imageRequest); - container.addView(photoView); - return photoView; - } - - @NonNull - private ViewSwitcher createViewSwitcher(final Context context, final int position, final String sliderDisplayUrl, final String displayUrl) { - - final ViewSwitcher viewSwitcher = new ViewSwitcher(context); - viewSwitcher.setLayoutParams(layoutParams); - - final FrameLayout frameLayout = new FrameLayout(context); - frameLayout.setLayoutParams(layoutParams); - - final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(context.getResources()) - .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - .build(); - final SimpleDraweeView simpleDraweeView = new SimpleDraweeView(context, hierarchy); - simpleDraweeView.setLayoutParams(layoutParams); - simpleDraweeView.setImageURI(sliderDisplayUrl); - frameLayout.addView(simpleDraweeView); - - final AppCompatImageView imageView = new AppCompatImageView(context); - final int px = Utils.convertDpToPx(50); - final FrameLayout.LayoutParams playButtonLayoutParams = new FrameLayout.LayoutParams(px, px); - playButtonLayoutParams.gravity = Gravity.CENTER; - imageView.setLayoutParams(playButtonLayoutParams); - imageView.setImageResource(R.drawable.exo_icon_play); - frameLayout.addView(imageView); - - viewSwitcher.addView(frameLayout); - - final PlayerView playerView = new PlayerView(context); - viewSwitcher.addView(playerView); - if (shouldAutoPlay && position == 0) { - loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener); - } else - frameLayout.setOnClickListener(v -> loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener)); - return viewSwitcher; - } - - @Override - public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { - final View view = container.getChildAt(position); - // Log.d(TAG, "destroy position: " + position + ", view: " + view); - if (view instanceof ViewSwitcher) { - final Object tag = view.getTag(); - if (tag instanceof SimpleExoPlayer) { - final SimpleExoPlayer player = (SimpleExoPlayer) tag; - player.release(); - } - } - container.removeView((View) object); - } - - @Override - public int getCount() { - return sliderItems != null ? sliderItems.length : 0; - } - - @Override - public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) { - return view.equals(object); - } - - public ViewerPostModel getItemAtPosition(final int position) { - return sliderItems[0]; - } - } } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java index 2887cbd4..b124699a 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java @@ -1,7 +1,6 @@ package awais.instagrabber.adapters.viewholder.feed; import android.content.Context; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.view.View; @@ -9,19 +8,14 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.interfaces.DraweeController; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import awais.instagrabber.R; +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.customviews.VideoPlayerCallbackAdapter; +import awais.instagrabber.customviews.VideoPlayerViewHelper; import awais.instagrabber.databinding.ItemFeedVideoBinding; import awais.instagrabber.interfaces.MentionClickListener; import awais.instagrabber.models.FeedModel; @@ -40,14 +34,13 @@ public class FeedVideoViewHolder extends FeedItemViewHolder { private CacheDataSourceFactory cacheDataSourceFactory; private FeedModel feedModel; - private SimpleExoPlayer player; - private final Runnable loadRunnable = new Runnable() { - @Override - public void run() { - loadPlayer(feedModel); - } - }; + // private final Runnable loadRunnable = new Runnable() { + // @Override + // public void run() { + // // loadPlayer(feedModel); + // } + // }; public FeedVideoViewHolder(@NonNull final ItemFeedVideoBinding binding, final MentionClickListener mentionClickListener, @@ -66,72 +59,55 @@ public class FeedVideoViewHolder extends FeedItemViewHolder { } @Override - public void bindItem(final FeedModel feedModel) { + public void bindItem(final FeedModel feedModel, + final FeedAdapterV2.OnPostClickListener postClickListener) { // Log.d(TAG, "Binding post: " + feedModel.getPostId()); this.feedModel = feedModel; - setThumbnail(feedModel); binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(feedModel.getViewCount())); - } + // showOrHideDetails(false); + final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; + final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() { - private void setThumbnail(final FeedModel feedModel) { - final ViewGroup.LayoutParams layoutParams = binding.thumbnailParent.getLayoutParams(); - final int requiredWidth = Utils.displayMetrics.widthPixels; - layoutParams.width = feedModel.getImageWidth() == 0 ? requiredWidth : feedModel.getImageWidth(); - layoutParams.height = feedModel.getImageHeight() == 0 ? requiredWidth + 1 : feedModel.getImageHeight(); - binding.thumbnailParent.requestLayout(); - final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(feedModel.getThumbnailUrl())) - .setProgressiveRenderingEnabled(true) - .build(); - final DraweeController controller = Fresco.newDraweeControllerBuilder() - .setImageRequest(thumbnailRequest) - .build(); - binding.thumbnail.setController(controller); - binding.thumbnailParent.setOnClickListener(v -> loadPlayer(feedModel)); - } + @Override + public void onThumbnailClick() { + postClickListener.onPostClick(feedModel, binding.itemFeedTop.ivProfilePic, binding.videoPost.thumbnail); + } - private void loadPlayer(final FeedModel feedModel) { - if (feedModel == null) { - return; - } - // Log.d(TAG, "playing post:" + feedModel.getPostId()); - if (binding.viewSwitcher.getDisplayedChild() == 0) { - binding.viewSwitcher.showNext(); - } - binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); - final ViewGroup.LayoutParams layoutParams = binding.playerView.getLayoutParams(); - final int requiredWidth = Utils.displayMetrics.widthPixels; - final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); - layoutParams.width = requiredWidth; - layoutParams.height = resultingHeight; - binding.playerView.requestLayout(); - float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; - if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; - setMuteIcon(vol); - player = (SimpleExoPlayer) binding.playerView.getPlayer(); - if (player != null) { - player.release(); - } - player = new SimpleExoPlayer.Builder(itemView.getContext()) - .setLooper(Looper.getMainLooper()) - .build(); - player.setVolume(vol); - player.setPlayWhenReady(true); - final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; - final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); - final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModel.getDisplayUrl())); - player.setRepeatMode(Player.REPEAT_MODE_ALL); - player.prepare(mediaSource); - binding.playerView.setPlayer(player); - final SimpleExoPlayer finalPlayer = player; + @Override + public void onPlayerViewLoaded() { + binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); + final ViewGroup.LayoutParams layoutParams = binding.videoPost.playerView.getLayoutParams(); + final int requiredWidth = Utils.displayMetrics.widthPixels; + final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + layoutParams.width = requiredWidth; + layoutParams.height = resultingHeight; + binding.videoPost.playerView.requestLayout(); + setMuteIcon(vol == 0f && Utils.sessionVolumeFull ? 1f : vol); + } + }; + // final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; + // final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); + // final Uri uri = Uri.parse(feedModel.getDisplayUrl()); + // final MediaItem mediaItem = MediaItem.fromUri(uri); + // final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(mediaItem); + final float aspectRatio = (float) feedModel.getImageWidth() / feedModel.getImageHeight(); + final VideoPlayerViewHelper videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(), + binding.videoPost, + feedModel.getDisplayUrl(), + vol, + aspectRatio, + feedModel.getThumbnailUrl(), + null, + videoPlayerCallback); binding.itemFeedBottom.btnMute.setOnClickListener(v -> { - final float intVol = finalPlayer.getVolume() == 0f ? 1f : 0f; - finalPlayer.setVolume(intVol); - setMuteIcon(intVol); - Utils.sessionVolumeFull = intVol == 1f; + final float newVol = videoPlayerViewHelper.toggleMute(); + setMuteIcon(newVol); + Utils.sessionVolumeFull = newVol == 1f; }); - binding.playerView.setOnClickListener(v -> finalPlayer.setPlayWhenReady(!finalPlayer.getPlayWhenReady())); + binding.videoPost.playerView.setOnClickListener(v -> videoPlayerViewHelper.togglePlayback()); } + private void setMuteIcon(final float vol) { binding.itemFeedBottom.btnMute.setImageResource(vol == 0f ? R.drawable.ic_volume_up_24 : R.drawable.ic_volume_off_24); } @@ -140,19 +116,29 @@ public class FeedVideoViewHolder extends FeedItemViewHolder { return feedModel; } - public void stopPlaying() { - // Log.d(TAG, "Stopping post: " + feedModel.getPostId() + ", player: " + player + ", player.isPlaying: " + (player != null && player.isPlaying())); - handler.removeCallbacks(loadRunnable); - if (player != null) { - player.release(); - } - if (binding.viewSwitcher.getDisplayedChild() == 1) { - binding.viewSwitcher.showPrevious(); - } - } + // public void stopPlaying() { + // // Log.d(TAG, "Stopping post: " + feedModel.getPostId() + ", player: " + player + ", player.isPlaying: " + (player != null && player.isPlaying())); + // handler.removeCallbacks(loadRunnable); + // if (player != null) { + // player.release(); + // } + // if (binding.videoPost.root.getDisplayedChild() == 1) { + // binding.videoPost.root.showPrevious(); + // } + // } + // + // public void startPlaying() { + // handler.removeCallbacks(loadRunnable); + // handler.postDelayed(loadRunnable, 800); + // } - public void startPlaying() { - handler.removeCallbacks(loadRunnable); - handler.postDelayed(loadRunnable, 800); + private void showOrHideDetails(final boolean show) { + if (show) { + binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE); + binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE); + } else { + binding.itemFeedTop.getRoot().setVisibility(View.GONE); + binding.itemFeedBottom.getRoot().setVisibility(View.GONE); + } } } diff --git a/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java b/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java new file mode 100644 index 00000000..cec093ac --- /dev/null +++ b/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java @@ -0,0 +1,45 @@ +package awais.instagrabber.animations; + +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +public class ResizeAnimation extends Animation { + private static final String TAG = "ResizeAnimation"; + + final View view; + final int startHeight; + final int targetHeight; + final int startWidth; + final int targetWidth; + + public ResizeAnimation(final View view, + final int startHeight, + final int startWidth, + final int targetHeight, + final int targetWidth) { + this.view = view; + this.startHeight = startHeight; + this.targetHeight = targetHeight; + this.startWidth = startWidth; + this.targetWidth = targetWidth; + } + + @Override + protected void applyTransformation(final float interpolatedTime, final Transformation t) { + // Log.d(TAG, "applyTransformation: interpolatedTime: " + interpolatedTime); + view.getLayoutParams().height = (int) (startHeight + (targetHeight - startHeight) * interpolatedTime); + view.getLayoutParams().width = (int) (startWidth + (targetWidth - startWidth) * interpolatedTime); + view.requestLayout(); + } + + @Override + public void initialize(final int width, final int height, final int parentWidth, final int parentHeight) { + super.initialize(width, height, parentWidth, parentHeight); + } + + @Override + public boolean willChangeBounds() { + return true; + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java b/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java index ddea126c..c470e478 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java +++ b/app/src/main/java/awais/instagrabber/asyncs/DownloadAsync.java @@ -12,7 +12,6 @@ import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; -import android.os.Environment; import android.util.Log; import android.util.Pair; @@ -177,11 +176,7 @@ public final class DownloadAsync extends AsyncTask { protected void onPostExecute(final File result) { if (result != null) { final Context context = this.context.get(); - - context.sendBroadcast(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT ? - new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(Environment.getExternalStorageDirectory())) : - new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(result.getAbsoluteFile())) - ); + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(result.getAbsoluteFile()))); MediaScannerConnection.scanFile(context, new String[]{result.getAbsolutePath()}, null, null); if (notificationManager != null) { diff --git a/app/src/main/java/awais/instagrabber/asyncs/DownloadV2AsyncTask.java b/app/src/main/java/awais/instagrabber/asyncs/DownloadV2AsyncTask.java new file mode 100644 index 00000000..8f2ac17a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/DownloadV2AsyncTask.java @@ -0,0 +1,310 @@ +// package awais.instagrabber.asyncs; +// +// import android.content.ContentResolver; +// import android.content.Context; +// import android.content.Intent; +// import android.graphics.Bitmap; +// import android.graphics.BitmapFactory; +// import android.media.MediaMetadataRetriever; +// import android.media.MediaScannerConnection; +// import android.net.Uri; +// import android.os.AsyncTask; +// import android.os.Build; +// import android.util.Log; +// +// import androidx.annotation.NonNull; +// import androidx.annotation.Nullable; +// import androidx.core.content.FileProvider; +// +// import java.io.BufferedInputStream; +// import java.io.File; +// import java.io.FileOutputStream; +// import java.io.InputStream; +// import java.net.URL; +// import java.net.URLConnection; +// import java.util.HashMap; +// import java.util.Map; +// +// import awais.instagrabber.BuildConfig; +// import awais.instagrabber.R; +// import awais.instagrabber.utils.Utils; +// +// import static awais.instagrabber.utils.Utils.logCollector; +// import static awaisomereport.LogCollector.LogFile; +// +// public final class DownloadV2AsyncTask extends AsyncTask { +// private static final String TAG = "DownloadAsync"; +// +// // private final int currentNotificationId; +// // private final int initialNotificationId; +// // private final File outFile; +// // private final String url; +// // private final FetchListener fetchListener; +// // private final NotificationCompat.Builder downloadNotif; +// private String shortCode, username; +// // private final NotificationManagerCompat notificationManager; +// +// public DownloadV2AsyncTask(@NonNull final Context context) { +// // this.shortCode = this.username = resources.getString(R.string.downloader_started); +// +// // @StringRes final int titleRes = R.string.downloader_downloading_post; +// // downloadNotif = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID) +// // .setCategory(NotificationCompat.CATEGORY_STATUS) +// // .setSmallIcon(R.mipmap.ic_launcher) +// // .setContentText(shortCode == null ? username : shortCode) +// // .setOngoing(true) +// // .setProgress(100, 0, false) +// // .setAutoCancel(false) +// // .setOnlyAlertOnce(true) +// // .setContentTitle(resources.getString(titleRes)); +// // +// // notificationManager = NotificationManagerCompat.from(context.getApplicationContext()); +// // notificationManager.notify(currentNotificationId, downloadNotif.build()); +// } +// +// // public DownloadV2AsyncTask setItems(final String shortCode, final String username) { +// // this.shortCode = shortCode; +// // this.username = username; +// // if (downloadNotif != null) downloadNotif.setContentText(this.shortCode == null ? this.username : this.shortCode); +// // return this; +// // } +// +// @Nullable +// @Override +// protected File doInBackground(final Void... voids) { +// try { +// final URLConnection urlConnection = new URL(url).openConnection(); +// final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() : +// urlConnection.getContentLength(); +// float totalRead = 0; +// +// try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream()); +// final FileOutputStream fos = new FileOutputStream(outFile)) { +// final byte[] buffer = new byte[0x2000]; +// +// int count; +// boolean deletedIPTC = false; +// while ((count = bis.read(buffer, 0, 0x2000)) != -1) { +// totalRead = totalRead + count; +// +// if (!deletedIPTC) { +// int iptcStart = -1; +// int fbmdStart = -1; +// int fbmdBytesLen = -1; +// +// for (int i = 0; i < buffer.length; ++i) { +// if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xED) +// iptcStart = i; +// else if (buffer[i] == (byte) 'F' && buffer[i + 1] == (byte) 'B' +// && buffer[i + 2] == (byte) 'M' && buffer[i + 3] == (byte) 'D') { +// fbmdStart = i; +// fbmdBytesLen = buffer[i - 10] << 24 | (buffer[i - 9] & 0xFF) << 16 | +// (buffer[i - 8] & 0xFF) << 8 | (buffer[i - 7] & 0xFF) | +// (buffer[i - 6] & 0xFF); +// break; +// } +// } +// +// if (iptcStart != -1 && fbmdStart != -1 && fbmdBytesLen != -1) { +// final int fbmdDataLen = (iptcStart + (fbmdStart - iptcStart) + (fbmdBytesLen - iptcStart)) - 4; +// +// fos.write(buffer, 0, iptcStart); +// fos.write(buffer, fbmdDataLen + iptcStart, count - fbmdDataLen - iptcStart); +// +// publishProgress(totalRead * 100f / fileSize); +// +// deletedIPTC = true; +// continue; +// } +// } +// +// fos.write(buffer, 0, count); +// publishProgress(totalRead * 100f / fileSize); +// } +// fos.flush(); +// } +// +// return outFile; +// } catch (final Exception e) { +// // if (logCollector != null) +// // logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "doInBackground", +// // new Pair<>("context", context.get()), +// // new Pair<>("resources", resources), +// // new Pair<>("lastNotifId", initialNotificationId), +// // new Pair<>("downloadNotif", downloadNotif), +// // new Pair<>("currentNotifId", currentNotificationId), +// // new Pair<>("notificationManager", notificationManager)); +// if (BuildConfig.DEBUG) Log.e(TAG, "", e); +// } +// return null; +// } +// +// @Override +// protected void onPreExecute() { +// // if (fetchListener != null) fetchListener.doBefore(); +// } +// +// @Override +// protected void onProgressUpdate(@NonNull final Float... values) { +// // if (downloadNotif != null) { +// // downloadNotif.setProgress(100, values[0].intValue(), false); +// // notificationManager.notify(currentNotificationId, downloadNotif.build()); +// // } +// } +// +// @Override +// protected void onPostExecute(final File result) { +// if (result != null) { +// // final Context context = this.context.get(); +// // context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(result.getAbsoluteFile()))); +// // MediaScannerConnection.scanFile(context, new String[]{result.getAbsolutePath()}, null, null); +// // +// // // if (notificationManager != null) { +// // final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", result); +// // +// // final ContentResolver contentResolver = context.getContentResolver(); +// // Bitmap bitmap = null; +// // if (Utils.isImage(uri, contentResolver)) { +// // try (final InputStream inputStream = contentResolver.openInputStream(uri)) { +// // bitmap = BitmapFactory.decodeStream(inputStream); +// // } catch (final Exception e) { +// // if (logCollector != null) +// // logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_1"); +// // if (BuildConfig.DEBUG) Log.e(TAG, "", e); +// // } +// // } +// // +// // if (bitmap == null) { +// // final MediaMetadataRetriever retriever = new MediaMetadataRetriever(); +// // try { +// // try { +// // retriever.setDataSource(context, uri); +// // } catch (final Exception e) { +// // retriever.setDataSource(result.getAbsolutePath()); +// // } +// // bitmap = retriever.getFrameAtTime(); +// // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) +// // try { +// // retriever.close(); +// // } catch (final Exception e) { +// // if (logCollector != null) +// // logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_2"); +// // } +// // } catch (final Exception e) { +// // if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); +// // if (logCollector != null) +// // logCollector.appendException(e, LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_3"); +// // } +// // } +// +// // final String downloadComplete = resources.getString(R.string.downloader_complete); +// +// // downloadNotif.setContentText(null).setContentTitle(downloadComplete).setProgress(0, 0, false) +// // .setWhen(System.currentTimeMillis()).setOngoing(false).setOnlyAlertOnce(false).setAutoCancel(true) +// // .setGroup(NOTIF_GROUP_NAME).setGroupSummary(true).setContentIntent( +// // PendingIntent.getActivity(context, 2020, new Intent(Intent.ACTION_VIEW, uri) +// // .addFlags( +// // Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_FROM_BACKGROUND | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION) +// // .putExtra(Intent.EXTRA_STREAM, uri), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT)); +// // +// // if (bitmap != null) +// // downloadNotif.setStyle(new NotificationCompat.BigPictureStyle().setBigContentTitle(downloadComplete).bigPicture(bitmap)) +// // .setLargeIcon(bitmap).setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); +// // +// // notificationManager.cancel(currentNotificationId); +// // notificationManager.notify(currentNotificationId + 1, downloadNotif.build()); +// // } +// } +// +// // if (fetchListener != null) fetchListener.onResult(result); +// } +// +// public static class DownloadOptions { +// private final Map urlToFileMap; +// private final Map callbackMap; +// private final boolean showNotification; +// +// public static class Builder { +// private Map urlToFileMap; +// private Map callbackMap; +// private boolean showNotification; +// +// public Builder setUrlToFileMap(@NonNull final Map urlToFileMap) { +// this.urlToFileMap = urlToFileMap; +// return this; +// } +// +// public Builder setCallbackMap(@NonNull final Map callbackMap) { +// this.callbackMap = callbackMap; +// return this; +// } +// +// public Builder addUrl(@NonNull final String url, @NonNull final File file) { +// if (urlToFileMap == null) { +// urlToFileMap = new HashMap<>(); +// } +// urlToFileMap.put(url, file); +// return this; +// } +// +// public Builder addUrl(@NonNull final String url, +// @NonNull final File file, +// @NonNull final DownloadCallback callback) { +// if (urlToFileMap == null) { +// urlToFileMap = new HashMap<>(); +// } +// if (callbackMap == null) { +// callbackMap = new HashMap<>(); +// } +// urlToFileMap.put(url, file); +// callbackMap.put(url, callback); +// return this; +// } +// +// public Builder setShowNotification(final boolean showNotification) { +// this.showNotification = showNotification; +// return this; +// } +// +// public DownloadOptions build() { +// return new DownloadOptions( +// urlToFileMap, +// callbackMap, +// showNotification +// ); +// } +// } +// +// public Builder builder() { +// return new Builder(); +// } +// +// private DownloadOptions(final Map urlToFileMap, +// final Map callbackMap, +// final boolean showNotification) { +// this.urlToFileMap = urlToFileMap; +// this.callbackMap = callbackMap; +// this.showNotification = showNotification; +// } +// +// public Map getUrlToFileMap() { +// return urlToFileMap; +// } +// +// public Map getCallbackMap() { +// return callbackMap; +// } +// +// public boolean isShowNotification() { +// return showNotification; +// } +// } +// +// public interface DownloadCallback { +// void onDownloadStart(); +// +// void onDownloadProgress(); +// +// void onDownloadComplete(); +// } +// } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java index 855f7f17..4ae5ad35 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java @@ -3,20 +3,22 @@ package awais.instagrabber.asyncs; import android.os.AsyncTask; import android.util.Log; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; +import java.util.List; import awais.instagrabber.BuildConfig; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.models.ViewerPostModel; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.NetworkUtils; @@ -26,27 +28,27 @@ import awaisomereport.LogCollector; import static awais.instagrabber.utils.Utils.logCollector; -public final class FeedFetcher extends AsyncTask { +public final class FeedFetcher extends AsyncTask> { private static final String TAG = "FeedFetcher"; - private static final int maxItemsToLoad = 25; // max is 50, but that's too many posts, setting more than 30 is gay + private static final int maxItemsToLoad = 25; // max is 50, but that's too many posts private final String endCursor; - private final FetchListener fetchListener; + private final FetchListener> fetchListener; - public FeedFetcher(final FetchListener fetchListener) { + public FeedFetcher(final FetchListener> fetchListener) { this.endCursor = ""; this.fetchListener = fetchListener; } - public FeedFetcher(final String endCursor, final FetchListener fetchListener) { + public FeedFetcher(final String endCursor, final FetchListener> fetchListener) { this.endCursor = endCursor == null ? "" : endCursor; this.fetchListener = fetchListener; } - @Nullable @Override - protected final FeedModel[] doInBackground(final Void... voids) { - FeedModel[] result = null; + protected final List doInBackground(final Void... voids) { + final List result = new ArrayList<>(); + HttpURLConnection urlConnection = null; try { // // stories: 04334405dbdef91f2c4e207b84c204d7 && https://i.instagram.com/api/v1/feed/reels_tray/ @@ -62,13 +64,14 @@ public final class FeedFetcher extends AsyncTask { final String url = "https://www.instagram.com/graphql/query/?query_hash=6b838488258d7a4820e48d209ef79eb1&variables=" + "{\"fetch_media_item_count\":" + maxItemsToLoad + ",\"has_threaded_comments\":true,\"fetch_media_item_cursor\":\"" + endCursor + "\"}"; - final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + urlConnection = (HttpURLConnection) new URL(url).openConnection(); if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { final String json = NetworkUtils.readFromConnection(urlConnection); // Log.d(TAG, json); final JSONObject timelineFeed = new JSONObject(json).getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER).getJSONObject("edge_web_feed_timeline"); + .getJSONObject(Constants.EXTRAS_USER) + .getJSONObject("edge_web_feed_timeline"); final String endCursor; final boolean hasNextPage; @@ -84,9 +87,7 @@ public final class FeedFetcher extends AsyncTask { final JSONArray feedItems = timelineFeed.getJSONArray("edges"); - final int feedLen = feedItems.length(); - final ArrayList feedModelsList = new ArrayList<>(feedLen); - for (int i = 0; i < feedLen; ++i) { + for (int i = 0; i < feedItems.length(); ++i) { final JSONObject feedItem = feedItems.getJSONObject(i).getJSONObject("node"); final String mediaType = feedItem.optString("__typename"); if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType)) @@ -99,9 +100,11 @@ public final class FeedFetcher extends AsyncTask { if (TextUtils.isEmpty(displayUrl)) continue; final String resourceUrl; - if (isVideo) resourceUrl = feedItem.getString("video_url"); - else + if (isVideo) { + resourceUrl = feedItem.getString("video_url"); + } else { resourceUrl = feedItem.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(feedItem) : displayUrl; + } ProfileModel profileModel = null; if (feedItem.has("owner")) { @@ -125,20 +128,17 @@ public final class FeedFetcher extends AsyncTask { false, false); } - JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment"); final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0; - tempJsonObject = feedItem.optJSONObject("edge_media_to_caption"); final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null; - String captionText = null; if (captions != null && captions.length() > 0) { if ((tempJsonObject = captions.optJSONObject(0)) != null && - (tempJsonObject = tempJsonObject.optJSONObject("node")) != null) + (tempJsonObject = tempJsonObject.optJSONObject("node")) != null) { captionText = tempJsonObject.getString("text"); + } } - final JSONObject location = feedItem.optJSONObject("location"); // Log.d(TAG, "location: " + (location == null ? null : location.toString())); String locationId = null; @@ -152,92 +152,119 @@ public final class FeedFetcher extends AsyncTask { } // Log.d(TAG, "locationId: " + locationId); } - final FeedModel feedModel = new FeedModel( - profileModel, - isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, - videoViews, - feedItem.getString(Constants.EXTRAS_ID), - resourceUrl, - displayUrl, - feedItem.getString(Constants.EXTRAS_SHORTCODE), - captionText, - commentsCount, - feedItem.optLong("taken_at_timestamp", -1), - feedItem.getBoolean("viewer_has_liked"), - feedItem.getBoolean("viewer_has_saved"), - feedItem.getJSONObject("edge_media_preview_like").getLong("count"), - locationName, - locationId); + int height = 0; + int width = 0; + final JSONObject dimensions = feedItem.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = feedItem.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setProfileModel(profileModel) + .setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setViewCount(videoViews) + .setPostId(feedItem.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(resourceUrl) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl) + .setShortCode(feedItem.getString(Constants.EXTRAS_SHORTCODE)) + .setPostCaption(captionText) + .setCommentsCount(commentsCount) + .setTimestamp(feedItem.optLong("taken_at_timestamp", -1)) + .setLiked(feedItem.getBoolean("viewer_has_liked")) + .setBookmarked(feedItem.getBoolean("viewer_has_saved")) + .setLikesCount(feedItem.getJSONObject("edge_media_preview_like") + .getLong("count")) + .setLocationName(locationName) + .setLocationId(locationId) + .setImageHeight(height) + .setImageWidth(width); final boolean isSlider = "GraphSidecar".equals(mediaType) && feedItem.has("edge_sidecar_to_children"); if (isSlider) { + feedModelBuilder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER); final JSONObject sidecar = feedItem.optJSONObject("edge_sidecar_to_children"); if (sidecar != null) { final JSONArray children = sidecar.optJSONArray("edges"); - if (children != null) { - final ViewerPostModel[] sliderItems = new ViewerPostModel[children.length()]; - - for (int j = 0; j < sliderItems.length; ++j) { - final JSONObject node = children.optJSONObject(j).getJSONObject("node"); - final boolean isChildVideo = node.optBoolean("is_video"); - - sliderItems[j] = new ViewerPostModel( - isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, - node.getString(Constants.EXTRAS_ID), - isChildVideo ? node.getString("video_url") : ResponseBodyUtils.getHighQualityImage(node), - null, - null, - null, - node.optLong("video_view_count", -1), - -1, - false, - false, - feedItem.getJSONObject("edge_media_preview_like").getLong("count"), - locationName, - locationId); - - sliderItems[j].setSliderDisplayUrl(node.getString("display_url")); - } - - feedModel.setSliderItems(sliderItems); + final List sliderItems = getSliderItems(children); + feedModelBuilder.setSliderItems(sliderItems); } } } - - feedModelsList.add(feedModel); + final FeedModel feedModel = feedModelBuilder.build(); + result.add(feedModel); } - - feedModelsList.trimToSize(); - - final FeedModel[] feedModels = feedModelsList.toArray(new FeedModel[0]); - final int length = feedModels.length; - if (length >= 1 && feedModels[length - 1] != null) { - feedModels[length - 1].setPageCursor(hasNextPage, endCursor); + if (!result.isEmpty() && result.get(result.size() - 1) != null) { + result.get(result.size() - 1).setPageCursor(hasNextPage, endCursor); } - result = feedModels; } - - urlConnection.disconnect(); } catch (final Exception e) { if (logCollector != null) logCollector.appendException(e, LogCollector.LogFile.ASYNC_FEED_FETCHER, "doInBackground"); if (BuildConfig.DEBUG) { Log.e(TAG, "", e); } + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } } return result; } + @NonNull + private List getSliderItems(final JSONArray children) throws JSONException { + final List sliderItems = new ArrayList<>(); + for (int j = 0; j < children.length(); ++j) { + final JSONObject childNode = children.optJSONObject(j).getJSONObject("node"); + final boolean isChildVideo = childNode.optBoolean("is_video"); + int height = 0; + int width = 0; + final JSONObject dimensions = childNode.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = childNode.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final PostChild sliderItem = new PostChild.Builder() + .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setPostId(childNode.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(isChildVideo ? childNode.getString("video_url") + : childNode.getString("display_url")) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl + : childNode.getString("display_url")) + .setVideoViews(childNode.optLong("video_view_count", -1)) + .setHeight(height) + .setWidth(width) + .build(); + // Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem); + sliderItems.add(sliderItem); + } + return sliderItems; + } + @Override protected void onPreExecute() { if (fetchListener != null) fetchListener.doBefore(); } @Override - protected void onPostExecute(final FeedModel[] postModels) { + protected void onPostExecute(final List postModels) { if (fetchListener != null) fetchListener.onResult(postModels); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java new file mode 100644 index 00000000..5236a120 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java @@ -0,0 +1,54 @@ +package awais.instagrabber.asyncs; + +import java.util.List; + +import awais.instagrabber.customviews.helpers.PostFetcher; +import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.repositories.responses.FeedFetchResponse; +import awais.instagrabber.webservices.FeedService; +import awais.instagrabber.webservices.ServiceCallback; + +public class FeedPostFetchService implements PostFetcher.PostFetchService { + private static final String TAG = "FeedPostFetchService"; + private final FeedService feedService; + private String nextCursor; + private boolean hasNextPage; + + public FeedPostFetchService() { + feedService = FeedService.getInstance(); + } + + @Override + public void fetch(final String cursor, final FetchListener> fetchListener) { + feedService.fetch(25, cursor, new ServiceCallback() { + @Override + public void onSuccess(final FeedFetchResponse result) { + if (result == null) return; + nextCursor = result.getNextCursor(); + hasNextPage = result.hasNextPage(); + if (fetchListener != null) { + fetchListener.onResult(result.getFeedModels()); + } + } + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + } + + @Override + public String getNextCursor() { + return nextCursor; + } + + @Override + public boolean hasNextPage() { + return hasNextPage; + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java index 71b69c54..8c372d53 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java @@ -10,14 +10,16 @@ import org.json.JSONObject; import java.io.File; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.models.ViewerPostModel; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.NetworkUtils; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; @@ -29,23 +31,23 @@ import static awais.instagrabber.utils.Constants.FOLDER_PATH; import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; import static awais.instagrabber.utils.Utils.logCollector; -public final class PostFetcher extends AsyncTask { +public final class PostFetcher extends AsyncTask { private static final String TAG = "PostFetcher"; private final String shortCode; - private final FetchListener fetchListener; + private final FetchListener fetchListener; - public PostFetcher(final String shortCode, final FetchListener fetchListener) { + public PostFetcher(final String shortCode, final FetchListener fetchListener) { this.shortCode = shortCode; this.fetchListener = fetchListener; } @Override - protected ViewerPostModel[] doInBackground(final Void... voids) { - ViewerPostModel[] result = null; + protected FeedModel doInBackground(final Void... voids) { CookieUtils.setupCookies(Utils.settingsHelper.getString(Constants.COOKIE)); // <- direct download + HttpURLConnection conn = null; try { - final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection(); + conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection(); conn.setUseCaches(false); conn.connect(); @@ -53,7 +55,6 @@ public final class PostFetcher extends AsyncTask final JSONObject media = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("graphql") .getJSONObject("shortcode_media"); - ProfileModel profileModel = null; if (media.has("owner")) { final JSONObject owner = media.getJSONObject("owner"); @@ -111,75 +112,63 @@ public final class PostFetcher extends AsyncTask JSONObject commentObject = media.optJSONObject("edge_media_to_parent_comment"); final long commentsCount = commentObject != null ? commentObject.optLong("count") : 0; - - String endCursor = null; - if (commentObject != null && (commentObject = commentObject.optJSONObject("page_info")) != null) { - endCursor = commentObject.optString("end_cursor"); - } - - if (mediaItemType != MediaItemType.MEDIA_TYPE_SLIDER) { - final ViewerPostModel postModel = new ViewerPostModel( - mediaItemType, - media.getString(Constants.EXTRAS_ID), - isVideo ? media.getString("video_url") : ResponseBodyUtils.getHighQualityImage(media), - shortCode, - TextUtils.isEmpty(postCaption) ? null : postCaption, - profileModel, - isVideo && media.has("video_view_count") ? media.getLong("video_view_count") : -1, - timestamp, media.getBoolean("viewer_has_liked"), media.getBoolean("viewer_has_saved"), - media.getJSONObject("edge_media_preview_like").getLong("count"), - media.isNull("location") ? null : media.getJSONObject("location").optString("name"), - media.isNull("location") ? null : - (media.getJSONObject("location").optString("id") + "/" + - media.getJSONObject("location").optString("slug"))); - - postModel.setCommentsCount(commentsCount); - - DownloadUtils.checkExistence(downloadDir, customDir, false, postModel); - - result = new ViewerPostModel[]{postModel}; - - } else { + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setItemType(mediaItemType) + .setPostId(media.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(isVideo ? media.getString("video_url") + : ResponseBodyUtils.getHighQualityImage(media)) + .setShortCode(shortCode) + .setPostCaption(TextUtils.isEmpty(postCaption) ? null : postCaption) + .setProfileModel(profileModel) + .setViewCount(isVideo && media.has("video_view_count") + ? media.getLong("video_view_count") + : -1) + .setTimestamp(timestamp) + .setLiked(media.getBoolean("viewer_has_liked")) + .setBookmarked(media.getBoolean("viewer_has_saved")) + .setLikesCount(media.getJSONObject("edge_media_preview_like") + .getLong("count")) + .setLocationName(media.isNull("location") + ? null + : media.getJSONObject("location").optString("name")) + .setLocationId(media.isNull("location") + ? null + : media.getJSONObject("location").optString("id")) + .setCommentsCount(commentsCount); + // DownloadUtils.checkExistence(downloadDir, customDir, false, feedModelBuilder); + if (isSlider) { final JSONArray children = media.getJSONObject("edge_sidecar_to_children").getJSONArray("edges"); - final ViewerPostModel[] postModels = new ViewerPostModel[children.length()]; - - for (int i = 0; i < postModels.length; ++i) { - final JSONObject node = children.getJSONObject(i).getJSONObject("node"); - final boolean isChildVideo = node.getBoolean("is_video"); - - postModels[i] = new ViewerPostModel( - isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, - media.getString(Constants.EXTRAS_ID), - isChildVideo ? node.getString("video_url") : ResponseBodyUtils.getHighQualityImage(node), - node.getString(Constants.EXTRAS_SHORTCODE), - postCaption, - profileModel, - isChildVideo && node.has("video_view_count") ? node.getLong("video_view_count") : -1, - timestamp, media.getBoolean("viewer_has_liked"), media.getBoolean("viewer_has_saved"), - media.getJSONObject("edge_media_preview_like").getLong("count"), - media.isNull("location") ? null : media.getJSONObject("location").optString("name"), - media.isNull("location") ? null : - (media.getJSONObject("location").optString("id") + "/" + - media.getJSONObject("location").optString("slug"))); - postModels[i].setSliderDisplayUrl(node.getString("display_url")); - - DownloadUtils.checkExistence(downloadDir, customDir, true, postModels[i]); + final List postModels = new ArrayList<>(); + for (int i = 0; i < children.length(); ++i) { + final JSONObject childNode = children.getJSONObject(i).getJSONObject("node"); + final boolean isChildVideo = childNode.getBoolean("is_video"); + postModels.add(new PostChild.Builder() + .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setDisplayUrl(isChildVideo ? childNode.getString("video_url") + : childNode.getString("display_url")) + // .setShortCode(childNode.getString(Constants.EXTRAS_SHORTCODE)) + .setVideoViews(isChildVideo && childNode.has("video_view_count") + ? childNode.getLong("video_view_count") + : -1) + .build()); + // DownloadUtils.checkExistence(downloadDir, customDir, true, postModels.get(i)); } - - postModels[0].setCommentsCount(commentsCount); - - result = postModels; + feedModelBuilder.setSliderItems(postModels); } + return feedModelBuilder.build(); } - - conn.disconnect(); } catch (Exception e) { if (logCollector != null) { logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground"); } Log.e(TAG, "Error fetching post", e); + } finally { + if (conn != null) { + conn.disconnect(); + } } - return result; + return null; } @Override @@ -188,7 +177,7 @@ public final class PostFetcher extends AsyncTask } @Override - protected void onPostExecute(final ViewerPostModel[] postModels) { - if (fetchListener != null) fetchListener.onResult(postModels); + protected void onPostExecute(final FeedModel feedModel) { + if (fetchListener != null) fetchListener.onResult(feedModel); } } diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java index 6d7eeaa4..ad0d8710 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java @@ -153,8 +153,8 @@ public final class PostsFetcher extends AsyncTask> { : null, mediaNode.getLong("taken_at_timestamp"), mediaNode.optBoolean("viewer_has_liked"), - mediaNode.optBoolean("viewer_has_saved"), - mediaNode.isNull("edge_liked_by") ? 0 : mediaNode.getJSONObject("edge_liked_by").getLong("count") + mediaNode.optBoolean("viewer_has_saved") + // , mediaNode.isNull("edge_liked_by") ? 0 : mediaNode.getJSONObject("edge_liked_by").getLong("count") ); result.add(model); DownloadUtils.checkExistence(downloadDir, customDir, isSlider, model); diff --git a/app/src/main/java/awais/instagrabber/asyncs/i/iLikedFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/i/iLikedFetcher.java index 58aec1ab..0c7b4af6 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/i/iLikedFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/i/iLikedFetcher.java @@ -96,8 +96,9 @@ public final class iLikedFetcher extends AsyncTask> mediaNode.isNull("caption") ? null : mediaNode.getJSONObject("caption").optString("text"), mediaNode.getLong("taken_at"), true, - mediaNode.optBoolean("has_viewer_saved"), - mediaNode.getLong("like_count")); + mediaNode.optBoolean("has_viewer_saved") + // , mediaNode.getLong("like_count") + ); result.add(model); String username = mediaNode.getJSONObject("user").getString("username"); final File downloadDir = new File(Environment.getExternalStorageDirectory(), "Download" + diff --git a/app/src/main/java/awais/instagrabber/asyncs/i/iPostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/i/iPostFetcher.java index dae6f368..b1e0b2fd 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/i/iPostFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/i/iPostFetcher.java @@ -10,14 +10,16 @@ import org.json.JSONObject; import java.io.File; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import awais.instagrabber.BuildConfig; import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.models.ViewerPostModel; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.NetworkUtils; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; @@ -29,22 +31,22 @@ import static awais.instagrabber.utils.Constants.FOLDER_PATH; import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; import static awais.instagrabber.utils.Utils.logCollector; -public final class iPostFetcher extends AsyncTask { +public final class iPostFetcher extends AsyncTask { private static final String TAG = "iPostFetcher"; private final String id; - private final FetchListener fetchListener; + private final FetchListener fetchListener; - public iPostFetcher(final String id, final FetchListener fetchListener) { + public iPostFetcher(final String id, final FetchListener fetchListener) { this.id = id; this.fetchListener = fetchListener; } @Override - protected ViewerPostModel[] doInBackground(final Void... voids) { - ViewerPostModel[] result = null; + protected FeedModel doInBackground(final Void... voids) { + HttpURLConnection conn = null; try { - final HttpURLConnection conn = (HttpURLConnection) new URL("https://i.instagram.com/api/v1/media/" + id + "/info").openConnection(); + conn = (HttpURLConnection) new URL("https://i.instagram.com/api/v1/media/" + id + "/info").openConnection(); conn.setUseCaches(false); conn.setRequestProperty("User-Agent", Constants.USER_AGENT); conn.connect(); @@ -86,7 +88,7 @@ public final class iPostFetcher extends AsyncTask ); } if (profileModel == null) { - return new ViewerPostModel[]{}; + return new FeedModel.Builder().build(); } // to check if file exists @@ -130,69 +132,65 @@ public final class iPostFetcher extends AsyncTask } } // final String locationString = location.optString("id") + "/" + location.optString("slug"); - if (mediaItemType != MediaItemType.MEDIA_TYPE_SLIDER) { - final ViewerPostModel postModel = new ViewerPostModel( - mediaItemType, - media.getString(Constants.EXTRAS_ID), - isVideo ? ResponseBodyUtils.getHighQualityPost(media.optJSONArray("video_versions"), true, true, false) - : ResponseBodyUtils.getHighQualityImage(media), - media.getString("code"), - TextUtils.isEmpty(postCaption) ? null : postCaption, - profileModel, - isVideo && media.has("view_count") ? media.getLong("view_count") : -1, - timestamp, media.optBoolean("has_liked"), - media.optBoolean("has_viewer_saved"), - media.getLong("like_count"), - locationName, - locationId); + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setItemType(mediaItemType) + .setPostId(media.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(isVideo ? ResponseBodyUtils.getHighQualityPost(media.optJSONArray("video_versions"), true, true, false) + : ResponseBodyUtils.getHighQualityImage(media)) + .setShortCode(media.getString("code")) + .setPostCaption(TextUtils.isEmpty(postCaption) ? null : postCaption) + .setProfileModel(profileModel) + .setViewCount(isVideo && media.has("view_count") + ? media.getLong("view_count") + : -1) + .setTimestamp(timestamp) + .setLiked(media.optBoolean("has_liked")) + .setBookmarked(media.optBoolean("has_viewer_saved")) + .setLikesCount(media.getLong("like_count")) + .setLocationName(locationName) + .setLocationId(locationId) + .setCommentsCount(commentsCount); + // DownloadUtils.checkExistence(downloadDir, customDir, false, postModel); - postModel.setCommentsCount(commentsCount); - - DownloadUtils.checkExistence(downloadDir, customDir, false, postModel); - - result = new ViewerPostModel[]{postModel}; - - } else { + if (isSlider) { final JSONArray children = media.getJSONArray("carousel_media"); - final ViewerPostModel[] postModels = new ViewerPostModel[children.length()]; - - for (int i = 0; i < postModels.length; ++i) { - final JSONObject node = children.getJSONObject(i); - final boolean isChildVideo = node.has("video_duration"); - - postModels[i] = new ViewerPostModel( - isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO - : MediaItemType.MEDIA_TYPE_IMAGE, - media.getString(Constants.EXTRAS_ID), - isChildVideo ? ResponseBodyUtils.getHighQualityPost(node.optJSONArray("video_versions"), true, true, false) - : ResponseBodyUtils.getHighQualityImage(node), - media.getString("code"), - postCaption, - profileModel, - -1, - timestamp, media.optBoolean("has_liked"), - media.optBoolean("has_viewer_saved"), - media.getLong("like_count"), - locationName, - locationId); - postModels[i].setSliderDisplayUrl(ResponseBodyUtils.getHighQualityImage(node)); - DownloadUtils.checkExistence(downloadDir, customDir, true, postModels[i]); + final List postModels = new ArrayList<>(); + for (int i = 0; i < children.length(); ++i) { + final JSONObject childNode = children.getJSONObject(i); + final boolean isChildVideo = childNode.has("video_duration"); + postModels.add(new PostChild.Builder() + .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setPostId(childNode.has(Constants.EXTRAS_ID) + ? childNode.getString(Constants.EXTRAS_ID) + : media.getString(Constants.EXTRAS_ID)) + // .setShortCode(childNode.optString(Constants.EXTRAS_SHORTCODE)) + .setDisplayUrl( + isChildVideo ? ResponseBodyUtils.getHighQualityPost( + childNode.optJSONArray("video_versions"), true, true, false) + : ResponseBodyUtils.getHighQualityImage(childNode)) + .setVideoViews(isChildVideo && childNode.has("video_view_count") + ? childNode.getLong("video_view_count") + : -1) + .build()); + // DownloadUtils.checkExistence(downloadDir, customDir, true, postModels[i]); } - - postModels[0].setCommentsCount(commentsCount); - result = postModels; + feedModelBuilder.setSliderItems(postModels); } + return feedModelBuilder.build(); } - - conn.disconnect(); } catch (Exception e) { if (logCollector != null) logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground (i)"); if (BuildConfig.DEBUG) { Log.e(TAG, "", e); } + } finally { + if (conn != null) { + conn.disconnect(); + } } - return result; + return null; } @Override @@ -201,7 +199,7 @@ public final class iPostFetcher extends AsyncTask } @Override - protected void onPostExecute(final ViewerPostModel[] postModels) { - if (fetchListener != null) fetchListener.onResult(postModels); + protected void onPostExecute(final FeedModel feedModel) { + if (fetchListener != null) fetchListener.onResult(feedModel); } } diff --git a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java index 441c21e3..29828709 100755 --- a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java +++ b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java @@ -2,10 +2,7 @@ package awais.instagrabber.customviews; import android.content.Context; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapShader; import android.graphics.Color; -import android.graphics.Paint; import android.util.AttributeSet; import androidx.annotation.Nullable; @@ -17,36 +14,27 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyInflater; import com.facebook.drawee.generic.RoundingParams; import com.facebook.drawee.view.SimpleDraweeView; -public final class CircularImageView extends SimpleDraweeView { - private final int borderSize = 8; - private int color = Color.TRANSPARENT; - private final Paint paint = new Paint(); - private final Paint paintBorder = new Paint(); - private BitmapShader shader; - private Bitmap bitmap; +import awais.instagrabber.R; +public class CircularImageView extends SimpleDraweeView { public CircularImageView(Context context, GenericDraweeHierarchy hierarchy) { super(context); setHierarchy(hierarchy); - setup(); } public CircularImageView(final Context context) { super(context); inflateHierarchy(context, null); - setup(); } public CircularImageView(final Context context, final AttributeSet attrs) { super(context, attrs); inflateHierarchy(context, attrs); - setup(); } public CircularImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) { super(context, attrs, defStyleAttr); inflateHierarchy(context, attrs); - setup(); } protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) { @@ -58,77 +46,12 @@ public final class CircularImageView extends SimpleDraweeView { GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs); setAspectRatio(builder.getDesiredAspectRatio()); setHierarchy(builder.build()); + setBackgroundResource(R.drawable.shape_oval_light); } - private void setup() { - // paint.setAntiAlias(true); - // paintBorder.setColor(color); - // paintBorder.setAntiAlias(true); - // - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // setOutlineProvider(new ViewOutlineProvider() { - // private int viewHeight; - // private int viewWidth; - // - // @Override - // public void getOutline(final View view, final Outline outline) { - // if (viewHeight == 0) viewHeight = getHeight(); - // if (viewWidth == 0) viewWidth = getWidth(); - // outline.setRoundRect(borderSize, borderSize, viewWidth - borderSize, viewHeight - borderSize, viewHeight >> 1); - // } - // }); - // } - // final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()) - // .setRoundingParams(RoundingParams.) - // .build(); - // setHierarchy(hierarchy); - // invalidate(); - } - - // @Override - // public void onDraw(final Canvas canvas) { - // final BitmapDrawable bitmapDrawable = (BitmapDrawable) getDrawable(); - // if (bitmapDrawable != null) { - // final Bitmap prevBitmap = bitmap; - // bitmap = bitmapDrawable.getBitmap(); - // final boolean changed = prevBitmap != bitmap; - // if (bitmap != null) { - // final int width = getWidth(); - // final int height = getHeight(); - // - // if (shader == null || changed) { - // shader = new BitmapShader(Bitmap.createScaledBitmap(bitmap, width, height, true), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - // paint.setShader(shader); - // } - // - // if (changed) color = 0; - // paintBorder.setColor(color); - // - // final int circleCenter = (width - borderSize) / 2; - // final int position = circleCenter + (borderSize / 2); - // canvas.drawCircle(position, position, position - 4.0f, paintBorder); - // canvas.drawCircle(position, position, circleCenter - 4.0f, paint); - // } - // } - // } - // - // @Override - // protected void onAttachedToWindow() { - // super.onAttachedToWindow(); - // setLayerType(LAYER_TYPE_HARDWARE, null); - // } - // - // @Override - // protected void onDetachedFromWindow() { - // setLayerType(LAYER_TYPE_NONE, null); - // super.onDetachedFromWindow(); - // } - public void setStoriesBorder() { - this.color = Color.GREEN; - // invalidate(); - // final RoundingParams roundingParams = RoundingParams.fromCornersRadius(5f); - // + // private final int borderSize = 8; + final int color = Color.GREEN; RoundingParams roundingParams = getHierarchy().getRoundingParams(); if (roundingParams == null) { roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY); diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java new file mode 100644 index 00000000..c91d92f9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -0,0 +1,259 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; +import androidx.transition.ChangeBounds; +import androidx.transition.Transition; +import androidx.transition.TransitionManager; + +import java.util.ArrayList; +import java.util.List; + +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.adapters.FeedAdapterV2.OnPostClickListener; +import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; +import awais.instagrabber.customviews.helpers.PostFetcher; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtBottom; +import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostsLayoutPreferences; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.viewmodels.FeedViewModel; + +public class PostsRecyclerView extends RecyclerView { + private static final String TAG = "PostsRecyclerView"; + + private StaggeredGridLayoutManager gridLayoutManager; + private PostsLayoutPreferences layoutPreferences; + private PostFetcher.PostFetchService postFetchService; + private Transition transition; + private OnClickListener postViewClickListener; + private MentionClickListener mentionClickListener; + private PostFetcher postFetcher; + private ViewModelStoreOwner viewModelStoreOwner; + private FeedAdapterV2 feedAdapter; + private LifecycleOwner lifeCycleOwner; + private FeedViewModel feedViewModel; + private boolean initCalled = false; + private GridSpacingItemDecoration gridSpacingItemDecoration; + private RecyclerLazyLoaderAtBottom lazyLoader; + private OnPostClickListener onPostClickListener; + + private final FetchListener> fetchListener = new FetchListener>() { + @Override + public void onResult(final List result) { + final int currentPage = lazyLoader.getCurrentPage(); + if (currentPage == 0) { + feedViewModel.getList().postValue(result); + dispatchFetchStatus(); + return; + } + final List models = feedViewModel.getList().getValue(); + final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models); + modelsCopy.addAll(result); + feedViewModel.getList().postValue(modelsCopy); + dispatchFetchStatus(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }; + private final List fetchStatusChangeListeners = new ArrayList<>(); + + public PostsRecyclerView(@NonNull final Context context) { + super(context); + } + + public PostsRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + } + + public PostsRecyclerView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public PostsRecyclerView setViewModelStoreOwner(final ViewModelStoreOwner owner) { + if (initCalled) { + throw new IllegalArgumentException("init already called!"); + } + this.viewModelStoreOwner = owner; + return this; + } + + public PostsRecyclerView setLifeCycleOwner(final LifecycleOwner lifeCycleOwner) { + if (initCalled) { + throw new IllegalArgumentException("init already called!"); + } + this.lifeCycleOwner = lifeCycleOwner; + return this; + } + + public PostsRecyclerView setPostFetchService(final PostFetcher.PostFetchService postFetchService) { + if (initCalled) { + throw new IllegalArgumentException("init already called!"); + } + this.postFetchService = postFetchService; + return this; + } + + public PostsRecyclerView setOnPostClickListener(@NonNull final OnPostClickListener onPostClickListener) { + this.onPostClickListener = onPostClickListener; + return this; + } + + public PostsRecyclerView setMentionClickListener(final MentionClickListener mentionClickListener) { + this.mentionClickListener = mentionClickListener; + return this; + } + + public PostsRecyclerView setLayoutPreferences(final PostsLayoutPreferences layoutPreferences) { + this.layoutPreferences = layoutPreferences; + if (initCalled) { + if (layoutPreferences == null) return this; + feedAdapter.setLayoutPreferences(layoutPreferences); + updateLayout(); + } + return this; + } + + public void init() { + initCalled = true; + if (viewModelStoreOwner == null) { + throw new IllegalArgumentException("ViewModelStoreOwner cannot be null"); + } else if (lifeCycleOwner == null) { + throw new IllegalArgumentException("LifecycleOwner cannot be null"); + } else if (postFetchService == null) { + throw new IllegalArgumentException("PostFetchService cannot be null"); + } + if (layoutPreferences == null) { + layoutPreferences = PostsLayoutPreferences.builder() + .setType(PostsLayoutPreferences.PostsLayoutType.GRID) + .setColCount(3) + .setAvatarVisible(true) + .setNameVisible(false) + .setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.TINY) + .setHasGap(true) + .setHasRoundedCorners(true) + .build(); + Utils.settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, layoutPreferences.getJson()); + } + gridSpacingItemDecoration = new GridSpacingItemDecoration(Utils.convertDpToPx(2)); + initTransition(); + initAdapter(); + initLayoutManager(); + initSelf(); + + } + + private void initTransition() { + transition = new ChangeBounds(); + // transition.addListener(new TransitionListenerAdapter(){ + // @Override + // public void onTransitionEnd(@NonNull final Transition transition) { + // super.onTransitionEnd(transition); + // } + // }); + transition.setDuration(300); + } + + private void initLayoutManager() { + gridLayoutManager = new StaggeredGridLayoutManager(layoutPreferences.getColCount(), StaggeredGridLayoutManager.VERTICAL); + setLayoutManager(gridLayoutManager); + } + + private void initAdapter() { + feedAdapter = new FeedAdapterV2( + layoutPreferences, + postViewClickListener, + mentionClickListener, + (feedModel, view, postImage) -> { + if (onPostClickListener != null) { + onPostClickListener.onPostClick(feedModel, view, postImage); + } + }); + feedAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + setAdapter(feedAdapter); + } + + private void initSelf() { + feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class); + feedViewModel.getList().observe(lifeCycleOwner, feedAdapter::submitList); + postFetcher = new PostFetcher(postFetchService, fetchListener); + addItemDecoration(gridSpacingItemDecoration); + setHasFixedSize(true); + lazyLoader = new RecyclerLazyLoaderAtBottom(gridLayoutManager, (page) -> { + if (postFetcher.hasMore()) { + postFetcher.fetchNextPage(); + dispatchFetchStatus(); + } + }); + addOnScrollListener(lazyLoader); + postFetcher.fetch(); + dispatchFetchStatus(); + } + + private void updateLayout() { + post(() -> { + TransitionManager.beginDelayedTransition(this, transition); + feedAdapter.notifyDataSetChanged(); + if (!layoutPreferences.getHasGap()) { + removeItemDecoration(gridSpacingItemDecoration); + } + gridLayoutManager.setSpanCount(layoutPreferences.getColCount()); + }); + } + + public void refresh() { + lazyLoader.resetState(); + postFetcher.fetch(); + dispatchFetchStatus(); + } + + public boolean isFetching() { + return postFetcher != null && postFetcher.isFetching(); + } + + public PostsRecyclerView addFetchStatusChangeListener(final FetchStatusChangeListener fetchStatusChangeListener) { + if (fetchStatusChangeListener == null) return this; + fetchStatusChangeListeners.add(fetchStatusChangeListener); + return this; + } + + public void removeFetchStatusListener(final FetchStatusChangeListener fetchStatusChangeListener) { + if (fetchStatusChangeListener == null) return; + fetchStatusChangeListeners.remove(fetchStatusChangeListener); + } + + private void dispatchFetchStatus() { + for (final FetchStatusChangeListener listener : fetchStatusChangeListeners) { + listener.onFetchStatusChange(isFetching()); + } + } + + public PostsLayoutPreferences getLayoutPreferences() { + return layoutPreferences; + } + + public interface FetchStatusChangeListener { + void onFetchStatusChange(boolean fetching); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + lifeCycleOwner = null; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java new file mode 100644 index 00000000..cd156d59 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java @@ -0,0 +1,141 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import androidx.annotation.DimenRes; +import androidx.annotation.NonNull; + +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.generic.RoundingParams; + +import java.util.HashMap; +import java.util.Map; + +import awais.instagrabber.R; + +public final class ProfilePicView extends CircularImageView { + private static final String TAG = "ProfilePicView"; + + private Size size; + private int dimensionPixelSize; + + public ProfilePicView(Context context, GenericDraweeHierarchy hierarchy) { + super(context); + setHierarchy(hierarchy); + size = Size.REGULAR; + updateLayout(); + } + + public ProfilePicView(final Context context) { + super(context); + size = Size.REGULAR; + updateLayout(); + } + + public ProfilePicView(final Context context, final AttributeSet attrs) { + super(context, attrs); + parseAttrs(context, attrs); + updateLayout(); + } + + public ProfilePicView(final Context context, + final AttributeSet attrs, + final int defStyleAttr) { + super(context, attrs, defStyleAttr); + parseAttrs(context, attrs); + } + + private void parseAttrs(final Context context, final AttributeSet attrs) { + final TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, + R.styleable.ProfilePicView, + 0, + 0); + try { + final int sizeValue = a.getInt(R.styleable.ProfilePicView_size, Size.REGULAR.getValue()); + size = Size.valueOf(sizeValue); + } finally { + a.recycle(); + } + } + + private void updateLayout() { + @DimenRes final int dimenRes; + switch (size) { + case SMALL: + dimenRes = R.dimen.profile_pic_size_small; + break; + case TINY: + dimenRes = R.dimen.profile_pic_size_tiny; + break; + case LARGE: + dimenRes = R.dimen.profile_pic_size_large; + break; + default: + case REGULAR: + dimenRes = R.dimen.profile_pic_size_regular; + break; + } + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + if (layoutParams == null) { + layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + dimensionPixelSize = getResources().getDimensionPixelSize(dimenRes); + layoutParams.width = dimensionPixelSize; + layoutParams.height = dimensionPixelSize; + + // invalidate(); + // requestLayout(); + } + + public void setStoriesBorder() { + // private final int borderSize = 8; + final int color = Color.GREEN; + RoundingParams roundingParams = getHierarchy().getRoundingParams(); + if (roundingParams == null) { + roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY); + } + roundingParams.setBorder(color, 5.0f); + getHierarchy().setRoundingParams(roundingParams); + } + + public enum Size { + TINY(0), + SMALL(1), + REGULAR(2), + LARGE(3); + + private final int value; + private static Map map = new HashMap<>(); + + static { + for (Size size : Size.values()) { + map.put(size.value, size); + } + } + + Size(final int value) { + this.value = value; + } + + @NonNull + public static Size valueOf(final int value) { + final Size size = map.get(value); + return size != null ? size : Size.REGULAR; + } + + public int getValue() { + return value; + } + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(dimensionPixelSize, dimensionPixelSize); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java b/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java index feca335c..b4b85673 100755 --- a/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java +++ b/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java @@ -34,7 +34,6 @@ public final class RamboTextView extends AppCompatTextView { private ClickableSpan clickableSpanUnderTouchOnActionDown; private MentionClickListener mentionClickListener; private boolean isUrlHighlighted; - private boolean isExpandable; private boolean isExpanded; private OnLongClickListener longClickListener; @@ -59,13 +58,13 @@ public final class RamboTextView extends AppCompatTextView { this.mentionClickListener = mentionClickListener; } - public void setCaptionIsExpandable(final boolean isExpandable) { - this.isExpandable = isExpandable; - } + // public void setCaptionIsExpandable(final boolean isExpandable) { + // this.isExpandable = isExpandable; + // } - public void setCaptionIsExpanded(final boolean isExpanded) { - this.isExpanded = isExpanded; - } + // public void setCaptionIsExpanded(final boolean isExpanded) { + // this.isExpanded = isExpanded; + // } @Override public void setOnLongClickListener(@Nullable final OnLongClickListener l) { @@ -98,7 +97,7 @@ public final class RamboTextView extends AppCompatTextView { case MotionEvent.ACTION_DOWN: final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); handler.postDelayed(longPressRunnable, longPressTimeout); - if (feedModel != null) feedModel.setMentionClicked(false); + // if (feedModel != null) feedModel.setMentionClicked(false); if (clickableSpanUnderTouch != null) { highlightUrl(clickableSpanUnderTouch, spanText); } @@ -107,19 +106,19 @@ public final class RamboTextView extends AppCompatTextView { handler.removeCallbacks(longPressRunnable); if (touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) { dispatchUrlClick(spanText, clickableSpanUnderTouch); - if (feedModel != null) feedModel.setMentionClicked(true); + // if (feedModel != null) feedModel.setMentionClicked(true); } cleanupOnTouchUp(spanText); return super.onTouchEvent(event); case MotionEvent.ACTION_MOVE: // handler.removeCallbacks(longPressRunnable); - if (feedModel != null) feedModel.setMentionClicked(false); + // if (feedModel != null) feedModel.setMentionClicked(false); if (clickableSpanUnderTouch != null) highlightUrl(clickableSpanUnderTouch, spanText); else removeUrlHighlightColor(spanText); return super.onTouchEvent(event); case MotionEvent.ACTION_CANCEL: handler.removeCallbacks(longPressRunnable); - if (feedModel != null) feedModel.setMentionClicked(false); + // if (feedModel != null) feedModel.setMentionClicked(false); cleanupOnTouchUp(spanText); return super.onTouchEvent(event); } diff --git a/app/src/main/java/awais/instagrabber/customviews/SharedElementTransitionDialogFragment.java b/app/src/main/java/awais/instagrabber/customviews/SharedElementTransitionDialogFragment.java new file mode 100644 index 00000000..bec8a690 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/SharedElementTransitionDialogFragment.java @@ -0,0 +1,302 @@ +package awais.instagrabber.customviews; + +import android.animation.Animator; +import android.graphics.Rect; +import android.os.Handler; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.transition.ChangeBounds; +import androidx.transition.ChangeTransform; +import androidx.transition.Transition; +import androidx.transition.TransitionListenerAdapter; +import androidx.transition.TransitionManager; +import androidx.transition.TransitionSet; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import awais.instagrabber.utils.Utils; + +public abstract class SharedElementTransitionDialogFragment extends DialogFragment { + // private static final String TAG = "SETDialogFragment"; + private static final int DURATION = 200; + + private final Map startViews = new HashMap<>(); + private final Map destViews = new HashMap<>(); + private final Map viewBoundsMap = new HashMap<>(); + private final List additionalAnimators = new ArrayList<>(); + private final Handler initialBoundsHandler = new Handler(); + + private boolean startCalled = false; + private boolean startInitiated = false; + private int boundsCalculatedCount = 0; + + protected int getAnimationDuration() { + return DURATION; + } + + public void addSharedElement(@NonNull final View startView, @NonNull final View destView) { + final int key = destView.hashCode(); + startViews.put(key, startView); + destViews.put(key, destView); + initialBoundsHandler.post(() -> setupInitialBounds(startView, destView)); + } + + public void startPostponedEnterTransition() { + startCalled = true; + if (startInitiated) return; + if (boundsCalculatedCount < startViews.size()) return; + startInitiated = true; + final Set keySet = startViews.keySet(); + final View view = getView(); + if (!(view instanceof ViewGroup)) return; + final TransitionSet transitionSet = new TransitionSet() + .setDuration(DURATION) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .addTransition(new ChangeBounds()) + .addTransition(new ChangeTransform()) + .addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionStart(@NonNull final Transition transition) { + for (Animator animator : additionalAnimators) { + animator.start(); + } + } + + @Override + public void onTransitionEnd(@NonNull final Transition transition) { + for (final Integer key : keySet) { + final View startView = startViews.get(key); + final View destView = destViews.get(key); + final ViewBounds viewBounds = viewBoundsMap.get(key); + onEndSharedElementAnimation(startView, destView, viewBounds); + } + } + }); + view.post(() -> { + TransitionManager.beginDelayedTransition((ViewGroup) view, transitionSet); + for (final Integer key : keySet) { + final View startView = startViews.get(key); + final View destView = destViews.get(key); + final ViewBounds viewBounds = viewBoundsMap.get(key); + onBeforeSharedElementAnimation(startView, destView, viewBounds); + setDestBounds(key); + } + }); + } + + private void setDestBounds(final int key) { + final View startView = startViews.get(key); + if (startView == null) return; + final View destView = destViews.get(key); + if (destView == null) return; + final ViewBounds viewBounds = viewBoundsMap.get(key); + if (viewBounds == null) return; + destView.setX((int) viewBounds.getDestX()); + destView.setY((int) viewBounds.getDestY()); + destView.setTranslationX(0); + destView.setTranslationY(0); + final ViewGroup.LayoutParams layoutParams = destView.getLayoutParams(); + layoutParams.height = viewBounds.getDestHeight(); + layoutParams.width = viewBounds.getDestWidth(); + destView.requestLayout(); + } + + protected void onBeforeSharedElementAnimation(@NonNull final View startView, + @NonNull final View destView, + @NonNull final ViewBounds viewBounds) {} + + protected void onEndSharedElementAnimation(@NonNull final View startView, + @NonNull final View destView, + @NonNull final ViewBounds viewBounds) {} + + private void setupInitialBounds(@NonNull final View startView, @NonNull final View destView) { + final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() { + private boolean firstPassDone; + + @Override + public boolean onPreDraw() { + destView.getViewTreeObserver().removeOnPreDrawListener(this); + if (!firstPassDone) { + getViewBounds(startView, destView, this); + firstPassDone = true; + return false; + } + final int[] location = new int[2]; + startView.getLocationOnScreen(location); + final int initX = location[0]; + final int initY = location[1]; + destView.setX(initX); + destView.setY(initY - Utils.getStatusBarHeight(getContext())); + destView.requestLayout(); + boundsCalculatedCount++; + if (startCalled) { + startPostponedEnterTransition(); + } + return true; + } + }; + destView.getViewTreeObserver().addOnPreDrawListener(preDrawListener); + } + + private void getViewBounds(@NonNull final View startView, + @NonNull final View destView, + @NonNull final ViewTreeObserver.OnPreDrawListener preDrawListener) { + final ViewBounds viewBounds = new ViewBounds(); + viewBounds.setDestWidth(destView.getWidth()); + viewBounds.setDestHeight(destView.getHeight()); + viewBounds.setDestX(destView.getX()); + viewBounds.setDestY(destView.getY()); + + final Rect destBounds = new Rect(); + destView.getDrawingRect(destBounds); + viewBounds.setDestBounds(destBounds); + + final ViewGroup.LayoutParams layoutParams = destView.getLayoutParams(); + + final Rect startBounds = new Rect(); + startView.getDrawingRect(startBounds); + viewBounds.setStartBounds(startBounds); + + final int key = destView.hashCode(); + viewBoundsMap.put(key, viewBounds); + + layoutParams.height = startView.getHeight(); + layoutParams.width = startView.getWidth(); + + destView.getViewTreeObserver().addOnPreDrawListener(preDrawListener); + destView.requestLayout(); + } + + // private void animateBounds(@NonNull final View startView, + // @NonNull final View destView, + // @NonNull final ViewBounds viewBounds) { + // final ValueAnimator heightAnimator = ObjectAnimator.ofInt(startView.getHeight(), viewBounds.getDestHeight()); + // final ValueAnimator widthAnimator = ObjectAnimator.ofInt(startView.getWidth(), viewBounds.getDestWidth()); + // heightAnimator.setDuration(DURATION); + // widthAnimator.setDuration(DURATION); + // additionalAnimators.add(heightAnimator); + // additionalAnimators.add(widthAnimator); + // heightAnimator.addUpdateListener(animation -> { + // ViewGroup.LayoutParams params = destView.getLayoutParams(); + // params.height = (int) animation.getAnimatedValue(); + // destView.requestLayout(); + // }); + // widthAnimator.addUpdateListener(animation -> { + // ViewGroup.LayoutParams params = destView.getLayoutParams(); + // params.width = (int) animation.getAnimatedValue(); + // destView.requestLayout(); + // }); + // onBeforeSharedElementAnimation(startView, destView, viewBounds); + // final float destX = viewBounds.getDestX(); + // final float destY = viewBounds.getDestY(); + // final AnimatorSet animatorSet = new AnimatorSet(); + // animatorSet.addListener(new AnimatorListenerAdapter() { + // @Override + // public void onAnimationEnd(final Animator animation) { + // animationEnded(startView, destView, viewBounds); + // } + // }); + // + // destView.animate() + // .x(destX) + // .y(destY) + // .setDuration(DURATION) + // .withStartAction(() -> { + // if (!additionalAnimatorsStarted && additionalAnimators.size() > 0) { + // additionalAnimatorsStarted = true; + // animatorSet.playTogether(additionalAnimators); + // animatorSet.start(); + // } + // }) + // .withEndAction(() -> animationEnded(startView, destView, viewBounds)) + // .start(); + // } + + // private int endCount = 0; + // private void animationEnded(final View startView, final View destView, final ViewBounds viewBounds) { + // ++endCount; + // if (endCount != startViews.size() + 1) return; + // onEndSharedElementAnimation(startView, destView, viewBounds); + // } + + protected void addAnimator(@NonNull final Animator animator) { + additionalAnimators.add(animator); + } + + protected static class ViewBounds { + private float destY; + private float destX; + private int destHeight; + private int destWidth; + private Rect startBounds; + private Rect destBounds; + + public ViewBounds() {} + + public float getDestY() { + return destY; + } + + public void setDestY(final float destY) { + this.destY = destY; + } + + public float getDestX() { + return destX; + } + + public void setDestX(final float destX) { + this.destX = destX; + } + + public int getDestHeight() { + return destHeight; + } + + public void setDestHeight(final int destHeight) { + this.destHeight = destHeight; + } + + public int getDestWidth() { + return destWidth; + } + + public void setDestWidth(final int destWidth) { + this.destWidth = destWidth; + } + + public Rect getStartBounds() { + return startBounds; + } + + public void setStartBounds(final Rect startBounds) { + this.startBounds = startBounds; + } + + public Rect getDestBounds() { + return destBounds; + } + + public void setDestBounds(final Rect destBounds) { + this.destBounds = destBounds; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + startViews.clear(); + destViews.clear(); + viewBoundsMap.clear(); + additionalAnimators.clear(); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/VerticalDragHelper.java b/app/src/main/java/awais/instagrabber/customviews/VerticalDragHelper.java new file mode 100644 index 00000000..e7527e9b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/VerticalDragHelper.java @@ -0,0 +1,203 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +import androidx.annotation.NonNull; + +public class VerticalDragHelper { + private static final String TAG = "VerticalDragHelper"; + private static final float PIXELS_PER_SECOND = 10; + + private final View view; + + private GestureDetector gestureDetector; + private Context context; + private float flingVelocity; + private OnVerticalDragListener onVerticalDragListener; + + private final GestureDetector.OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { + + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + view.performClick(); + return true; + } + + @Override + public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) { + float maxFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); + float velocityPercentY = velocityY / maxFlingVelocity; + float normalizedVelocityY = velocityPercentY * PIXELS_PER_SECOND; + if (Math.abs(normalizedVelocityY) > 4) { + flingVelocity = normalizedVelocityY; + } + return super.onFling(e1, e2, velocityX, velocityY); + } + + + }; + + private final GestureDetector.OnGestureListener dragPreventionGestureListener = new GestureDetector.SimpleOnGestureListener() { + float prevDistanceY = 0; + + @Override + public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, final float distanceY) { + Log.d(TAG, "onScroll: distanceX: " + distanceX + ", distanceY: " + distanceY); + return super.onScroll(e1, e2, distanceX, distanceY); + } + + @Override + public boolean onSingleTapUp(final MotionEvent e) { + Log.d(TAG, "onSingleTapUp"); + return super.onSingleTapUp(e); + } + }; + + private float prevRawY; + private boolean isDragging; + private float prevRawX; + private float dX; + private float prevDY; + private GestureDetector dragPreventionGestureDetector; + + public VerticalDragHelper(@NonNull final View view) { + this.view = view; + final Context context = view.getContext(); + if (context == null) return; + this.context = context; + init(); + } + + public void setOnVerticalDragListener(@NonNull final OnVerticalDragListener onVerticalDragListener) { + this.onVerticalDragListener = onVerticalDragListener; + } + + protected void init() { + gestureDetector = new GestureDetector(context, gestureListener); + dragPreventionGestureDetector = new GestureDetector(context, dragPreventionGestureListener); + } + + public boolean onDragTouch(final MotionEvent event) { + if (onVerticalDragListener == null) { + return false; + } + // dragPreventionGestureDetector.onTouchEvent(event); + if (gestureDetector.onTouchEvent(event)) { + return true; + } + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + return true; + case MotionEvent.ACTION_MOVE: + boolean handled = false; + final float rawY = event.getRawY(); + final float dY = rawY - prevRawY; + if (!isDragging) { + final float rawX = event.getRawX(); + if (prevRawX != 0) { + dX = rawX - prevRawX; + } + prevRawX = rawX; + if (prevRawY != 0) { + final float dYAbs = Math.abs(dY - prevDY); + if (!isDragging && dYAbs < 50) { + final float abs = Math.abs(dY) - Math.abs(dX); + if (abs > 0) { + isDragging = true; + } + } + } + } + if (isDragging) { + final ViewParent parent = view.getParent(); + parent.requestDisallowInterceptTouchEvent(true); + onVerticalDragListener.onDrag(dY); + handled = true; + } + prevDY = dY; + prevRawY = rawY; + return handled; + case MotionEvent.ACTION_UP: + // Log.d(TAG, "onDragTouch: reset prevRawY"); + prevRawY = 0; + if (flingVelocity != 0) { + onVerticalDragListener.onFling(flingVelocity); + flingVelocity = 0; + isDragging = false; + return true; + } + if (isDragging) { + onVerticalDragListener.onDragEnd(); + isDragging = false; + return true; + } + return false; + default: + return false; + } + } + + public boolean isDragging() { + return isDragging; + } + + public boolean onGestureTouchEvent(final MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + + private final static int DIRECTION_UP = 0; + private final static int DIRECTION_DOWN = 1; + float prevY = -1; + int edgeHitCount = 0; + float prevDirection = -1; + + + // private boolean shouldPreventDrag(final MotionEvent event) { + // switch (event.getAction()) { + // case MotionEvent.ACTION_DOWN: + // if (!firstDrag) { + // firstDrag = true; + // } + // return false; + // case MotionEvent.ACTION_MOVE: + // float y = event.getY(); + // int direction = -2; + // if (prevY != -1) { + // final float dy = y - prevY; + // // Log.d(TAG, "shouldPreventDrag: dy: " + dy); + // if (dy > 0) { + // direction = DIRECTION_DOWN; + // // move direction is down + // } else { + // direction = DIRECTION_UP; + // // move direction is up + // } + // } + // prevY = y; + // if (prevDirection == direction) { + // edgeHitCount++; + // } else { + // edgeHitCount = 1; + // } + // if (edgeHitCount >= 2) { + // return false; + // } + // return true; + // break; + // } + // } + + public interface OnVerticalDragListener { + void onDrag(final float dY); + + void onDragEnd(); + + void onFling(final float flingVelocity); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java new file mode 100644 index 00000000..7e393800 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java @@ -0,0 +1,18 @@ +package awais.instagrabber.customviews; + +public class VideoPlayerCallbackAdapter implements VideoPlayerViewHelper.VideoPlayerCallback { + @Override + public void onThumbnailLoaded() {} + + @Override + public void onThumbnailClick() {} + + @Override + public void onPlayerViewLoaded() {} + + @Override + public void onPlay() {} + + @Override + public void onPause() {} +} diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java new file mode 100644 index 00000000..eb23cbe8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java @@ -0,0 +1,373 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.graphics.drawable.Animatable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.appcompat.widget.PopupMenu; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioListener; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; + +import awais.instagrabber.R; +import awais.instagrabber.databinding.LayoutExoCustomControlsBinding; +import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding; +import awais.instagrabber.utils.TextUtils; + +import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.android.exoplayer2.Player.STATE_ENDED; +import static com.google.android.exoplayer2.Player.STATE_IDLE; +import static com.google.android.exoplayer2.Player.STATE_READY; + +public class VideoPlayerViewHelper implements Player.EventListener { + private static final String TAG = "VideoPlayerViewHelper"; + + private final Context context; + private final awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding binding; + private final float initialVolume; + private final float thumbnailAspectRatio; + private final String thumbnailUrl; + private final awais.instagrabber.databinding.LayoutExoCustomControlsBinding controlsBinding; + private final VideoPlayerCallback videoPlayerCallback; + private final String videoUrl; + private final DefaultDataSourceFactory dataSourceFactory; + private SimpleExoPlayer player; + private PopupMenu speedPopup; + + public VideoPlayerViewHelper(@NonNull final Context context, + @NonNull final LayoutVideoPlayerWithThumbnailBinding binding, + @NonNull final String videoUrl, + final float initialVolume, + final float thumbnailAspectRatio, + final String thumbnailUrl, + final LayoutExoCustomControlsBinding controlsBinding, + final VideoPlayerCallback videoPlayerCallback) { + this.context = context; + this.binding = binding; + this.initialVolume = initialVolume; + this.thumbnailAspectRatio = thumbnailAspectRatio; + this.thumbnailUrl = thumbnailUrl; + this.controlsBinding = controlsBinding; + this.videoPlayerCallback = videoPlayerCallback; + this.videoUrl = videoUrl; + this.dataSourceFactory = new DefaultDataSourceFactory(binding.getRoot().getContext(), "instagram"); + bind(); + } + + private void bind() { + binding.thumbnailParent.setOnClickListener(v -> { + if (videoPlayerCallback != null) { + videoPlayerCallback.onThumbnailClick(); + } + loadPlayer(); + }); + setThumbnail(); + setupControls(); + } + + private void setThumbnail() { + binding.thumbnail.setAspectRatio(thumbnailAspectRatio); + final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)) + .build(); + final DraweeController controller = Fresco.newDraweeControllerBuilder() + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + if (videoPlayerCallback != null) { + videoPlayerCallback.onThumbnailLoaded(); + } + } + + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + if (videoPlayerCallback != null) { + videoPlayerCallback.onThumbnailLoaded(); + } + } + }) + .setImageRequest(thumbnailRequest) + .build(); + binding.thumbnail.setController(controller); + } + + private void loadPlayer() { + if (videoUrl == null) return; + if (binding.root.getDisplayedChild() == 0) { + binding.root.showNext(); + } + if (videoPlayerCallback != null) { + videoPlayerCallback.onPlayerViewLoaded(); + } + player = (SimpleExoPlayer) binding.playerView.getPlayer(); + if (player != null) { + player.release(); + } + player = new SimpleExoPlayer.Builder(context) + .setLooper(Looper.getMainLooper()) + .build(); + player.addListener(this); + player.setVolume(initialVolume); + player.setPlayWhenReady(true); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(dataSourceFactory); + final MediaItem mediaItem = MediaItem.fromUri(videoUrl); + final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(mediaItem); + player.setMediaSource(mediaSource); + setupControls(); + player.prepare(); + binding.playerView.setPlayer(player); + } + + private void setupControls() { + if (controlsBinding == null) return; + binding.playerView.setUseController(false); + if (player == null) { + enableControls(false); + controlsBinding.playPause.setEnabled(true); + controlsBinding.playPause.setOnClickListener(v -> binding.thumbnailParent.performClick()); + return; + } + enableControls(true); + final Handler handler = new Handler(); + final long initialDelay = 0; + final long recurringDelay = 60; + final Runnable positionChecker = new Runnable() { + @Override + public void run() { + handler.removeCallbacks(this); + if (player == null) return; + final long currentPosition = player.getCurrentPosition(); + final long duration = player.getDuration(); + if (duration == TIME_UNSET) { + controlsBinding.timeline.setValueFrom(0); + controlsBinding.timeline.setValueTo(0); + controlsBinding.timeline.setEnabled(false); + return; + } + controlsBinding.timeline.setValue(Math.min(currentPosition, duration)); + controlsBinding.fromTime.setText(TextUtils.millisToTimeString(currentPosition)); + handler.postDelayed(this, recurringDelay); + } + }; + updatePlayPauseDrawable(player.getPlayWhenReady()); + updateMuteIcon(player.getVolume()); + player.addListener(new Player.EventListener() { + @Override + public void onPlaybackStateChanged(final int state) { + switch (state) { + case Player.STATE_BUFFERING: + case STATE_IDLE: + case STATE_ENDED: + handler.removeCallbacks(positionChecker); + return; + case STATE_READY: + setupTimeline(); + handler.postDelayed(positionChecker, initialDelay); + break; + } + } + + @Override + public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) { + updatePlayPauseDrawable(playWhenReady); + } + }); + player.addAudioListener(new AudioListener() { + @Override + public void onVolumeChanged(final float volume) { + updateMuteIcon(volume); + } + }); + controlsBinding.timeline.addOnChangeListener((slider, value, fromUser) -> { + if (!fromUser) return; + long actualValue = (long) value; + if (actualValue < 0) { + actualValue = 0; + } else if (actualValue > player.getDuration()) { + actualValue = player.getDuration(); + } + player.seekTo(actualValue); + }); + controlsBinding.timeline.setLabelFormatter(value -> TextUtils.millisToTimeString((long) value)); + controlsBinding.playPause.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); + controlsBinding.mute.setOnClickListener(v -> toggleMute()); + controlsBinding.rewWithAmount.setOnClickListener(v -> { + final long positionMs = player.getCurrentPosition() - 5000; + player.seekTo(positionMs < 0 ? 0 : positionMs); + }); + controlsBinding.ffWithAmount.setOnClickListener(v -> { + long positionMs = player.getCurrentPosition() + 5000; + long duration = player.getDuration(); + if (duration == TIME_UNSET) { + duration = 0; + } + player.seekTo(Math.min(positionMs, duration)); + }); + controlsBinding.speed.setOnClickListener(this::showMenu); + } + + private void setupTimeline() { + final long duration = player.getDuration(); + controlsBinding.timeline.setEnabled(true); + controlsBinding.timeline.setValueFrom(0); + controlsBinding.timeline.setValueTo(duration); + controlsBinding.fromTime.setText(TextUtils.millisToTimeString(0)); + controlsBinding.toTime.setText(TextUtils.millisToTimeString(duration)); + } + + private void enableControls(final boolean enable) { + controlsBinding.speed.setEnabled(enable); + controlsBinding.mute.setEnabled(enable); + controlsBinding.ffWithAmount.setEnabled(enable); + controlsBinding.rewWithAmount.setEnabled(enable); + controlsBinding.fromTime.setEnabled(enable); + controlsBinding.toTime.setEnabled(enable); + controlsBinding.playPause.setEnabled(enable); + } + + public void showMenu(View anchor) { + PopupMenu popup = getPopupMenu(anchor); + popup.show(); + } + + @NonNull + private PopupMenu getPopupMenu(final View anchor) { + if (speedPopup != null) { + return speedPopup; + } + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.popupMenuStyle); + // final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context, R.style.Widget_MaterialComponents_PopupMenu_Exoplayer); + speedPopup = new PopupMenu(themeWrapper, anchor); + speedPopup.getMenuInflater().inflate(R.menu.speed_menu, speedPopup.getMenu()); + speedPopup.setOnMenuItemClickListener(item -> { + float nextSpeed; + int textResId; + switch (item.getItemId()) { + case R.id.pt_two_five_x: + nextSpeed = 0.25f; + textResId = R.string.pt_two_five_x; + break; + case R.id.pt_five_x: + nextSpeed = 0.5f; + textResId = R.string.pt_five_x; + break; + case R.id.pt_seven_five_x: + nextSpeed = 0.75f; + textResId = R.string.pt_seven_five_x; + break; + case R.id.one_x: + nextSpeed = 1f; + textResId = R.string.one_x; + break; + case R.id.one_pt_two_five_x: + nextSpeed = 1.25f; + textResId = R.string.one_pt_two_five_x; + break; + case R.id.one_pt_five_x: + nextSpeed = 1.5f; + textResId = R.string.one_pt_five_x; + break; + case R.id.two_x: + nextSpeed = 2f; + textResId = R.string.two_x; + break; + default: + nextSpeed = 1; + textResId = R.string.one_x; + } + player.setPlaybackParameters(new PlaybackParameters(nextSpeed)); + controlsBinding.speed.setText(textResId); + return true; + }); + return speedPopup; + } + + private void updateMuteIcon(final float volume) { + if (volume == 0) { + controlsBinding.mute.setIconResource(R.drawable.ic_volume_off_24); + return; + } + controlsBinding.mute.setIconResource(R.drawable.ic_volume_up_24); + } + + private void updatePlayPauseDrawable(final boolean playWhenReady) { + if (playWhenReady) { + controlsBinding.playPause.setIconResource(R.drawable.ic_pause_24); + return; + } + controlsBinding.playPause.setIconResource(R.drawable.ic_play_arrow_24); + } + + @Override + public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) { + if (videoPlayerCallback == null) return; + if (playWhenReady) { + videoPlayerCallback.onPlay(); + return; + } + videoPlayerCallback.onPause(); + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + Log.e(TAG, "onPlayerError", error); + } + + public float toggleMute() { + if (player == null) return 0; + final float vol = player.getVolume() == 0f ? 1f : 0f; + player.setVolume(vol); + return vol; + } + + public void togglePlayback() { + if (player == null) return; + final int playbackState = player.getPlaybackState(); + if (playbackState == STATE_IDLE || playbackState == STATE_ENDED) return; + final boolean playWhenReady = player.getPlayWhenReady(); + player.setPlayWhenReady(!playWhenReady); + } + + public void releasePlayer() { + if (player == null) return; + player.release(); + player = null; + } + + public void pause() { + if (player == null) return; + player.pause(); + } + + public interface VideoPlayerCallback { + void onThumbnailLoaded(); + + void onThumbnailClick(); + + void onPlayerViewLoaded(); + + void onPlay(); + + void onPause(); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java index 0ee5d0cd..7e83dc2f 100644 --- a/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java @@ -572,16 +572,16 @@ public class DefaultZoomableController RectF b = mTempRect; b.set(mImageBounds); transform.mapRect(b); - float offsetLeft = - shouldLimit(limitTypes, LIMIT_TRANSLATION_X) - ? getOffset( - b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()) - : 0; - float offsetTop = - shouldLimit(limitTypes, LIMIT_TRANSLATION_Y) - ? getOffset( - b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()) - : 0; + final boolean shouldLimitX = shouldLimit(limitTypes, LIMIT_TRANSLATION_X); + float offsetLeft = shouldLimitX + ? getOffset(b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()) + : 0; + float offsetTop = shouldLimit(limitTypes, LIMIT_TRANSLATION_Y) + ? getOffset(b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()) + : 0; + if (mListener != null) { + mListener.onTranslationLimited(offsetLeft, offsetTop); + } if (offsetLeft != 0 || offsetTop != 0) { transform.postTranslate(offsetLeft, offsetTop); return true; diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/DraggableZoomableDraweeView.java b/app/src/main/java/awais/instagrabber/customviews/drawee/DraggableZoomableDraweeView.java new file mode 100644 index 00000000..c204e8a4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/DraggableZoomableDraweeView.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package awais.instagrabber.customviews.drawee; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Matrix; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; + +import com.facebook.drawee.generic.GenericDraweeHierarchy; + +import awais.instagrabber.customviews.VerticalDragHelper; +import awais.instagrabber.customviews.VerticalDragHelper.OnVerticalDragListener; + +public class DraggableZoomableDraweeView extends ZoomableDraweeView { + private static final String TAG = "DraggableZoomableDV"; + + private VerticalDragHelper verticalDragHelper; + + public DraggableZoomableDraweeView(final Context context, final GenericDraweeHierarchy hierarchy) { + super(context, hierarchy); + verticalDragHelper = new VerticalDragHelper(this); + } + + public DraggableZoomableDraweeView(final Context context) { + super(context); + verticalDragHelper = new VerticalDragHelper(this); + } + + public DraggableZoomableDraweeView(final Context context, final AttributeSet attrs) { + super(context, attrs); + verticalDragHelper = new VerticalDragHelper(this); + } + + public DraggableZoomableDraweeView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + verticalDragHelper = new VerticalDragHelper(this); + } + + public void setOnVerticalDragListener(@NonNull final OnVerticalDragListener onVerticalDragListener) { + verticalDragHelper.setOnVerticalDragListener(onVerticalDragListener); + } + + private int lastPointerCount; + private int lastNewPointerCount; + private boolean wasTransformCorrected; + + @Override + protected void onTransformEnd(final Matrix transform) { + super.onTransformEnd(transform); + final AnimatedZoomableController zoomableController = (AnimatedZoomableController) getZoomableController(); + final TransformGestureDetector detector = zoomableController.getDetector(); + lastNewPointerCount = detector.getNewPointerCount(); + lastPointerCount = detector.getPointerCount(); + } + + @Override + protected void onTranslationLimited(final float offsetLeft, final float offsetTop) { + super.onTranslationLimited(offsetLeft, offsetTop); + wasTransformCorrected = offsetTop != 0; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(final MotionEvent event) { + boolean superResult = false; + if (verticalDragHelper.isDragging()) { + final boolean onDragTouch = verticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + } + if (!verticalDragHelper.isDragging()) { + superResult = super.onTouchEvent(event); + if (wasTransformCorrected + && (lastPointerCount == 1 || lastPointerCount == 0) + && (lastNewPointerCount == 1 || lastNewPointerCount == 0)) { + final boolean onDragTouch = verticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + } + } + final boolean gestureListenerResult = verticalDragHelper.onGestureTouchEvent(event); + if (gestureListenerResult) { + return true; + } + return superResult; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java index b459ece6..578c6ecb 100644 --- a/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java @@ -41,6 +41,13 @@ public class MultiZoomableControllerListener implements ZoomableController.Liste } } + @Override + public void onTranslationLimited(final float offsetLeft, final float offsetTop) { + for (ZoomableController.Listener listener : mListeners) { + listener.onTranslationLimited(offsetLeft, offsetTop); + } + } + public synchronized void addListener(ZoomableController.Listener listener) { mListeners.add(listener); } diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java index 93d83586..dc31ea97 100644 --- a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java @@ -42,6 +42,8 @@ public interface ZoomableController { * @param transform the current transform matrix */ void onTransformEnd(Matrix transform); + + void onTranslationLimited(float offsetLeft, float offsetTop); } /** diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java index 5f5f4286..5a7d55ad 100644 --- a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java @@ -33,8 +33,6 @@ import com.facebook.drawee.generic.GenericDraweeHierarchyInflater; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.view.DraweeView; -import awais.instagrabber.customviews.helpers.SwipeGestureListener; - /** * DraweeView that has zoomable capabilities. @@ -54,7 +52,7 @@ public class ZoomableDraweeView extends DraweeView private DraweeController mHugeImageController; private ZoomableController mZoomableController; private GestureDetector mTapGestureDetector; - private boolean mAllowTouchInterceptionWhileZoomed = true; + private boolean mAllowTouchInterceptionWhileZoomed = false; private boolean mIsDialtoneEnabled = false; private boolean mZoomingEnabled = true; @@ -76,7 +74,9 @@ public class ZoomableDraweeView extends DraweeView private final ZoomableController.Listener mZoomableListener = new ZoomableController.Listener() { @Override - public void onTransformBegin(Matrix transform) {} + public void onTransformBegin(Matrix transform) { + ZoomableDraweeView.this.onTransformBegin(transform); + } @Override public void onTransformChanged(Matrix transform) { @@ -84,7 +84,14 @@ public class ZoomableDraweeView extends DraweeView } @Override - public void onTransformEnd(Matrix transform) {} + public void onTransformEnd(Matrix transform) { + ZoomableDraweeView.this.onTransformEnd(transform); + } + + @Override + public void onTranslationLimited(final float offsetLeft, final float offsetTop) { + ZoomableDraweeView.this.onTranslationLimited(offsetLeft, offsetTop); + } }; private final GestureListenerWrapper mTapListenerWrapper = new GestureListenerWrapper(); @@ -302,11 +309,10 @@ public class ZoomableDraweeView extends DraweeView int a = event.getActionMasked(); FLog.v(getLogTag(), "onTouchEvent: %d, view %x, received", a, this.hashCode()); if (!mIsDialtoneEnabled && mTapGestureDetector.onTouchEvent(event)) { - FLog.v( - getLogTag(), - "onTouchEvent: %d, view %x, handled by tap gesture detector", - a, - this.hashCode()); + FLog.v(getLogTag(), + "onTouchEvent: %d, view %x, handled by tap gesture detector", + a, + this.hashCode()); return true; } @@ -389,23 +395,29 @@ public class ZoomableDraweeView extends DraweeView mZoomableController.setEnabled(false); } + protected void onTransformBegin(final Matrix transform) {} + protected void onTransformChanged(Matrix transform) { FLog.v(getLogTag(), "onTransformChanged: view %x, transform: %s", this.hashCode(), transform); maybeSetHugeImageController(); invalidate(); } + protected void onTransformEnd(final Matrix transform) {} + + protected void onTranslationLimited(final float offsetLeft, final float offsetTop) {} + protected void updateZoomableControllerBounds() { getImageBounds(mImageBounds); getLimitBounds(mViewBounds); + // Log.d(TAG.getSimpleName(), "updateZoomableControllerBounds: mImageBounds: " + mImageBounds); mZoomableController.setImageBounds(mImageBounds); mZoomableController.setViewBounds(mViewBounds); - FLog.v( - getLogTag(), - "updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s", - this.hashCode(), - mViewBounds, - mImageBounds); + FLog.v(getLogTag(), + "updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s", + this.hashCode(), + mViewBounds, + mImageBounds); } protected Class getLogTag() { diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java index c0f24d21..81b4be92 100755 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java @@ -4,7 +4,6 @@ import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { @@ -16,16 +15,14 @@ public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - final RecyclerView.LayoutManager manager = parent.getLayoutManager(); - if (manager instanceof GridLayoutManager) { - final int spanCount = ((GridLayoutManager) manager).getSpanCount(); - final int position = parent.getChildAdapterPosition(view); - final int column = position % spanCount; - - outRect.left = column * spacing / spanCount; - outRect.right = spacing - (column + 1) * spacing / spanCount; - if (position < spanCount) outRect.top = spacing; - outRect.bottom = spacing; + final int halfSpace = spacing / 2; + if (parent.getPaddingLeft() != halfSpace) { + parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace); + parent.setClipToPadding(false); } + outRect.top = halfSpace; + outRect.bottom = halfSpace; + outRect.left = halfSpace; + outRect.right = halfSpace; } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java new file mode 100644 index 00000000..75493bdd --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java @@ -0,0 +1,50 @@ +package awais.instagrabber.customviews.helpers; + +import java.util.List; + +import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; + +public class PostFetcher { + private final PostFetchService postFetchService; + private final FetchListener> fetchListener; + private boolean fetching; + + public PostFetcher(final PostFetchService postFetchService, + final FetchListener> fetchListener) { + this.postFetchService = postFetchService; + this.fetchListener = fetchListener; + } + + public void fetch() { + fetch(null); + } + + public void fetchNextPage() { + fetch(postFetchService.getNextCursor()); + } + + public void fetch(final String cursor) { + fetching = true; + postFetchService.fetch(cursor, result -> { + fetching = false; + fetchListener.onResult(result); + }); + } + + public boolean isFetching() { + return fetching; + } + + public boolean hasMore() { + return postFetchService.hasNextPage(); + } + + public interface PostFetchService { + void fetch(String cursor, FetchListener> fetchListener); + + String getNextCursor(); + + boolean hasNextPage(); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java index 6a165451..37de2ff2 100755 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoader.java @@ -1,23 +1,41 @@ package awais.instagrabber.customviews.helpers; +import android.os.Handler; + import androidx.annotation.NonNull; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import awais.instagrabber.interfaces.LazyLoadListener; -// thanks to nesquena's EndlessRecyclerViewScrollListener -// https://gist.github.com/nesquena/d09dc68ff07e845cc622 +/** + * thanks to nesquena's EndlessRecyclerViewScrollListener + */ public final class RecyclerLazyLoader extends RecyclerView.OnScrollListener { - private int currentPage = 0; // The current offset index of data you have loaded - private int previousTotalItemCount = 0; // The total number of items in the dataset after the last load - private boolean loading = true; // True if we are still waiting for the last set of data to load. - private final int visibleThreshold; // The minimum amount of items to have below your current scroll position before loading more. + /** + * The current offset index of data you have loaded + */ + private int currentPage = 0; + /** + * The total number of items in the data set after the last load + */ + private int previousTotalItemCount = 0; + /** + * true if we are still waiting for the last set of data to load. + */ + private boolean loading = true; + /** + * The minimum amount of items to have below your current scroll position before loading more. + */ + private final int visibleThreshold; private final LazyLoadListener lazyLoadListener; private final RecyclerView.LayoutManager layoutManager; - public RecyclerLazyLoader(@NonNull final RecyclerView.LayoutManager layoutManager, final LazyLoadListener lazyLoadListener, final int threshold) { + public RecyclerLazyLoader(@NonNull final RecyclerView.LayoutManager layoutManager, + final LazyLoadListener lazyLoadListener, + final int threshold) { this.layoutManager = layoutManager; this.lazyLoadListener = lazyLoadListener; if (threshold > 0) { @@ -26,6 +44,8 @@ public final class RecyclerLazyLoader extends RecyclerView.OnScrollListener { } if (layoutManager instanceof GridLayoutManager) { this.visibleThreshold = 5 * Math.max(3, ((GridLayoutManager) layoutManager).getSpanCount()); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + this.visibleThreshold = 4 * Math.max(3, ((StaggeredGridLayoutManager) layoutManager).getSpanCount()); } else if (layoutManager instanceof LinearLayoutManager) { this.visibleThreshold = ((LinearLayoutManager) layoutManager).getReverseLayout() ? 4 : 8; } else { @@ -33,7 +53,8 @@ public final class RecyclerLazyLoader extends RecyclerView.OnScrollListener { } } - public RecyclerLazyLoader(@NonNull final RecyclerView.LayoutManager layoutManager, final LazyLoadListener lazyLoadListener) { + public RecyclerLazyLoader(@NonNull final RecyclerView.LayoutManager layoutManager, + final LazyLoadListener lazyLoadListener) { this(layoutManager, lazyLoadListener, -1); } @@ -52,22 +73,37 @@ public final class RecyclerLazyLoader extends RecyclerView.OnScrollListener { previousTotalItemCount = totalItemCount; } - final int lastVisibleItemPosition; + int lastVisibleItemPosition; if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager layoutManager = (GridLayoutManager) this.layoutManager; lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + final StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) this.layoutManager; + final int spanCount = layoutManager.getSpanCount(); + final int[] lastVisibleItemPositions = layoutManager.findLastVisibleItemPositions(null); + lastVisibleItemPosition = 0; + for (final int itemPosition : lastVisibleItemPositions) { + if (itemPosition > lastVisibleItemPosition) { + lastVisibleItemPosition = itemPosition; + } + } } else { final LinearLayoutManager layoutManager = (LinearLayoutManager) this.layoutManager; lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); } if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) { - if (lazyLoadListener != null) - lazyLoadListener.onLoadMore(++currentPage, totalItemCount); loading = true; + if (lazyLoadListener != null) { + new Handler().postDelayed(() -> lazyLoadListener.onLoadMore(++currentPage, totalItemCount), 200); + } } } + public int getCurrentPage() { + return currentPage; + } + public void resetState() { this.currentPage = 0; this.previousTotalItemCount = 0; diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java new file mode 100644 index 00000000..0af5e021 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java @@ -0,0 +1,51 @@ +package awais.instagrabber.customviews.helpers; + +import android.os.Handler; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public final class RecyclerLazyLoaderAtBottom extends RecyclerView.OnScrollListener { + + @NonNull + private final RecyclerView.LayoutManager layoutManager; + private final LazyLoadListener lazyLoadListener; + private int currentPage; + private int previousItemCount; + private boolean loading; + + public RecyclerLazyLoaderAtBottom(@NonNull final RecyclerView.LayoutManager layoutManager, + final LazyLoadListener lazyLoadListener) { + this.layoutManager = layoutManager; + this.lazyLoadListener = lazyLoadListener; + } + + @Override + public void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) { + super.onScrollStateChanged(recyclerView, newState); + final int itemCount = layoutManager.getItemCount(); + if (itemCount > previousItemCount) { + loading = false; + } + if (!recyclerView.canScrollVertically(RecyclerView.SCROLL_AXIS_HORIZONTAL) && newState == RecyclerView.SCROLL_STATE_IDLE) { + if (!loading && lazyLoadListener != null) { + loading = true; + new Handler().postDelayed(() -> lazyLoadListener.onLoadMore(++currentPage), 1000); + } + } + } + + public int getCurrentPage() { + return currentPage; + } + + public void resetState() { + currentPage = 0; + previousItemCount = 0; + loading = true; + } + + public interface LazyLoadListener { + void onLoadMore(final int page); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java b/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java index 2cd49f14..263052b8 100755 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java @@ -70,7 +70,7 @@ public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { final FeedVideoViewHolder videoHolder = getFirstVideoHolder(recyclerView, firstVisibleItemPos, lastVisibleItemPos); if (videoHolder == null || videoHolder.getCurrentFeedModel() == null) { if (currentlyPlayingViewHolder != null) { - currentlyPlayingViewHolder.stopPlaying(); + // currentlyPlayingViewHolder.stopPlaying(); currentlyPlayingViewHolder = null; } return; @@ -80,9 +80,9 @@ public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { return; } if (currentlyPlayingViewHolder != null) { - currentlyPlayingViewHolder.stopPlaying(); + // currentlyPlayingViewHolder.stopPlaying(); } - videoHolder.startPlaying(); + // videoHolder.startPlaying(); currentlyPlayingViewHolder = videoHolder; } // boolean processFirstItem = false, processLastItem = false; @@ -196,7 +196,7 @@ public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { for (int pos = firstVisibleItemPos; pos <= lastVisibleItemPos; pos++) { final View view = layoutManager.findViewByPosition(pos); if (view != null && view.getId() == R.id.videoHolder) { - final View viewSwitcher = view.findViewById(R.id.view_switcher); + final View viewSwitcher = view.findViewById(R.id.root); if (viewSwitcher == null) { continue; } @@ -220,113 +220,113 @@ public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { if (currentlyPlayingViewHolder == null) { return; } - currentlyPlayingViewHolder.startPlaying(); + // currentlyPlayingViewHolder.startPlaying(); } public void stopPlaying() { if (currentlyPlayingViewHolder == null) { return; } - currentlyPlayingViewHolder.stopPlaying(); + // currentlyPlayingViewHolder.stopPlaying(); } -// private synchronized void attachVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { -// synchronized (LOCK) { -// if (recyclerView != null) { -// final RecyclerView.Adapter adapter = recyclerView.getAdapter(); -// if (adapter instanceof FeedAdapter) { -// final SimpleExoPlayer pagerPlayer = ((FeedAdapter) adapter).pagerPlayer; -// if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); -// } -// } -// if (itemView == null) { -// return; -// } -// final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); -// final FeedModel feedModel = feedModels.get(itemPos); -// // loadVideo(itemPos, itemView, shouldAutoplay, feedModel); -// } -// } -// -// private void loadVideo(final int itemPos, final View itemView, final boolean shouldAutoplay, final FeedModel feedModel) { -// final PlayerView playerView = itemView.findViewById(R.id.playerView); -// if (playerView == null) { -// return; -// } -// if (player != null) { -// player.stop(true); -// player.release(); -// player = null; -// } -// -// player = new SimpleExoPlayer.Builder(context) -// .setUseLazyPreparation(!shouldAutoplay) -// .build(); -// player.setPlayWhenReady(shouldAutoplay); -// -// final View btnComments = itemView.findViewById(R.id.btnComments); -// if (btnComments != null) { -// if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false); -// else { -// btnComments.setTag(feedModel); -// btnComments.setEnabled(true); -// btnComments.setOnClickListener(commentClickListener); -// } -// } -// playerView.setPlayer(player); -// btnMute = itemView.findViewById(R.id.btnMute); -// float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; -// if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; -// player.setVolume(vol); -// -// if (btnMute != null) { -// btnMute.setVisibility(View.VISIBLE); -// btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); -// btnMute.setOnClickListener(muteClickListener); -// } -// final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; -// final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); -// final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModel.getDisplayUrl())); -// -// player.setRepeatMode(Player.REPEAT_MODE_ALL); -// player.prepare(mediaSource); -// player.setVolume(vol); -// -// playerView.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); -// -// if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player); -// } -// -// private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { -// // Log.d("AWAISKING_APP", "release: " + itemPos); -// // if (player != null) { -// // player.stop(true); -// // player.release(); -// // } -// // player = null; -// } -// -// private void playVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { -// // if (player != null) { -// // final int playbackState = player.getPlaybackState(); -// // if (!player.isPlaying() -// // || playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED -// // ) { -// // player.setPlayWhenReady(true); -// // } -// // } -// // if (player != null) { -// // player.setPlayWhenReady(true); -// // player.getPlaybackState(); -// // } -// } -// -// private void stopVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { -// if (player != null) { -// player.setPlayWhenReady(false); -// player.getPlaybackState(); -// } -// } + // private synchronized void attachVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { + // synchronized (LOCK) { + // if (recyclerView != null) { + // final RecyclerView.Adapter adapter = recyclerView.getAdapter(); + // if (adapter instanceof FeedAdapter) { + // final SimpleExoPlayer pagerPlayer = ((FeedAdapter) adapter).pagerPlayer; + // if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); + // } + // } + // if (itemView == null) { + // return; + // } + // final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); + // final FeedModel feedModel = feedModels.get(itemPos); + // // loadVideo(itemPos, itemView, shouldAutoplay, feedModel); + // } + // } + // + // private void loadVideo(final int itemPos, final View itemView, final boolean shouldAutoplay, final FeedModel feedModel) { + // final PlayerView playerView = itemView.findViewById(R.id.playerView); + // if (playerView == null) { + // return; + // } + // if (player != null) { + // player.stop(true); + // player.release(); + // player = null; + // } + // + // player = new SimpleExoPlayer.Builder(context) + // .setUseLazyPreparation(!shouldAutoplay) + // .build(); + // player.setPlayWhenReady(shouldAutoplay); + // + // final View btnComments = itemView.findViewById(R.id.btnComments); + // if (btnComments != null) { + // if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false); + // else { + // btnComments.setTag(feedModel); + // btnComments.setEnabled(true); + // btnComments.setOnClickListener(commentClickListener); + // } + // } + // playerView.setPlayer(player); + // btnMute = itemView.findViewById(R.id.btnMute); + // float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; + // if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; + // player.setVolume(vol); + // + // if (btnMute != null) { + // btnMute.setVisibility(View.VISIBLE); + // btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); + // btnMute.setOnClickListener(muteClickListener); + // } + // final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; + // final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); + // final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModel.getDisplayUrl())); + // + // player.setRepeatMode(Player.REPEAT_MODE_ALL); + // player.prepare(mediaSource); + // player.setVolume(vol); + // + // playerView.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); + // + // if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player); + // } + // + // private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { + // // Log.d("AWAISKING_APP", "release: " + itemPos); + // // if (player != null) { + // // player.stop(true); + // // player.release(); + // // } + // // player = null; + // } + // + // private void playVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { + // // if (player != null) { + // // final int playbackState = player.getPlaybackState(); + // // if (!player.isPlaying() + // // || playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED + // // ) { + // // player.setPlayWhenReady(true); + // // } + // // } + // // if (player != null) { + // // player.setPlayWhenReady(true); + // // player.getPlaybackState(); + // // } + // } + // + // private void stopVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { + // if (player != null) { + // player.setPlayWhenReady(false); + // player.getPlaybackState(); + // } + // } public interface VideoChangeCallback { void playerChanged(final int itemPos, final SimpleExoPlayer player); diff --git a/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java new file mode 100644 index 00000000..e051a3e6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java @@ -0,0 +1,219 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import awais.instagrabber.R; +import awais.instagrabber.databinding.DialogPostLayoutPreferencesBinding; +import awais.instagrabber.models.PostsLayoutPreferences; +import awais.instagrabber.utils.Constants; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class PostsLayoutPreferencesDialogFragment extends DialogFragment { + + private final PostsLayoutPreferences.Builder preferencesBuilder; + @NonNull + private final OnApplyListener onApplyListener; + private DialogPostLayoutPreferencesBinding binding; + private Context context; + + public PostsLayoutPreferencesDialogFragment(@NonNull final OnApplyListener onApplyListener) { + final PostsLayoutPreferences preferences = PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_POSTS_LAYOUT)); + this.preferencesBuilder = PostsLayoutPreferences.builder().mergeFrom(preferences); + this.onApplyListener = onApplyListener; + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + this.context = context; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + binding = DialogPostLayoutPreferencesBinding.inflate(LayoutInflater.from(context), null, false); + init(); + return new MaterialAlertDialogBuilder(context) + .setView(binding.getRoot()) + .setPositiveButton(R.string.apply, (dialog, which) -> { + final PostsLayoutPreferences preferences = preferencesBuilder.build(); + final String json = preferences.getJson(); + settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, json); + onApplyListener.onApply(preferences); + }) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final Window window = dialog.getWindow(); + if (window == null) return; + window.setWindowAnimations(R.style.dialog_window_animation); + } + + private void init() { + initLayoutToggle(); + if (preferencesBuilder.getType() != PostsLayoutPreferences.PostsLayoutType.LINEAR) { + initStaggeredOrGridOptions(); + } + } + + private void initStaggeredOrGridOptions() { + initColCountToggle(); + initNamesToggle(); + initAvatarsToggle(); + initCornersToggle(); + initGapToggle(); + } + + private void initLayoutToggle() { + binding.layoutToggle.check(getSelectedLayoutId()); + // binding.staggeredOrGridOptions.setVisibility(getSelectedLayoutId() != R.id.layout_linear ? View.VISIBLE : View.GONE); + binding.layoutToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + if (isChecked) { + switch (checkedId) { + case R.id.layout_linear: + preferencesBuilder.setType(PostsLayoutPreferences.PostsLayoutType.LINEAR); + binding.staggeredOrGridOptions.setVisibility(View.GONE); + break; + case R.id.layout_staggered: + preferencesBuilder.setType(PostsLayoutPreferences.PostsLayoutType.STAGGERED_GRID); + binding.staggeredOrGridOptions.setVisibility(View.VISIBLE); + initStaggeredOrGridOptions(); + break; + case R.id.layout_grid: + default: + preferencesBuilder.setType(PostsLayoutPreferences.PostsLayoutType.GRID); + binding.staggeredOrGridOptions.setVisibility(View.VISIBLE); + initStaggeredOrGridOptions(); + break; + } + } + }); + } + + private void initColCountToggle() { + binding.colCountToggle.check(getSelectedColCountId()); + binding.colCountToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + if (!isChecked) return; + switch (checkedId) { + case R.id.col_count_two: + preferencesBuilder.setColCount(2); + break; + case R.id.col_count_three: + default: + preferencesBuilder.setColCount(3); + break; + } + }); + } + + private void initAvatarsToggle() { + binding.showAvatarToggle.setChecked(preferencesBuilder.isAvatarVisible()); + binding.avatarSizeToggle.check(getSelectedAvatarSizeId()); + binding.showAvatarToggle.setOnCheckedChangeListener((buttonView, isChecked) -> { + preferencesBuilder.setAvatarVisible(isChecked); + binding.labelAvatarSize.setVisibility(isChecked ? View.VISIBLE : View.GONE); + binding.avatarSizeToggle.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + binding.labelAvatarSize.setVisibility(preferencesBuilder.isAvatarVisible() ? View.VISIBLE : View.GONE); + binding.avatarSizeToggle.setVisibility(preferencesBuilder.isAvatarVisible() ? View.VISIBLE : View.GONE); + binding.avatarSizeToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + if (!isChecked) return; + switch (checkedId) { + case R.id.avatar_size_tiny: + preferencesBuilder.setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.TINY); + break; + case R.id.avatar_size_small: + preferencesBuilder.setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.SMALL); + break; + case R.id.avatar_size_regular: + default: + preferencesBuilder.setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.REGULAR); + break; + } + }); + } + + private void initNamesToggle() { + binding.showNamesToggle.setChecked(preferencesBuilder.isNameVisible()); + binding.showNamesToggle.setOnCheckedChangeListener((buttonView, isChecked) -> preferencesBuilder.setNameVisible(isChecked)); + } + + private void initCornersToggle() { + binding.cornersToggle.check(getSelectedCornersId()); + binding.cornersToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + if (!isChecked) return; + if (checkedId == R.id.corners_round) { + preferencesBuilder.setHasRoundedCorners(true); + return; + } + preferencesBuilder.setHasRoundedCorners(false); + }); + } + + private void initGapToggle() { + binding.showGapToggle.setChecked(preferencesBuilder.getHasGap()); + binding.showGapToggle.setOnCheckedChangeListener((buttonView, isChecked) -> preferencesBuilder.setHasGap(isChecked)); + } + + private int getSelectedLayoutId() { + switch (preferencesBuilder.getType()) { + case STAGGERED_GRID: + return R.id.layout_staggered; + case LINEAR: + return R.id.layout_linear; + default: + case GRID: + return R.id.layout_grid; + } + } + + private int getSelectedColCountId() { + switch (preferencesBuilder.getColCount()) { + case 2: + return R.id.col_count_two; + case 3: + default: + return R.id.col_count_three; + } + } + + private int getSelectedCornersId() { + if (preferencesBuilder.getHasRoundedCorners()) { + return R.id.corners_round; + } + return R.id.corners_square; + } + + private int getSelectedAvatarSizeId() { + switch (preferencesBuilder.getProfilePicSize()) { + case TINY: + return R.id.avatar_size_tiny; + case SMALL: + return R.id.avatar_size_small; + case REGULAR: + default: + return R.id.avatar_size_regular; + } + } + + public interface OnApplyListener { + void onApply(final PostsLayoutPreferences preferences); + } +} diff --git a/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java b/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java index 39819e64..f5cd0ccf 100755 --- a/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java +++ b/app/src/main/java/awais/instagrabber/directdownload/DirectDownload.java @@ -21,15 +21,16 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; -import java.util.Arrays; +import java.util.Collections; import awais.instagrabber.R; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.IntentModel; -import awais.instagrabber.models.ViewerPostModel; import awais.instagrabber.models.enums.DownloadMethod; import awais.instagrabber.models.enums.IntentModelType; +import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.IntentUtils; @@ -115,7 +116,7 @@ public final class DirectDownload extends Activity { if (model != null && model.getType() == IntentModelType.POST) { final String text = model.getText(); - new PostFetcher(text, new FetchListener() { + new PostFetcher(text, new FetchListener() { @Override public void doBefore() { final Notification fetchingPostNotif = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID) @@ -126,13 +127,15 @@ public final class DirectDownload extends Activity { } @Override - public void onResult(final ViewerPostModel[] result) { + public void onResult(final FeedModel result) { if (notificationManager != null) notificationManager.cancel(1900000000); if (result != null) { - if (result.length == 1) { - DownloadUtils.batchDownload(context, result[0].getProfileModel().getUsername(), DownloadMethod.DOWNLOAD_DIRECT, - Arrays.asList(result)); - } else if (result.length > 1) { + if (result.getItemType() != MediaItemType.MEDIA_TYPE_SLIDER) { + DownloadUtils.batchDownload(context, + result.getProfileModel().getUsername(), + DownloadMethod.DOWNLOAD_DIRECT, + Collections.singletonList(result)); + } else { context.startActivity(new Intent(context, MultiDirectDialog.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) .putExtra(Constants.EXTRAS_POST, result)); diff --git a/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java b/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java index 72553a9c..1f58ee03 100755 --- a/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java +++ b/app/src/main/java/awais/instagrabber/directdownload/MultiDirectDialog.java @@ -18,8 +18,8 @@ import awais.instagrabber.adapters.PostsAdapter; import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; import awais.instagrabber.models.BasePostModel; +import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostModel; -import awais.instagrabber.models.ViewerPostModel; import awais.instagrabber.models.enums.DownloadMethod; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DownloadUtils; @@ -39,30 +39,29 @@ public final class MultiDirectDialog extends BaseLanguageActivity { final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - final ViewerPostModel[] postModels; + final FeedModel feedModel; final Intent intent = getIntent(); if (intent == null || !intent.hasExtra(Constants.EXTRAS_POST) - || (postModels = (ViewerPostModel[]) intent.getSerializableExtra(Constants.EXTRAS_POST)) == null) { + || (feedModel = (FeedModel) intent.getSerializableExtra(Constants.EXTRAS_POST)) == null) { Utils.errorFinish(this); return; } - username = postModels[0].getProfileModel().getUsername(); + username = feedModel.getProfileModel().getUsername(); toolbar.setTitle(username); - toolbar.setSubtitle(postModels[0].getShortCode()); + toolbar.setSubtitle(feedModel.getShortCode()); final RecyclerView recyclerView = findViewById(R.id.mainPosts); recyclerView.setNestedScrollingEnabled(false); recyclerView.setLayoutManager(new GridAutofitLayoutManager(this, Utils.convertDpToPx(130))); recyclerView.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); + // final ArrayList models = new ArrayList<>(feedModel.length - 1); + // for (final ViewerPostModel postModel : feedModel) + // models.add(new PostModel(postModel.getItemType(), postModel.getPostId(), postModel.getDisplayUrl(), + // postModel.getDisplayUrl(), postModel.getShortCode(), postModel.getPostCaption(), postModel.getTimestamp(), + // postModel.getLike(), postModel.isSaved(), postModel.getLikes())); - final ArrayList models = new ArrayList<>(postModels.length - 1); - for (final ViewerPostModel postModel : postModels) - models.add(new PostModel(postModel.getItemType(), postModel.getPostId(), postModel.getDisplayUrl(), - postModel.getSliderDisplayUrl(), postModel.getShortCode(), postModel.getPostCaption(), postModel.getTimestamp(), - postModel.getLike(), postModel.getBookmark(), postModel.getLikes())); - - // postsAdapter = new PostsAdapter(models, v -> { + // postsAdapter = new PostsAdapter(v -> { // final Object tag = v.getTag(); // if (tag instanceof PostModel) { // final PostModel postModel = (PostModel) tag; @@ -80,8 +79,8 @@ public final class MultiDirectDialog extends BaseLanguageActivity { // } // return true; // }); - - recyclerView.setAdapter(postsAdapter); + // + // recyclerView.setAdapter(postsAdapter); } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index c8837682..fbace444 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -327,8 +327,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR binding.mainLocPostCount.setVisibility(View.VISIBLE); binding.locationFullName.setText(locationModel.getName()); CharSequence biography = locationModel.getBio(); - binding.locationBiography.setCaptionIsExpandable(true); - binding.locationBiography.setCaptionIsExpanded(true); + // binding.locationBiography.setCaptionIsExpandable(true); + // binding.locationBiography.setCaptionIsExpanded(true); if (TextUtils.isEmpty(biography)) { binding.locationBiography.setVisibility(View.GONE); diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewFragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewFragment.java index dd0574fc..97cee140 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewFragment.java @@ -1,342 +1,340 @@ -package awais.instagrabber.fragments; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavDirections; -import androidx.navigation.fragment.NavHostFragment; -import androidx.viewpager2.widget.ViewPager2; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import awais.instagrabber.R; -import awais.instagrabber.adapters.PostViewAdapter; -import awais.instagrabber.adapters.PostViewAdapter.OnPostViewChildViewClickListener; -import awais.instagrabber.asyncs.PostFetcher; -import awais.instagrabber.asyncs.i.iPostFetcher; -import awais.instagrabber.databinding.FragmentPostViewBinding; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.ViewerPostModel; -import awais.instagrabber.models.ViewerPostModelWrapper; -import awais.instagrabber.models.enums.DownloadMethod; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DownloadUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.ViewerPostViewModel; -import awais.instagrabber.webservices.MediaService; -import awais.instagrabber.webservices.ServiceCallback; - -import static androidx.core.content.ContextCompat.checkSelfPermission; -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class PostViewFragment extends Fragment { - private static final String TAG = "PostViewFragment"; - private static final String COOKIE = settingsHelper.getString(Constants.COOKIE); - - private FragmentActivity fragmentActivity; - private FragmentPostViewBinding binding; - private ViewPager2 root; - private boolean shouldRefresh = true; - private ViewerPostViewModel viewerPostViewModel; - private boolean isId; - private int currentPostIndex; - private List idOrCodeList; - private boolean hasInitialResult = false; - private PostViewAdapter adapter; - private boolean session; - private MediaService mediaService; - - private FetchListener pfl = result -> { - if (result == null) return; - if (result.length <= 0) return; - final List viewerPostModels = viewerPostViewModel.getList().getValue(); - final List temp = viewerPostModels == null ? new ArrayList<>(idOrCodeList.size()) - : new ArrayList<>(viewerPostModels); - final ViewerPostModel firstPost = result[0]; - if (firstPost == null) return; - String idOrCode = isId ? firstPost.getPostId() : firstPost.getShortCode(); - if (idOrCode == null) return; - if (isId) { - // the post id is appended with `_` in the result - idOrCode = idOrCode.substring(0, idOrCode.indexOf('_')); - } - final int index = idOrCodeList.indexOf(idOrCode); - if (index < 0) return; - final ViewerPostModelWrapper viewerPostModelWrapper = temp.get(index); - viewerPostModelWrapper.setViewerPostModels(result); - temp.set(index, viewerPostModelWrapper); - viewerPostViewModel.getList().setValue(temp); - adapter.notifyItemChanged(index); - if (!hasInitialResult) { - Log.d(TAG, "setting delayed position to: " + currentPostIndex); - binding.getRoot() - .postDelayed(() -> binding.getRoot().setCurrentItem(currentPostIndex), 200); - } - hasInitialResult = true; - }; - private MentionClickListener mentionListener = (view, text, isHashtag, isLocation) -> { - if (isHashtag) { - final NavDirections action = PostViewFragmentDirections - .actionGlobalHashTagFragment(text); - NavHostFragment.findNavController(this).navigate(action); - return; - } - if (isLocation) { - final NavDirections action = PostViewFragmentDirections - .actionGlobalLocationFragment(text); - NavHostFragment.findNavController(this).navigate(action); - return; - } - final NavDirections action = PostViewFragmentDirections - .actionGlobalProfileFragment("@" + text); - NavHostFragment.findNavController(this).navigate(action); - }; - private OnPostViewChildViewClickListener clickListener = (v, wrapper, postPosition, childPosition) -> { - final ViewerPostModel postModel = wrapper.getViewerPostModels()[0]; - final String username = postModel.getProfileModel().getUsername(); - final int id = v.getId(); - switch (id) { - case R.id.viewerCaption: - break; - case R.id.btnComments: - String postId = postModel.getPostId(); - if (postId.contains("_")) postId = postId.substring(0, postId.indexOf("_")); - final NavDirections commentsAction = PostViewFragmentDirections.actionGlobalCommentsViewerFragment( - postModel.getShortCode(), - postId, - postModel.getProfileModel().getId() - ); - NavHostFragment.findNavController(this).navigate(commentsAction); - break; - case R.id.btnDownload: - final Context context = getContext(); - if (context == null) return; - if (checkSelfPermission(context, - DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { - showDownloadDialog(Arrays.asList(wrapper.getViewerPostModels()), - childPosition, - username); - return; - } - requestPermissions(DownloadUtils.PERMS, 8020); - break; - case R.id.ivProfilePic: - case R.id.title: - mentionListener.onClick(null, username, false, false); - break; - case R.id.btnLike: - if (mediaService != null) { - final String userId = CookieUtils.getUserIdFromCookie(COOKIE); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); - v.setEnabled(false); - final ServiceCallback likeCallback = new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - v.setEnabled(true); - if (result) { - postModel.setManualLike(!postModel.getLike()); - adapter.notifyItemChanged(postPosition); - return; - } - Log.e(TAG, "like/unlike unsuccessful!"); - } - - @Override - public void onFailure(final Throwable t) { - v.setEnabled(true); - Log.e(TAG, "Error during like/unlike", t); - } - }; - if (!postModel.getLike()) { - mediaService.like(postModel.getPostId(), userId, csrfToken, likeCallback); - } else { - mediaService.unlike(postModel.getPostId(), userId, csrfToken, likeCallback); - } - } - break; - case R.id.btnBookmark: - if (mediaService != null) { - final String userId = CookieUtils.getUserIdFromCookie(COOKIE); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); - v.setEnabled(false); - final ServiceCallback saveCallback = new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - v.setEnabled(true); - if (result) { - postModel.setBookmarked(!postModel.getBookmark()); - adapter.notifyItemChanged(postPosition); - return; - } - Log.e(TAG, "save/unsave unsuccessful!"); - } - - @Override - public void onFailure(final Throwable t) { - v.setEnabled(true); - Log.e(TAG, "Error during save/unsave", t); - } - }; - if (!postModel.getBookmark()) { - mediaService.save(postModel.getPostId(), userId, csrfToken, saveCallback); - } else { - mediaService.unsave(postModel.getPostId(), userId, csrfToken, saveCallback); - } - } - break; - } - }; - private PostViewAdapter.OnPostCaptionLongClickListener captionLongClickListener = text -> { - final Context context = getContext(); - if (context == null) return; - Utils.copyText(context, text); - }; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - fragmentActivity = getActivity(); - mediaService = MediaService.getInstance(); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - if (root != null) { - shouldRefresh = false; - return root; - } - binding = FragmentPostViewBinding.inflate(inflater, container, false); - root = binding.getRoot(); - setupViewPager(); - return root; - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - if (!shouldRefresh) return; - init(); - shouldRefresh = false; - } - - private void setupViewPager() { - viewerPostViewModel = new ViewModelProvider(fragmentActivity) - .get(ViewerPostViewModel.class); - adapter = new PostViewAdapter(clickListener, captionLongClickListener, mentionListener); - root.setAdapter(adapter); - root.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - - @Override - public void onPageSelected(final int position) { - // Log.d(TAG, "onPageSelected: " + position + ", hasInitialResult: " + hasInitialResult); - if (!hasInitialResult) { - return; - } - currentPostIndex = position; - fetchPost(); - } - }); - viewerPostViewModel.getList().observe(fragmentActivity, list -> adapter.submitList(list)); - } - - private void init() { - if (getArguments() == null) return; - final PostViewFragmentArgs fragmentArgs = PostViewFragmentArgs.fromBundle(getArguments()); - final String[] idOrCodeArray = fragmentArgs.getIdOrCodeArray(); - if (idOrCodeArray.length == 0) return; - currentPostIndex = fragmentArgs.getIndex(); - if (currentPostIndex < 0) return; - if (currentPostIndex >= idOrCodeArray.length) return; - idOrCodeList = Arrays.asList(idOrCodeArray); - viewerPostViewModel.getList().setValue(createPlaceholderModels(idOrCodeArray.length)); - isId = fragmentArgs.getIsId(); - fetchPost(); - } - - private List createPlaceholderModels(final int size) { - final List viewerPostModels = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - // viewerPostModels.add(new ViewerPostModel[]{ViewerPostModel.getDefaultModel(-i, "")}); - viewerPostModels.add(new ViewerPostModelWrapper(i, null)); - } - return viewerPostModels; - } - - private void fetchPost() { - // Log.d(TAG, "fetchPost, currentPostIndex: " + currentPostIndex); - final List list = viewerPostViewModel.getList().getValue(); - if (list != null) { - final ViewerPostModelWrapper viewerPostModels = list.get(currentPostIndex); - if (viewerPostModels != null && viewerPostModels - .getViewerPostModels() != null && viewerPostModels - .getViewerPostModels().length > 0) { - Log.d(TAG, "returning without fetching"); - return; - } - } - if (currentPostIndex >= idOrCodeList.size() || currentPostIndex < 0) return; - final String idOrShortCode = idOrCodeList.get(currentPostIndex); - if (isId) { - new iPostFetcher(idOrShortCode, pfl).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - return; - } - new PostFetcher(idOrShortCode, pfl).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void showDownloadDialog(final List postModels, - final int childPosition, - final String username) { - final List postModelsToDownload = new ArrayList<>(); - final Context context = getContext(); - if (context == null) return; - if (!session && postModels.size() > 1) { - final DialogInterface.OnClickListener clickListener = (dialog, which) -> { - if (which == DialogInterface.BUTTON_NEGATIVE) { - postModelsToDownload.addAll(postModels); - } else if (which == DialogInterface.BUTTON_POSITIVE) { - postModelsToDownload.add(postModels.get(childPosition)); - } else { - session = true; - postModelsToDownload.add(postModels.get(childPosition)); - } - if (postModelsToDownload.size() > 0) { - DownloadUtils.batchDownload(context, - username, - DownloadMethod.DOWNLOAD_POST_VIEWER, - postModelsToDownload); - } - }; - new AlertDialog.Builder(context) - .setTitle(R.string.post_viewer_download_dialog_title) - .setMessage(R.string.post_viewer_download_message) - .setNeutralButton(R.string.post_viewer_download_session, clickListener) - .setPositiveButton(R.string.post_viewer_download_current, clickListener) - .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); - } else { - DownloadUtils.batchDownload(context, - username, - DownloadMethod.DOWNLOAD_POST_VIEWER, - Collections.singletonList(postModels.get(childPosition))); - } - } -} \ No newline at end of file +// package awais.instagrabber.fragments; +// +// import android.content.Context; +// import android.content.DialogInterface; +// import android.content.pm.PackageManager; +// import android.os.AsyncTask; +// import android.os.Bundle; +// import android.util.Log; +// import android.view.LayoutInflater; +// import android.view.View; +// import android.view.ViewGroup; +// +// import androidx.annotation.NonNull; +// import androidx.annotation.Nullable; +// import androidx.appcompat.app.AlertDialog; +// import androidx.fragment.app.Fragment; +// import androidx.fragment.app.FragmentActivity; +// import androidx.lifecycle.ViewModelProvider; +// import androidx.navigation.NavDirections; +// import androidx.navigation.fragment.NavHostFragment; +// import androidx.viewpager2.widget.ViewPager2; +// +// import java.util.ArrayList; +// import java.util.Arrays; +// import java.util.Collections; +// import java.util.List; +// +// import awais.instagrabber.R; +// import awais.instagrabber.adapters.PostViewAdapter; +// import awais.instagrabber.adapters.PostViewAdapter.OnPostViewChildViewClickListener; +// import awais.instagrabber.asyncs.PostFetcher; +// import awais.instagrabber.asyncs.i.iPostFetcher; +// import awais.instagrabber.databinding.FragmentPostViewBinding; +// import awais.instagrabber.interfaces.FetchListener; +// import awais.instagrabber.interfaces.MentionClickListener; +// import awais.instagrabber.models.FeedModel; +// import awais.instagrabber.models.ViewerPostModel; +// import awais.instagrabber.models.ViewerPostModelWrapper; +// import awais.instagrabber.models.enums.DownloadMethod; +// import awais.instagrabber.utils.Constants; +// import awais.instagrabber.utils.CookieUtils; +// import awais.instagrabber.utils.DownloadUtils; +// import awais.instagrabber.utils.Utils; +// import awais.instagrabber.viewmodels.ViewerPostViewModel; +// import awais.instagrabber.webservices.MediaService; +// import awais.instagrabber.webservices.ServiceCallback; +// +// import static androidx.core.content.ContextCompat.checkSelfPermission; +// import static awais.instagrabber.utils.Utils.settingsHelper; +// +// public class PostViewFragment extends Fragment { +// private static final String TAG = "PostViewFragment"; +// private static final String COOKIE = settingsHelper.getString(Constants.COOKIE); +// +// private FragmentActivity fragmentActivity; +// private FragmentPostViewBinding binding; +// private ViewPager2 root; +// private boolean shouldRefresh = true; +// private ViewerPostViewModel viewerPostViewModel; +// private boolean isId; +// private int currentPostIndex; +// private List idOrCodeList; +// private boolean hasInitialResult = false; +// private PostViewAdapter adapter; +// private boolean session; +// private MediaService mediaService; +// +// private FetchListener pfl = result -> { +// if (result == null) return; +// final List viewerPostModels = viewerPostViewModel.getList().getValue(); +// final List temp = viewerPostModels == null ? new ArrayList<>(idOrCodeList.size()) +// : new ArrayList<>(viewerPostModels); +// String idOrCode = isId ? result.getPostId() : result.getShortCode(); +// if (idOrCode == null) return; +// if (isId) { +// // the post id is appended with `_` in the result +// idOrCode = idOrCode.substring(0, idOrCode.indexOf('_')); +// } +// final int index = idOrCodeList.indexOf(idOrCode); +// if (index < 0) return; +// final ViewerPostModelWrapper viewerPostModelWrapper = temp.get(index); +// viewerPostModelWrapper.setViewerPostModels(result.getSliderItems() == null ? Collections.emptyList() : result.getSliderItems()); +// temp.set(index, viewerPostModelWrapper); +// viewerPostViewModel.getList().setValue(temp); +// adapter.notifyItemChanged(index); +// if (!hasInitialResult) { +// Log.d(TAG, "setting delayed position to: " + currentPostIndex); +// binding.getRoot() +// .postDelayed(() -> binding.getRoot().setCurrentItem(currentPostIndex), 200); +// } +// hasInitialResult = true; +// }; +// private MentionClickListener mentionListener = (view, text, isHashtag, isLocation) -> { +// if (isHashtag) { +// final NavDirections action = PostViewFragmentDirections +// .actionGlobalHashTagFragment(text); +// NavHostFragment.findNavController(this).navigate(action); +// return; +// } +// if (isLocation) { +// final NavDirections action = PostViewFragmentDirections +// .actionGlobalLocationFragment(text); +// NavHostFragment.findNavController(this).navigate(action); +// return; +// } +// final NavDirections action = PostViewFragmentDirections +// .actionGlobalProfileFragment("@" + text); +// NavHostFragment.findNavController(this).navigate(action); +// }; +// private OnPostViewChildViewClickListener clickListener = (v, wrapper, postPosition, childPosition) -> { +// final ViewerPostModel postModel = wrapper.getViewerPostModels().get(0); +// final String username = postModel.getProfileModel().getUsername(); +// final int id = v.getId(); +// switch (id) { +// case R.id.viewerCaption: +// break; +// case R.id.btnComments: +// String postId = postModel.getPostId(); +// if (postId.contains("_")) postId = postId.substring(0, postId.indexOf("_")); +// final NavDirections commentsAction = PostViewFragmentDirections.actionGlobalCommentsViewerFragment( +// postModel.getShortCode(), +// postId, +// postModel.getProfileModel().getId() +// ); +// NavHostFragment.findNavController(this).navigate(commentsAction); +// break; +// case R.id.btnDownload: +// final Context context = getContext(); +// if (context == null) return; +// if (checkSelfPermission(context, +// DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { +// showDownloadDialog(wrapper.getViewerPostModels(), +// childPosition, +// username); +// return; +// } +// requestPermissions(DownloadUtils.PERMS, 8020); +// break; +// case R.id.ivProfilePic: +// case R.id.title: +// mentionListener.onClick(null, username, false, false); +// break; +// case R.id.btnLike: +// if (mediaService != null) { +// final String userId = CookieUtils.getUserIdFromCookie(COOKIE); +// final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); +// v.setEnabled(false); +// final ServiceCallback likeCallback = new ServiceCallback() { +// @Override +// public void onSuccess(final Boolean result) { +// v.setEnabled(true); +// if (result) { +// postModel.setManualLike(!postModel.getLike()); +// adapter.notifyItemChanged(postPosition); +// return; +// } +// Log.e(TAG, "like/unlike unsuccessful!"); +// } +// +// @Override +// public void onFailure(final Throwable t) { +// v.setEnabled(true); +// Log.e(TAG, "Error during like/unlike", t); +// } +// }; +// if (!postModel.getLike()) { +// mediaService.like(postModel.getPostId(), userId, csrfToken, likeCallback); +// } else { +// mediaService.unlike(postModel.getPostId(), userId, csrfToken, likeCallback); +// } +// } +// break; +// case R.id.btnBookmark: +// if (mediaService != null) { +// final String userId = CookieUtils.getUserIdFromCookie(COOKIE); +// final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); +// v.setEnabled(false); +// final ServiceCallback saveCallback = new ServiceCallback() { +// @Override +// public void onSuccess(final Boolean result) { +// v.setEnabled(true); +// if (result) { +// // postModel.setBookmarked(!postModel.isSaved()); +// adapter.notifyItemChanged(postPosition); +// return; +// } +// Log.e(TAG, "save/unsave unsuccessful!"); +// } +// +// @Override +// public void onFailure(final Throwable t) { +// v.setEnabled(true); +// Log.e(TAG, "Error during save/unsave", t); +// } +// }; +// if (!postModel.isSaved()) { +// mediaService.save(postModel.getPostId(), userId, csrfToken, saveCallback); +// } else { +// mediaService.unsave(postModel.getPostId(), userId, csrfToken, saveCallback); +// } +// } +// break; +// } +// }; +// private PostViewAdapter.OnPostCaptionLongClickListener captionLongClickListener = text -> { +// final Context context = getContext(); +// if (context == null) return; +// Utils.copyText(context, text); +// }; +// +// @Override +// public void onCreate(@Nullable final Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// fragmentActivity = getActivity(); +// mediaService = MediaService.getInstance(); +// } +// +// @Nullable +// @Override +// public View onCreateView(@NonNull final LayoutInflater inflater, +// @Nullable final ViewGroup container, +// @Nullable final Bundle savedInstanceState) { +// if (root != null) { +// shouldRefresh = false; +// return root; +// } +// binding = FragmentPostViewBinding.inflate(inflater, container, false); +// root = binding.getRoot(); +// setupViewPager(); +// return root; +// } +// +// @Override +// public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { +// if (!shouldRefresh) return; +// init(); +// shouldRefresh = false; +// } +// +// private void setupViewPager() { +// viewerPostViewModel = new ViewModelProvider(fragmentActivity) +// .get(ViewerPostViewModel.class); +// adapter = new PostViewAdapter(clickListener, captionLongClickListener, mentionListener); +// root.setAdapter(adapter); +// root.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { +// +// @Override +// public void onPageSelected(final int position) { +// // Log.d(TAG, "onPageSelected: " + position + ", hasInitialResult: " + hasInitialResult); +// if (!hasInitialResult) { +// return; +// } +// currentPostIndex = position; +// fetchPost(); +// } +// }); +// viewerPostViewModel.getList().observe(fragmentActivity, list -> adapter.submitList(list)); +// } +// +// private void init() { +// if (getArguments() == null) return; +// final PostViewFragmentArgs fragmentArgs = PostViewFragmentArgs.fromBundle(getArguments()); +// final String[] idOrCodeArray = fragmentArgs.getIdOrCodeArray(); +// if (idOrCodeArray.length == 0) return; +// currentPostIndex = fragmentArgs.getIndex(); +// if (currentPostIndex < 0) return; +// if (currentPostIndex >= idOrCodeArray.length) return; +// idOrCodeList = Arrays.asList(idOrCodeArray); +// viewerPostViewModel.getList().setValue(createPlaceholderModels(idOrCodeArray.length)); +// isId = fragmentArgs.getIsId(); +// fetchPost(); +// } +// +// private List createPlaceholderModels(final int size) { +// final List viewerPostModels = new ArrayList<>(size); +// for (int i = 0; i < size; i++) { +// // viewerPostModels.add(new ViewerPostModel[]{ViewerPostModel.getDefaultModel(-i, "")}); +// viewerPostModels.add(new ViewerPostModelWrapper(i, null)); +// } +// return viewerPostModels; +// } +// +// private void fetchPost() { +// // Log.d(TAG, "fetchPost, currentPostIndex: " + currentPostIndex); +// final List list = viewerPostViewModel.getList().getValue(); +// if (list != null) { +// final ViewerPostModelWrapper viewerPostModels = list.get(currentPostIndex); +// if (viewerPostModels != null && viewerPostModels +// .getViewerPostModels() != null && viewerPostModels +// .getViewerPostModels().size() > 0) { +// Log.d(TAG, "returning without fetching"); +// return; +// } +// } +// if (currentPostIndex >= idOrCodeList.size() || currentPostIndex < 0) return; +// final String idOrShortCode = idOrCodeList.get(currentPostIndex); +// if (isId) { +// new iPostFetcher(idOrShortCode, pfl).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); +// return; +// } +// new PostFetcher(idOrShortCode, pfl).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); +// } +// +// private void showDownloadDialog(final List postModels, +// final int childPosition, +// final String username) { +// final List postModelsToDownload = new ArrayList<>(); +// final Context context = getContext(); +// if (context == null) return; +// if (!session && postModels.size() > 1) { +// final DialogInterface.OnClickListener clickListener = (dialog, which) -> { +// if (which == DialogInterface.BUTTON_NEGATIVE) { +// postModelsToDownload.addAll(postModels); +// } else if (which == DialogInterface.BUTTON_POSITIVE) { +// postModelsToDownload.add(postModels.get(childPosition)); +// } else { +// session = true; +// postModelsToDownload.add(postModels.get(childPosition)); +// } +// if (postModelsToDownload.size() > 0) { +// DownloadUtils.batchDownload(context, +// username, +// DownloadMethod.DOWNLOAD_POST_VIEWER, +// postModelsToDownload); +// } +// }; +// new AlertDialog.Builder(context) +// .setTitle(R.string.post_viewer_download_dialog_title) +// .setMessage(R.string.post_viewer_download_message) +// .setNeutralButton(R.string.post_viewer_download_session, clickListener) +// .setPositiveButton(R.string.post_viewer_download_current, clickListener) +// .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); +// } else { +// DownloadUtils.batchDownload(context, +// username, +// DownloadMethod.DOWNLOAD_POST_VIEWER, +// Collections.singletonList(postModels.get(childPosition))); +// } +// } +// } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java new file mode 100644 index 00000000..c205bcfb --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -0,0 +1,1245 @@ +package awais.instagrabber.fragments; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ScrollView; +import android.widget.ViewSwitcher; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.PermissionChecker; +import androidx.core.widget.NestedScrollView; +import androidx.fragment.app.DialogFragment; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.RecyclerView; +import androidx.transition.TransitionManager; +import androidx.viewpager2.widget.ViewPager2; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; + +import java.io.Serializable; + +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.adapters.SliderCallbackAdapter; +import awais.instagrabber.adapters.SliderItemsAdapter; +import awais.instagrabber.adapters.viewholder.SliderVideoViewHolder; +import awais.instagrabber.customviews.SharedElementTransitionDialogFragment; +import awais.instagrabber.customviews.VerticalDragHelper; +import awais.instagrabber.customviews.VideoPlayerCallbackAdapter; +import awais.instagrabber.customviews.VideoPlayerViewHelper; +import awais.instagrabber.customviews.drawee.AnimatedZoomableController; +import awais.instagrabber.databinding.DialogPostViewBinding; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.DownloadUtils; +import awais.instagrabber.utils.NumberUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.MediaService; +import awais.instagrabber.webservices.ServiceCallback; +import io.github.armcha.autolink.MODE_EMAIL; +import io.github.armcha.autolink.MODE_HASHTAG; +import io.github.armcha.autolink.MODE_MENTION; +import kotlin.Unit; + +import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { + private static final String TAG = "PostViewV2Fragment"; + private static final String COOKIE = settingsHelper.getString(Constants.COOKIE); + private static final int DETAILS_HIDE_DELAY_MILLIS = 2000; + private static final String ARG_FEED_MODEL = "feedModel"; + private static final int STORAGE_PERM_REQUEST_CODE = 8020; + + private FeedModel feedModel; + private View sharedProfilePicElement; + private View sharedMainPostElement; + private MainActivity fragmentActivity; + private DialogPostViewBinding binding; + private MediaService mediaService; + private Context context; + private BottomSheetBehavior bottomSheetBehavior; + private boolean detailsVisible = true; + private VideoPlayerViewHelper videoPlayerViewHelper; + private SliderItemsAdapter sliderItemsAdapter; + private boolean wasControlsVisible; + private boolean wasPaused; + + private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener = new VerticalDragHelper.OnVerticalDragListener() { + + @Override + public void onDrag(final float dY) { + // allow the view to be draggable + final ConstraintLayout v = binding.getRoot(); + final float finalY = v.getY() + dY; + animateY(v, finalY, 0, null); + } + + @Override + public void onDragEnd() { + // animate and dismiss if user drags the view more that 30% of the view + if (Math.abs(binding.getRoot().getY()) > Utils.displayMetrics.heightPixels * 0.35) { + animateAndDismiss(binding.getRoot().getY() < 0 ? -1 : 1); + return; + } + // animate back the view to proper position + animateY(binding.getRoot(), 0, 200, null); + } + + @Override + public void onFling(final float flingVelocity) { + // animate and dismiss if user flings up/down + animateAndDismiss(flingVelocity < 0 ? -1 : 1); + } + + private void animateAndDismiss(final int direction) { + final int height = binding.getRoot().getHeight(); + final int finalYDist = height + Utils.getStatusBarHeight(context); + // less than 0 means up direction, else down + final int finalY = direction < 0 ? -finalYDist : finalYDist; + animateY(binding.getRoot(), finalY, 200, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + dismiss(); + } + }); + } + }; + + public static class Builder { + private final FeedModel feedModel; + private View profilePicElement; + private View mainPostElement; + + public Builder setSharedProfilePicElement(final View profilePicElement) { + this.profilePicElement = profilePicElement; + return this; + } + + public Builder setSharedMainPostElement(final View mainPostElement) { + this.mainPostElement = mainPostElement; + return this; + } + + public PostViewV2Fragment build() { + return PostViewV2Fragment.newInstance(feedModel, profilePicElement, mainPostElement); + } + + public Builder(final FeedModel feedModel) { + this.feedModel = feedModel; + } + } + + private static PostViewV2Fragment newInstance(final FeedModel feedModel, final View profilePicElement, final View mainPostElement) { + final PostViewV2Fragment f = new PostViewV2Fragment(profilePicElement, mainPostElement); + final Bundle args = new Bundle(); + args.putSerializable(ARG_FEED_MODEL, feedModel); + f.setArguments(args); + return f; + } + + public static Builder builder(final FeedModel feedModel) { + return new Builder(feedModel); + } + + // default constructor for fragment manager + public PostViewV2Fragment() {} + + private PostViewV2Fragment(final View sharedProfilePicElement, + final View sharedMainPostElement) { + this.sharedProfilePicElement = sharedProfilePicElement; + this.sharedMainPostElement = sharedMainPostElement; + } + + + // private FetchListener pfl = result -> { + // if (result == null) return; + // final List viewerPostModels = viewerPostViewModel.getList().getValue(); + // final List temp = viewerPostModels == null ? new ArrayList<>(idOrCodeList.size()) + // : new ArrayList<>(viewerPostModels); + // String idOrCode = isId ? result.getPostId() : result.getShortCode(); + // if (idOrCode == null) return; + // if (isId) { + // // the post id is appended with `_` in the result + // idOrCode = idOrCode.substring(0, idOrCode.indexOf('_')); + // } + // final int index = idOrCodeList.indexOf(idOrCode); + // if (index < 0) return; + // final ViewerPostModelWrapper viewerPostModelWrapper = temp.get(index); + // viewerPostModelWrapper.setViewerPostModels(result.getSliderItems() == null ? Collections.emptyList() : result.getSliderItems()); + // temp.set(index, viewerPostModelWrapper); + // viewerPostViewModel.getList().setValue(temp); + // adapter.notifyItemChanged(index); + // if (!hasInitialResult) { + // Log.d(TAG, "setting delayed position to: " + currentPostIndex); + // binding.getRoot() + // .postDelayed(() -> binding.getRoot().setCurrentItem(currentPostIndex), 200); + // } + // hasInitialResult = true; + // }; + // private MentionClickListener mentionListener = (view, text, isHashtag, isLocation) -> { + // if (isHashtag) { + // final NavDirections action = PostViewFragmentDirections + // .actionGlobalHashTagFragment(text); + // NavHostFragment.findNavController(this).navigate(action); + // return; + // } + // if (isLocation) { + // final NavDirections action = PostViewFragmentDirections + // .actionGlobalLocationFragment(text); + // NavHostFragment.findNavController(this).navigate(action); + // return; + // } + // final NavDirections action = PostViewFragmentDirections + // .actionGlobalProfileFragment("@" + text); + // NavHostFragment.findNavController(this).navigate(action); + // }; + // private OnPostViewChildViewClickListener clickListener = (v, wrapper, postPosition, childPosition) -> { + // final ViewerPostModel postModel = wrapper.getViewerPostModels().get(0); + // final String username = postModel.getProfileModel().getUsername(); + // final int id = v.getId(); + // switch (id) { + // case R.id.viewerCaption: + // break; + // case R.id.btnComments: + // String postId = postModel.getPostId(); + // if (postId.contains("_")) postId = postId.substring(0, postId.indexOf("_")); + // final NavDirections commentsAction = PostViewFragmentDirections.actionGlobalCommentsViewerFragment( + // postModel.getShortCode(), + // postId, + // postModel.getProfileModel().getId() + // ); + // NavHostFragment.findNavController(this).navigate(commentsAction); + // break; + // case R.id.btnDownload: + // final Context context = getContext(); + // if (context == null) return; + // if (checkSelfPermission(context, + // DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + // showDownloadDialog(wrapper.getViewerPostModels(), + // childPosition, + // username); + // return; + // } + // requestPermissions(DownloadUtils.PERMS, 8020); + // break; + // case R.id.ivProfilePic: + // case R.id.title: + // mentionListener.onClick(null, username, false, false); + // break; + // case R.id.btnLike: + // if (mediaService != null) { + // final String userId = CookieUtils.getUserIdFromCookie(COOKIE); + // final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); + // v.setEnabled(false); + // final ServiceCallback likeCallback = new ServiceCallback() { + // @Override + // public void onSuccess(final Boolean result) { + // v.setEnabled(true); + // if (result) { + // postModel.setManualLike(!postModel.getLike()); + // adapter.notifyItemChanged(postPosition); + // return; + // } + // Log.e(TAG, "like/unlike unsuccessful!"); + // } + // + // @Override + // public void onFailure(final Throwable t) { + // v.setEnabled(true); + // Log.e(TAG, "Error during like/unlike", t); + // } + // }; + // if (!postModel.getLike()) { + // mediaService.like(postModel.getPostId(), userId, csrfToken, likeCallback); + // } else { + // mediaService.unlike(postModel.getPostId(), userId, csrfToken, likeCallback); + // } + // } + // break; + // case R.id.btnBookmark: + // if (mediaService != null) { + // final String userId = CookieUtils.getUserIdFromCookie(COOKIE); + // final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); + // v.setEnabled(false); + // final ServiceCallback saveCallback = new ServiceCallback() { + // @Override + // public void onSuccess(final Boolean result) { + // v.setEnabled(true); + // if (result) { + // // postModel.setBookmarked(!postModel.isSaved()); + // adapter.notifyItemChanged(postPosition); + // return; + // } + // Log.e(TAG, "save/unsave unsuccessful!"); + // } + // + // @Override + // public void onFailure(final Throwable t) { + // v.setEnabled(true); + // Log.e(TAG, "Error during save/unsave", t); + // } + // }; + // if (!postModel.isSaved()) { + // mediaService.save(postModel.getPostId(), userId, csrfToken, saveCallback); + // } else { + // mediaService.unsave(postModel.getPostId(), userId, csrfToken, saveCallback); + // } + // } + // break; + // } + // }; + // private PostViewAdapter.OnPostCaptionLongClickListener captionLongClickListener = text -> { + // final Context context = getContext(); + // if (context == null) return; + // Utils.copyText(context, text); + // }; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NO_FRAME, android.R.style.Theme_Translucent); + fragmentActivity = (MainActivity) getActivity(); + mediaService = MediaService.getInstance(); + final Bundle arguments = getArguments(); + if (arguments == null) return; + final Serializable feedModelSerializable = arguments.getSerializable(ARG_FEED_MODEL); + if (feedModelSerializable == null) { + Log.e(TAG, "onCreate: feedModelSerializable is null"); + return; + } + if (!(feedModelSerializable instanceof FeedModel)) { + return; + } + feedModel = (FeedModel) feedModelSerializable; + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + binding = DialogPostViewBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + setupToolbar(); + init(); + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + this.context = context; + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final Window window = dialog.getWindow(); + if (window == null) return; + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.setDimAmount(0); + int width = ViewGroup.LayoutParams.MATCH_PARENT; + int height = ViewGroup.LayoutParams.MATCH_PARENT; + window.setLayout(width, height); + if (!wasPaused && (sharedProfilePicElement != null || sharedMainPostElement != null)) { + final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + binding.getRoot().getBackground().mutate(), + PropertyValuesHolder.ofInt("alpha", 0, 255) + ); + addAnimator(animator); + } + } + + @Override + public void onPause() { + super.onPause(); + wasPaused = true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + switch (feedModel.getItemType()) { + case MEDIA_TYPE_VIDEO: + if (videoPlayerViewHelper != null) { + videoPlayerViewHelper.releasePlayer(); + } + return; + case MEDIA_TYPE_SLIDER: + if (sliderItemsAdapter != null) { + releaseAllSliderPlayers(); + } + default: + } + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + Log.d(TAG, "onSaveInstanceState"); + } + + @Override + protected void onBeforeSharedElementAnimation(@NonNull final View startView, + @NonNull final View destView, + @NonNull final SharedElementTransitionDialogFragment.ViewBounds viewBounds) { + GenericDraweeHierarchy hierarchy = null; + if (destView == binding.postImage) { + hierarchy = binding.postImage.getHierarchy(); + } else if (destView == binding.videoPost.thumbnailParent) { + hierarchy = binding.videoPost.thumbnail.getHierarchy(); + } + if (hierarchy != null) { + final ScalingUtils.ScaleType scaleTypeTo = ScalingUtils.ScaleType.FIT_CENTER; + final ScalingUtils.InterpolatingScaleType scaleType = new ScalingUtils.InterpolatingScaleType( + ScalingUtils.ScaleType.CENTER_CROP, + scaleTypeTo, + viewBounds.getStartBounds(), + viewBounds.getDestBounds() + ); + hierarchy.setActualImageScaleType(scaleType); + final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setDuration(getAnimationDuration()); + animator.addUpdateListener(animation -> { + float fraction = (float) animation.getAnimatedValue(); + scaleType.setValue(fraction); + }); + final GenericDraweeHierarchy finalHierarchy = hierarchy; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finalHierarchy.setActualImageScaleType(scaleTypeTo); + destView.requestLayout(); + } + }); + addAnimator(animator); + } + } + + @Override + protected void onEndSharedElementAnimation(@NonNull final View startView, + @NonNull final View destView, + @NonNull final ViewBounds viewBounds) { + if (destView == binding.postImage) { + binding.postImage.setTranslationX(0); + binding.postImage.setTranslationY(0); + binding.postImage.setX(0); + binding.postImage.setY(0); + binding.postImage.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + binding.postImage.requestLayout(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + return; + } + if (destView == binding.sliderParent) { + binding.sliderParent.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + binding.sliderParent.requestLayout(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + return; + } + if (destView == binding.videoPost.thumbnailParent) { + final FrameLayout.LayoutParams params = new ViewSwitcher.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.CENTER; + binding.videoPost.thumbnailParent.setLayoutParams(params); + binding.videoPost.thumbnailParent.requestLayout(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + } + + @Override + public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showDownloadDialog(); + } + } + + private void init() { + binding.getRoot().getBackground().mutate().setAlpha(0); + // if (getArguments() == null) return; + // final PostViewV2FragmentArgs fragmentArgs = PostViewV2FragmentArgs.fromBundle(getArguments()); + // feedModel = fragmentArgs.getFeedModel(); + setupProfilePic(); + setupTitles(); + setupCaption(); + setupCounts(); + setupPostTypeLayout(); + setupCommonActions(); + // binding.getRoot().setOnTouchListener(onTouchListener); + // final String[] idOrCodeArray = fragmentArgs.getIdOrCodeArray(); + // if (idOrCodeArray.length == 0) return; + // currentPostIndex = fragmentArgs.getIndex(); + // if (currentPostIndex < 0) return; + // if (currentPostIndex >= idOrCodeArray.length) return; + // idOrCodeList = Arrays.asList(idOrCodeArray); + // viewerPostViewModel.getList().setValue(createPlaceholderModels(idOrCodeArray.length)); + // isId = fragmentArgs.getIsId(); + // fetchPost(); + } + + private void setupCommonActions() { + setupLike(); + setupSave(); + setupDownload(); + } + + private void setupDownload() { + binding.download.setOnClickListener(v -> { + if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + showDownloadDialog(); + return; + } + requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + }); + } + + private void setupLike() { + if (mediaService == null) return; + setLikedResources(feedModel.getLike()); + final ServiceCallback likeCallback = new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + binding.like.setEnabled(true); + if (result) { + setLikedResources(!feedModel.getLike()); + final long currentLikesCount = feedModel.getLikesCount(); + final long updatedCount; + if (!feedModel.getLike()) { + updatedCount = currentLikesCount + 1; + feedModel.setLiked(true); + } else { + updatedCount = currentLikesCount - 1; + feedModel.setLiked(false); + } + feedModel.setLikesCount(updatedCount); + setupCounts(); + return; + } + unsuccessfulLike(); + } + + @Override + public void onFailure(final Throwable t) { + binding.like.setEnabled(true); + Log.e(TAG, "Error during like/unlike", t); + unsuccessfulLike(); + } + + private void unsuccessfulLike() { + final int errorTextResId; + if (!feedModel.getLike()) { + Log.e(TAG, "like unsuccessful!"); + errorTextResId = R.string.like_unsuccessful; + } else { + Log.e(TAG, "unlike unsuccessful!"); + errorTextResId = R.string.unlike_unsuccessful; + } + setLikedResources(feedModel.getLike()); + final Snackbar snackbar = Snackbar.make(binding.getRoot(), errorTextResId, BaseTransientBottomBar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.ok, null); + snackbar.show(); + } + }; + binding.like.setOnClickListener(v -> { + final String userId = CookieUtils.getUserIdFromCookie(COOKIE); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); + v.setEnabled(false); + final int textRes; + if (!feedModel.getLike()) { + textRes = R.string.liking; + } else { + textRes = R.string.unliking; + } + binding.like.setText(textRes); + if (!feedModel.getLike()) { + mediaService.like(feedModel.getPostId(), userId, csrfToken, likeCallback); + } else { + mediaService.unlike(feedModel.getPostId(), userId, csrfToken, likeCallback); + } + }); + } + + private void setLikedResources(final boolean liked) { + final int iconResource; + final int tintResource; + final int textResId; + if (liked) { + iconResource = R.drawable.ic_like; + tintResource = R.color.red_600; + textResId = R.string.unlike_without_count; + } else { + iconResource = R.drawable.ic_not_liked; + tintResource = R.color.white; + textResId = R.string.like_without_count; + } + binding.like.setIconResource(iconResource); + binding.like.setIconTintResource(tintResource); + binding.like.setText(textResId); + } + + private void setupSave() { + if (mediaService == null) return; + setSavedResources(feedModel.isSaved()); + final ServiceCallback saveCallback = new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + binding.save.setEnabled(true); + if (result) { + setSavedResources(!feedModel.isSaved()); + feedModel.setSaved(!feedModel.isSaved()); + return; + } + unsuccessfulSave(); + } + + private void unsuccessfulSave() { + final int errorTextResId; + if (!feedModel.isSaved()) { + Log.e(TAG, "save unsuccessful!"); + errorTextResId = R.string.save_unsuccessful; + } else { + Log.e(TAG, "save remove unsuccessful!"); + errorTextResId = R.string.save_remove_unsuccessful; + } + setSavedResources(feedModel.isSaved()); + final Snackbar snackbar = Snackbar.make(binding.getRoot(), errorTextResId, BaseTransientBottomBar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.ok, null); + snackbar.show(); + } + + @Override + public void onFailure(final Throwable t) { + binding.save.setEnabled(true); + Log.e(TAG, "Error during save/unsave", t); + unsuccessfulSave(); + } + }; + binding.save.setOnClickListener(v -> { + final String userId = CookieUtils.getUserIdFromCookie(COOKIE); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(COOKIE); + binding.save.setEnabled(false); + final int textRes; + if (!feedModel.isSaved()) { + textRes = R.string.saving; + } else { + textRes = R.string.removing; + } + binding.save.setText(textRes); + if (!feedModel.isSaved()) { + mediaService.save(feedModel.getPostId(), userId, csrfToken, saveCallback); + } else { + mediaService.unsave(feedModel.getPostId(), userId, csrfToken, saveCallback); + } + }); + } + + private void setSavedResources(final boolean saved) { + final int iconResource; + final int tintResource; + final int textResId; + if (saved) { + iconResource = R.drawable.ic_class_24; + tintResource = R.color.blue_700; + textResId = R.string.saved; + } else { + iconResource = R.drawable.ic_outline_class_24; + tintResource = R.color.white; + textResId = R.string.save; + } + binding.save.setIconResource(iconResource); + binding.save.setIconTintResource(tintResource); + binding.save.setText(textResId); + } + + private void setupProfilePic() { + if (!wasPaused && sharedProfilePicElement != null) { + addSharedElement(sharedProfilePicElement, binding.profilePic); + } + final String uri = feedModel.getProfileModel().getSdProfilePic(); + final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri)).build(); + final DraweeController controller = Fresco + .newDraweeControllerBuilder() + .setImageRequest(requestBuilder) + .setOldController(binding.profilePic.getController()) + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + startPostponedEnterTransition(); + } + + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + startPostponedEnterTransition(); + } + }) + .build(); + binding.profilePic.setController(controller); + binding.profilePic.setOnClickListener(v -> navigateToProfile("@" + feedModel.getProfileModel().getUsername())); + } + + private void setupToolbar() { + // fragmentActivity.fitSystemWindows(true); + // final ActionBar actionBar = fragmentActivity.getSupportActionBar(); + // if (actionBar == null) return; + // actionBar.setTitle(null); + // actionBar.setSubtitle(null); + } + + private void setupTitles() { + binding.title.setText(feedModel.getProfileModel().getName()); + binding.title.setOnClickListener(v -> navigateToProfile("@" + feedModel.getProfileModel().getUsername())); + final String locationName = feedModel.getLocationName(); + if (!TextUtils.isEmpty(locationName)) { + binding.subtitle.setText(locationName); + binding.subtitle.setVisibility(View.VISIBLE); + binding.subtitle.setOnClickListener(v -> { + final NavController navController = getNavController(); + if (navController == null) return; + final Bundle bundle = new Bundle(); + bundle.putString("locationId", feedModel.getLocationId()); + navController.navigate(R.id.action_global_locationFragment, bundle); + }); + return; + } + binding.subtitle.setVisibility(View.GONE); + } + + private void setupCaption() { + final CharSequence postCaption = feedModel.getPostCaption(); + binding.caption.addAutoLinkMode(MODE_HASHTAG.INSTANCE, MODE_MENTION.INSTANCE, MODE_EMAIL.INSTANCE); + binding.caption.onAutoLinkClick(autoLinkItem -> { + // Log.d(TAG, "setupCaption: autoLinkItem: " + autoLinkItem.getMode().getModeName() + " : " + autoLinkItem.getOriginalText()); + final String originalText = autoLinkItem.getOriginalText(); + if (autoLinkItem.getMode().equals(MODE_HASHTAG.INSTANCE)) { + final NavController navController = NavHostFragment.findNavController(this); + final Bundle bundle = new Bundle(); + bundle.putString("hashtag", originalText); + navController.navigate(R.id.action_global_hashTagFragment, bundle); + return Unit.INSTANCE; + } + if (autoLinkItem.getMode().equals(MODE_MENTION.INSTANCE)) { + navigateToProfile(originalText); + return Unit.INSTANCE; + } + return Unit.INSTANCE; + }); + binding.caption.setOnLongClickListener(v -> { + Utils.copyText(getContext(), postCaption); + return true; + }); + binding.caption.setText(postCaption); + bottomSheetBehavior = BottomSheetBehavior.from(binding.captionParent); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull final View bottomSheet, final int newState) {} + + @Override + public void onSlide(@NonNull final View bottomSheet, final float slideOffset) { + binding.captionParent.getBackground().mutate().setAlpha((int) (128 + (128 * (slideOffset < 0 ? 0 : slideOffset)))); + } + }); + binding.caption.setOnClickListener(v -> { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) return; + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }); + binding.captionToggle.setOnClickListener(v -> { + switch (bottomSheetBehavior.getState()) { + case BottomSheetBehavior.STATE_HIDDEN: + binding.captionParent.fullScroll(ScrollView.FOCUS_UP); // reset scroll position + if (binding.playerControls.getRoot().getVisibility() == View.VISIBLE) { + hidePlayerControls(); + } + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + return; + case BottomSheetBehavior.STATE_COLLAPSED: + case BottomSheetBehavior.STATE_EXPANDED: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + return; + case BottomSheetBehavior.STATE_DRAGGING: + case BottomSheetBehavior.STATE_HALF_EXPANDED: + case BottomSheetBehavior.STATE_SETTLING: + default: + } + }); + if (sharedProfilePicElement == null || sharedMainPostElement == null) { + binding.getRoot().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + binding.getRoot().getViewTreeObserver().removeOnGlobalLayoutListener(this); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + } + } + + private void setupCounts() { + final int commentsCount = (int) feedModel.getCommentsCount(); + final String commentsString = getResources().getQuantityString(R.plurals.comments_count, commentsCount, commentsCount); + binding.commentsCount.setText(commentsString); + final int likesCount = (int) feedModel.getLikesCount(); + final String likesString = getResources().getQuantityString(R.plurals.likes_count, likesCount, likesCount); + binding.likesCount.setText(likesString); + } + + private void setupPostTypeLayout() { + switch (feedModel.getItemType()) { + case MEDIA_TYPE_IMAGE: + setupPostImage(); + break; + case MEDIA_TYPE_SLIDER: + setupSlider(); + break; + case MEDIA_TYPE_VIDEO: + setupVideo(); + break; + } + } + + @SuppressLint("ClickableViewAccessibility") + private void setupPostImage() { + binding.videoPost.root.setVisibility(View.GONE); + binding.sliderParent.setVisibility(View.GONE); + binding.playerControlsToggle.setVisibility(View.GONE); + binding.playerControls.getRoot().setVisibility(View.GONE); + binding.mediaCounter.setVisibility(View.GONE); + binding.postImage.setVisibility(View.VISIBLE); + if (!wasPaused && sharedMainPostElement != null) { + binding.postImage.getHierarchy().setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP); + addSharedElement(sharedMainPostElement, binding.postImage); + } + final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(feedModel.getDisplayUrl())) + .setLocalThumbnailPreviewsEnabled(true) + .build(); + final DraweeController controller = Fresco + .newDraweeControllerBuilder() + .setLowResImageRequest(ImageRequest.fromUri(feedModel.getThumbnailUrl())) + .setImageRequest(requestBuilder) + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + startPostponedEnterTransition(); + } + + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + startPostponedEnterTransition(); + } + }) + .build(); + binding.postImage.setController(controller); + binding.postImage.setOnClickListener(v -> toggleDetails()); + final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance(); + zoomableController.setMaxScaleFactor(3f); + binding.postImage.setZoomableController(zoomableController); + binding.postImage.setAllowTouchInterceptionWhileZoomed(true); + binding.postImage.setOnVerticalDragListener(onVerticalDragListener); + } + + private void setupSlider() { + binding.postImage.setVisibility(View.GONE); + binding.videoPost.root.setVisibility(View.GONE); + binding.playerControlsToggle.setVisibility(View.GONE); + binding.playerControls.getRoot().setVisibility(View.GONE); + binding.sliderParent.setVisibility(View.VISIBLE); + binding.mediaCounter.setVisibility(View.VISIBLE); + if (sharedMainPostElement != null) { + addSharedElement(sharedMainPostElement, binding.sliderParent); + } + sliderItemsAdapter = new SliderItemsAdapter(onVerticalDragListener, binding.playerControls, new SliderCallbackAdapter() { + @Override + public void onThumbnailLoaded(final int position) { + if (position != 0) return; + startPostponedEnterTransition(); + } + + @Override + public void onItemClicked(final int position) { + toggleDetails(); + } + + @Override + public void onPlayerPlay(final int position) { + if (!detailsVisible) return; + toggleDetails(); + showPlayerControls(); + } + + @Override + public void onPlayerPause(final int position) { + if (detailsVisible) return; + toggleDetails(); + } + }); + binding.sliderParent.setAdapter(sliderItemsAdapter); + binding.sliderParent.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + int prevPosition = -1; + + @Override + public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { + if (prevPosition != -1) { + final View view = binding.sliderParent.getChildAt(0); + if (view instanceof RecyclerView) { + pausePlayerAtPosition(prevPosition, (RecyclerView) view); + pausePlayerAtPosition(position, (RecyclerView) view); + } + } + if (positionOffset == 0) { + prevPosition = position; + } + } + + @Override + public void onPageSelected(final int position) { + final int size = feedModel.getSliderItems().size(); + if (position < 0 || position >= size) return; + final String text = (position + 1) + "/" + size; + binding.mediaCounter.setText(text); + final PostChild postChild = feedModel.getSliderItems().get(position); + if (postChild.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + enablePlayerControls(true); + return; + } + enablePlayerControls(false); + } + + private void pausePlayerAtPosition(final int position, final RecyclerView view) { + final RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(position); + if (viewHolder instanceof SliderVideoViewHolder) { + ((SliderVideoViewHolder) viewHolder).pause(); + } + } + }); + final String text = "1/" + feedModel.getSliderItems().size(); + binding.mediaCounter.setText(text); + sliderItemsAdapter.submitList(feedModel.getSliderItems()); + } + + private void releaseAllSliderPlayers() { + if (binding.sliderParent.getVisibility() != View.VISIBLE) return; + final View view = binding.sliderParent.getChildAt(0); + if (!(view instanceof RecyclerView)) return; + final int itemCount = sliderItemsAdapter.getItemCount(); + for (int position = itemCount - 1; position >= 0; position--) { + final RecyclerView.ViewHolder viewHolder = ((RecyclerView) view).findViewHolderForAdapterPosition(position); + if (!(viewHolder instanceof SliderVideoViewHolder)) continue; + ((SliderVideoViewHolder) viewHolder).releasePlayer(); + } + } + + @SuppressLint("ClickableViewAccessibility") + private void setupVideo() { + binding.postImage.setVisibility(View.GONE); + binding.sliderParent.setVisibility(View.GONE); + binding.mediaCounter.setVisibility(View.GONE); + // binding.playerControls.getRoot().setVisibility(View.VISIBLE); + if (sharedMainPostElement != null) { + final GenericDraweeHierarchy hierarchy = binding.videoPost.thumbnail.getHierarchy(); + hierarchy.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP); + addSharedElement(sharedMainPostElement, binding.videoPost.thumbnailParent); + } + binding.videoPost.root.setVisibility(View.VISIBLE); + final VerticalDragHelper thumbnailVerticalDragHelper = new VerticalDragHelper(binding.videoPost.thumbnailParent); + final VerticalDragHelper playerVerticalDragHelper = new VerticalDragHelper(binding.videoPost.playerView); + thumbnailVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener); + playerVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener); + enablePlayerControls(true); + binding.videoPost.thumbnailParent.setOnTouchListener((v, event) -> { + final boolean onDragTouch = thumbnailVerticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + return thumbnailVerticalDragHelper.onGestureTouchEvent(event); + }); + binding.videoPost.playerView.setOnTouchListener((v, event) -> { + final boolean onDragTouch = playerVerticalDragHelper.onDragTouch(event); + if (onDragTouch) { + return true; + } + return playerVerticalDragHelper.onGestureTouchEvent(event); + }); + binding.videoPost.playerView.setOnClickListener(v -> toggleDetails()); + final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; + final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() { + @Override + public void onThumbnailLoaded() { + startPostponedEnterTransition(); + } + + @Override + public void onPlayerViewLoaded() { + binding.playerControls.getRoot().setVisibility(View.VISIBLE); + final ViewGroup.LayoutParams layoutParams = binding.videoPost.playerView.getLayoutParams(); + final int requiredWidth = Utils.displayMetrics.widthPixels; + final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + layoutParams.width = requiredWidth; + layoutParams.height = resultingHeight; + binding.videoPost.playerView.requestLayout(); + } + + @Override + public void onPlay() { + if (detailsVisible) { + new Handler().postDelayed(() -> toggleDetails(), DETAILS_HIDE_DELAY_MILLIS); + } + } + }; + final float aspectRatio = (float) feedModel.getImageWidth() / feedModel.getImageHeight(); + videoPlayerViewHelper = new VideoPlayerViewHelper( + binding.getRoot().getContext(), + binding.videoPost, + feedModel.getDisplayUrl(), + vol, + aspectRatio, + feedModel.getThumbnailUrl(), + binding.playerControls, + videoPlayerCallback); + } + + private void enablePlayerControls(final boolean enable) { + if (enable) { + binding.playerControlsToggle.setVisibility(View.VISIBLE); + binding.playerControlsToggle.setOnClickListener(v -> { + final int visibility = binding.playerControls.getRoot().getVisibility(); + if (visibility == View.GONE) { + showPlayerControls(); + return; + } + hidePlayerControls(); + }); + return; + } + binding.playerControlsToggle.setVisibility(View.GONE); + hidePlayerControls(); + } + + private void hideCaption() { + if (bottomSheetBehavior == null) return; + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + + private void showPlayerControls() { + hideCaption(); + + // previously invisible view + View view = binding.playerControls.getRoot(); + // get the center for the clipping circle + int cx = view.getWidth() / 2; + // int cy = view.getHeight() / 2; + int cy = view.getHeight(); + + // get the final radius for the clipping circle + float finalRadius = (float) Math.hypot(cx, cy); + + // create the animator for this view (the start radius is zero) + Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius); + + // make the view visible and start the animation + view.setVisibility(View.VISIBLE); + anim.start(); + + } + + private void hidePlayerControls() { + // previously visible view + final View view = binding.playerControls.getRoot(); + + // get the center for the clipping circle + int cx = view.getWidth() / 2; + // int cy = view.getHeight() / 2; + int cy = view.getHeight(); + + // get the initial radius for the clipping circle + float initialRadius = (float) Math.hypot(cx, cy); + + // create the animation (the final radius is zero) + Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius, 0f); + + // make the view invisible when the animation is done + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + view.setVisibility(View.GONE); + } + }); + + // start the animation + anim.start(); + } + + private void toggleDetails() { + binding.getRoot().post(() -> { + TransitionManager.beginDelayedTransition(binding.getRoot()); + if (detailsVisible) { + detailsVisible = false; + binding.profilePic.setVisibility(View.GONE); + binding.title.setVisibility(View.GONE); + binding.topBg.setVisibility(View.GONE); + if (!TextUtils.isEmpty(binding.subtitle.getText())) { + binding.subtitle.setVisibility(View.GONE); + } + binding.captionParent.setVisibility(View.GONE); + binding.bottomBg.setVisibility(View.GONE); + binding.likesCount.setVisibility(View.GONE); + binding.commentsCount.setVisibility(View.GONE); + binding.captionToggle.setVisibility(View.GONE); + binding.playerControlsToggle.setVisibility(View.GONE); + binding.like.setVisibility(View.GONE); + binding.save.setVisibility(View.GONE); + binding.download.setVisibility(View.GONE); + binding.mediaCounter.setVisibility(View.GONE); + wasControlsVisible = binding.playerControls.getRoot().getVisibility() == View.VISIBLE; + if (wasControlsVisible) { + hidePlayerControls(); + } + return; + } + binding.profilePic.setVisibility(View.VISIBLE); + binding.title.setVisibility(View.VISIBLE); + binding.topBg.setVisibility(View.VISIBLE); + if (!TextUtils.isEmpty(binding.subtitle.getText())) { + binding.subtitle.setVisibility(View.VISIBLE); + } + binding.captionParent.setVisibility(View.VISIBLE); + binding.bottomBg.setVisibility(View.VISIBLE); + binding.likesCount.setVisibility(View.VISIBLE); + binding.commentsCount.setVisibility(View.VISIBLE); + binding.captionToggle.setVisibility(View.VISIBLE); + binding.playerControlsToggle.setVisibility(View.VISIBLE); + binding.download.setVisibility(View.VISIBLE); + binding.like.setVisibility(View.VISIBLE); + binding.save.setVisibility(View.VISIBLE); + if (wasControlsVisible) { + showPlayerControls(); + } + if (feedModel.getItemType() == MediaItemType.MEDIA_TYPE_SLIDER) { + binding.mediaCounter.setVisibility(View.VISIBLE); + } + detailsVisible = true; + }); + } + + private void showDownloadDialog() { + DownloadUtils.download(context, feedModel); + // switch (feedModel.getItemType()) { + // case MEDIA_TYPE_IMAGE: + // case MEDIA_TYPE_VIDEO: + // break; + // case MEDIA_TYPE_SLIDER: + // break; + // } + // final List postModelsToDownload = new ArrayList<>(); + // // if (!session) { + // final DialogInterface.OnClickListener clickListener = (dialog, which) -> { + // if (which == DialogInterface.BUTTON_NEGATIVE) { + // postModelsToDownload.addAll(postModels); + // } else if (which == DialogInterface.BUTTON_POSITIVE) { + // postModelsToDownload.add(postModels.get(childPosition)); + // } else { + // session = true; + // postModelsToDownload.add(postModels.get(childPosition)); + // } + // if (postModelsToDownload.size() > 0) { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // postModelsToDownload); + // } + // }; + // new AlertDialog.Builder(context) + // .setTitle(R.string.post_viewer_download_dialog_title) + // .setMessage(R.string.post_viewer_download_message) + // .setNeutralButton(R.string.post_viewer_download_session, clickListener) + // .setPositiveButton(R.string.post_viewer_download_current, clickListener) + // .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); + // } else { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // Collections.singletonList(postModels.get(childPosition))); + } + + private void animateY(final View v, + final float finalY, + final int duration, + final AnimatorListenerAdapter listener) { + v.animate() + .y(finalY) + .setDuration(duration) + .setListener(listener).start(); + } + + private void navigateToProfile(final String username) { + final NavController navController = getNavController(); + if (navController == null) return; + final Bundle bundle = new Bundle(); + bundle.putString("username", username); + navController.navigate(R.id.action_global_profileFragment, bundle); + } + + @Nullable + private NavController getNavController() { + NavController navController = null; + try { + navController = NavHostFragment.findNavController(this); + } catch (IllegalStateException e) { + Log.e(TAG, "navigateToProfile", e); + } + return navController; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index fbbea90a..d7d07b28 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -45,8 +45,11 @@ import com.facebook.drawee.interfaces.DraweeController; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -81,7 +84,6 @@ import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -622,8 +624,11 @@ public class StoryViewerFragment extends Fragment { final String storyUrl = currentStory.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO ? currentStory.getVideoUrl() : currentStory.getStoryUrl(); - final File saveFile = new File(dir, currentStory.getStoryMediaId() + "_" + currentStory.getTimestamp() - + DownloadUtils.getExtensionFromModel(storyUrl, currentStory)); + final File saveFile = new File( + dir, + currentStory.getStoryMediaId() + + "_" + currentStory.getTimestamp() + + DownloadUtils.getFileExtensionFromUrl(storyUrl)); new DownloadAsync(context, storyUrl, saveFile, result -> { final int toastRes = result != null && result.exists() ? R.string.downloader_complete @@ -687,8 +692,10 @@ public class StoryViewerFragment extends Fragment { binding.playerView.setPlayer(player); player.setPlayWhenReady(settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); + final Uri uri = Uri.parse(url); + final MediaItem mediaItem = MediaItem.fromUri(uri); final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) - .createMediaSource(Uri.parse(url)); + .createMediaSource(mediaItem); mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() { @Override public void onLoadCompleted(final int windowIndex, @@ -732,7 +739,8 @@ public class StoryViewerFragment extends Fragment { binding.progressView.setVisibility(View.GONE); } }); - player.prepare(mediaSource); + player.setMediaSource(mediaSource); + player.prepare(); binding.playerView.setOnClickListener(v -> { if (player != null) { @@ -751,12 +759,10 @@ public class StoryViewerFragment extends Fragment { if (t == '@') { final NavDirections action = HashTagFragmentDirections.actionGlobalProfileFragment(username); NavHostFragment.findNavController(this).navigate(action); - } - else if (t == '#') { + } else if (t == '#') { final NavDirections action = HashTagFragmentDirections.actionGlobalHashTagFragment(username.substring(1)); NavHostFragment.findNavController(this).navigate(action); - } - else { + } else { final NavDirections action = ProfileFragmentDirections.actionGlobalLocationFragment(username.split(" \\(")[1].replace(")", "")); NavHostFragment.findNavController(this).navigate(action); } 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 0c312837..33368f43 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -1,18 +1,18 @@ package awais.instagrabber.fragments.main; import android.content.Context; -import android.content.DialogInterface; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -20,47 +20,25 @@ import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import com.facebook.common.executors.UiThreadImmediateExecutorService; -import com.facebook.datasource.BaseDataSubscriber; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.request.ImageRequest; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.FeedAdapter; import awais.instagrabber.adapters.FeedStoriesAdapter; -import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder; -import awais.instagrabber.asyncs.FeedFetcher; -import awais.instagrabber.customviews.RamboTextView; -import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; +import awais.instagrabber.asyncs.FeedPostFetchService; import awais.instagrabber.customviews.helpers.VideoAwareRecyclerScroller; import awais.instagrabber.databinding.FragmentFeedBinding; -import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; +import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.BasePostModel; -import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedStoryModel; -import awais.instagrabber.models.PostModel; -import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.models.ViewerPostModel; -import awais.instagrabber.models.enums.DownloadMethod; -import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.DownloadUtils; -import awais.instagrabber.utils.NumberUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FeedStoriesViewModel; -import awais.instagrabber.viewmodels.FeedViewModel; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; @@ -77,98 +55,101 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre private StoriesService storiesService; private boolean feedHasNextPage = false; private String feedEndCursor = null; - private FeedViewModel feedViewModel; + // private FeedViewModel feedViewModel; private VideoAwareRecyclerScroller videoAwareRecyclerScroller; private boolean shouldRefresh = true; private boolean isPullToRefresh; + private FeedStoriesViewModel feedStoriesViewModel; + private StaggeredGridLayoutManager gridLayoutManager; + private boolean storiesFetching; private final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); - private final FetchListener feedFetchListener = new FetchListener() { - @Override - public void doBefore() { - binding.feedSwipeRefreshLayout.post(() -> binding.feedSwipeRefreshLayout.setRefreshing(true)); - } - - @Override - public void onResult(final FeedModel[] result) { - if (result == null || result.length <= 0) { - binding.feedSwipeRefreshLayout.setRefreshing(false); - return; - } - final List currentFeedModelList = feedViewModel.getList().getValue(); - final Map thumbToFeedMap = new HashMap<>(); - for (final FeedModel feedModel : result) { - thumbToFeedMap.put(feedModel.getThumbnailUrl(), feedModel); - } - final BaseDataSubscriber subscriber = new BaseDataSubscriber() { - int success = 0; - int failed = 0; - - @Override - protected void onNewResultImpl(@NonNull final DataSource dataSource) { - final Map extras = dataSource.getExtras(); - if (extras == null) return; - final Uri thumbUri = (Uri) extras.get("uri_source"); - if (thumbUri == null) return; - final Integer encodedWidth = (Integer) extras.get("encoded_width"); - final Integer encodedHeight = (Integer) extras.get("encoded_height"); - if (encodedWidth == null || encodedHeight == null) return; - final FeedModel feedModel = thumbToFeedMap.get(thumbUri.toString()); - if (feedModel == null) return; - int requiredWidth = Utils.displayMetrics.widthPixels; - int resultingHeight = NumberUtils - .getResultingHeight(requiredWidth, encodedHeight, encodedWidth); - if (feedModel - .getItemType() == MediaItemType.MEDIA_TYPE_VIDEO && resultingHeight >= MAX_VIDEO_HEIGHT) { - // If its a video and the height is too large, need to reduce the height, - // so that entire video fits on screen - resultingHeight = RESIZED_VIDEO_HEIGHT; - requiredWidth = NumberUtils.getResultingWidth(RESIZED_VIDEO_HEIGHT, - resultingHeight, - requiredWidth); - } - feedModel.setImageWidth(requiredWidth); - feedModel.setImageHeight(resultingHeight); - success++; - updateAdapter(); - } - - @Override - protected void onFailureImpl(@NonNull final DataSource dataSource) { - failed++; - updateAdapter(); - } - - public void updateAdapter() { - if (failed + success != result.length) return; - List finalList = currentFeedModelList == null || currentFeedModelList.isEmpty() - ? new ArrayList<>() - : new ArrayList<>(currentFeedModelList); - final List resultList = Arrays.asList(result); - if (isPullToRefresh) { - finalList = resultList; - isPullToRefresh = false; - } else { - finalList.addAll(resultList); - } - feedViewModel.getList().postValue(finalList); - final PostModel feedPostModel = result[result.length - 1]; - if (feedPostModel != null) { - feedEndCursor = feedPostModel.getEndCursor(); - feedHasNextPage = feedPostModel.hasNextPage(); - feedPostModel.setPageCursor(false, null); - } - binding.feedSwipeRefreshLayout.setRefreshing(false); - } - }; - - for (final FeedModel feedModel : result) { - final DataSource ds = Fresco.getImagePipeline() - .prefetchToBitmapCache(ImageRequest.fromUri(feedModel.getThumbnailUrl()), null); - ds.subscribe(subscriber, UiThreadImmediateExecutorService.getInstance()); - } - } - }; + // private final FetchListener feedFetchListener = new FetchListener() { + // @Override + // public void doBefore() { + // binding.feedSwipeRefreshLayout.post(() -> binding.feedSwipeRefreshLayout.setRefreshing(true)); + // } + // + // @Override + // public void onResult(final FeedModel[] result) { + // if (result == null || result.length <= 0) { + // binding.feedSwipeRefreshLayout.setRefreshing(false); + // return; + // } + // final List currentFeedModelList = feedViewModel.getList().getValue(); + // final Map thumbToFeedMap = new HashMap<>(); + // for (final FeedModel feedModel : result) { + // thumbToFeedMap.put(feedModel.getThumbnailUrl(), feedModel); + // } + // final BaseDataSubscriber subscriber = new BaseDataSubscriber() { + // int success = 0; + // int failed = 0; + // + // @Override + // protected void onNewResultImpl(@NonNull final DataSource dataSource) { + // final Map extras = dataSource.getExtras(); + // if (extras == null) return; + // final Uri thumbUri = (Uri) extras.get("uri_source"); + // if (thumbUri == null) return; + // final Integer encodedWidth = (Integer) extras.get("encoded_width"); + // final Integer encodedHeight = (Integer) extras.get("encoded_height"); + // if (encodedWidth == null || encodedHeight == null) return; + // final FeedModel feedModel = thumbToFeedMap.get(thumbUri.toString()); + // if (feedModel == null) return; + // int requiredWidth = Utils.displayMetrics.widthPixels; + // int resultingHeight = NumberUtils + // .getResultingHeight(requiredWidth, encodedHeight, encodedWidth); + // if (feedModel + // .getItemType() == MediaItemType.MEDIA_TYPE_VIDEO && resultingHeight >= MAX_VIDEO_HEIGHT) { + // // If its a video and the height is too large, need to reduce the height, + // // so that entire video fits on screen + // resultingHeight = RESIZED_VIDEO_HEIGHT; + // requiredWidth = NumberUtils.getResultingWidth(RESIZED_VIDEO_HEIGHT, + // resultingHeight, + // requiredWidth); + // } + // feedModel.setImageWidth(requiredWidth); + // feedModel.setImageHeight(resultingHeight); + // success++; + // updateAdapter(); + // } + // + // @Override + // protected void onFailureImpl(@NonNull final DataSource dataSource) { + // failed++; + // updateAdapter(); + // } + // + // public void updateAdapter() { + // if (failed + success != result.length) return; + // + // } + // }; + // for (final FeedModel feedModel : result) { + // final DataSource ds = Fresco.getImagePipeline() + // .prefetchToBitmapCache(ImageRequest.fromUri(feedModel.getThumbnailUrl()), null); + // ds.subscribe(subscriber, UiThreadImmediateExecutorService.getInstance()); + // } + // List finalList = currentFeedModelList == null || currentFeedModelList.isEmpty() + // ? new ArrayList<>() + // : new ArrayList<>(currentFeedModelList); + // final List resultList = Arrays.asList(result); + // if (isPullToRefresh) { + // finalList = resultList; + // isPullToRefresh = false; + // } else { + // finalList.addAll(resultList); + // } + // // feedViewModel.getList().postValue(finalList); + // final PostModel feedPostModel = result[result.length - 1]; + // if (feedPostModel != null) { + // feedEndCursor = feedPostModel.getEndCursor(); + // feedHasNextPage = feedPostModel.hasNextPage(); + // feedPostModel.setPageCursor(false, null); + // } + // binding.feedSwipeRefreshLayout.setRefreshing(false); + // } + // }; private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> { if (isHashtag) { final NavDirections action = FeedFragmentDirections.actionGlobalHashTagFragment(text); @@ -184,105 +165,105 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre NavHostFragment.findNavController(this).navigate(action); }; private final View.OnClickListener postViewClickListener = v -> { - final Object tag = v.getTag(); - if (!(tag instanceof FeedModel)) return; - - final FeedModel feedModel = (FeedModel) tag; - if (v instanceof RamboTextView) { - if (feedModel.isMentionClicked()) feedModel.toggleCaption(); - feedModel.setMentionClicked(false); - if (!FeedItemViewHolder.expandCollapseTextView((RamboTextView) v, feedModel.getPostCaption())) - feedModel.toggleCaption(); - return; - } - - final int id = v.getId(); - switch (id) { - case R.id.btnComments: - final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment( - feedModel.getShortCode(), - feedModel.getPostId(), - feedModel.getProfileModel().getId() - ); - NavHostFragment.findNavController(this).navigate(commentsAction); - break; - case R.id.viewStoryPost: - final List feedModels = feedViewModel.getList().getValue(); - if (feedModels == null || feedModels.size() == 0) return; - if (feedModels.get(0) == null) return; - final String postId = feedModels.get(0).getPostId(); - final boolean isId = postId != null; - final String[] idsOrShortCodes = new String[feedModels.size()]; - for (int i = 0; i < feedModels.size(); i++) { - idsOrShortCodes[i] = isId ? feedModels.get(i).getPostId() - : feedModels.get(i).getShortCode(); - } - final NavDirections action = FeedFragmentDirections.actionGlobalPostViewFragment( - feedModel.getPosition(), - idsOrShortCodes, - isId); - NavHostFragment.findNavController(this).navigate(action); - break; - - case R.id.btnDownload: - ProfileModel profileModel = feedModel.getProfileModel(); - final String username = profileModel != null ? profileModel.getUsername() : null; - - final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); - - final Context context = getContext(); - if (context == null) return; - if (feedModel - .getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1) - DownloadUtils.batchDownload(context, - username, - DownloadMethod.DOWNLOAD_FEED, - Collections.singletonList(feedModel)); - else { - final ArrayList postModels = new ArrayList<>(); - final DialogInterface.OnClickListener clickListener1 = (dialog, which) -> { - postModels.clear(); - - final boolean breakWhenFoundSelected = which == DialogInterface.BUTTON_POSITIVE; - - for (final ViewerPostModel sliderItem : sliderItems) { - if (sliderItem != null) { - if (!breakWhenFoundSelected) postModels.add(sliderItem); - else if (sliderItem.isSelected()) { - postModels.add(sliderItem); - break; - } - } - } - - // shows 0 items on first item of viewpager cause onPageSelected hasn't been called yet - if (breakWhenFoundSelected && postModels.size() == 0) { - postModels.add(sliderItems[0]); - } - if (postModels.size() > 0) { - DownloadUtils.batchDownload(context, - username, - DownloadMethod.DOWNLOAD_FEED, - postModels); - } - }; - - new AlertDialog.Builder(context) - .setTitle(R.string.post_viewer_download_dialog_title).setPositiveButton( - R.string.post_viewer_download_current, - clickListener1) - .setNegativeButton(R.string.post_viewer_download_album, clickListener1) - .show(); - } - break; - - case R.id.ivProfilePic: - profileModel = feedModel.getProfileModel(); - if (profileModel != null) mentionClickListener.onClick(null, profileModel.getUsername(), false, false); - break; - } + // gridLayoutManager.setSpanCount(1); + // final Object tag = v.getTag(); + // if (!(tag instanceof FeedModel)) return; + // + // final FeedModel feedModel = (FeedModel) tag; + // if (v instanceof RamboTextView) { + // if (feedModel.isMentionClicked()) feedModel.toggleCaption(); + // feedModel.setMentionClicked(false); + // if (!FeedItemViewHolder.expandCollapseTextView((RamboTextView) v, feedModel.getPostCaption())) + // feedModel.toggleCaption(); + // return; + // } + // + // final int id = v.getId(); + // switch (id) { + // case R.id.btnComments: + // final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment( + // feedModel.getShortCode(), + // feedModel.getPostId(), + // feedModel.getProfileModel().getId() + // ); + // NavHostFragment.findNavController(this).navigate(commentsAction); + // break; + // // case R.id.viewStoryPost: + // // final List feedModels = feedViewModel.getList().getValue(); + // // if (feedModels == null || feedModels.size() == 0) return; + // // if (feedModels.get(0) == null) return; + // // final String postId = feedModels.get(0).getPostId(); + // // final boolean isId = postId != null; + // // final String[] idsOrShortCodes = new String[feedModels.size()]; + // // for (int i = 0; i < feedModels.size(); i++) { + // // idsOrShortCodes[i] = isId ? feedModels.get(i).getPostId() + // // : feedModels.get(i).getShortCode(); + // // } + // // final NavDirections action = FeedFragmentDirections.actionGlobalPostViewFragment( + // // feedModel.getPosition(), + // // idsOrShortCodes, + // // isId); + // // NavHostFragment.findNavController(this).navigate(action); + // // break; + // case R.id.btnDownload: + // ProfileModel profileModel = feedModel.getProfileModel(); + // final String username = profileModel != null ? profileModel.getUsername() : null; + // + // final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); + // + // final Context context = getContext(); + // if (context == null) return; + // if (feedModel + // .getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1) + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_FEED, + // Collections.singletonList(feedModel)); + // else { + // final ArrayList postModels = new ArrayList<>(); + // final DialogInterface.OnClickListener clickListener1 = (dialog, which) -> { + // postModels.clear(); + // + // final boolean breakWhenFoundSelected = which == DialogInterface.BUTTON_POSITIVE; + // + // for (final ViewerPostModel sliderItem : sliderItems) { + // if (sliderItem != null) { + // if (!breakWhenFoundSelected) postModels.add(sliderItem); + // else if (sliderItem.isSelected()) { + // postModels.add(sliderItem); + // break; + // } + // } + // } + // + // // shows 0 items on first item of viewpager cause onPageSelected hasn't been called yet + // if (breakWhenFoundSelected && postModels.size() == 0) { + // postModels.add(sliderItems[0]); + // } + // if (postModels.size() > 0) { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_FEED, + // postModels); + // } + // }; + // + // new AlertDialog.Builder(context) + // .setTitle(R.string.post_viewer_download_dialog_title) + // .setPositiveButton(R.string.post_viewer_download_current, clickListener1) + // .setNegativeButton(R.string.post_viewer_download_album, clickListener1) + // .show(); + // } + // break; + // + // case R.id.ivProfilePic: + // profileModel = feedModel.getProfileModel(); + // if (profileModel != null) mentionClickListener.onClick(null, profileModel.getUsername(), false, false); + // break; + // default: + // break; + // } }; - private FeedStoriesViewModel feedStoriesViewModel; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -290,6 +271,9 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre fragmentActivity = (MainActivity) requireActivity(); storiesService = StoriesService.getInstance(); // feedService = FeedService.getInstance(); + // final TransitionInflater inflater = TransitionInflater.from(getContext()); + // setExitTransition(inflater.inflateTransition(android.R.transition.move)); + setHasOptionsMenu(true); } @Override @@ -312,6 +296,21 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre setupFeedStories(); setupFeed(); shouldRefresh = false; + // showPostsLayoutPreferences(); + } + + @Override + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + inflater.inflate(R.menu.feed_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getItemId() == R.id.layout) { + showPostsLayoutPreferences(); + return true; + } + return super.onOptionsItemSelected(item); } @Override @@ -335,37 +334,60 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre public void onRefresh() { isPullToRefresh = true; feedEndCursor = null; - fetchFeed(); + binding.feedRecyclerView.refresh(); fetchStories(); } private void setupFeed() { - feedViewModel = new ViewModelProvider(fragmentActivity).get(FeedViewModel.class); + // feedViewModel = new ViewModelProvider(fragmentActivity).get(FeedViewModel.class); final Context context = getContext(); if (context == null) return; - final LinearLayoutManager layoutManager = new LinearLayoutManager(context); - binding.feedRecyclerView.setLayoutManager(layoutManager); - binding.feedRecyclerView.setHasFixedSize(true); - final FeedAdapter feedAdapter = new FeedAdapter(postViewClickListener, mentionClickListener); - feedAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); - binding.feedRecyclerView.setAdapter(feedAdapter); - feedViewModel.getList().observe(fragmentActivity, feedAdapter::submitList); - final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (feedHasNextPage) { - fetchFeed(); - } - }); - if (shouldAutoPlay) { - videoAwareRecyclerScroller = new VideoAwareRecyclerScroller(); - binding.feedRecyclerView.addOnScrollListener(videoAwareRecyclerScroller); - } - binding.feedRecyclerView.addOnScrollListener(lazyLoader); - fetchFeed(); + // final PostFetcher.PostFetchService feedPostsFetchService = new PostFetcher.PostFetchService() { + // @Override + // public void fetch(final int page, final FetchListener> fetchListener) { + // new FeedFetcher(feedEndCursor, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + // } + // }; + binding.feedRecyclerView.setViewModelStoreOwner(this) + .setLifeCycleOwner(this) + .setPostFetchService(new FeedPostFetchService()) + .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_POSTS_LAYOUT))) + .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + .setOnPostClickListener((feedModel, profilePicView, mainPostImage) -> { + final PostViewV2Fragment fragment = PostViewV2Fragment + .builder(feedModel) + .setSharedProfilePicElement(profilePicView) + .setSharedMainPostElement(mainPostImage) + .build(); + fragment.show(getChildFragmentManager(), "post_view"); + }) + .init(); + binding.feedSwipeRefreshLayout.setRefreshing(true); + // final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + // binding.feedRecyclerView.setLayoutManager(gridLayoutManager); + // feedAdapter = new FeedAdapterV2(spanCount[0], postViewClickListener, mentionClickListener, feedModel -> { + // final ChangeBounds transition = new ChangeBounds(); + // transition.setDuration(200); + // TransitionManager.beginDelayedTransition(binding.feedRecyclerView, transition); + // spanCount[0] = spanCount[0] - 1; + // if (spanCount[0] == 0) { + // spanCount[0] = 3; + // } + // gridLayoutManager.setSpanCount(spanCount[0]); + // }); + // feedAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + // binding.feedRecyclerView.setAdapter(feedAdapter); + // feedViewModel.getList().observe(fragmentActivity, feedAdapter::submitList); + // if (shouldAutoPlay) { + // videoAwareRecyclerScroller = new VideoAwareRecyclerScroller(); + // binding.feedRecyclerView.addOnScrollListener(videoAwareRecyclerScroller); + // } + // fetchFeed(); + } - private void fetchFeed() { - binding.feedSwipeRefreshLayout.setRefreshing(true); - new FeedFetcher(feedEndCursor, feedFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + private void updateSwipeRefreshState() { + binding.feedSwipeRefreshLayout.setRefreshing(binding.feedRecyclerView.isFetching() || storiesFetching); } private void setupFeedStories() { @@ -383,16 +405,28 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void fetchStories() { + storiesFetching = true; + updateSwipeRefreshState(); storiesService.getFeedStories(new ServiceCallback>() { @Override public void onSuccess(final List result) { feedStoriesViewModel.getList().postValue(result); + storiesFetching = false; + updateSwipeRefreshState(); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "failed", t); + storiesFetching = false; + updateSwipeRefreshState(); } }); } + + private void showPostsLayoutPreferences() { + final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(preferences -> new Handler() + .postDelayed(() -> binding.feedRecyclerView.setLayoutPreferences(preferences), 200)); + fragment.show(getChildFragmentManager(), "posts_layout_preferences"); + } } 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 24a5035e..3f4d9993 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -3,6 +3,7 @@ package awais.instagrabber.fragments.main; import android.content.Context; import android.content.DialogInterface; import android.graphics.Typeface; +import android.graphics.drawable.Animatable; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -39,6 +40,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.controller.ControllerListener; +import com.facebook.imagepipeline.image.ImageInfo; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; @@ -170,8 +174,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.privatePage2.setText(R.string.empty_acc); binding.privatePage.setVisibility(View.VISIBLE); return; - } - else { + } else { binding.privatePage.setVisibility(View.GONE); } binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE)); @@ -352,6 +355,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onDestroy() { super.onDestroy(); + postsAdapter = null; if (usernameSettingHandler != null) { usernameSettingHandler.removeCallbacks(usernameSettingRunnable); } @@ -446,31 +450,31 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe final String myId = CookieUtils.getUserIdFromCookie(cookie); if (isLoggedIn) { storiesService.getUserStory(profileId, - profileModel.getUsername(), - false, - false, - false, - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - binding.mainProfileImage.setStoriesBorder(); - hasStories = true; - } - } + profileModel.getUsername(), + false, + false, + false, + new ServiceCallback>() { + @Override + public void onSuccess(final List storyModels) { + if (storyModels != null && !storyModels.isEmpty()) { + binding.mainProfileImage.setStoriesBorder(); + hasStories = true; + } + } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - } - }); + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + } + }); new HighlightsFetcher(profileId, - result -> { - if (result != null) { - binding.highlightsList.setVisibility(View.VISIBLE); - highlightsViewModel.getList().postValue(result); - } else binding.highlightsList.setVisibility(View.GONE); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + result -> { + if (result != null) { + binding.highlightsList.setVisibility(View.VISIBLE); + highlightsViewModel.getList().postValue(result); + } else binding.highlightsList.setVisibility(View.GONE); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); if (profileId.equals(myId)) { binding.btnTagged.setVisibility(View.VISIBLE); binding.btnSaved.setVisibility(View.VISIBLE); @@ -529,7 +533,13 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } else { binding.favCb.setVisibility(View.GONE); } - binding.mainProfileImage.setImageURI(profileModel.getSdProfilePic()); + final ControllerListener listener = new BaseControllerListener() { + @Override + public void onFinalImageSet(final String id, final ImageInfo imageInfo, final Animatable animatable) { + startPostponedEnterTransition(); + } + }; + binding.mainProfileImage.setImageURI(profileModel.getHdProfilePic()); final long followersCount = profileModel.getFollowersCount(); final long followingCount = profileModel.getFollowingCount(); @@ -562,8 +572,8 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe : profileModel.getName()); CharSequence biography = profileModel.getBiography(); - binding.mainBiography.setCaptionIsExpandable(true); - binding.mainBiography.setCaptionIsExpanded(true); + // binding.mainBiography.setCaptionIsExpandable(true); + // binding.mainBiography.setCaptionIsExpanded(true); if (TextUtils.hasMentions(biography)) { biography = TextUtils.getMentionText(biography); binding.mainBiography.setText(biography, TextView.BufferType.SPANNABLE); diff --git a/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java b/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java index e9b43f00..e424f539 100755 --- a/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java +++ b/app/src/main/java/awais/instagrabber/interfaces/FetchListener.java @@ -2,5 +2,8 @@ package awais.instagrabber.interfaces; public interface FetchListener { void onResult(T result); - default void doBefore() { } + + default void doBefore() {} + + default void onFailure(Throwable t) {} } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/BasePostModel.java b/app/src/main/java/awais/instagrabber/models/BasePostModel.java index 016be019..8c124926 100755 --- a/app/src/main/java/awais/instagrabber/models/BasePostModel.java +++ b/app/src/main/java/awais/instagrabber/models/BasePostModel.java @@ -20,14 +20,15 @@ public abstract class BasePostModel implements Serializable, Selectable { protected boolean isDownloaded; protected long timestamp; protected int position; - boolean liked, bookmarked; + boolean liked; + boolean saved; public boolean getLike() { return liked; } - public boolean getBookmark() { - return bookmarked; + public boolean isSaved() { + return saved; } public MediaItemType getItemType() { diff --git a/app/src/main/java/awais/instagrabber/models/FeedModel.java b/app/src/main/java/awais/instagrabber/models/FeedModel.java index 2106aa22..b41323fe 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedModel.java @@ -1,47 +1,174 @@ package awais.instagrabber.models; +import java.util.List; + import awais.instagrabber.models.enums.MediaItemType; public final class FeedModel extends PostModel { private final ProfileModel profileModel; private final long commentsCount; + private long likesCount; private final long viewCount; private boolean captionExpanded = false; private boolean mentionClicked = false; - private ViewerPostModel[] sliderItems; + private List sliderItems; private int imageWidth; private int imageHeight; private String locationName; private String locationId; - public FeedModel(final ProfileModel profileModel, - final MediaItemType itemType, - final long viewCount, - final String postId, - final String displayUrl, - final String thumbnailUrl, - final String shortCode, - final String postCaption, - final long commentsCount, - final long timestamp, - final boolean liked, - final boolean bookmarked, - final long likes, - final String locationName, - final String locationId) { - super(itemType, postId, displayUrl, thumbnailUrl, shortCode, postCaption, timestamp, liked, bookmarked, likes); + public static class Builder { + + private ProfileModel profileModel; + private MediaItemType itemType; + private long viewCount; + private String postId; + private String displayUrl; + private String thumbnailUrl; + private String shortCode; + private String postCaption; + private long commentsCount; + private long timestamp; + private boolean liked; + private boolean bookmarked; + private long likesCount; + private String locationName; + private String locationId; + private List sliderItems; + private int imageWidth; + private int imageHeight; + + public Builder setProfileModel(final ProfileModel profileModel) { + this.profileModel = profileModel; + return this; + } + + public Builder setItemType(final MediaItemType itemType) { + this.itemType = itemType; + return this; + } + + public Builder setViewCount(final long viewCount) { + this.viewCount = viewCount; + return this; + } + + public Builder setPostId(final String postId) { + this.postId = postId; + return this; + } + + public Builder setDisplayUrl(final String displayUrl) { + this.displayUrl = displayUrl; + return this; + } + + public Builder setThumbnailUrl(final String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + return this; + } + + public Builder setShortCode(final String shortCode) { + this.shortCode = shortCode; + return this; + } + + public Builder setPostCaption(final String postCaption) { + this.postCaption = postCaption; + return this; + } + + public Builder setCommentsCount(final long commentsCount) { + this.commentsCount = commentsCount; + return this; + } + + public Builder setTimestamp(final long timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder setLiked(final boolean liked) { + this.liked = liked; + return this; + } + + public Builder setBookmarked(final boolean bookmarked) { + this.bookmarked = bookmarked; + return this; + } + + public Builder setLikesCount(final long likesCount) { + this.likesCount = likesCount; + return this; + } + + public Builder setLocationName(final String locationName) { + this.locationName = locationName; + return this; + } + + public Builder setLocationId(final String locationId) { + this.locationId = locationId; + return this; + } + + public Builder setSliderItems(final List sliderItems) { + this.sliderItems = sliderItems; + return this; + } + + public Builder setImageHeight(final int imageHeight) { + this.imageHeight = imageHeight; + return this; + } + + public Builder setImageWidth(final int imageWidth) { + this.imageWidth = imageWidth; + return this; + } + + public FeedModel build() { + return new FeedModel(profileModel, itemType, viewCount, postId, displayUrl, thumbnailUrl, shortCode, postCaption, commentsCount, + timestamp, liked, bookmarked, likesCount, locationName, locationId, sliderItems, imageHeight, imageWidth); + } + } + + private FeedModel(final ProfileModel profileModel, + final MediaItemType itemType, + final long viewCount, + final String postId, + final String displayUrl, + final String thumbnailUrl, + final String shortCode, + final String postCaption, + final long commentsCount, + final long timestamp, + final boolean liked, + final boolean bookmarked, + final long likesCount, + final String locationName, + final String locationId, + final List sliderItems, + final int imageHeight, + final int imageWidth) { + super(itemType, postId, displayUrl, thumbnailUrl, shortCode, postCaption, timestamp, liked, bookmarked); this.profileModel = profileModel; this.commentsCount = commentsCount; + this.likesCount = likesCount; this.viewCount = viewCount; this.locationName = locationName; this.locationId = locationId; + this.sliderItems = sliderItems; + this.imageHeight = imageHeight; + this.imageWidth = imageWidth; } public ProfileModel getProfileModel() { return profileModel; } - public ViewerPostModel[] getSliderItems() { + public List getSliderItems() { return sliderItems; } @@ -53,6 +180,10 @@ public final class FeedModel extends PostModel { return commentsCount; } + public long getLikesCount() { + return likesCount; + } + public boolean isCaptionExpanded() { return captionExpanded; } @@ -61,14 +192,14 @@ public final class FeedModel extends PostModel { return !mentionClicked; } - public void setMentionClicked(final boolean mentionClicked) { - this.mentionClicked = mentionClicked; - } - - public void setSliderItems(final ViewerPostModel[] sliderItems) { - this.sliderItems = sliderItems; - setItemType(MediaItemType.MEDIA_TYPE_SLIDER); - } + // public void setMentionClicked(final boolean mentionClicked) { + // this.mentionClicked = mentionClicked; + // } + // + // public void setSliderItems(final ViewerPostModel[] sliderItems) { + // this.sliderItems = sliderItems; + // setItemType(MediaItemType.MEDIA_TYPE_SLIDER); + // } public void toggleCaption() { captionExpanded = !captionExpanded; @@ -78,13 +209,13 @@ public final class FeedModel extends PostModel { return imageWidth; } - public void setImageWidth(final int imageWidth) { - this.imageWidth = imageWidth; - } + // public void setImageWidth(final int imageWidth) { + // this.imageWidth = imageWidth; + // } - public void setImageHeight(final int imageHeight) { - this.imageHeight = imageHeight; - } + // public void setImageHeight(final int imageHeight) { + // this.imageHeight = imageHeight; + // } public int getImageHeight() { return imageHeight; @@ -94,15 +225,45 @@ public final class FeedModel extends PostModel { return locationName; } - public void setLocationName(final String locationName) { - this.locationName = locationName; - } + // public void setLocationName(final String locationName) { + // this.locationName = locationName; + // } public String getLocationId() { return locationId; } - public void setLocationId(final String locationId) { - this.locationId = locationId; + // public void setLocationId(final String locationId) { + // this.locationId = locationId; + // } + + public void setLiked(final boolean liked) { + this.liked = liked; + } + + public void setLikesCount(final long count) { + this.likesCount = count; + } + + public void setSaved(final boolean saved) { + this.saved = saved; + } + + @Override + public String toString() { + return "FeedModel{" + + "type=" + itemType + + ", displayUrl=" + displayUrl + + ", thumbnailUrl=" + thumbnailUrl + + ", commentsCount=" + commentsCount + + ", viewCount=" + viewCount + + ", captionExpanded=" + captionExpanded + + ", mentionClicked=" + mentionClicked + + // ", sliderItems=" + sliderItems + + ", imageWidth=" + imageWidth + + ", imageHeight=" + imageHeight + + ", locationName='" + locationName + '\'' + + ", locationId='" + locationId + '\'' + + '}'; } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/PostChild.java b/app/src/main/java/awais/instagrabber/models/PostChild.java new file mode 100644 index 00000000..954a11b3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/PostChild.java @@ -0,0 +1,121 @@ +package awais.instagrabber.models; + +import java.io.Serializable; + +import awais.instagrabber.models.enums.MediaItemType; + +public final class PostChild implements Serializable { + private String postId; + private MediaItemType itemType; + private String displayUrl; + private final String thumbnailUrl; + private final long videoViews; + private int width; + private int height; + + public static class Builder { + private String postId; + private MediaItemType itemType; + private String displayUrl; + private long videoViews; + private String thumbnailUrl; + private int width; + private int height; + + public Builder setPostId(final String postId) { + this.postId = postId; + return this; + } + + public Builder setItemType(final MediaItemType itemType) { + this.itemType = itemType; + return this; + } + + public Builder setDisplayUrl(final String displayUrl) { + this.displayUrl = displayUrl; + return this; + } + + public Builder setVideoViews(final long videoViews) { + this.videoViews = videoViews; + return this; + } + + public Builder setHeight(final int height) { + this.height = height; + return this; + } + + public Builder setWidth(final int width) { + this.width = width; + return this; + } + + public Builder setThumbnailUrl(final String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + return this; + } + + public PostChild build() { + return new PostChild(postId, itemType, displayUrl, thumbnailUrl, videoViews, height, width); + } + } + + public PostChild(final String postId, + final MediaItemType itemType, + final String displayUrl, + final String thumbnailUrl, + final long videoViews, + final int height, + final int width) { + this.postId = postId; + this.itemType = itemType; + this.displayUrl = displayUrl; + this.thumbnailUrl = thumbnailUrl; + this.height = height; + this.width = width; + this.videoViews = videoViews; + } + + public String getPostId() { + return postId; + } + + public MediaItemType getItemType() { + return itemType; + } + + public String getDisplayUrl() { + return displayUrl; + } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public long getVideoViews() { + return videoViews; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + @Override + public String toString() { + return "PostChild{" + + "postId='" + postId + '\'' + + ", itemType=" + itemType + + ", displayUrl='" + displayUrl + '\'' + + ", thumbnailUrl='" + thumbnailUrl + '\'' + + ", videoViews=" + videoViews + + ", width=" + width + + ", height=" + height + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/PostModel.java b/app/src/main/java/awais/instagrabber/models/PostModel.java index 27aeb6a2..e74924d7 100755 --- a/app/src/main/java/awais/instagrabber/models/PostModel.java +++ b/app/src/main/java/awais/instagrabber/models/PostModel.java @@ -13,9 +13,15 @@ public class PostModel extends BasePostModel { this.thumbnailUrl = null; } - public PostModel(final MediaItemType itemType, final String postId, final String displayUrl, final String thumbnailUrl, - final String shortCode, final CharSequence postCaption, long timestamp, boolean liked, boolean bookmarked, - long likes) { + public PostModel(final MediaItemType itemType, + final String postId, + final String displayUrl, + final String thumbnailUrl, + final String shortCode, + final CharSequence postCaption, + long timestamp, + boolean liked, + boolean bookmarked) { this.itemType = itemType; this.postId = postId; this.displayUrl = displayUrl; @@ -24,7 +30,7 @@ public class PostModel extends BasePostModel { this.postCaption = postCaption; this.timestamp = timestamp; this.liked = liked; - this.bookmarked = bookmarked; + this.saved = bookmarked; } public String getThumbnailUrl() { diff --git a/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java b/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java new file mode 100644 index 00000000..eee16129 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java @@ -0,0 +1,204 @@ +package awais.instagrabber.models; + +import com.google.gson.Gson; + +import java.util.Objects; + +public final class PostsLayoutPreferences { + private final PostsLayoutType type; + private final int colCount; + private final boolean isAvatarVisible; + private final boolean isNameVisible; + private final ProfilePicSize profilePicSize; + private final boolean hasRoundedCorners; + private final boolean hasGap; + + public static class Builder { + private PostsLayoutType type = PostsLayoutType.GRID; + private int colCount = 2; + private boolean isAvatarVisible = false; + private boolean isNameVisible = false; + private ProfilePicSize profilePicSize = ProfilePicSize.REGULAR; + private boolean hasRoundedCorners = true; + private boolean hasGap = true; + + public Builder setType(final PostsLayoutType type) { + this.type = type; + return this; + } + + public Builder setColCount(final int colCount) { + this.colCount = (colCount <= 0 || colCount > 3) ? 1 : colCount; + return this; + } + + public Builder setAvatarVisible(final boolean avatarVisible) { + this.isAvatarVisible = avatarVisible; + return this; + } + + public Builder setNameVisible(final boolean nameVisible) { + this.isNameVisible = nameVisible; + return this; + } + + public Builder setProfilePicSize(final ProfilePicSize profilePicSize) { + this.profilePicSize = profilePicSize; + return this; + } + + public Builder setHasRoundedCorners(final boolean hasRoundedCorners) { + this.hasRoundedCorners = hasRoundedCorners; + return this; + } + + public Builder setHasGap(final boolean hasGap) { + this.hasGap = hasGap; + return this; + } + + // Breaking builder pattern and adding getters to avoid too many object creations in PostsLayoutPreferencesDialogFragment + public PostsLayoutType getType() { + return type; + } + + public int getColCount() { + return colCount; + } + + public boolean isAvatarVisible() { + return isAvatarVisible; + } + + public boolean isNameVisible() { + return isNameVisible; + } + + public ProfilePicSize getProfilePicSize() { + return profilePicSize; + } + + public boolean getHasRoundedCorners() { + return hasRoundedCorners; + } + + public boolean getHasGap() { + return hasGap; + } + + public Builder mergeFrom(final PostsLayoutPreferences preferences) { + setColCount(preferences.getColCount()); + setAvatarVisible(preferences.isAvatarVisible()); + setNameVisible(preferences.isNameVisible()); + setType(preferences.getType()); + setProfilePicSize(preferences.getProfilePicSize()); + setHasRoundedCorners(preferences.getHasRoundedCorners()); + setHasGap(preferences.getHasGap()); + return this; + } + + public PostsLayoutPreferences build() { + return new PostsLayoutPreferences(type, colCount, isAvatarVisible, isNameVisible, profilePicSize, hasRoundedCorners, hasGap); + } + } + + public static Builder builder() { + return new Builder(); + } + + private PostsLayoutPreferences(final PostsLayoutType type, + final int colCount, + final boolean isAvatarVisible, + final boolean isNameVisible, + final ProfilePicSize profilePicSize, + final boolean hasRoundedCorners, + final boolean hasGap) { + + this.type = type; + this.colCount = colCount; + this.isAvatarVisible = isAvatarVisible; + this.isNameVisible = isNameVisible; + this.profilePicSize = profilePicSize; + this.hasRoundedCorners = hasRoundedCorners; + this.hasGap = hasGap; + } + + public PostsLayoutType getType() { + return type; + } + + public int getColCount() { + return colCount; + } + + public boolean isAvatarVisible() { + return isAvatarVisible; + } + + public boolean isNameVisible() { + return isNameVisible; + } + + public ProfilePicSize getProfilePicSize() { + return profilePicSize; + } + + public boolean getHasRoundedCorners() { + return hasRoundedCorners; + } + + public boolean getHasGap() { + return hasGap; + } + + public String getJson() { + return new Gson().toJson(this); + } + + public static PostsLayoutPreferences fromJson(final String json) { + if (json == null) return null; + return new Gson().fromJson(json, PostsLayoutPreferences.class); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final PostsLayoutPreferences that = (PostsLayoutPreferences) o; + return colCount == that.colCount && + isAvatarVisible == that.isAvatarVisible && + isNameVisible == that.isNameVisible && + type == that.type && + profilePicSize == that.profilePicSize; + } + + @Override + public int hashCode() { + return Objects.hash(type, colCount, isAvatarVisible, isNameVisible, profilePicSize); + } + + @Override + public String toString() { + return "PostsLayoutPreferences{" + + "type=" + type + + ", colCount=" + colCount + + ", isAvatarVisible=" + isAvatarVisible + + ", isNameVisible=" + isNameVisible + + ", profilePicSize=" + profilePicSize + + ", hasRoundedCorners=" + hasRoundedCorners + + ", hasGap=" + hasGap + + '}'; + } + + public enum PostsLayoutType { + GRID, + STAGGERED_GRID, + LINEAR + } + + public enum ProfilePicSize { + REGULAR, + SMALL, + TINY + } +} diff --git a/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java b/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java index 6e5eac70..5e9589ce 100755 --- a/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java +++ b/app/src/main/java/awais/instagrabber/models/ViewerPostModel.java @@ -7,13 +7,124 @@ public final class ViewerPostModel extends BasePostModel { protected final String locationName; protected final String location; protected final long videoViews; - protected String sliderDisplayUrl; - protected long commentsCount, likes; + private final String thumbnailUrl; + protected long commentsCount; + protected long likes; + private int imageWidth; + private int imageHeight; private boolean isCurrentSlide = false; + public static class Builder { + private MediaItemType itemType; + private String postId; + private String displayUrl; + private String shortCode; + private String postCaption; + private ProfileModel profileModel; + private long videoViews; + private long timestamp; + private boolean liked; + private boolean bookmarked; + private long likes; + private String locationName; + private String location; + private String thumbnailUrl; + private int imageWidth; + private int imageHeight; + + public Builder setItemType(final MediaItemType itemType) { + this.itemType = itemType; + return this; + } + + public Builder setPostId(final String postId) { + this.postId = postId; + return this; + } + + public Builder setDisplayUrl(final String displayUrl) { + this.displayUrl = displayUrl; + return this; + } + + public Builder setShortCode(final String shortCode) { + this.shortCode = shortCode; + return this; + } + + // public Builder setPostCaption(final String postCaption) { + // this.postCaption = postCaption; + // return this; + // } + + // public Builder setProfileModel(final ProfileModel profileModel) { + // this.profileModel = profileModel; + // return this; + // } + + public Builder setVideoViews(final long videoViews) { + this.videoViews = videoViews; + return this; + } + + // public Builder setTimestamp(final long timestamp) { + // this.timestamp = timestamp; + // return this; + // } + // + // public Builder setLiked(final boolean liked) { + // this.liked = liked; + // return this; + // } + // + // public Builder setBookmarked(final boolean bookmarked) { + // this.bookmarked = bookmarked; + // return this; + // } + // + // public Builder setLikes(final long likes) { + // this.likes = likes; + // return this; + // } + + // public Builder setLocationName(final String locationName) { + // this.locationName = locationName; + // return this; + // } + // + // public Builder setLocation(final String location) { + // this.location = location; + // return this; + // } + + + public Builder setImageHeight(final int imageHeight) { + this.imageHeight = imageHeight; + return this; + } + + public Builder setImageWidth(final int imageWidth) { + this.imageWidth = imageWidth; + return this; + } + + public Builder setThumbnailUrl(final String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + return this; + } + + public ViewerPostModel build() { + return new ViewerPostModel(itemType, postId, displayUrl, thumbnailUrl, imageHeight, imageWidth, shortCode, postCaption, profileModel, + videoViews, timestamp, liked, bookmarked, likes, locationName, location); + } + } + public ViewerPostModel(final MediaItemType itemType, final String postId, final String displayUrl, + final String thumbnailUrl, + final int imageHeight, + final int imageWidth, final String shortCode, final String postCaption, final ProfileModel profileModel, @@ -27,6 +138,9 @@ public final class ViewerPostModel extends BasePostModel { this.itemType = itemType; this.postId = postId; this.displayUrl = displayUrl; + this.thumbnailUrl = thumbnailUrl; + this.imageHeight = imageHeight; + this.imageWidth = imageWidth; this.postCaption = postCaption; this.profileModel = profileModel; this.shortCode = shortCode; @@ -34,23 +148,15 @@ public final class ViewerPostModel extends BasePostModel { this.timestamp = timestamp; this.liked = liked; this.likes = likes; - this.bookmarked = bookmarked; + this.saved = bookmarked; this.locationName = locationName; this.location = location; } - public static ViewerPostModel getDefaultModel(final int postId, final String shortCode) { - return new ViewerPostModel(null, String.valueOf(postId), null, "", null, null, -1, -1, false, false, -1, null, null); - } - public long getCommentsCount() { return commentsCount; } - public String getSliderDisplayUrl() { - return sliderDisplayUrl; - } - public ProfileModel getProfileModel() { return profileModel; } @@ -72,29 +178,42 @@ public final class ViewerPostModel extends BasePostModel { } // setManualLike means user liked from InstaGrabber - public boolean setManualLike(final boolean like) { + public void setManualLike(final boolean like) { liked = like; likes = (like) ? (likes + 1) : (likes - 1); - return liked; - } - - public void setBookmarked(final boolean bookmarked) { - this.bookmarked = bookmarked; - } - - public void setSliderDisplayUrl(final String sliderDisplayUrl) { - this.sliderDisplayUrl = sliderDisplayUrl; - } - - public void setCommentsCount(final long commentsCount) { - this.commentsCount = commentsCount; - } - - public void setCurrentSlide(final boolean currentSlide) { - this.isCurrentSlide = currentSlide; } public boolean isCurrentSlide() { return isCurrentSlide; } + + public String getThumbnailUrl() { + return thumbnailUrl; + } + + public int getImageHeight() { + return imageHeight; + } + + public int getImageWidth() { + return imageWidth; + } + + @Override + public String toString() { + return "ViewerPostModel{" + + "type=" + itemType + + ", displayUrl=" + displayUrl + + ", thumbnailUrl=" + thumbnailUrl + + ", locationName='" + locationName + '\'' + + ", location='" + location + '\'' + + ", videoViews=" + videoViews + + ", thumbnailUrl='" + thumbnailUrl + '\'' + + ", commentsCount=" + commentsCount + + ", likes=" + likes + + ", imageWidth=" + imageWidth + + ", imageHeight=" + imageHeight + + ", isCurrentSlide=" + isCurrentSlide + + '}'; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/ViewerPostModelWrapper.java b/app/src/main/java/awais/instagrabber/models/ViewerPostModelWrapper.java index 170793ba..69dfb088 100644 --- a/app/src/main/java/awais/instagrabber/models/ViewerPostModelWrapper.java +++ b/app/src/main/java/awais/instagrabber/models/ViewerPostModelWrapper.java @@ -1,10 +1,12 @@ package awais.instagrabber.models; +import java.util.List; + public class ViewerPostModelWrapper { private int position; - private ViewerPostModel[] viewerPostModels; + private List viewerPostModels; - public ViewerPostModelWrapper(final int position, final ViewerPostModel[] viewerPostModels) { + public ViewerPostModelWrapper(final int position, final List viewerPostModels) { this.position = position; this.viewerPostModels = viewerPostModels; } @@ -13,11 +15,11 @@ public class ViewerPostModelWrapper { return position; } - public ViewerPostModel[] getViewerPostModels() { + public List getViewerPostModels() { return viewerPostModels; } - public void setViewerPostModels(final ViewerPostModel[] viewerPostModels) { + public void setViewerPostModels(final List viewerPostModels) { this.viewerPostModels = viewerPostModels; } } diff --git a/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java b/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java new file mode 100644 index 00000000..3672fb90 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java @@ -0,0 +1,12 @@ +package awais.instagrabber.repositories; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public interface FeedRepository { + @GET("/graphql/query/") + Call fetch(@QueryMap(encoded = true) Map queryParams); +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java new file mode 100644 index 00000000..f3aa0f50 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java @@ -0,0 +1,29 @@ +package awais.instagrabber.repositories.responses; + +import java.util.List; + +import awais.instagrabber.models.FeedModel; + +public class FeedFetchResponse { + private List feedModels; + private boolean hasNextPage; + private String nextCursor; + + public FeedFetchResponse(final List feedModels, final boolean hasNextPage, final String nextCursor) { + this.feedModels = feedModels; + this.hasNextPage = hasNextPage; + this.nextCursor = nextCursor; + } + + public List getFeedModels() { + return feedModels; + } + + public boolean hasNextPage() { + return hasNextPage; + } + + public String getNextCursor() { + return nextCursor; + } +} diff --git a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java index 255d051c..f0ccea70 100644 --- a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java +++ b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java @@ -18,7 +18,6 @@ import java.util.List; import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.asyncs.GetActivityAsyncTask; import awais.instagrabber.asyncs.GetActivityAsyncTask.NotificationCounts; import awais.instagrabber.asyncs.GetActivityAsyncTask.OnTaskCompleteListener; import awais.instagrabber.utils.Constants; @@ -37,8 +36,8 @@ public class ActivityCheckerService extends Service { private final IBinder binder = new LocalBinder(); private final Runnable runnable = () -> { final String cookie = settingsHelper.getString(Constants.COOKIE); - final GetActivityAsyncTask activityAsyncTask = new GetActivityAsyncTask(onTaskCompleteListener); - activityAsyncTask.execute(cookie); + // final GetActivityAsyncTask activityAsyncTask = new GetActivityAsyncTask(onTaskCompleteListener); + // activityAsyncTask.execute(cookie); }; public class LocalBinder extends Binder { diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index bb02453a..f32d7db2 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -51,6 +51,10 @@ public final class Constants { public static final String EXTRAS_END_CURSOR = "endCursor"; public static final String FEED = "feed"; public static final String FEED_ORDER = "feedOrder"; + + // Notification ids + public static final int ACTIVITY_NOTIFICATION_ID = 10; + // spoof public static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 8.1.0; motorola one Build/OPKS28.63-18-3; wv) " + "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 " + @@ -78,10 +82,10 @@ public final class Constants { public static final String ACTIVITY_CHANNEL_NAME = "Activity"; public static final String DOWNLOAD_CHANNEL_NAME = "Downloads"; public static final String NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; - public static final int ACTIVITY_NOTIFICATION_ID = 1800000000; public static final String ACTION_SHOW_ACTIVITY = "show_activity"; public static final String PREF_DARK_THEME = "dark_theme"; public static final String PREF_LIGHT_THEME = "light_theme"; public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png"; public static final String SHARED_PREFERENCES_NAME = "settings"; + public static final String PREF_POSTS_LAYOUT = "posts_layout"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index 48576a98..84dde0fe 100644 --- a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java @@ -8,16 +8,29 @@ import android.os.AsyncTask; import android.os.Environment; import android.util.Log; import android.util.Pair; +import android.webkit.MimeTypeMap; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import com.google.gson.Gson; import java.io.File; import java.io.FilenameFilter; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.regex.Pattern; import awais.instagrabber.BuildConfig; @@ -25,17 +38,29 @@ import awais.instagrabber.R; import awais.instagrabber.asyncs.DownloadAsync; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.models.BasePostModel; -import awais.instagrabber.models.StoryModel; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.direct_messages.DirectItemModel; import awais.instagrabber.models.enums.DownloadMethod; import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.workers.DownloadWorker; import awaisomereport.LogCollector; import static awais.instagrabber.utils.Constants.FOLDER_PATH; import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; public final class DownloadUtils { + public static final String WRITE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE; public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; + private static int lastNotificationId = UUID.randomUUID().hashCode(); + + public synchronized static int getNextDownloadNotificationId(@NonNull final Context context) { + lastNotificationId = lastNotificationId + 1; + if (lastNotificationId == Integer.MAX_VALUE) { + lastNotificationId = UUID.randomUUID().hashCode(); + } + return lastNotificationId; + } public static void batchDownload(@NonNull final Context context, @Nullable String username, @@ -57,20 +82,8 @@ public final class DownloadUtils { @Nullable final String username, final DownloadMethod method, final List itemsToDownload) { - File dir = new File(Environment.getExternalStorageDirectory(), "Download"); - - if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { - final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); - if (!TextUtils.isEmpty(customPath)) dir = new File(customPath); - } - - if (Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) && !TextUtils.isEmpty(username)) - dir = new File(dir, username); - - if (!dir.exists() && !dir.mkdirs()) { - Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); - return; - } + final File dir = getDownloadDir(context, username); + if (dir == null) return; boolean checkEachPost = false; switch (method) { case DOWNLOAD_SAVED: @@ -86,7 +99,11 @@ public final class DownloadUtils { final BasePostModel selectedItem = itemsToDownload.get(i); if (!checkEachPost) { final boolean isSlider = itemsToDownloadSize > 1; - final File saveFile = getDownloadSaveFile(dir, selectedItem, isSlider ? "_slide_" + (i + 1) : ""); + final File saveFile = getDownloadSaveFile(dir, + selectedItem.getPostId(), + isSlider ? "_slide_" + (i + 1) : "", + selectedItem.getDisplayUrl() + ); new DownloadAsync(context, selectedItem.getDisplayUrl(), saveFile, @@ -95,25 +112,64 @@ public final class DownloadUtils { } else { final File finalDir = dir; new PostFetcher(selectedItem.getShortCode(), result -> { - if (result != null) { - final int resultsSize = result.length; - final boolean multiResult = resultsSize > 1; - for (int j = 0; j < resultsSize; j++) { - final BasePostModel model = result[j]; - final File saveFile = getDownloadSaveFile(finalDir, model, multiResult ? "_slide_" + (j + 1) : ""); + if (result == null) return; + final boolean isSlider = result.getItemType() == MediaItemType.MEDIA_TYPE_SLIDER; + if (isSlider) { + for (int j = 0; j < result.getSliderItems().size(); j++) { + final PostChild model = result.getSliderItems().get(j); + final File saveFile = getDownloadSaveFile( + finalDir, + model.getPostId(), + "_slide_" + (j + 1), + model.getDisplayUrl() + ); new DownloadAsync(context, model.getDisplayUrl(), saveFile, - file -> model.setDownloaded(true)) + file -> {}/*model.setDownloaded(true)*/) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + } else { + final File saveFile = getDownloadSaveFile( + finalDir, + result.getPostId(), + result.getDisplayUrl() + ); + new DownloadAsync(context, + result.getDisplayUrl(), + saveFile, + file -> result.setDownloaded(true)) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } } - public static void dmDownload(@NonNull final Context context, @Nullable final String username, final DownloadMethod method, + @Nullable + private static File getDownloadDir(@NonNull final Context context, @Nullable final String username) { + File dir = new File(Environment.getExternalStorageDirectory(), "Download"); + + if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { + final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); + if (!TextUtils.isEmpty(customPath)) dir = new File(customPath); + } + + if (Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) && !TextUtils.isEmpty(username)) { + final String finaleUsername = username.startsWith("@") ? username : "@" + username; + dir = new File(dir, finaleUsername); + } + + if (!dir.exists() && !dir.mkdirs()) { + Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); + return null; + } + return dir; + } + + public static void dmDownload(@NonNull final Context context, + @Nullable final String username, + final DownloadMethod method, final DirectItemModel.DirectItemMediaModel itemsToDownload) { if (Utils.settingsHelper == null) Utils.settingsHelper = new SettingsHelper(context); @@ -125,8 +181,10 @@ public final class DownloadUtils { ActivityCompat.requestPermissions((Activity) context, PERMS, 8020); } - private static void dmDownloadImpl(@NonNull final Context context, @Nullable final String username, - final DownloadMethod method, final DirectItemModel.DirectItemMediaModel selectedItem) { + private static void dmDownloadImpl(@NonNull final Context context, + @Nullable final String username, + final DownloadMethod method, + final DirectItemModel.DirectItemMediaModel selectedItem) { File dir = new File(Environment.getExternalStorageDirectory(), "Download"); if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { @@ -140,46 +198,83 @@ public final class DownloadUtils { if (dir.exists() || dir.mkdirs()) { new DownloadAsync(context, selectedItem.getMediaType() == MediaItemType.MEDIA_TYPE_VIDEO ? selectedItem.getVideoUrl() : selectedItem.getThumbUrl(), - getDownloadSaveFileDm(dir, selectedItem, ""), - null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + getDownloadSaveFileDm(dir, selectedItem), + null) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); } @NonNull - private static File getDownloadSaveFile(final File finalDir, @NonNull final BasePostModel model, final String sliderPrefix) { - final String displayUrl = model.getDisplayUrl(); - return new File(finalDir, model.getPostId() + '_' + model.getPosition() + sliderPrefix + - getExtensionFromModel(displayUrl, model)); + private static File getDownloadSaveFile(final File finalDir, + final String postId, + final String displayUrl) { + return getDownloadSaveFile(finalDir, postId, "", displayUrl); + } + + private static File getDownloadChildSaveFile(final File downloadDir, + final String postId, + final int childPosition, + final String url) { + final String sliderPostfix = "_slide_" + childPosition; + return getDownloadSaveFile(downloadDir, postId, sliderPostfix, url); + } + + @NonNull + private static File getDownloadSaveFile(final File finalDir, + final String postId, + final String sliderPostfix, + final String displayUrl) { + final String fileName = postId + sliderPostfix + "." + getFileExtensionFromUrl(displayUrl); + return new File(finalDir, fileName); } @NonNull private static File getDownloadSaveFileDm(final File finalDir, - @NonNull final DirectItemModel.DirectItemMediaModel model, - final String sliderPrefix) { - final String displayUrl = model.getMediaType() == MediaItemType.MEDIA_TYPE_VIDEO ? model.getVideoUrl() : model.getThumbUrl(); - return new File(finalDir, model.getId() + sliderPrefix + - getExtensionFromModel(displayUrl, model)); + @NonNull final DirectItemModel.DirectItemMediaModel model) { + final boolean isVideo = model.getMediaType() == MediaItemType.MEDIA_TYPE_VIDEO; + final String displayUrl = isVideo ? model.getVideoUrl() : model.getThumbUrl(); + return new File(finalDir, model.getId() + getFileExtensionFromUrl(displayUrl)); } - @NonNull - public static String getExtensionFromModel(@NonNull final String url, final Object model) { - final String extension; - final int index = url.indexOf('?'); + /** + * Copied from {@link MimeTypeMap#getFileExtensionFromUrl(String)}) + *

+ * Returns the file extension or an empty string if there is no + * extension. This method is a convenience method for obtaining the + * extension of a url and has undefined results for other Strings. + * + * @param url URL + * @return The file extension of the given url. + */ + public static String getFileExtensionFromUrl(String url) { + if (!TextUtils.isEmpty(url)) { + int fragment = url.lastIndexOf('#'); + if (fragment > 0) { + url = url.substring(0, fragment); + } - if (index != -1) extension = url.substring(index - 4, index); - else { - final boolean isVideo; - if (model instanceof StoryModel) - isVideo = ((StoryModel) model).getItemType() == MediaItemType.MEDIA_TYPE_VIDEO; - else if (model instanceof BasePostModel) - isVideo = ((BasePostModel) model).getItemType() == MediaItemType.MEDIA_TYPE_VIDEO; - else - isVideo = false; - extension = isVideo || url.contains(".mp4") ? ".mp4" : ".jpg"; + int query = url.lastIndexOf('?'); + if (query > 0) { + url = url.substring(0, query); + } + + int filenamePos = url.lastIndexOf('/'); + String filename = + 0 <= filenamePos ? url.substring(filenamePos + 1) : url; + + // if the filename contains special characters, we don't + // consider it valid for our matching purposes: + if (!filename.isEmpty() && + Pattern.matches("[a-zA-Z_0-9.\\-()%]+", filename)) { + int dotPos = filename.lastIndexOf('.'); + if (0 <= dotPos) { + return filename.substring(dotPos + 1); + } + } } - return extension; + return ""; } public static void checkExistence(final File downloadDir, final File customDir, final boolean isSlider, @@ -198,7 +293,7 @@ public final class DownloadUtils { final String fileWithoutPrefix = fileName + '0' + extension; exists = new File(downloadDir, fileWithoutPrefix).exists(); if (!exists) { - final String fileWithPrefix = fileName + "[\\d]+(|_slide_[\\d]+)(\\.mp4|\\" + extension + ")"; + final String fileWithPrefix = fileName + "[\\d]+(|_slide_[\\d]+)(\\.mp4|\\\\" + extension + ")"; final FilenameFilter filenameFilter = (dir, name) -> Pattern.matches(fileWithPrefix, name); File[] files = downloadDir.listFiles(filenameFilter); @@ -217,4 +312,66 @@ public final class DownloadUtils { model.setDownloaded(exists); } + + public static void download(@NonNull final Context context, + @NonNull final FeedModel feedModel) { + download(context, feedModel, -1); + } + + public static void download(@NonNull final Context context, + @NonNull final FeedModel feedModel, + final int position) { + final File downloadDir = getDownloadDir(context, "@" + feedModel.getProfileModel().getUsername()); + if (downloadDir == null) return; + switch (feedModel.getItemType()) { + case MEDIA_TYPE_IMAGE: + case MEDIA_TYPE_VIDEO: { + final String url = feedModel.getDisplayUrl(); + final File file = getDownloadSaveFile(downloadDir, feedModel.getPostId(), url); + download(context, url, file.getAbsolutePath()); + break; + } + case MEDIA_TYPE_SLIDER: + final List sliderItems = feedModel.getSliderItems(); + final Map map = new HashMap<>(); + for (int i = 0; i < sliderItems.size(); i++) { + final PostChild child = sliderItems.get(i); + final String url = child.getDisplayUrl(); + final File file = getDownloadChildSaveFile(downloadDir, feedModel.getPostId(), i + 1, url); + map.put(url, file.getAbsolutePath()); + } + download(context, map); + break; + default: + } + + } + + private static void download(final Context context, + final String url, + final String filePath) { + if (context == null || url == null || filePath == null) return; + download(context, Collections.singletonMap(url, filePath)); + } + + private static void download(final Context context, final Map urlFilePathMap) { + final Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + final DownloadWorker.DownloadRequest request = DownloadWorker.DownloadRequest.builder() + .setUrlToFilePathMap(urlFilePathMap) + .build(); + final WorkRequest downloadWorkRequest = new OneTimeWorkRequest.Builder(DownloadWorker.class) + .setInputData( + new Data.Builder() + .putString(DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON, + new Gson().toJson(request)) + .build() + ) + .setConstraints(constraints) + .addTag("download") + .build(); + WorkManager.getInstance(context) + .enqueue(downloadWorkRequest); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java index 4343a103..7b5d1654 100644 --- a/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java +++ b/app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java @@ -131,6 +131,7 @@ public class NavigationExtensions { } final NavHostFragment navHostFragment = NavHostFragment.create(navGraphId); fragmentManager.beginTransaction() + .setReorderingAllowed(true) .add(containerId, navHostFragment, fragmentTag) .commitNow(); return navHostFragment; diff --git a/app/src/main/java/awais/instagrabber/utils/NumberUtils.java b/app/src/main/java/awais/instagrabber/utils/NumberUtils.java index 1c8db120..028567f2 100644 --- a/app/src/main/java/awais/instagrabber/utils/NumberUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/NumberUtils.java @@ -65,4 +65,8 @@ public final class NumberUtils { } return new Pair<>(tempWidth, tempHeight); } + + public static float roundFloat2Decimals(final float value) { + return ((int) ((value + (value >= 0 ? 1 : -1) * 0.005f) * 100)) / 100f; + } } diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index 5032cea3..cfcedd7b 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -30,6 +30,7 @@ import static awais.instagrabber.utils.Constants.MARK_AS_SEEN; import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME; +import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREV_INSTALL_VERSION; import static awais.instagrabber.utils.Constants.SHOW_QUICK_ACCESS_DIALOG; import static awais.instagrabber.utils.Constants.SKIPPED_VERSION; @@ -113,7 +114,7 @@ public final class SettingsHelper { @StringDef( {APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT, - DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME}) + DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, diff --git a/app/src/main/java/awais/instagrabber/utils/TextUtils.java b/app/src/main/java/awais/instagrabber/utils/TextUtils.java index b93a8304..fdaa6621 100644 --- a/app/src/main/java/awais/instagrabber/utils/TextUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/TextUtils.java @@ -7,6 +7,8 @@ import android.text.style.URLSpan; import androidx.annotation.NonNull; +import java.util.Locale; + import awais.instagrabber.customviews.CommentMentionClickSpan; public final class TextUtils { @@ -109,4 +111,22 @@ public final class TextUtils { } return "null".contentEquals(charSequence) || "".contentEquals(charSequence) || charSequence.length() < 1; } + + public static String millisToTimeString(final long millis) { + return millisToTimeString(millis, false); + } + + public static String millisToTimeString(final long millis, final boolean includeHoursAlways) { + final int sec = (int) (millis / 1000) % 60; + int min = (int) (millis / (1000 * 60)); + if (min >= 60) { + min = (int) ((millis / (1000 * 60)) % 60); + final int hr = (int) ((millis / (1000 * 60 * 60)) % 24); + return String.format(Locale.ENGLISH, "%02d:%02d:%02d", hr, min, sec); + } + if (includeHoursAlways) { + return String.format(Locale.ENGLISH, "%02d:%02d:%02d", 0, min, sec); + } + return String.format(Locale.ENGLISH, "%02d:%02d", min, sec); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index 6e3b9ca6..d9f11113 100755 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -47,6 +47,7 @@ public final class Utils { public static DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); public static SimpleDateFormat datetimeParser; public static SimpleCache simpleCache; + private static int statusBarHeight; public static int convertDpToPx(final float dp) { if (displayMetrics == null) @@ -146,4 +147,15 @@ public final class Utils { } return null; } + + public static int getStatusBarHeight(final Context context) { + if (statusBarHeight > 0) { + return statusBarHeight; + } + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + statusBarHeight = context.getResources().getDimensionPixelSize(resourceId); + } + return statusBarHeight; + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/FeedService.java b/app/src/main/java/awais/instagrabber/webservices/FeedService.java new file mode 100644 index 00000000..fca0b210 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/FeedService.java @@ -0,0 +1,307 @@ +package awais.instagrabber.webservices; + +import android.os.Handler; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.FeedRepository; +import awais.instagrabber.repositories.responses.FeedFetchResponse; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; + +public class FeedService extends BaseService { + private static final String TAG = "FeedService"; + private static final boolean loadFromMock = false; + + private final FeedRepository repository; + + private static FeedService instance; + + private FeedService() { + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://www.instagram.com") + .build(); + repository = retrofit.create(FeedRepository.class); + } + + public static FeedService getInstance() { + if (instance == null) { + instance = new FeedService(); + } + return instance; + } + + public void fetch(final int maxItemsToLoad, + final String cursor, + final ServiceCallback callback) { + if (loadFromMock) { + final Handler handler = new Handler(); + handler.postDelayed(() -> { + final ClassLoader classLoader = getClass().getClassLoader(); + if (classLoader == null) { + Log.e(TAG, "fetch: classLoader is null!"); + return; + } + try (InputStream resourceAsStream = classLoader.getResourceAsStream("feed_response.json"); + Reader in = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8)) { + final int bufferSize = 1024; + final char[] buffer = new char[bufferSize]; + final StringBuilder out = new StringBuilder(); + int charsRead; + while ((charsRead = in.read(buffer, 0, buffer.length)) > 0) { + out.append(buffer, 0, charsRead); + } + callback.onSuccess(parseResponseBody(out.toString())); + } catch (IOException | JSONException e) { + Log.e(TAG, "fetch: ", e); + } + }, 1000); + return; + } + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", "6b838488258d7a4820e48d209ef79eb1"); + queryMap.put("variables", "{" + + "\"fetch_media_item_count\":" + maxItemsToLoad + "," + + "\"has_threaded_comments\":true," + + "\"fetch_media_item_cursor\":\"" + (cursor == null ? "" : cursor) + "\"" + + "}"); + final Call request = repository.fetch(queryMap); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + // Log.d(TAG, "onResponse: body: " + response.body()); + final FeedFetchResponse feedFetchResponse = parseResponse(response); + if (callback != null) { + callback.onSuccess(feedFetchResponse); + } + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + if (callback != null) { + callback.onFailure(e); + } + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + + } + + @NonNull + private FeedFetchResponse parseResponse(@NonNull final Response response) throws JSONException { + if (TextUtils.isEmpty(response.body())) { + Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); + return new FeedFetchResponse(Collections.emptyList(), false, null); + } + return parseResponseBody(response.body()); + } + + @NonNull + private FeedFetchResponse parseResponseBody(@NonNull final String body) + throws JSONException { + final List feedModels = new ArrayList<>(); + final JSONObject timelineFeed = new JSONObject(body) + .getJSONObject("data") + .getJSONObject(Constants.EXTRAS_USER) + .getJSONObject("edge_web_feed_timeline"); + final String endCursor; + final boolean hasNextPage; + + final JSONObject pageInfo = timelineFeed.getJSONObject("page_info"); + if (pageInfo.has("has_next_page")) { + hasNextPage = pageInfo.getBoolean("has_next_page"); + endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null; + } else { + hasNextPage = false; + endCursor = null; + } + + final JSONArray feedItems = timelineFeed.getJSONArray("edges"); + + for (int i = 0; i < feedItems.length(); ++i) { + final JSONObject feedItem = feedItems.getJSONObject(i).getJSONObject("node"); + final String mediaType = feedItem.optString("__typename"); + if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType)) + continue; + + final boolean isVideo = feedItem.optBoolean("is_video"); + final long videoViews = feedItem.optLong("video_view_count", 0); + + final String displayUrl = feedItem.optString("display_url"); + if (TextUtils.isEmpty(displayUrl)) continue; + final String resourceUrl; + + if (isVideo) { + resourceUrl = feedItem.getString("video_url"); + } else { + resourceUrl = feedItem.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(feedItem) : displayUrl; + } + + ProfileModel profileModel = null; + if (feedItem.has("owner")) { + final JSONObject owner = feedItem.getJSONObject("owner"); + profileModel = new ProfileModel( + owner.optBoolean("is_private"), + false, // if you can see it then you def follow + owner.optBoolean("is_verified"), + owner.getString(Constants.EXTRAS_ID), + owner.getString(Constants.EXTRAS_USERNAME), + owner.optString("full_name"), + null, + null, + owner.getString("profile_pic_url"), + null, + 0, + 0, + 0, + false, + false, + false, + false); + } + JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment"); + final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0; + tempJsonObject = feedItem.optJSONObject("edge_media_preview_like"); + final long likesCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0; + tempJsonObject = feedItem.optJSONObject("edge_media_to_caption"); + final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null; + String captionText = null; + if (captions != null && captions.length() > 0) { + if ((tempJsonObject = captions.optJSONObject(0)) != null && + (tempJsonObject = tempJsonObject.optJSONObject("node")) != null) { + captionText = tempJsonObject.getString("text"); + } + } + final JSONObject location = feedItem.optJSONObject("location"); + // Log.d(TAG, "location: " + (location == null ? null : location.toString())); + String locationId = null; + String locationName = null; + if (location != null) { + locationName = location.optString("name"); + if (location.has("id")) { + locationId = location.getString("id"); + } else if (location.has("pk")) { + locationId = location.getString("pk"); + } + // Log.d(TAG, "locationId: " + locationId); + } + int height = 0; + int width = 0; + final JSONObject dimensions = feedItem.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = feedItem.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setProfileModel(profileModel) + .setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setViewCount(videoViews) + .setPostId(feedItem.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(resourceUrl) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl) + .setShortCode(feedItem.getString(Constants.EXTRAS_SHORTCODE)) + .setPostCaption(captionText) + .setCommentsCount(commentsCount) + .setTimestamp(feedItem.optLong("taken_at_timestamp", -1)) + .setLiked(feedItem.getBoolean("viewer_has_liked")) + .setBookmarked(feedItem.getBoolean("viewer_has_saved")) + .setLikesCount(likesCount) + .setLocationName(locationName) + .setLocationId(locationId) + .setImageHeight(height) + .setImageWidth(width); + + final boolean isSlider = "GraphSidecar".equals(mediaType) && feedItem.has("edge_sidecar_to_children"); + + if (isSlider) { + feedModelBuilder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER); + final JSONObject sidecar = feedItem.optJSONObject("edge_sidecar_to_children"); + if (sidecar != null) { + final JSONArray children = sidecar.optJSONArray("edges"); + if (children != null) { + final List sliderItems = getSliderItems(children); + feedModelBuilder.setSliderItems(sliderItems); + } + } + } + final FeedModel feedModel = feedModelBuilder.build(); + feedModels.add(feedModel); + } + return new FeedFetchResponse(feedModels, hasNextPage, endCursor); + } + + @NonNull + private List getSliderItems(final JSONArray children) throws JSONException { + final List sliderItems = new ArrayList<>(); + for (int j = 0; j < children.length(); ++j) { + final JSONObject childNode = children.optJSONObject(j).getJSONObject("node"); + final boolean isChildVideo = childNode.optBoolean("is_video"); + int height = 0; + int width = 0; + final JSONObject dimensions = childNode.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = childNode.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final PostChild sliderItem = new PostChild.Builder() + .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setPostId(childNode.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(isChildVideo ? childNode.getString("video_url") + : childNode.getString("display_url")) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl + : childNode.getString("display_url")) + .setVideoViews(childNode.optLong("video_view_count", 0)) + .setHeight(height) + .setWidth(width) + .build(); + // Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem); + sliderItems.add(sliderItem); + } + return sliderItems; + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index 7830adf0..6eeb53af 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -1,5 +1,6 @@ package awais.instagrabber.webservices; +import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; @@ -8,6 +9,11 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -30,6 +36,7 @@ import retrofit2.Retrofit; public class StoriesService extends BaseService { private static final String TAG = "StoriesService"; + private static final boolean loadFromMock = false; private final StoriesRepository repository; @@ -50,6 +57,30 @@ public class StoriesService extends BaseService { } public void getFeedStories(final ServiceCallback> callback) { + if (loadFromMock) { + final Handler handler = new Handler(); + handler.postDelayed(() -> { + final ClassLoader classLoader = getClass().getClassLoader(); + if (classLoader == null) { + Log.e(TAG, "getFeedStories: classLoader is null!"); + return; + } + try (InputStream resourceAsStream = classLoader.getResourceAsStream("stories_response.json"); + Reader in = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8)) { + final int bufferSize = 1024; + final char[] buffer = new char[bufferSize]; + final StringBuilder out = new StringBuilder(); + int charsRead; + while ((charsRead = in.read(buffer, 0, buffer.length)) > 0) { + out.append(buffer, 0, charsRead); + } + parseStoriesBody(out.toString(), callback); + } catch (IOException e) { + Log.e(TAG, "getFeedStories: ", e); + } + }, 1000); + return; + } final Map queryMap = new HashMap<>(); queryMap.put("query_hash", "b7b84d884400bc5aa7cfe12ae843a091"); queryMap.put("variables", "{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}"); @@ -62,31 +93,7 @@ public class StoriesService extends BaseService { Log.e(TAG, "getFeedStories: body is empty"); return; } - try { - final List feedStoryModels = new ArrayList<>(); - final JSONArray feedStoriesReel = new JSONObject(body) - .getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER) - .getJSONObject("feed_reels_tray") - .getJSONObject("edge_reels_tray_to_reel") - .getJSONArray("edges"); - for (int i = 0; i < feedStoriesReel.length(); ++i) { - final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node"); - final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner"); - final ProfileModel profileModel = new ProfileModel(false, false, false, - user.getString("id"), - user.getString("username"), - null, null, null, - user.getString("profile_pic_url"), - null, 0, 0, 0, false, false, false, false); - final String id = node.getString("id"); - final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == node.getLong("latest_reel_media"); - feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead)); - } - callback.onSuccess(feedStoryModels); - } catch (JSONException e) { - Log.e(TAG, "Error parsing json", e); - } + parseStoriesBody(body, callback); } @Override @@ -96,6 +103,34 @@ public class StoriesService extends BaseService { }); } + private void parseStoriesBody(final String body, final ServiceCallback> callback) { + try { + final List feedStoryModels = new ArrayList<>(); + final JSONArray feedStoriesReel = new JSONObject(body) + .getJSONObject("data") + .getJSONObject(Constants.EXTRAS_USER) + .getJSONObject("feed_reels_tray") + .getJSONObject("edge_reels_tray_to_reel") + .getJSONArray("edges"); + for (int i = 0; i < feedStoriesReel.length(); ++i) { + final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node"); + final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner"); + final ProfileModel profileModel = new ProfileModel(false, false, false, + user.getString("id"), + user.getString("username"), + null, null, null, + user.getString("profile_pic_url"), + null, 0, 0, 0, false, false, false, false); + final String id = node.getString("id"); + final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == node.getLong("latest_reel_media"); + feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead)); + } + callback.onSuccess(feedStoryModels); + } catch (JSONException e) { + Log.e(TAG, "Error parsing json", e); + } + } + public void getUserStory(final String id, final String username, final boolean isLoc, @@ -138,12 +173,15 @@ public class StoriesService extends BaseService { data = media.getJSONObject(i); final boolean isVideo = data.has("video_duration"); final StoryModel model = new StoryModel(data.getString("id"), - data.getJSONObject("image_versions2").getJSONArray("candidates").getJSONObject(0).getString("url"), - isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, - data.optLong("taken_at", 0), - (isLoc || isHashtag) ? data.getJSONObject("user").getString("username") : localUsername, - data.getJSONObject("user").getString("pk"), - data.getBoolean("can_reply")); + data.getJSONObject("image_versions2").getJSONArray("candidates").getJSONObject(0) + .getString("url"), + isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, + data.optLong("taken_at", 0), + (isLoc || isHashtag) + ? data.getJSONObject("user").getString("username") + : localUsername, + data.getJSONObject("user").getString("pk"), + data.getBoolean("can_reply")); final JSONArray videoResources = data.optJSONArray("video_versions"); if (isVideo && videoResources != null) @@ -174,7 +212,8 @@ public class StoriesService extends BaseService { )); } if (data.has("story_questions")) { - final JSONObject tappableObject = data.getJSONArray("story_questions").getJSONObject(0).optJSONObject("question_sticker"); + final JSONObject tappableObject = data.getJSONArray("story_questions").getJSONObject(0) + .optJSONObject("question_sticker"); if (tappableObject != null && !tappableObject.getString("question_type").equals("music")) model.setQuestion(new QuestionModel( String.valueOf(tappableObject.getLong("question_id")), @@ -229,8 +268,7 @@ public class StoriesService extends BaseService { models.add(model); } callback.onSuccess(models); - } - else { + } else { callback.onSuccess(null); } } catch (JSONException e) { @@ -251,14 +289,11 @@ public class StoriesService extends BaseService { builder.append("https://i.instagram.com/api/v1/"); if (isLoc) { builder.append("locations/"); - } - else if (isHashtag) { + } else if (isHashtag) { builder.append("tags/"); - } - else if (highlight) { + } else if (highlight) { builder.append("feed/reels_media/?user_ids="); - } - else { + } else { builder.append("feed/user/"); } builder.append(userId); diff --git a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java new file mode 100644 index 00000000..5949f420 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java @@ -0,0 +1,382 @@ +package awais.instagrabber.workers; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadataRetriever; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.FileProvider; +import androidx.work.Data; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awaisomereport.LogCollector; + +import static awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID; +import static awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME; +import static awais.instagrabber.utils.Utils.logCollector; + +public class DownloadWorker extends Worker { + private static final String TAG = "DownloadWorker"; + private static final String PROGRESS = "PROGRESS"; + private static final String URL = "URL"; + private static final String DOWNLOAD_GROUP = "DOWNLOAD_GROUP"; + + public static final String KEY_DOWNLOAD_REQUEST_JSON = "download_request_json"; + public static final int DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE = 2020; + public static final int DELETE_IMAGE_REQUEST_CODE = 2030; + + private final NotificationManagerCompat notificationManager; + + public DownloadWorker(@NonNull final Context context, @NonNull final WorkerParameters workerParams) { + super(context, workerParams); + notificationManager = NotificationManagerCompat.from(context); + } + + @NonNull + @Override + public Result doWork() { + final String downloadRequestString = getInputData().getString(KEY_DOWNLOAD_REQUEST_JSON); + if (TextUtils.isEmpty(downloadRequestString)) { + return Result.failure(new Data.Builder() + .putString("error", "downloadRequest is empty or null") + .build()); + } + final DownloadRequest downloadRequest; + try { + downloadRequest = new Gson().fromJson(downloadRequestString, DownloadRequest.class); + } catch (JsonSyntaxException e) { + Log.e(TAG, "doWork", e); + return Result.failure(new Data.Builder() + .putString("error", e.getLocalizedMessage()) + .build()); + } + if (downloadRequest == null) { + return Result.failure(new Data.Builder() + .putString("error", "downloadRequest is null") + .build()); + } + final Map urlToFilePathMap = downloadRequest.getUrlToFilePathMap(); + download(urlToFilePathMap); + new Handler(Looper.getMainLooper()).postDelayed(() -> showSummary(urlToFilePathMap), 500); + return Result.success(); + } + + private void download(final Map urlToFilePathMap) { + final int notificationId = getNotificationId(); + final Set> entries = urlToFilePathMap.entrySet(); + int count = 1; + final int total = urlToFilePathMap.size(); + for (final Map.Entry urlAndFilePath : entries) { + final String url = urlAndFilePath.getKey(); + updateDownloadProgress(notificationId, count, total, 0); + download(notificationId, count, total, url, urlAndFilePath.getValue()); + count++; + } + } + + private int getNotificationId() { + return Math.abs(getId().hashCode()); + } + + private void download(final int notificationId, + final int position, + final int total, + final String url, + final String filePath) { + final File outFile = new File(filePath); + try { + final URLConnection urlConnection = new URL(url).openConnection(); + final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() : + urlConnection.getContentLength(); + float totalRead = 0; + try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream()); + final FileOutputStream fos = new FileOutputStream(outFile)) { + final byte[] buffer = new byte[0x2000]; + int count; + boolean deletedIPTC = false; + while ((count = bis.read(buffer, 0, 0x2000)) != -1) { + totalRead = totalRead + count; + if (!deletedIPTC) { + int iptcStart = -1; + int fbmdStart = -1; + int fbmdBytesLen = -1; + for (int i = 0; i < buffer.length; ++i) { + if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xED) + iptcStart = i; + else if (buffer[i] == (byte) 'F' && buffer[i + 1] == (byte) 'B' + && buffer[i + 2] == (byte) 'M' && buffer[i + 3] == (byte) 'D') { + fbmdStart = i; + fbmdBytesLen = buffer[i - 10] << 24 | (buffer[i - 9] & 0xFF) << 16 | + (buffer[i - 8] & 0xFF) << 8 | (buffer[i - 7] & 0xFF) | + (buffer[i - 6] & 0xFF); + break; + } + } + if (iptcStart != -1 && fbmdStart != -1 && fbmdBytesLen != -1) { + final int fbmdDataLen = (iptcStart + (fbmdStart - iptcStart) + (fbmdBytesLen - iptcStart)) - 4; + fos.write(buffer, 0, iptcStart); + fos.write(buffer, fbmdDataLen + iptcStart, count - fbmdDataLen - iptcStart); + // setProgressAsync(new Data.Builder().putString(URL, url) + // .putFloat(PROGRESS, totalRead * 100f / fileSize) + // .build()); + updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize); + deletedIPTC = true; + continue; + } + } + fos.write(buffer, 0, count); + // setProgressAsync(new Data.Builder().putString(URL, url) + // .putFloat(PROGRESS, totalRead * 100f / fileSize) + // .build()); + updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize); + } + fos.flush(); + } + } catch (final Exception e) { + Log.e(TAG, "Error while downloading: " + url, e); + } + updateDownloadProgress(notificationId, position, total, 100); + } + + // private void showCompleteNotification(final String url) { + // final Context context = getApplicationContext(); + // final Notification notification = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID) + // .setCategory(NotificationCompat.CATEGORY_STATUS) + // .setSmallIcon(R.drawable.ic_download) + // .setAutoCancel(false) + // .setOnlyAlertOnce(true) + // .setContentTitle(context.getString(R.string.downloader_complete)) + // .setGroup(DOWNLOAD_GROUP) + // .build(); + // final int id = Math.abs(url.hashCode()); + // Log.d(TAG, "showCompleteNotification: cancelling: " + id); + // notificationManager.cancel(id); + // // WorkManager.getInstance(getApplicationContext()). + // notificationManager.notify(id + 1, notification); + // } + + private void updateDownloadProgress(final int notificationId, + final int position, + final int total, + final float percent) { + final Notification notification = createProgressNotification(position, total, percent); + try { + setForegroundAsync(new ForegroundInfo(notificationId, notification)).get(); + } catch (ExecutionException | InterruptedException e) { + Log.e(TAG, "updateDownloadProgress", e); + } + } + + private Notification createProgressNotification(final int position, final int total, final float percent) { + final Context context = getApplicationContext(); + boolean ongoing = true; + int totalPercent; + if (position == total && percent == 100) { + ongoing = false; + totalPercent = 100; + } else { + totalPercent = (int) ((100f * (position - 1) / total) + (1f / total) * (percent)); + } + // Log.d(TAG, "createProgressNotification: position: " + position + // + ", total: " + total + // + ", percent: " + percent + // + ", totalPercent: " + totalPercent); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(ongoing) + .setProgress(100, totalPercent, totalPercent < 0) + .setAutoCancel(false) + .setOnlyAlertOnce(true) + .setContentTitle(context.getString(R.string.downloader_downloading_post)); + if (total > 1) { + builder.setContentText(context.getString(R.string.downloader_downloading_child, position, total)); + } + return builder.build(); + } + + private void showSummary(final Map urlToFilePathMap) { + final Context context = getApplicationContext(); + final Collection filePaths = urlToFilePathMap.values(); + final List notifications = filePaths + .stream() + .map(filePath -> { + final File file = new File(filePath); + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null); + final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file); + final ContentResolver contentResolver = context.getContentResolver(); + Bitmap bitmap = null; + if (Utils.isImage(uri, contentResolver)) { + try (final InputStream inputStream = contentResolver.openInputStream(uri)) { + bitmap = BitmapFactory.decodeStream(inputStream); + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_1"); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } + if (bitmap == null) { + final MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + try { + retriever.setDataSource(context, uri); + } catch (final Exception e) { + retriever.setDataSource(file.getAbsolutePath()); + } + bitmap = retriever.getFrameAtTime(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + try { + retriever.close(); + } catch (final Exception e) { + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_2"); + } + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + if (logCollector != null) + logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_3"); + } + } + final String downloadComplete = context.getString(R.string.downloader_complete); + final Intent intent = new Intent(Intent.ACTION_VIEW, uri) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_FROM_BACKGROUND + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, uri); + final PendingIntent pendingIntent = PendingIntent.getActivity( + context, + DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT + ); + final Intent deleteIntent = new Intent(getApplicationContext(), MainActivity.class) + .setAction(Constants.ACTION_SHOW_ACTIVITY) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + final PendingIntent deleteItemIntent = PendingIntent + .getActivity(getApplicationContext(), DELETE_IMAGE_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_download) + .setContentText(null) + .setContentTitle(downloadComplete) + .setWhen(System.currentTimeMillis()) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setGroup(NOTIF_GROUP_NAME + "_" + getId()) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent(pendingIntent) + .addAction(R.drawable.ic_delete, context.getString(R.string.delete), deleteItemIntent); + if (bitmap != null) { + builder.setLargeIcon(bitmap) + .setStyle(new NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null)) + .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); + } + return builder; + }) + .collect(Collectors.toList()); + Notification summaryNotification = null; + if (urlToFilePathMap.size() != 1) { + final String text = "Downloaded " + urlToFilePathMap.size() + " items"; + summaryNotification = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setContentTitle("Downloaded") + .setContentText(text) + .setSmallIcon(R.drawable.ic_download) + .setStyle(new NotificationCompat.InboxStyle().setSummaryText(text)) + .setGroup(NOTIF_GROUP_NAME + "_" + getId()) + .setGroupSummary(true) + .build(); + } + int count = 1; + for (final NotificationCompat.Builder builder : notifications) { + // only make sound and vibrate for the last notification + if (count != notifications.size()) { + builder.setSound(null) + .setVibrate(null); + } + notificationManager.notify(getNotificationId() + count, builder.build()); + count++; + } + if (summaryNotification != null) { + notificationManager.notify(getNotificationId() + count, summaryNotification); + } + + } + + public static class DownloadRequest { + private final Map urlToFilePathMap; + + public static class Builder { + private Map urlToFilePathMap; + + public Builder setUrlToFilePathMap(final Map urlToFilePathMap) { + this.urlToFilePathMap = urlToFilePathMap; + return this; + } + + public Builder addUrl(@NonNull final String url, @NonNull final String filePath) { + if (urlToFilePathMap == null) { + urlToFilePathMap = new HashMap<>(); + } + urlToFilePathMap.put(url, filePath); + return this; + } + + public DownloadRequest build() { + return new DownloadRequest(urlToFilePathMap); + } + } + + public static Builder builder() { + return new Builder(); + } + + private DownloadRequest(final Map urlToFilePathMap) { + this.urlToFilePathMap = urlToFilePathMap; + } + + public Map getUrlToFilePathMap() { + return urlToFilePathMap; + } + } +} diff --git a/app/src/main/res/anim/dialog_anim_in.xml b/app/src/main/res/anim/dialog_anim_in.xml new file mode 100644 index 00000000..1579ba6d --- /dev/null +++ b/app/src/main/res/anim/dialog_anim_in.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/anim/dialog_anim_out.xml b/app/src/main/res/anim/dialog_anim_out.xml new file mode 100644 index 00000000..a6e9683b --- /dev/null +++ b/app/src/main/res/anim/dialog_anim_out.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/background_grey_ripple.xml b/app/src/main/res/drawable/background_grey_ripple.xml new file mode 100644 index 00000000..1e1519b6 --- /dev/null +++ b/app/src/main/res/drawable/background_grey_ripple.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_border_style_flipped_24.xml b/app/src/main/res/drawable/ic_border_style_flipped_24.xml new file mode 100644 index 00000000..34053277 --- /dev/null +++ b/app/src/main/res/drawable/ic_border_style_flipped_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_checkbox_multiple_blank.xml b/app/src/main/res/drawable/ic_checkbox_multiple_blank.xml new file mode 100644 index 00000000..7356a61f --- /dev/null +++ b/app/src/main/res/drawable/ic_checkbox_multiple_blank.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_checkbox_multiple_blank_stroke.xml b/app/src/main/res/drawable/ic_checkbox_multiple_blank_stroke.xml new file mode 100644 index 00000000..7e55407b --- /dev/null +++ b/app/src/main/res/drawable/ic_checkbox_multiple_blank_stroke.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_class_24.xml b/app/src/main/res/drawable/ic_class_24.xml new file mode 100644 index 00000000..4a4cbeae --- /dev/null +++ b/app/src/main/res/drawable/ic_class_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard_24.xml b/app/src/main/res/drawable/ic_dashboard_24.xml new file mode 100644 index 00000000..a882fd83 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_forward_5_24.xml b/app/src/main/res/drawable/ic_forward_5_24.xml new file mode 100644 index 00000000..4553d899 --- /dev/null +++ b/app/src/main/res/drawable/ic_forward_5_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notes_24.xml b/app/src/main/res/drawable/ic_notes_24.xml new file mode 100644 index 00000000..30d74859 --- /dev/null +++ b/app/src/main/res/drawable/ic_notes_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_24.xml b/app/src/main/res/drawable/ic_pause_24.xml new file mode 100644 index 00000000..13d6d2ec --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow_24.xml b/app/src/main/res/drawable/ic_play_arrow_24.xml new file mode 100644 index 00000000..13c137a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_circle_outline_24.xml b/app/src/main/res/drawable/ic_play_circle_outline_24.xml new file mode 100644 index 00000000..969804a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_circle_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile_24.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/res/drawable/ic_profile.xml rename to app/src/main/res/drawable/ic_profile_24.xml diff --git a/app/src/main/res/drawable/ic_profile_40.xml b/app/src/main/res/drawable/ic_profile_40.xml new file mode 100644 index 00000000..4dbe4c4d --- /dev/null +++ b/app/src/main/res/drawable/ic_profile_40.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_profile_48.xml b/app/src/main/res/drawable/ic_profile_48.xml new file mode 100644 index 00000000..a7280830 --- /dev/null +++ b/app/src/main/res/drawable/ic_profile_48.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_replay_5_24.xml b/app/src/main/res/drawable/ic_replay_5_24.xml new file mode 100644 index 00000000..b7312113 --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_5_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_rounded_corner_24.xml b/app/src/main/res/drawable/ic_rounded_corner_24.xml new file mode 100644 index 00000000..a29f14af --- /dev/null +++ b/app/src/main/res/drawable/ic_rounded_corner_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_agenda_24.xml b/app/src/main/res/drawable/ic_view_agenda_24.xml new file mode 100644 index 00000000..238ff4d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_agenda_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_grid_24.xml b/app/src/main/res/drawable/ic_view_grid_24.xml new file mode 100644 index 00000000..35160ee2 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_grid_24.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/popup_background_exoplayer.xml b/app/src/main/res/drawable/popup_background_exoplayer.xml new file mode 100644 index 00000000..ece4d1b9 --- /dev/null +++ b/app/src/main/res/drawable/popup_background_exoplayer.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounder_corner_bg.xml b/app/src/main/res/drawable/rounder_corner_bg.xml new file mode 100644 index 00000000..d87ca066 --- /dev/null +++ b/app/src/main/res/drawable/rounder_corner_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounder_corner_semi_black_bg.xml b/app/src/main/res/drawable/rounder_corner_semi_black_bg.xml index 67e019e3..1fe24ca8 100644 --- a/app/src/main/res/drawable/rounder_corner_semi_black_bg.xml +++ b/app/src/main/res/drawable/rounder_corner_semi_black_bg.xml @@ -1,11 +1,12 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_oval_light.xml b/app/src/main/res/drawable/shape_oval_light.xml new file mode 100644 index 00000000..522eb166 --- /dev/null +++ b/app/src/main/res/drawable/shape_oval_light.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_post_layout_preferences.xml b/app/src/main/res/layout/dialog_post_layout_preferences.xml new file mode 100644 index 00000000..4679e632 --- /dev/null +++ b/app/src/main/res/layout/dialog_post_layout_preferences.xml @@ -0,0 +1,252 @@ + + + + + + + + + + +