From da98cff507d440159edb9b6711222beba8e3578a Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Tue, 25 Aug 2020 00:05:54 +0900 Subject: [PATCH] Update Feed view, check description Changes: 1. Separate out view holders for feed post types. 2. Improve performance for binding. 4. Initiating migration from Glide to Fresco, as performance of Fresco is way better. 3. Prefetch feed view thumbnails to get aspect ratio, to improve layout performance. 4. If auto play is off, the videos in feed are not loaded until play is pressed. 5. Lots of optimizations here and there. --- app/build.gradle | 6 +- .../instagrabber/InstaGrabberApplication.java | 7 +- .../java/awais/instagrabber/MainHelper.java | 462 +++++++---- .../instagrabber/activities/MainActivity.java | 17 +- .../instagrabber/activities/PostViewer.java | 122 ++- .../activities/ProfileViewer.java | 201 +++-- .../instagrabber/adapters/FeedAdapter.java | 517 ++----------- .../adapters/FeedStoriesAdapter.java | 6 +- .../viewholder/FeedItemViewHolder.java | 53 -- .../viewholder/HighlightViewHolder.java | 3 +- .../viewholder/feed/FeedItemViewHolder.java | 153 ++++ .../viewholder/feed/FeedPhotoViewHolder.java | 87 +++ .../viewholder/feed/FeedSliderViewHolder.java | 339 +++++++++ .../viewholder/feed/FeedVideoViewHolder.java | 156 ++++ .../customviews/CircularImageView.java | 162 ++-- .../customviews/RamboTextView.java | 4 + .../AbstractAnimatedZoomableController.java | 170 +++++ .../drawee/AnimatedZoomableController.java | 105 +++ .../drawee/DefaultZoomableController.java | 720 ++++++++++++++++++ .../drawee/DoubleTapGestureListener.java | 84 ++ .../drawee/GestureListenerWrapper.java | 72 ++ .../drawee/MultiGestureListener.java | 156 ++++ .../drawee/MultiPointerGestureDetector.java | 286 +++++++ .../MultiZoomableControllerListener.java | 51 ++ .../drawee/TransformGestureDetector.java | 203 +++++ .../drawee/ZoomableController.java | 134 ++++ .../drawee/ZoomableDraweeView.java | 416 ++++++++++ .../ImageResizingControllerListener.java | 43 ++ .../PauseGlideOnFlingScrollListener.java | 45 ++ .../helpers/VideoAwareRecyclerScroller.java | 497 ++++++------ .../awais/instagrabber/models/FeedModel.java | 22 +- .../models/enums/MediaItemType.java | 31 +- .../java/awais/instagrabber/utils/Utils.java | 92 ++- .../drawable/rounder_corner_semi_black_bg.xml | 11 + app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/layout/item_feed.xml | 17 - app/src/main/res/layout/item_feed_bottom.xml | 11 +- app/src/main/res/layout/item_feed_photo.xml | 25 + app/src/main/res/layout/item_feed_slider.xml | 19 +- app/src/main/res/layout/item_feed_top.xml | 15 +- app/src/main/res/layout/item_feed_video.xml | 53 +- app/src/main/res/layout/layout_feed_view.xml | 2 +- .../main/res/layout/layout_profile_view.xml | 59 +- app/src/main/res/values/color.xml | 1 + 44 files changed, 4369 insertions(+), 1268 deletions(-) delete mode 100755 app/src/main/java/awais/instagrabber/adapters/viewholder/FeedItemViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/AbstractAnimatedZoomableController.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/AnimatedZoomableController.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/DoubleTapGestureListener.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/GestureListenerWrapper.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/MultiGestureListener.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/MultiPointerGestureDetector.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/TransformGestureDetector.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/ImageResizingControllerListener.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/helpers/PauseGlideOnFlingScrollListener.java create mode 100644 app/src/main/res/drawable/rounder_corner_semi_black_bg.xml delete mode 100755 app/src/main/res/layout/item_feed.xml create mode 100644 app/src/main/res/layout/item_feed_photo.xml diff --git a/app/build.gradle b/app/build.gradle index e5b0c4fa..c05bbec6 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,8 +39,8 @@ android { dependencies { implementation('androidx.appcompat:appcompat:1.3.0-alpha01@aar') { transitive true } implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation('com.google.android.material:material:1.3.0-alpha01@aar') { transitive true } - implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01') { transitive true } + implementation('com.google.android.material:material:1.3.0-alpha02@aar') { transitive true } + implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01') { transitive true } def nav_version = "2.3.0" implementation "androidx.navigation:navigation-fragment:$nav_version" @@ -52,4 +52,6 @@ dependencies { implementation('com.github.bumptech.glide:glide:4.11.0') { transitive true } implementation('com.github.chrisbanes:PhotoView:v2.0.0@aar') { transitive true } implementation('com.google.android.exoplayer:exoplayer:2.11.1@aar') { transitive true } + + implementation 'com.facebook.fresco:fresco:2.3.0' } diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java index 1f2b41b4..38c62547 100644 --- a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java +++ b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java @@ -7,6 +7,8 @@ import android.util.Log; import androidx.core.app.NotificationManagerCompat; import androidx.multidex.MultiDexApplication; +import com.facebook.drawee.backends.pipeline.Fresco; + import java.net.CookieHandler; import java.text.SimpleDateFormat; import java.util.UUID; @@ -34,12 +36,13 @@ public final class InstaGrabberApplication extends MultiDexApplication { @Override public void onCreate() { super.onCreate(); + Fresco.initialize(this); if (BuildConfig.DEBUG) { try { Class.forName("dalvik.system.CloseGuard") - .getMethod("setEnabled", boolean.class) - .invoke(null, true); + .getMethod("setEnabled", boolean.class) + .invoke(null, true); } catch (Exception e) { Log.e(TAG, "Error", e); } diff --git a/app/src/main/java/awais/instagrabber/MainHelper.java b/app/src/main/java/awais/instagrabber/MainHelper.java index cf213043..c5f6bef5 100755 --- a/app/src/main/java/awais/instagrabber/MainHelper.java +++ b/app/src/main/java/awais/instagrabber/MainHelper.java @@ -1,10 +1,10 @@ package awais.instagrabber; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; @@ -32,14 +32,25 @@ import androidx.core.widget.ImageViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestManager; +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 com.google.android.exoplayer2.SimpleExoPlayer; import java.io.DataOutputStream; -import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import awais.instagrabber.activities.CommentsViewer; import awais.instagrabber.activities.FollowViewer; import awais.instagrabber.activities.MainActivity; import awais.instagrabber.activities.PostViewer; @@ -49,6 +60,7 @@ import awais.instagrabber.adapters.DiscoverAdapter; import awais.instagrabber.adapters.FeedAdapter; import awais.instagrabber.adapters.FeedStoriesAdapter; import awais.instagrabber.adapters.PostsAdapter; +import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder; import awais.instagrabber.asyncs.DiscoverFetcher; import awais.instagrabber.asyncs.FeedFetcher; import awais.instagrabber.asyncs.FeedStoriesFetcher; @@ -63,6 +75,7 @@ import awais.instagrabber.customviews.MouseDrawer; import awais.instagrabber.customviews.RamboTextView; import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; +import awais.instagrabber.customviews.helpers.PauseGlideOnFlingScrollListener; import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.customviews.helpers.VideoAwareRecyclerScroller; import awais.instagrabber.interfaces.FetchListener; @@ -74,8 +87,12 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.IntentModel; 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.IntentModelType; import awais.instagrabber.models.enums.ItemGetType; +import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.Utils; @@ -87,13 +104,18 @@ import static awais.instagrabber.utils.Utils.logCollector; import static awais.instagrabber.utils.Utils.settingsHelper; public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { + private static final String TAG = "MainHelper"; + private static final double MAX_VIDEO_HEIGHT = 0.9 * Utils.displayMetrics.heightPixels; + private static final int RESIZED_VIDEO_HEIGHT = (int) (0.8 * Utils.displayMetrics.heightPixels); + private static AsyncTask currentlyExecuting; private AsyncTask prevStoriesFetcher; - private final boolean autoloadPosts; private FeedStoryModel[] stories; private boolean hasNextPage = false, feedHasNextPage = false, discoverHasMore = false; private String endCursor = null, feedEndCursor = null, discoverEndMaxId = null, topic = null, rankToken = null; private String[] topicIds = null; + + private final boolean autoloadPosts; private final FetchListener postsFetchListener = new FetchListener() { @Override public void onResult(final PostModel[] result) { @@ -112,7 +134,8 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.toolbar.toolbar.setTitle(mainActivity.userQuery); else if (isLocation) mainActivity.mainBinding.toolbar.toolbar.setTitle(mainActivity.locationModel.getName()); - else mainActivity.mainBinding.toolbar.toolbar.setTitle("@"+ mainActivity.profileModel.getUsername()); + else + mainActivity.mainBinding.toolbar.toolbar.setTitle("@" + mainActivity.profileModel.getUsername()); final PostModel model = result[result.length - 1]; if (model != null) { @@ -121,7 +144,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (autoloadPosts && hasNextPage) currentlyExecuting = new PostsFetcher( mainActivity.profileModel != null ? mainActivity.profileModel.getId() - : (mainActivity.hashtagModel != null ? mainActivity.userQuery : mainActivity.locationModel.getId()), endCursor, this) + : (mainActivity.hashtagModel != null ? mainActivity.userQuery : mainActivity.locationModel.getId()), endCursor, this) .setUsername((isLocation || isHashtag) ? null : mainActivity.profileModel.getUsername()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); else { @@ -129,8 +152,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } model.setPageCursor(false, null); } - } - else { + } else { mainActivity.mainBinding.profileView.swipeRefreshLayout.setRefreshing(false); mainActivity.mainBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); mainActivity.mainBinding.profileView.privatePage2.setText(R.string.empty_acc); @@ -146,22 +168,80 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { @Override public void onResult(final FeedModel[] result) { - if (result != null) { - final int oldSize = mainActivity.feedItems.size(); - mainActivity.feedItems.addAll(Arrays.asList(result)); - feedAdapter.notifyItemRangeInserted(oldSize, result.length); + if (result == null) { + return; + } + final int oldSize = mainActivity.feedItems.size(); + final HashMap thumbToFeedMap = new HashMap<>(); + for (final FeedModel feedModel : result) { + thumbToFeedMap.put(feedModel.getThumbnailUrl(), feedModel); + } + final BaseDataSubscriber subscriber = new BaseDataSubscriber() { + int success = 0; + int failed = 0; - mainActivity.mainBinding.feedView.feedPosts.post(() -> mainActivity.mainBinding.feedView.feedPosts.setNestedScrollingEnabled(true)); + @Override + protected void onNewResultImpl(@NonNull final DataSource dataSource) { + // dataSource + final Map extras = dataSource.getExtras(); + if (extras == null) { + return; + } + // Log.d(TAG, "extras: " + extras); + 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 = Utils.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 = Utils.getResultingWidth(RESIZED_VIDEO_HEIGHT, resultingHeight, requiredWidth); + } + feedModel.setImageWidth(requiredWidth); + feedModel.setImageHeight(resultingHeight); + success++; + updateAdapter(); + } - final PostModel feedPostModel = result[result.length - 1]; - if (feedPostModel != null) { - feedEndCursor = feedPostModel.getEndCursor(); - feedHasNextPage = feedPostModel.hasNextPage(); - feedPostModel.setPageCursor(false, null); + @Override + protected void onFailureImpl(@NonNull final DataSource dataSource) { + failed++; + updateAdapter(); } - } - mainActivity.mainBinding.feedView.feedSwipeRefreshLayout.setRefreshing(false); + public void updateAdapter() { + if (failed + success != result.length) return; + mainActivity.feedItems.addAll(Arrays.asList(result)); + feedAdapter.submitList(mainActivity.feedItems); + feedAdapter.notifyItemRangeInserted(oldSize, result.length); + + mainActivity.mainBinding.feedView.feedPosts.post(() -> mainActivity.mainBinding.feedView.feedPosts.setNestedScrollingEnabled(true)); + + final PostModel feedPostModel = result[result.length - 1]; + if (feedPostModel != null) { + feedEndCursor = feedPostModel.getEndCursor(); + feedHasNextPage = feedPostModel.hasNextPage(); + feedPostModel.setPageCursor(false, null); + } + mainActivity.mainBinding.feedView.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 discoverFetchListener = new FetchListener() { @@ -174,8 +254,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { public void onResult(final DiscoverItemModel[] result) { if (result == null || result.length == 0) { Toast.makeText(mainActivity, R.string.discover_empty, Toast.LENGTH_SHORT).show(); - } - else if (result != null) { + } else { final int oldSize = mainActivity.discoverItems.size(); mainActivity.discoverItems.addAll(Arrays.asList(result)); discoverAdapter.notifyItemRangeInserted(oldSize, result.length); @@ -201,7 +280,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { topicIds = result.getIds(); rankToken = result.getToken(); ArrayAdapter spinnerArrayAdapter = new ArrayAdapter( - mainActivity, android.R.layout.simple_spinner_dropdown_item, result.getNames() ); + mainActivity, android.R.layout.simple_spinner_dropdown_item, result.getNames()); mainActivity.mainBinding.discoverType.setAdapter(spinnerArrayAdapter); } } @@ -225,7 +304,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { @Override public void onClick(final RamboTextView view, final String text, final boolean isHashtag) { new AlertDialog.Builder(mainActivity).setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search) - .setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> { + .setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> { if (MainActivity.scanHack != null) MainActivity.scanHack.onResult(text); }).show(); } @@ -240,29 +319,31 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { new iStoryStatusFetcher(feedStoryModel.getStoryMediaId(), null, false, false, false, false, result -> { if (result != null && result.length > 0) mainActivity.startActivity(new Intent(mainActivity, StoryViewer.class) - .putExtra(Constants.EXTRAS_STORIES, result) - .putExtra(Constants.EXTRAS_USERNAME, feedStoryModel.getProfileModel().getUsername()) - .putExtra(Constants.FEED, stories) - .putExtra(Constants.FEED_ORDER, index) + .putExtra(Constants.EXTRAS_STORIES, result) + .putExtra(Constants.EXTRAS_USERNAME, feedStoryModel.getProfileModel().getUsername()) + .putExtra(Constants.FEED, stories) + .putExtra(Constants.FEED_ORDER, index) ); - else Toast.makeText(mainActivity, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + else + Toast.makeText(mainActivity, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } }); - @NonNull - private final MainActivity mainActivity; + private MainActivity mainActivity; private Resources resources; private final View collapsingToolbar; private final RecyclerLazyLoader lazyLoader; - private boolean isHashtag, isUser, isLocation; + private boolean isHashtag; + private boolean isLocation; private PostsAdapter postsAdapter; private FeedAdapter feedAdapter; private RecyclerLazyLoader feedLazyLoader, discoverLazyLoader; private DiscoverAdapter discoverAdapter; public SimpleExoPlayer currentFeedPlayer; // hack for remix drawer layout private String cookie = settingsHelper.getString(Constants.COOKIE); - public boolean isLoggedIn = !Utils.isEmpty(cookie) && Utils.getUserIdFromCookie(cookie) != null; + private boolean isLoggedIn; + private RequestManager glide; public MainHelper(@NonNull final MainActivity mainActivity) { stopCurrentExecutor(); @@ -270,6 +351,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { this.mainActivity = mainActivity; this.resources = mainActivity.getResources(); this.autoloadPosts = settingsHelper.getBoolean(AUTOLOAD_POSTS); + glide = Glide.with(mainActivity); mainActivity.mainBinding.profileView.swipeRefreshLayout.setOnRefreshListener(this); mainActivity.mainBinding.profileView.mainUrl.setMovementMethod(new LinkMovementMethod()); @@ -280,7 +362,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { final ImageView iconDiscover = (ImageView) iconSlider.getChildAt(2); final boolean isBottomToolbar = settingsHelper.getBoolean(BOTTOM_TOOLBAR); - isLoggedIn = !Utils.isEmpty(cookie); + isLoggedIn = !Utils.isEmpty(cookie) && Utils.getUserIdFromCookie(cookie) != null; if (!isLoggedIn) { mainActivity.mainBinding.drawerLayout.removeView(mainActivity.mainBinding.feedView.feedLayout); mainActivity.mainBinding.drawerLayout.removeView(mainActivity.mainBinding.discoverLayout); @@ -474,16 +556,101 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.mainPosts.addOnScrollListener(lazyLoader); } + private final View.OnClickListener clickListener = v -> { + if (mainActivity == null) { + return; + } + final Object tag = v.getTag(); + final Context context = v.getContext(); + + if (tag instanceof FeedModel) { + 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(); + + } else { + final int id = v.getId(); + switch (id) { + case R.id.btnComments: + mainActivity.startActivityForResult(new Intent(mainActivity, CommentsViewer.class) + .putExtra(Constants.EXTRAS_SHORTCODE, feedModel.getShortCode()) + .putExtra(Constants.EXTRAS_POST, feedModel.getPostId()) + .putExtra(Constants.EXTRAS_USER, feedModel.getProfileModel().getId()), 6969); + break; + + case R.id.viewStoryPost: + mainActivity.startActivity(new Intent(mainActivity, PostViewer.class) + .putExtra(Constants.EXTRAS_INDEX, feedModel.getPosition()) + .putExtra(Constants.EXTRAS_POST, new PostModel(feedModel.getShortCode(), false)) + .putExtra(Constants.EXTRAS_TYPE, ItemGetType.FEED_ITEMS)); + break; + + case R.id.btnDownload: + ProfileModel profileModel = feedModel.getProfileModel(); + final String username = profileModel != null ? profileModel.getUsername() : null; + + final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); + + if (feedModel.getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1) + Utils.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) + Utils.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); + break; + } + } + } + }; + private void setupFeed() { mainActivity.mainBinding.feedView.feedStories.setLayoutManager(new LinearLayoutManager(mainActivity, LinearLayoutManager.HORIZONTAL, false)); mainActivity.mainBinding.feedView.feedStories.setAdapter(feedStoriesAdapter); refreshFeedStories(); final LinearLayoutManager layoutManager = new LinearLayoutManager(mainActivity); + mainActivity.mainBinding.feedView.feedPosts.setHasFixedSize(true); mainActivity.mainBinding.feedView.feedPosts.setLayoutManager(layoutManager); - mainActivity.mainBinding.feedView.feedPosts.setAdapter(feedAdapter = new FeedAdapter(mainActivity, mainActivity.feedItems, (view, text, isHashtag) -> + mainActivity.mainBinding.feedView.feedPosts.setAdapter(feedAdapter = new FeedAdapter(glide, clickListener, (view, text, isHashtag) -> new AlertDialog.Builder(mainActivity).setMessage(isHashtag ? R.string.comment_view_mention_hash_search : R.string.comment_view_mention_user_search) - .setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> { + .setTitle(text).setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, (dialog, which) -> { if (MainActivity.scanHack != null) { mainActivity.mainBinding.drawerLayout.closeDrawers(); MainActivity.scanHack.onResult(text); @@ -507,8 +674,11 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } })); - mainActivity.mainBinding.feedView.feedPosts.addOnScrollListener(new VideoAwareRecyclerScroller(mainActivity, mainActivity.feedItems, - (itemPos, player) -> currentFeedPlayer = player)); + final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); + if (shouldAutoPlay) { + mainActivity.mainBinding.feedView.feedPosts.addOnScrollListener(new VideoAwareRecyclerScroller()); + } + mainActivity.mainBinding.feedView.feedPosts.addOnScrollListener(new PauseGlideOnFlingScrollListener(glide)); new FeedFetcher(feedFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -623,7 +793,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } else { mainActivity.addToStack(); mainActivity.userQuery = modelType == IntentModelType.HASHTAG ? ('#' + modelText) : - (modelType == IntentModelType.LOCATION ? modelText : ('@'+modelText)); + (modelType == IntentModelType.LOCATION ? modelText : ('@' + modelText)); onRefresh(); } } @@ -646,9 +816,9 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.appBarLayout.setExpanded(true, true); mainActivity.mainBinding.profileView.privatePage.setVisibility(View.GONE); mainActivity.mainBinding.profileView.privatePage2.setTextSize(28); - mainActivity.mainBinding.profileView.mainProfileImage.setImageBitmap(null); - mainActivity.mainBinding.profileView.mainHashtagImage.setImageBitmap(null); - mainActivity.mainBinding.profileView.mainLocationImage.setImageBitmap(null); + // mainActivity.mainBinding.profileView.mainProfileImage.setImageBitmap(null); + // mainActivity.mainBinding.profileView.mainHashtagImage.setImageBitmap(null); + // mainActivity.mainBinding.profileView.mainLocationImage.setImageBitmap(null); mainActivity.mainBinding.profileView.mainUrl.setText(null); mainActivity.mainBinding.profileView.locationUrl.setText(null); mainActivity.mainBinding.profileView.mainFullName.setText(null); @@ -702,7 +872,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } isHashtag = mainActivity.userQuery.charAt(0) == '#'; - isUser = mainActivity.userQuery.charAt(0) == '@'; + final boolean isUser = mainActivity.userQuery.charAt(0) == '@'; isLocation = mainActivity.userQuery.contains("/"); collapsingToolbar.setVisibility(isUser ? View.VISIBLE : View.GONE); @@ -731,15 +901,15 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (isLoggedIn) { new iStoryStatusFetcher(hashtagModel.getName(), null, false, true, false, false, result -> { mainActivity.storyModels = result; - if (result != null && result.length > 0) mainActivity.mainBinding.profileView.mainHashtagImage.setStoriesBorder(); + if (result != null && result.length > 0) + mainActivity.mainBinding.profileView.mainHashtagImage.setStoriesBorder(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - if (hashtagModel.getFollowing() == true) { + if (hashtagModel.getFollowing()) { mainActivity.mainBinding.profileView.btnFollowTag.setText(R.string.unfollow); mainActivity.mainBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_purple_background))); - } - else { + } else { mainActivity.mainBinding.profileView.btnFollowTag.setText(R.string.follow); mainActivity.mainBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_pink_background))); @@ -749,8 +919,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.btnFollowTag.setText(R.string.unfavorite_short); mainActivity.mainBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_purple_background))); - } - else { + } else { mainActivity.mainBinding.profileView.btnFollowTag.setText(R.string.favorite_short); mainActivity.mainBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_pink_background))); @@ -758,7 +927,8 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } mainActivity.mainBinding.profileView.mainHashtagImage.setEnabled(false); - new MyTask().execute(); + // new MyTask().execute(); + mainActivity.mainBinding.profileView.mainHashtagImage.setImageURI(hashtagModel.getSdProfilePic()); mainActivity.mainBinding.profileView.mainHashtagImage.setEnabled(true); final String postCount = String.valueOf(hashtagModel.getPostCount()); @@ -790,20 +960,21 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { final String profileId = profileModel.getId(); if (isLoggedIn || settingsHelper.getBoolean(Constants.STORIESIG)) { - new iStoryStatusFetcher(profileId, profileModel.getUsername(), false, false, - (!isLoggedIn && settingsHelper.getBoolean(Constants.STORIESIG)), false, - result -> { - mainActivity.storyModels = result; - if (result != null && result.length > 0) mainActivity.mainBinding.profileView.mainProfileImage.setStoriesBorder(); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - new HighlightsFetcher(profileId, (!isLoggedIn && settingsHelper.getBoolean(Constants.STORIESIG)), result -> { - if (result != null && result.length > 0) { - mainActivity.mainBinding.profileView.highlightsList.setVisibility(View.VISIBLE); - mainActivity.highlightsAdapter.setData(result); - } - else mainActivity.mainBinding.profileView.highlightsList.setVisibility(View.GONE); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + new iStoryStatusFetcher(profileId, profileModel.getUsername(), false, false, + (!isLoggedIn && settingsHelper.getBoolean(Constants.STORIESIG)), false, + result -> { + mainActivity.storyModels = result; + // if (result != null && result.length > 0) + mainActivity.mainBinding.profileView.mainProfileImage.setStoriesBorder(); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + new HighlightsFetcher(profileId, (!isLoggedIn && settingsHelper.getBoolean(Constants.STORIESIG)), result -> { + if (result != null && result.length > 0) { + mainActivity.mainBinding.profileView.highlightsList.setVisibility(View.VISIBLE); + mainActivity.highlightsAdapter.setData(result); + } else + mainActivity.mainBinding.profileView.highlightsList.setVisibility(View.GONE); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } if (isLoggedIn) { @@ -817,13 +988,11 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.btnFollow.setText(R.string.unfollow); mainActivity.mainBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_purple_background))); - } - else if (profileModel.getRequested() == true) { + } else if (profileModel.getRequested() == true) { mainActivity.mainBinding.profileView.btnFollow.setText(R.string.cancel); mainActivity.mainBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_purple_background))); - } - else { + } else { mainActivity.mainBinding.profileView.btnFollow.setText(R.string.follow); mainActivity.mainBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_pink_background))); @@ -833,8 +1002,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.btnRestrict.setText(R.string.unrestrict); mainActivity.mainBinding.profileView.btnRestrict.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_green_background))); - } - else { + } else { mainActivity.mainBinding.profileView.btnRestrict.setText(R.string.restrict); mainActivity.mainBinding.profileView.btnRestrict.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_orange_background))); @@ -865,8 +1033,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity, R.color.btn_red_background))); } } - } - else { + } else { mainActivity.mainBinding.profileView.btnTagged.setVisibility(View.VISIBLE); mainActivity.mainBinding.profileView.btnSaved.setVisibility(View.VISIBLE); mainActivity.mainBinding.profileView.btnLiked.setVisibility(View.VISIBLE); @@ -879,8 +1046,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.btnFollow.setText(R.string.unfavorite_short); mainActivity.mainBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_purple_background))); - } - else { + } else { mainActivity.mainBinding.profileView.btnFollow.setText(R.string.favorite_short); mainActivity.mainBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( mainActivity, R.color.btn_pink_background))); @@ -894,9 +1060,10 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } } - mainActivity.mainBinding.profileView.mainProfileImage.setEnabled(false); - new MyTask().execute(); - mainActivity.mainBinding.profileView.mainProfileImage.setEnabled(true); + // mainActivity.mainBinding.profileView.mainProfileImage.setEnabled(false); + // new MyTask().execute(); + mainActivity.mainBinding.profileView.mainProfileImage.setImageURI(profileModel.getSdProfilePic()); + // mainActivity.mainBinding.profileView.mainProfileImage.setEnabled(true); final long followersCount = profileModel.getFollowersCount(); final long followingCount = profileModel.getFollowingCount(); @@ -966,12 +1133,11 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); mainActivity.mainBinding.profileView.privatePage2.setText(R.string.empty_acc); mainActivity.mainBinding.profileView.privatePage.setVisibility(View.VISIBLE); - } - else { + } else { mainActivity.mainBinding.profileView.swipeRefreshLayout.setRefreshing(true); mainActivity.mainBinding.profileView.mainPosts.setVisibility(View.VISIBLE); currentlyExecuting = new PostsFetcher(profileId, postsFetchListener).setUsername(profileModel.getUsername()) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } else { mainActivity.mainBinding.profileView.mainFollowers.setClickable(false); @@ -985,8 +1151,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { } } ).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - else if (isLocation) { + } else if (isLocation) { mainActivity.profileModel = null; mainActivity.hashtagModel = null; mainActivity.mainBinding.toolbar.toolbar.setTitle(mainActivity.userQuery); @@ -1008,13 +1173,15 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (isLoggedIn) { new iStoryStatusFetcher(profileId.split("/")[0], null, true, false, false, false, result -> { mainActivity.storyModels = result; - if (result != null && result.length > 0) mainActivity.mainBinding.profileView.mainLocationImage.setStoriesBorder(); + if (result != null && result.length > 0) + mainActivity.mainBinding.profileView.mainLocationImage.setStoriesBorder(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - mainActivity.mainBinding.profileView.mainLocationImage.setEnabled(false); - new MyTask().execute(); - mainActivity.mainBinding.profileView.mainLocationImage.setEnabled(true); + // mainActivity.mainBinding.profileView.mainLocationImage.setEnabled(false); + // new MyTask().execute(); + mainActivity.mainBinding.profileView.mainLocationImage.setImageURI(locationModel.getSdProfilePic()); + // mainActivity.mainBinding.profileView.mainLocationImage.setEnabled(true); final String postCount = String.valueOf(locationModel.getPostCount()); @@ -1031,8 +1198,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (Utils.isEmpty(biography)) { mainActivity.mainBinding.profileView.locationBiography.setVisibility(View.GONE); - } - else if (Utils.hasMentions(biography)) { + } else if (Utils.hasMentions(biography)) { mainActivity.mainBinding.profileView.locationBiography.setVisibility(View.VISIBLE); biography = Utils.getMentionText(biography); mainActivity.mainBinding.profileView.locationBiography.setText(biography, TextView.BufferType.SPANNABLE); @@ -1050,8 +1216,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { intent.setData(Uri.parse(locationModel.getGeo())); mainActivity.startActivity(intent); }); - } - else { + } else { mainActivity.mainBinding.profileView.btnMap.setVisibility(View.GONE); mainActivity.mainBinding.profileView.btnMap.setOnClickListener(null); } @@ -1061,7 +1226,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.locationUrl.setVisibility(View.GONE); } else if (!url.startsWith("http")) { mainActivity.mainBinding.profileView.locationUrl.setVisibility(View.VISIBLE); - mainActivity.mainBinding.profileView.locationUrl.setText(Utils.getSpannableUrl("http://"+url)); + mainActivity.mainBinding.profileView.locationUrl.setText(Utils.getSpannableUrl("http://" + url)); } else { mainActivity.mainBinding.profileView.locationUrl.setVisibility(View.VISIBLE); mainActivity.mainBinding.profileView.locationUrl.setText(Utils.getSpannableUrl(url)); @@ -1075,8 +1240,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { mainActivity.mainBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); mainActivity.mainBinding.profileView.privatePage2.setText(R.string.empty_acc); mainActivity.mainBinding.profileView.privatePage.setVisibility(View.VISIBLE); - } - else { + } else { mainActivity.mainBinding.profileView.swipeRefreshLayout.setRefreshing(true); mainActivity.mainBinding.profileView.mainPosts.setVisibility(View.VISIBLE); currentlyExecuting = new PostsFetcher(profileId, postsFetchListener) @@ -1102,14 +1266,12 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { private void toggleSelection(final PostModel postModel) { if (postModel != null && postsAdapter != null && mainActivity.selectedItems.size() >= 100) { Toast.makeText(mainActivity, R.string.downloader_too_many, Toast.LENGTH_SHORT); - } - else if (postModel != null && postsAdapter != null) { + } else if (postModel != null && postsAdapter != null) { if (postModel.isSelected()) mainActivity.selectedItems.remove(postModel); else if (mainActivity.selectedItems.size() >= 100) { Toast.makeText(mainActivity, R.string.downloader_too_many, Toast.LENGTH_SHORT); return; - } - else mainActivity.selectedItems.add(postModel); + } else mainActivity.selectedItems.add(postModel); postModel.setSelected(!postModel.isSelected()); notifyAdapter(postModel); } @@ -1120,10 +1282,10 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (postModel.getPosition() < 0) postsAdapter.notifyDataSetChanged(); else postsAdapter.notifyItemChanged(postModel.getPosition(), postModel); - if (mainActivity.downloadAction != null) mainActivity.downloadAction.setVisible(postsAdapter.isSelecting); + if (mainActivity.downloadAction != null) + mainActivity.downloadAction.setVisible(postsAdapter.isSelecting); } - /////////////////////////////////////////////////// private void toggleDiscoverSelection(final DiscoverItemModel itemModel) { if (itemModel != null && discoverAdapter != null) { if (itemModel.isSelected()) mainActivity.selectedDiscoverItems.remove(itemModel); @@ -1138,19 +1300,22 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { if (itemModel.getPosition() < 0) discoverAdapter.notifyDataSetChanged(); else discoverAdapter.notifyItemChanged(itemModel.getPosition(), itemModel); - if (mainActivity.downloadAction != null) mainActivity.downloadAction.setVisible(discoverAdapter.isSelecting); + if (mainActivity.downloadAction != null) + mainActivity.downloadAction.setVisible(discoverAdapter.isSelecting); } public boolean isSelectionCleared() { if (postsAdapter != null && postsAdapter.isSelecting) { - for (final PostModel postModel : mainActivity.selectedItems) postModel.setSelected(false); + for (final PostModel postModel : mainActivity.selectedItems) + postModel.setSelected(false); mainActivity.selectedItems.clear(); postsAdapter.isSelecting = false; postsAdapter.notifyDataSetChanged(); if (mainActivity.downloadAction != null) mainActivity.downloadAction.setVisible(false); return false; } else if (discoverAdapter != null && discoverAdapter.isSelecting) { - for (final DiscoverItemModel itemModel : mainActivity.selectedDiscoverItems) itemModel.setSelected(false); + for (final DiscoverItemModel itemModel : mainActivity.selectedDiscoverItems) + itemModel.setSelected(false); mainActivity.selectedDiscoverItems.clear(); discoverAdapter.isSelecting = false; discoverAdapter.notifyDataSetChanged(); @@ -1196,29 +1361,31 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { return returnvalue; } - class MyTask extends AsyncTask { - private Bitmap mIcon_val; - - protected Void doInBackground(Void... voids) { - try { - mIcon_val = BitmapFactory.decodeStream((InputStream) new URL( - (mainActivity.hashtagModel != null) ? mainActivity.hashtagModel.getSdProfilePic() : ( - (mainActivity.locationModel != null) ? mainActivity.locationModel.getSdProfilePic() : - mainActivity.profileModel.getSdProfilePic()) - ).getContent()); - } catch (Throwable ex) { - Log.e("austin_debug", "bitmap: " + ex); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (mainActivity.hashtagModel != null) mainActivity.mainBinding.profileView.mainHashtagImage.setImageBitmap(mIcon_val); - else if (mainActivity.locationModel != null) mainActivity.mainBinding.profileView.mainLocationImage.setImageBitmap(mIcon_val); - else mainActivity.mainBinding.profileView.mainProfileImage.setImageBitmap(mIcon_val); - } - } + // class MyTask extends AsyncTask { + // private Bitmap mIcon_val; + // + // protected Void doInBackground(Void... voids) { + // try { + // mIcon_val = BitmapFactory.decodeStream((InputStream) new URL( + // (mainActivity.hashtagModel != null) ? mainActivity.hashtagModel.getSdProfilePic() : ( + // (mainActivity.locationModel != null) ? mainActivity.locationModel.getSdProfilePic() : + // mainActivity.profileModel.getSdProfilePic()) + // ).getContent()); + // } catch (Throwable ex) { + // Log.e("austin_debug", "bitmap: " + ex); + // } + // return null; + // } + // + // @Override + // protected void onPostExecute(Void result) { + // if (mainActivity.hashtagModel != null) + // mainActivity.mainBinding.profileView.mainHashtagImage.setImageBitmap(mIcon_val); + // else if (mainActivity.locationModel != null) + // mainActivity.mainBinding.profileView.mainLocationImage.setImageBitmap(mIcon_val); + // else mainActivity.mainBinding.profileView.mainProfileImage.setImageBitmap(mIcon_val); + // } + // } private final View.OnClickListener profileActionListener = new View.OnClickListener() { @Override @@ -1243,18 +1410,18 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { new ProfileAction().execute("followtag"); } else if (v == mainActivity.mainBinding.profileView.btnTagged || (v == mainActivity.mainBinding.profileView.btnRestrict && !isLoggedIn)) { mainActivity.startActivity(new Intent(mainActivity, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "%"+ mainActivity.profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+ mainActivity.profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "%" + mainActivity.profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + mainActivity.profileModel.getUsername()) ); } else if (v == mainActivity.mainBinding.profileView.btnSaved) { mainActivity.startActivity(new Intent(mainActivity, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "$"+ mainActivity.profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+ mainActivity.profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "$" + mainActivity.profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + mainActivity.profileModel.getUsername()) ); } else if (v == mainActivity.mainBinding.profileView.btnLiked) { mainActivity.startActivity(new Intent(mainActivity, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "^"+ mainActivity.profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+ mainActivity.profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "^" + mainActivity.profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + mainActivity.profileModel.getUsername()) ); } } @@ -1266,17 +1433,17 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { protected Void doInBackground(String... rawAction) { action = rawAction[0]; - final String url = "https://www.instagram.com/web/"+ - ((action == "followtag" && mainActivity.hashtagModel != null) ? ("tags/"+ - (mainActivity.hashtagModel.getFollowing() == true ? "unfollow/" : "follow/")+ mainActivity.hashtagModel.getName()+"/") : ( - ((action == "restrict" && mainActivity.profileModel != null) ? "restrict_action" : ("friendships/"+ mainActivity.profileModel.getId()))+"/"+ - ((action == "follow" && mainActivity.profileModel != null) ? - ((mainActivity.profileModel.getFollowing() == true || - (mainActivity.profileModel.getFollowing() == false && mainActivity.profileModel.getRequested() == true)) - ? "unfollow/" : "follow/") : - ((action == "restrict" && mainActivity.profileModel != null) ? - (mainActivity.profileModel.getRestricted() == true ? "unrestrict/" : "restrict/") : - (mainActivity.profileModel.getBlocked() == true ? "unblock/" : "block/"))))); + final String url = "https://www.instagram.com/web/" + + ((action == "followtag" && mainActivity.hashtagModel != null) ? ("tags/" + + (mainActivity.hashtagModel.getFollowing() == true ? "unfollow/" : "follow/") + mainActivity.hashtagModel.getName() + "/") : ( + ((action == "restrict" && mainActivity.profileModel != null) ? "restrict_action" : ("friendships/" + mainActivity.profileModel.getId())) + "/" + + ((action == "follow" && mainActivity.profileModel != null) ? + ((mainActivity.profileModel.getFollowing() == true || + (mainActivity.profileModel.getFollowing() == false && mainActivity.profileModel.getRequested() == true)) + ? "unfollow/" : "follow/") : + ((action == "restrict" && mainActivity.profileModel != null) ? + (mainActivity.profileModel.getRestricted() == true ? "unrestrict/" : "restrict/") : + (mainActivity.profileModel.getBlocked() == true ? "unblock/" : "block/"))))); try { final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); urlConnection.setRequestMethod("POST"); @@ -1284,7 +1451,7 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { urlConnection.setRequestProperty("User-Agent", Constants.USER_AGENT); urlConnection.setRequestProperty("x-csrftoken", cookie.split("csrftoken=")[1].split(";")[0]); if (action == "restrict") { - final String urlParameters = "target_user_id="+ mainActivity.profileModel.getId(); + final String urlParameters = "target_user_id=" + mainActivity.profileModel.getId(); urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); urlConnection.setRequestProperty("Content-Length", "" + urlParameters.getBytes().length); @@ -1293,15 +1460,14 @@ public final class MainHelper implements SwipeRefreshLayout.OnRefreshListener { wr.writeBytes(urlParameters); wr.flush(); wr.close(); - } - else urlConnection.connect(); + } else urlConnection.connect(); if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { ok = true; - } - else Toast.makeText(mainActivity, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } else + Toast.makeText(mainActivity, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); urlConnection.disconnect(); } catch (Throwable ex) { - Log.e("austin_debug", action+": " + ex); + Log.e("austin_debug", action + ": " + ex); } return null; } diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 9fe739d2..40f0996e 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -73,14 +73,13 @@ public final class MainActivity extends BaseLanguageActivity { private static final int INITIAL_DELAY_MILLIS = 200; public static FetchListener scanHack; public static ItemGetter itemGetter; - // -------- items -------- + public final ArrayList allItems = new ArrayList<>(); public final ArrayList feedItems = new ArrayList<>(); public final ArrayList discoverItems = new ArrayList<>(); - // -------- items -------- public final ArrayList selectedItems = new ArrayList<>(); public final ArrayList selectedDiscoverItems = new ArrayList<>(); - // -------- items -------- + public final HighlightsAdapter highlightsAdapter = new HighlightsAdapter(null, new View.OnClickListener() { @Override public void onClick(final View v) { @@ -88,7 +87,7 @@ public final class MainActivity extends BaseLanguageActivity { if (tag instanceof HighlightModel) { final HighlightModel highlightModel = (HighlightModel) tag; new iStoryStatusFetcher(highlightModel.getId(), null, false, false, - (!mainHelper.isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), true, result -> { + (!isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), true, result -> { if (result != null && result.length > 0) startActivity(new Intent(MainActivity.this, StoryViewer.class) .putExtra(Constants.EXTRAS_USERNAME, userQuery.replace("@", "")) @@ -101,6 +100,7 @@ public final class MainActivity extends BaseLanguageActivity { } } }); + private SuggestionsAdapter suggestionAdapter; private MenuItem searchAction; public ActivityMainBinding mainBinding; @@ -119,6 +119,7 @@ public final class MainActivity extends BaseLanguageActivity { private DataBox.CookieModel cookieModel; private Runnable runnable; private Handler handler; + private boolean isLoggedIn; @Override protected void onCreate(@Nullable final Bundle bundle) { @@ -142,7 +143,7 @@ public final class MainActivity extends BaseLanguageActivity { setStack(bundle); userQuery = bundle.getString("query"); } - mainHelper.isLoggedIn = !Utils.isEmpty(Utils.settingsHelper.getString(Constants.COOKIE)); + isLoggedIn = !Utils.isEmpty(cookie) && Utils.getUserIdFromCookie(cookie) != null; itemGetter = itemGetType -> { if (itemGetType == ItemGetType.MAIN_ITEMS) return allItems; @@ -164,11 +165,11 @@ public final class MainActivity extends BaseLanguageActivity { if (uid != null) { final FetchListener fetchListener = username -> { if (!Utils.isEmpty(username)) { - if (!BuildConfig.DEBUG) { + // if (!BuildConfig.DEBUG) { userQuery = username; if (mainHelper != null && !mainBinding.profileView.swipeRefreshLayout.isRefreshing()) mainHelper.onRefresh(); - } + // } // adds cookies to database for quick access cookieModel = Utils.dataBox.getCookie(uid); if (Utils.dataBox.getCookieCount() == 0 || cookieModel == null || Utils.isEmpty(cookieModel.getUsername())) @@ -251,7 +252,7 @@ public final class MainActivity extends BaseLanguageActivity { allItems.clear(); mainBinding.profileView.privatePage1.setImageResource(R.drawable.ic_info); mainBinding.profileView.privatePage2.setTextSize(20); - mainBinding.profileView.privatePage2.setText(mainHelper.isLoggedIn ? R.string.no_acc_logged_in : R.string.no_acc); + mainBinding.profileView.privatePage2.setText(isLoggedIn ? R.string.no_acc_logged_in : R.string.no_acc); mainBinding.profileView.privatePage.setVisibility(View.VISIBLE); } if (!mainBinding.profileView.swipeRefreshLayout.isRefreshing() && userQuery != null) diff --git a/app/src/main/java/awais/instagrabber/activities/PostViewer.java b/app/src/main/java/awais/instagrabber/activities/PostViewer.java index 61270011..1ef75aa9 100755 --- a/app/src/main/java/awais/instagrabber/activities/PostViewer.java +++ b/app/src/main/java/awais/instagrabber/activities/PostViewer.java @@ -44,6 +44,8 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import org.json.JSONObject; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; @@ -51,8 +53,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.json.JSONObject; - import awais.instagrabber.R; import awais.instagrabber.adapters.PostsMediaAdapter; import awais.instagrabber.asyncs.PostFetcher; @@ -131,7 +131,7 @@ public final class PostViewer extends BaseLanguageActivity { public void onClick(final View v) { if (v == viewerBinding.topPanel.ivProfilePic) { new AlertDialog.Builder(PostViewer.this).setAdapter(profileDialogAdapter, profileDialogListener) - .setNeutralButton(R.string.cancel, null).setTitle(viewerPostModel.getUsername()).show(); + .setNeutralButton(R.string.cancel, null).setTitle(viewerPostModel.getUsername()).show(); } else if (v == viewerBinding.ivToggleFullScreen) { toggleFullscreen(); @@ -243,7 +243,7 @@ public final class PostViewer extends BaseLanguageActivity { } } - setupPostInfoBar("@"+viewerPostModel.getUsername(), viewerPostModel.getItemType(), viewerPostModel.getLocation()); + setupPostInfoBar("@" + viewerPostModel.getUsername(), viewerPostModel.getItemType(), viewerPostModel.getLocation()); postCaption = postModel.getPostCaption(); viewerCaptionParent.setVisibility(View.VISIBLE); @@ -289,8 +289,7 @@ public final class PostViewer extends BaseLanguageActivity { )); containerLayoutParams.weight = (containerLayoutParams.weight == 3.3f) ? 3.3f : 2.2f; viewerBinding.container.setLayoutParams(containerLayoutParams); - } - else { + } else { viewerBinding.btnLike.setOnClickListener(onClickListener); viewerBinding.btnBookmark.setOnClickListener(onClickListener); } @@ -321,8 +320,7 @@ public final class PostViewer extends BaseLanguageActivity { if (itemGetType == ItemGetType.SAVED_ITEMS && SavedViewer.itemGetter != null) { itemGetterItems = SavedViewer.itemGetter.get(itemGetType); isMainSwipe = !(itemGetterItems.size() < 1 || itemGetType == ItemGetType.SAVED_ITEMS && isFromShare); - } - else if (itemGetType != null && MainActivity.itemGetter != null) { + } else if (itemGetType != null && MainActivity.itemGetter != null) { itemGetterItems = MainActivity.itemGetter.get(itemGetType); isMainSwipe = !(itemGetterItems.size() < 1 || itemGetType == ItemGetType.MAIN_ITEMS && isFromShare); } else { @@ -401,7 +399,7 @@ public final class PostViewer extends BaseLanguageActivity { private void searchUsername(final String text) { startActivity( new Intent(getApplicationContext(), ProfileViewer.class) - .putExtra(Constants.EXTRAS_USERNAME, text) + .putExtra(Constants.EXTRAS_USERNAME, text) ); } @@ -509,9 +507,9 @@ public final class PostViewer extends BaseLanguageActivity { }; new AlertDialog.Builder(this).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(); + .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 { Utils.batchDownload(this, viewerPostModel.getUsername(), DownloadMethod.DOWNLOAD_POST_VIEWER, Collections.singletonList(viewerPostModel)); } @@ -586,7 +584,7 @@ public final class PostViewer extends BaseLanguageActivity { viewerBinding.bottomPanel.viewerCaption.setText(postCaption); } - setupPostInfoBar("@"+viewerPostModel.getUsername(), viewerPostModel.getItemType(), + setupPostInfoBar("@" + viewerPostModel.getUsername(), viewerPostModel.getItemType(), viewerPostModel.getLocation()); if (postModel instanceof PostModel) { @@ -599,8 +597,7 @@ public final class PostViewer extends BaseLanguageActivity { + ((ok && viewerPostModel.getLike() != liked) ? (liked ? 1L : -1L) : 0L))); viewerBinding.btnLike.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( getApplicationContext(), R.color.btn_pink_background))); - } - else { + } else { viewerBinding.btnLike.setText(resources.getString(R.string.like, viewerPostModel.getLikes() + ((ok && viewerPostModel.getLike() != liked) ? (liked ? 1L : -1L) : 0L))); viewerBinding.btnLike.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( @@ -610,8 +607,7 @@ public final class PostViewer extends BaseLanguageActivity { viewerBinding.btnBookmark.setText(R.string.unbookmark); viewerBinding.btnBookmark.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( getApplicationContext(), R.color.btn_orange_background))); - } - else { + } else { viewerBinding.btnBookmark.setText(R.string.bookmark); viewerBinding.btnBookmark.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( getApplicationContext(), R.color.btn_lightorange_background))); @@ -639,55 +635,55 @@ public final class PostViewer extends BaseLanguageActivity { private void setupPostInfoBar(final String from, final MediaItemType mediaItemType, final JSONObject location) { if (prevUsername == null || !prevUsername.equals(from)) { - viewerBinding.topPanel.ivProfilePic.setImageBitmap(null); - viewerBinding.topPanel.ivProfilePic.setImageDrawable(null); - viewerBinding.topPanel.ivProfilePic.setImageResource(0); + // viewerBinding.topPanel.ivProfilePic.setImageBitmap(null); + // viewerBinding.topPanel.ivProfilePic.setImageDrawable(null); + // viewerBinding.topPanel.ivProfilePic.setImageResource(0); + viewerBinding.topPanel.ivProfilePic.setImageRequest(null); if (!Utils.isEmpty(from) && from.charAt(0) == '@') new ProfileFetcher(from.substring(1), result -> { profileModel = result; if (result != null) { - final String hdProfilePic = result.getHdProfilePic(); - final String sdProfilePic = result.getSdProfilePic(); + // final String hdProfilePic = result.getHdProfilePic(); + // final String sdProfilePic = result.getSdProfilePic(); postUserId = result.getId(); - final boolean hdPicEmpty = Utils.isEmpty(hdProfilePic); - glideRequestManager.load(hdPicEmpty ? sdProfilePic : hdProfilePic).listener(new RequestListener() { - private boolean loaded = true; - - @Override - public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { - viewerBinding.topPanel.ivProfilePic.setEnabled(false); - viewerBinding.topPanel.ivProfilePic.setOnClickListener(null); - if (loaded) { - loaded = false; - if (!Utils.isEmpty(sdProfilePic)) glideRequestManager.load(sdProfilePic).listener(this) - .into(viewerBinding.topPanel.ivProfilePic); - } - return false; - } - - @Override - public boolean onResourceReady(final Drawable resource, final Object model, final Target target, final DataSource dataSource, final boolean isFirstResource) { - viewerBinding.topPanel.ivProfilePic.setEnabled(true); - viewerBinding.topPanel.ivProfilePic.setOnClickListener(onClickListener); - return false; - } - }).into(viewerBinding.topPanel.ivProfilePic); + // final boolean hdPicEmpty = Utils.isEmpty(hdProfilePic); + // glideRequestManager.load(hdPicEmpty ? sdProfilePic : hdProfilePic).listener(new RequestListener() { + // private boolean loaded = true; + // + // @Override + // public boolean onLoadFailed(@Nullable final GlideException e, final Object model, final Target target, final boolean isFirstResource) { + // viewerBinding.topPanel.ivProfilePic.setEnabled(false); + // viewerBinding.topPanel.ivProfilePic.setOnClickListener(null); + // if (loaded) { + // loaded = false; + // if (!Utils.isEmpty(sdProfilePic)) glideRequestManager.load(sdProfilePic).listener(this) + // .into(viewerBinding.topPanel.ivProfilePic); + // } + // return false; + // } + // + // @Override + // public boolean onResourceReady(final Drawable resource, final Object model, final Target target, final DataSource dataSource, final boolean isFirstResource) { + // viewerBinding.topPanel.ivProfilePic.setEnabled(true); + // viewerBinding.topPanel.ivProfilePic.setOnClickListener(onClickListener); + // return false; + // } + // }).into(viewerBinding.topPanel.ivProfilePic); + viewerBinding.topPanel.ivProfilePic.setImageURI(profileModel.getSdProfilePic()); final View viewStoryPost = findViewById(R.id.viewStoryPost); if (viewStoryPost != null) { - viewStoryPost.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (result.isPrivate()) - Toast.makeText(getApplicationContext(), R.string.share_private_post, Toast.LENGTH_LONG).show(); - Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); - sharingIntent.setType("text/plain"); - sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, "https://instagram.com/p/"+postShortCode); - startActivity(Intent.createChooser(sharingIntent, - (result.isPrivate()) ? getString(R.string.share_private_post) : getString(R.string.share_public_post))); - } + viewStoryPost.setOnClickListener(v -> { + if (result.isPrivate()) + Toast.makeText(getApplicationContext(), R.string.share_private_post, Toast.LENGTH_LONG).show(); + Intent sharingIntent = new Intent(Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(Intent.EXTRA_TEXT, "https://instagram.com/p/" + postShortCode); + startActivity(Intent.createChooser(sharingIntent, + (result.isPrivate()) ? getString(R.string.share_private_post) : getString(R.string.share_public_post))); }); } } @@ -710,11 +706,10 @@ public final class PostViewer extends BaseLanguageActivity { viewerBinding.topPanel.title.setLayoutParams(new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT )); - } - else { + } else { viewerBinding.topPanel.location.setVisibility(View.VISIBLE); viewerBinding.topPanel.location.setText(location.optString("name")); - viewerBinding.topPanel.location.setOnClickListener(v -> searchUsername(location.optString("id")+"/"+location.optString("slug"))); + viewerBinding.topPanel.location.setOnClickListener(v -> searchUsername(location.optString("id") + "/" + location.optString("slug"))); viewerBinding.topPanel.title.setLayoutParams(new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT )); @@ -736,7 +731,7 @@ public final class PostViewer extends BaseLanguageActivity { protected Void doInBackground(String... rawAction) { action = rawAction[0]; - final String url = "https://www.instagram.com/web/"+action+"/"+postModel.getPostId()+"/"+ (action == "save" ? + final String url = "https://www.instagram.com/web/" + action + "/" + postModel.getPostId() + "/" + (action == "save" ? (saved ? "unsave/" : "save/") : (liked ? "unlike/" : "like/")); try { @@ -752,7 +747,7 @@ public final class PostViewer extends BaseLanguageActivity { } urlConnection.disconnect(); } catch (Throwable ex) { - Log.e("austin_debug", action+": " + ex); + Log.e("austin_debug", action + ": " + ex); } return null; } @@ -762,12 +757,11 @@ public final class PostViewer extends BaseLanguageActivity { if (ok == true && action == "likes") { liked = !liked; refreshPost(); - } - else if (ok == true && action == "save") { + } else if (ok == true && action == "save") { saved = !saved; refreshPost(); - } - else Toast.makeText(getApplicationContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } else + Toast.makeText(getApplicationContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); } } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java b/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java index 587f6427..2705746f 100755 --- a/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java +++ b/app/src/main/java/awais/instagrabber/activities/ProfileViewer.java @@ -16,15 +16,10 @@ import android.text.method.LinkMovementMethod; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.Log; -import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -32,8 +27,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; -import androidx.core.view.GravityCompat; -import androidx.core.widget.ImageViewCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -54,12 +47,10 @@ import awais.instagrabber.asyncs.LocationFetcher; import awais.instagrabber.asyncs.PostsFetcher; import awais.instagrabber.asyncs.ProfileFetcher; import awais.instagrabber.asyncs.i.iStoryStatusFetcher; -import awais.instagrabber.customviews.MouseDrawer; import awais.instagrabber.customviews.RamboTextView; import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; -import awais.instagrabber.customviews.helpers.VideoAwareRecyclerScroller; import awais.instagrabber.databinding.ActivityProfileBinding; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.interfaces.MentionClickListener; @@ -78,7 +69,6 @@ import awais.instagrabber.utils.Utils; import awaisomereport.LogCollector; import static awais.instagrabber.utils.Constants.AUTOLOAD_POSTS; -import static awais.instagrabber.utils.Constants.BOTTOM_TOOLBAR; import static awais.instagrabber.utils.Utils.logCollector; public final class ProfileViewer extends BaseLanguageActivity implements SwipeRefreshLayout.OnRefreshListener { @@ -111,7 +101,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.toolbar.toolbar.setTitle(userQuery); else if (isLocation) profileBinding.toolbar.toolbar.setTitle(locationModel.getName()); - else profileBinding.toolbar.toolbar.setTitle("@"+profileModel.getUsername()); + else profileBinding.toolbar.toolbar.setTitle("@" + profileModel.getUsername()); final PostModel model = result[result.length - 1]; if (model != null) { @@ -120,7 +110,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe if (autoloadPosts && hasNextPage) currentlyExecuting = new PostsFetcher( profileModel != null ? profileModel.getId() - : (hashtagModel != null ? ("#"+hashtagModel.getName()) : locationModel.getId()), endCursor, this) + : (hashtagModel != null ? ("#" + hashtagModel.getName()) : locationModel.getId()), endCursor, this) .setUsername((isLocation || isHashtag) ? null : profileModel.getUsername()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); else { @@ -128,8 +118,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe } model.setPageCursor(false, null); } - } - else { + } else { profileBinding.profileView.swipeRefreshLayout.setRefreshing(false); profileBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); profileBinding.profileView.privatePage2.setText(R.string.empty_acc); @@ -157,7 +146,8 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe .putExtra(Constants.EXTRAS_HIGHLIGHT, highlightModel.getTitle()) .putExtra(Constants.EXTRAS_STORIES, result) ); - else Toast.makeText(ProfileViewer.this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + else + Toast.makeText(ProfileViewer.this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } @@ -183,7 +173,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe return; } - userQuery = (userQuery.contains("/") || userQuery.startsWith("#") || userQuery.startsWith("@")) ? userQuery : ("@"+userQuery); + userQuery = (userQuery.contains("/") || userQuery.startsWith("#") || userQuery.startsWith("@")) ? userQuery : ("@" + userQuery); profileBinding = ActivityProfileBinding.inflate(getLayoutInflater()); setContentView(profileBinding.getRoot()); @@ -198,10 +188,10 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe newintent = new Intent(this, ProfilePicViewer.class).putExtra( ((hashtagModel != null) ? Constants.EXTRAS_HASHTAG : (locationModel != null ? Constants.EXTRAS_LOCATION : Constants.EXTRAS_PROFILE)), ((hashtagModel != null) ? hashtagModel : (locationModel != null ? locationModel : profileModel))); - } - else newintent = new Intent(this, StoryViewer.class).putExtra(Constants.EXTRAS_USERNAME, userQuery.replace("@", "")) - .putExtra(Constants.EXTRAS_STORIES, storyModels) - .putExtra(Constants.EXTRAS_HASHTAG, (hashtagModel != null)); + } else + newintent = new Intent(this, StoryViewer.class).putExtra(Constants.EXTRAS_USERNAME, userQuery.replace("@", "")) + .putExtra(Constants.EXTRAS_STORIES, storyModels) + .putExtra(Constants.EXTRAS_HASHTAG, (hashtagModel != null)); startActivity(newintent); }; @@ -249,7 +239,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.swipeRefreshLayout.setRefreshing(true); stopCurrentExecutor(); currentlyExecuting = new PostsFetcher(profileModel != null ? profileModel.getId() - : (hashtagModel != null ? ("#"+hashtagModel.getName()) : locationModel.getId()), endCursor, postsFetchListener) + : (hashtagModel != null ? ("#" + hashtagModel.getName()) : locationModel.getId()), endCursor, postsFetchListener) .setUsername((isHashtag || isLocation) ? null : profileModel.getUsername()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); endCursor = null; @@ -268,7 +258,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe } else { // because sometimes configuration changes made this crash on some phones new AlertDialog.Builder(this).setAdapter(profileDialogAdapter, profileDialogListener) - .setNeutralButton(R.string.cancel, null).show(); + .setNeutralButton(R.string.cancel, null).show(); } } }; @@ -295,9 +285,9 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.appBarLayout.setExpanded(true, true); profileBinding.profileView.privatePage.setVisibility(View.GONE); profileBinding.profileView.privatePage2.setTextSize(28); - profileBinding.profileView.mainProfileImage.setImageBitmap(null); - profileBinding.profileView.mainHashtagImage.setImageBitmap(null); - profileBinding.profileView.mainLocationImage.setImageBitmap(null); + // profileBinding.profileView.mainProfileImage.setImageBitmap(null); + // profileBinding.profileView.mainHashtagImage.setImageBitmap(null); + // profileBinding.profileView.mainLocationImage.setImageBitmap(null); profileBinding.profileView.mainUrl.setText(null); profileBinding.profileView.locationUrl.setText(null); profileBinding.profileView.mainFullName.setText(null); @@ -380,15 +370,15 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe if (isLoggedIn) { new iStoryStatusFetcher(hashtagModel.getName(), null, false, true, false, false, stories -> { storyModels = stories; - if (stories != null && stories.length > 0) profileBinding.profileView.mainHashtagImage.setStoriesBorder(); + if (stories != null && stories.length > 0) + profileBinding.profileView.mainHashtagImage.setStoriesBorder(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); if (hashtagModel.getFollowing() == true) { profileBinding.profileView.btnFollowTag.setText(R.string.unfollow); profileBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_purple_background))); - } - else { + } else { profileBinding.profileView.btnFollowTag.setText(R.string.follow); profileBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_pink_background))); @@ -398,16 +388,15 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.btnFollowTag.setText(R.string.unfavorite_short); profileBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_purple_background))); - } - else { + } else { profileBinding.profileView.btnFollowTag.setText(R.string.favorite_short); profileBinding.profileView.btnFollowTag.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_pink_background))); } } - profileBinding.profileView.mainHashtagImage.setEnabled(false); - new MyTask().execute(); + // profileBinding.profileView.mainHashtagImage.setEnabled(false); + profileBinding.profileView.mainHashtagImage.setImageURI(hashtagModel.getSdProfilePic()); profileBinding.profileView.mainHashtagImage.setEnabled(true); final String postCount = String.valueOf(hashtagModel.getPostCount()); @@ -439,20 +428,20 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe final String profileId = profileModel.getId(); if (isLoggedIn || Utils.settingsHelper.getBoolean(Constants.STORIESIG)) { - new iStoryStatusFetcher(profileId, profileModel.getUsername(), false, false, - (!isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), false, - stories -> { - storyModels = stories; - if (stories != null && stories.length > 0) profileBinding.profileView.mainProfileImage.setStoriesBorder(); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - new HighlightsFetcher(profileId, (!isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), hls -> { - if (hls != null && hls.length > 0) { - profileBinding.profileView.highlightsList.setVisibility(View.VISIBLE); - highlightsAdapter.setData(hls); - } - else profileBinding.profileView.highlightsList.setVisibility(View.GONE); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + new iStoryStatusFetcher(profileId, profileModel.getUsername(), false, false, + (!isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), false, + stories -> { + storyModels = stories; + // if (stories != null && stories.length > 0) + profileBinding.profileView.mainProfileImage.setStoriesBorder(); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + new HighlightsFetcher(profileId, (!isLoggedIn && Utils.settingsHelper.getBoolean(Constants.STORIESIG)), hls -> { + if (hls != null && hls.length > 0) { + profileBinding.profileView.highlightsList.setVisibility(View.VISIBLE); + highlightsAdapter.setData(hls); + } else profileBinding.profileView.highlightsList.setVisibility(View.GONE); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } if (isLoggedIn) { @@ -466,13 +455,11 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.btnFollow.setText(R.string.unfollow); profileBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_purple_background))); - } - else if (profileModel.getRequested() == true) { + } else if (profileModel.getRequested() == true) { profileBinding.profileView.btnFollow.setText(R.string.cancel); profileBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_purple_background))); - } - else { + } else { profileBinding.profileView.btnFollow.setText(R.string.follow); profileBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_pink_background))); @@ -482,8 +469,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.btnRestrict.setText(R.string.unrestrict); profileBinding.profileView.btnRestrict.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_green_background))); - } - else { + } else { profileBinding.profileView.btnRestrict.setText(R.string.restrict); profileBinding.profileView.btnRestrict.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_orange_background))); @@ -514,8 +500,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe ProfileViewer.this, R.color.btn_red_background))); } } - } - else { + } else { profileBinding.profileView.btnTagged.setVisibility(View.VISIBLE); profileBinding.profileView.btnSaved.setVisibility(View.VISIBLE); profileBinding.profileView.btnLiked.setVisibility(View.VISIBLE); @@ -528,8 +513,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.btnFollow.setText(R.string.unfavorite_short); profileBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_purple_background))); - } - else { + } else { profileBinding.profileView.btnFollow.setText(R.string.favorite_short); profileBinding.profileView.btnFollow.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor( ProfileViewer.this, R.color.btn_pink_background))); @@ -543,9 +527,9 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe } } - profileBinding.profileView.mainProfileImage.setEnabled(false); - new MyTask().execute(); - profileBinding.profileView.mainProfileImage.setEnabled(true); + // profileBinding.profileView.mainProfileImage.setEnabled(false); + profileBinding.profileView.mainProfileImage.setImageURI(profileModel.getSdProfilePic(), null); + // profileBinding.profileView.mainProfileImage.setEnabled(true); final long followersCount = profileModel.getFollowersCount(); final long followingCount = profileModel.getFollowingCount(); @@ -615,12 +599,11 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); profileBinding.profileView.privatePage2.setText(R.string.empty_acc); profileBinding.profileView.privatePage.setVisibility(View.VISIBLE); - } - else { + } else { profileBinding.profileView.swipeRefreshLayout.setRefreshing(true); profileBinding.profileView.mainPosts.setVisibility(View.VISIBLE); currentlyExecuting = new PostsFetcher(profileId, postsFetchListener).setUsername(profileModel.getUsername()) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } else { profileBinding.profileView.mainFollowers.setClickable(false); @@ -634,8 +617,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe } } ).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - else if (isLocation) { + } else if (isLocation) { profileModel = null; hashtagModel = null; profileBinding.toolbar.toolbar.setTitle(userQuery); @@ -657,12 +639,13 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe if (isLoggedIn) { new iStoryStatusFetcher(profileId.split("/")[0], null, true, false, false, false, stories -> { storyModels = stories; - if (stories != null && stories.length > 0) profileBinding.profileView.mainLocationImage.setStoriesBorder(); + if (stories != null && stories.length > 0) + profileBinding.profileView.mainLocationImage.setStoriesBorder(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - profileBinding.profileView.mainLocationImage.setEnabled(false); - new MyTask().execute(); + // profileBinding.profileView.mainLocationImage.setEnabled(false); + profileBinding.profileView.mainLocationImage.setImageURI(locationModel.getSdProfilePic()); profileBinding.profileView.mainLocationImage.setEnabled(true); final String postCount = String.valueOf(locationModel.getPostCount()); @@ -680,8 +663,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe if (Utils.isEmpty(biography)) { profileBinding.profileView.locationBiography.setVisibility(View.GONE); - } - else if (Utils.hasMentions(biography)) { + } else if (Utils.hasMentions(biography)) { profileBinding.profileView.locationBiography.setVisibility(View.VISIBLE); biography = Utils.getMentionText(biography); profileBinding.profileView.locationBiography.setText(biography, TextView.BufferType.SPANNABLE); @@ -699,8 +681,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe intent.setData(Uri.parse(locationModel.getGeo())); startActivity(intent); }); - } - else { + } else { profileBinding.profileView.btnMap.setVisibility(View.GONE); profileBinding.profileView.btnMap.setOnClickListener(null); } @@ -710,7 +691,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.locationUrl.setVisibility(View.GONE); } else if (!url.startsWith("http")) { profileBinding.profileView.locationUrl.setVisibility(View.VISIBLE); - profileBinding.profileView.locationUrl.setText(Utils.getSpannableUrl("http://"+url)); + profileBinding.profileView.locationUrl.setText(Utils.getSpannableUrl("http://" + url)); } else { profileBinding.profileView.locationUrl.setVisibility(View.VISIBLE); profileBinding.profileView.locationUrl.setText(Utils.getSpannableUrl(url)); @@ -724,8 +705,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe profileBinding.profileView.privatePage1.setImageResource(R.drawable.ic_cancel); profileBinding.profileView.privatePage2.setText(R.string.empty_acc); profileBinding.profileView.privatePage.setVisibility(View.VISIBLE); - } - else { + } else { profileBinding.profileView.swipeRefreshLayout.setRefreshing(true); profileBinding.profileView.mainPosts.setVisibility(View.VISIBLE); currentlyExecuting = new PostsFetcher(profileId, postsFetchListener) @@ -771,8 +751,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe Utils.dataBox.addFavorite(new DataBox.FavoriteModel(userQuery, System.currentTimeMillis(), locationModel != null ? locationModel.getName() : userQuery.replaceAll("^@", ""))); favouriteAction.setIcon(R.drawable.ic_like); - } - else { + } else { Utils.dataBox.delFavorite(new DataBox.FavoriteModel(userQuery, Long.parseLong(Utils.dataBox.getFavorite(userQuery).split("/")[1]), locationModel != null ? locationModel.getName() : userQuery.replaceAll("^@", ""))); @@ -790,8 +769,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe else if (selectedItems.size() >= 100) { Toast.makeText(ProfileViewer.this, R.string.downloader_too_many, Toast.LENGTH_SHORT); return; - } - else selectedItems.add(postModel); + } else selectedItems.add(postModel); postModel.setSelected(!postModel.isSelected()); notifyAdapter(postModel); } @@ -825,11 +803,15 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe protected Void doInBackground(Void... voids) { try { - mIcon_val = BitmapFactory.decodeStream((InputStream) new URL( - (hashtagModel != null) ? hashtagModel.getSdProfilePic() : ( - (locationModel != null) ? locationModel.getSdProfilePic() : - profileModel.getSdProfilePic()) - ).getContent()); + String url; + if (hashtagModel != null) { + url = hashtagModel.getSdProfilePic(); + } else if (locationModel != null) { + url = locationModel.getSdProfilePic(); + } else { + url = profileModel.getSdProfilePic(); + } + mIcon_val = BitmapFactory.decodeStream((InputStream) new URL(url).getContent()); } catch (Throwable ex) { Log.e("austin_debug", "bitmap: " + ex); } @@ -838,8 +820,10 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe @Override protected void onPostExecute(Void result) { - if (hashtagModel != null) profileBinding.profileView.mainHashtagImage.setImageBitmap(mIcon_val); - else if (locationModel != null) profileBinding.profileView.mainLocationImage.setImageBitmap(mIcon_val); + if (hashtagModel != null) + profileBinding.profileView.mainHashtagImage.setImageBitmap(mIcon_val); + else if (locationModel != null) + profileBinding.profileView.mainLocationImage.setImageBitmap(mIcon_val); else profileBinding.profileView.mainProfileImage.setImageBitmap(mIcon_val); } } @@ -867,18 +851,18 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe new ProfileAction().execute("followtag"); } else if (v == profileBinding.profileView.btnTagged || (v == profileBinding.profileView.btnRestrict && !isLoggedIn)) { startActivity(new Intent(ProfileViewer.this, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "%"+profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "%" + profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + profileModel.getUsername()) ); } else if (v == profileBinding.profileView.btnSaved) { startActivity(new Intent(ProfileViewer.this, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "$"+profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "$" + profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + profileModel.getUsername()) ); } else if (v == profileBinding.profileView.btnLiked) { startActivity(new Intent(ProfileViewer.this, SavedViewer.class) - .putExtra(Constants.EXTRAS_INDEX, "^"+profileModel.getId()) - .putExtra(Constants.EXTRAS_USER, "@"+profileModel.getUsername()) + .putExtra(Constants.EXTRAS_INDEX, "^" + profileModel.getId()) + .putExtra(Constants.EXTRAS_USER, "@" + profileModel.getUsername()) ); } } @@ -890,17 +874,17 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe protected Void doInBackground(String... rawAction) { action = rawAction[0]; - final String url = "https://www.instagram.com/web/"+ - ((action == "followtag" && hashtagModel != null) ? ("tags/"+ - (hashtagModel.getFollowing() == true ? "unfollow/" : "follow/")+hashtagModel.getName()+"/") : ( - ((action == "restrict" && profileModel != null) ? "restrict_action" : ("friendships/"+profileModel.getId()))+"/"+ - ((action == "follow" && profileModel != null) ? - ((profileModel.getFollowing() == true || - (profileModel.getFollowing() == false && profileModel.getRequested() == true)) - ? "unfollow/" : "follow/") : - ((action == "restrict" && profileModel != null) ? - (profileModel.getRestricted() == true ? "unrestrict/" : "restrict/") : - (profileModel.getBlocked() == true ? "unblock/" : "block/"))))); + final String url = "https://www.instagram.com/web/" + + ((action.equals("followtag") && hashtagModel != null) ? ("tags/" + + (hashtagModel.getFollowing() ? "unfollow/" : "follow/") + hashtagModel.getName() + "/") : ( + ((action.equals("restrict") && profileModel != null) ? "restrict_action" : ("friendships/" + profileModel.getId())) + "/" + + ((action.equals("follow") && profileModel != null) ? + ((profileModel.getFollowing() || + (!profileModel.getFollowing() && profileModel.getRequested())) + ? "unfollow/" : "follow/") : + ((action.equals("restrict") && profileModel != null) ? + (profileModel.getRestricted() ? "unrestrict/" : "restrict/") : + (profileModel.getBlocked() ? "unblock/" : "block/"))))); try { final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); urlConnection.setRequestMethod("POST"); @@ -908,7 +892,7 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe urlConnection.setRequestProperty("User-Agent", Constants.USER_AGENT); urlConnection.setRequestProperty("x-csrftoken", cookie.split("csrftoken=")[1].split(";")[0]); if (action == "restrict") { - final String urlParameters = "target_user_id="+profileModel.getId(); + final String urlParameters = "target_user_id=" + profileModel.getId(); urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); urlConnection.setRequestProperty("Content-Length", "" + urlParameters.getBytes().length); @@ -917,22 +901,21 @@ public final class ProfileViewer extends BaseLanguageActivity implements SwipeRe wr.writeBytes(urlParameters); wr.flush(); wr.close(); - } - else urlConnection.connect(); + } else urlConnection.connect(); if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { ok = true; - } - else Toast.makeText(ProfileViewer.this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } else + Toast.makeText(ProfileViewer.this, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); urlConnection.disconnect(); } catch (Throwable ex) { - Log.e("austin_debug", action+": " + ex); + Log.e("austin_debug", action + ": " + ex); } return null; } @Override protected void onPostExecute(Void result) { - if (ok == true) { + if (ok) { onRefresh(); } } diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java index c9dd3810..c3209451 100755 --- a/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/FeedAdapter.java @@ -1,499 +1,118 @@ package awais.instagrabber.adapters; -import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Typeface; -import android.net.Uri; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.StyleSpan; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; -import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; -import com.github.chrisbanes.photoview.PhotoView; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import java.util.ArrayList; -import java.util.Collections; - -import org.json.JSONObject; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.R; -import awais.instagrabber.activities.CommentsViewer; -import awais.instagrabber.activities.PostViewer; -import awais.instagrabber.adapters.viewholder.FeedItemViewHolder; -import awais.instagrabber.customviews.CommentMentionClickSpan; +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.ItemFeedPhotoBinding; +import awais.instagrabber.databinding.ItemFeedSliderBinding; +import awais.instagrabber.databinding.ItemFeedVideoBinding; import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.BasePostModel; import awais.instagrabber.models.FeedModel; -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.ItemGetType; import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Utils; -import static awais.instagrabber.utils.Utils.settingsHelper; - -public final class FeedAdapter extends RecyclerView.Adapter { - private final static String ellipsize = "… more"; - private final Activity activity; - private final LayoutInflater layoutInflater; - private final ArrayList feedModels; +public final class FeedAdapter extends ListAdapter { + private static final String TAG = "FeedAdapter"; + // private final static String ellipsize = "… more"; + private final RequestManager glide; + private final View.OnClickListener clickListener; private final MentionClickListener mentionClickListener; - private final View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(@NonNull final View v) { - final Object tag = v.getTag(); - - if (tag instanceof FeedModel) { - final FeedModel feedModel = (FeedModel) tag; - - if (v instanceof RamboTextView) { - if (feedModel.isMentionClicked()) - feedModel.toggleCaption(); - feedModel.setMentionClicked(false); - if (!expandCollapseTextView((RamboTextView) v, feedModel)) - feedModel.toggleCaption(); - - } else { - final int id = v.getId(); - switch (id) { - case R.id.btnComments: - activity.startActivityForResult(new Intent(activity, CommentsViewer.class) - .putExtra(Constants.EXTRAS_SHORTCODE, feedModel.getShortCode()) - .putExtra(Constants.EXTRAS_POST, feedModel.getPostId()) - .putExtra(Constants.EXTRAS_USER, feedModel.getProfileModel().getId()), 6969); - break; - - case R.id.viewStoryPost: - activity.startActivity(new Intent(activity, PostViewer.class) - .putExtra(Constants.EXTRAS_INDEX, feedModel.getPosition()) - .putExtra(Constants.EXTRAS_POST, new PostModel(feedModel.getShortCode(), false)) - .putExtra(Constants.EXTRAS_TYPE, ItemGetType.FEED_ITEMS)); - break; - - case R.id.btnDownload: - final Context context = v.getContext(); - ProfileModel profileModel = feedModel.getProfileModel(); - final String username = profileModel != null ? profileModel.getUsername() : null; - - final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); - - if (feedModel.getItemType() != MediaItemType.MEDIA_TYPE_SLIDER || sliderItems == null || sliderItems.length == 1) - Utils.batchDownload(context, username, DownloadMethod.DOWNLOAD_FEED, Collections.singletonList(feedModel)); - else { - final ArrayList postModels = new ArrayList<>(); - final DialogInterface.OnClickListener clickListener = (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) - Utils.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, clickListener) - .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); - } - break; - - case R.id.ivProfilePic: - if (mentionClickListener != null) { - profileModel = feedModel.getProfileModel(); - if (profileModel != null) - mentionClickListener.onClick(null, profileModel.getUsername(), false); - } - break; - } - } - } - } - }; + public SimpleExoPlayer pagerPlayer; 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; }; - public SimpleExoPlayer pagerPlayer; - private final PlayerChangeListener playerChangeListener = (childPos, player) -> { - // todo - pagerPlayer = player; + + private static final DiffUtil.ItemCallback diffCallback = 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 FeedAdapter(final Activity activity, final ArrayList FeedModels, final MentionClickListener mentionClickListener) { - this.activity = activity; - this.feedModels = FeedModels; + public FeedAdapter(final RequestManager glide, + final View.OnClickListener clickListener, + final MentionClickListener mentionClickListener) { + super(diffCallback); + this.glide = glide; + this.clickListener = clickListener; this.mentionClickListener = mentionClickListener; - this.layoutInflater = LayoutInflater.from(activity); } @NonNull @Override public FeedItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - final View view; - if (viewType == MediaItemType.MEDIA_TYPE_VIDEO.ordinal()) - view = layoutInflater.inflate(R.layout.item_feed_video, parent, false); - else if (viewType == MediaItemType.MEDIA_TYPE_SLIDER.ordinal()) - view = layoutInflater.inflate(R.layout.item_feed_slider, parent, false); - else - view = layoutInflater.inflate(R.layout.item_feed, parent, false); - return new FeedItemViewHolder(view); - } - - @SuppressLint("SetTextI18n") - @Override - public void onBindViewHolder(@NonNull final FeedItemViewHolder viewHolder, final int position) { - final FeedModel feedModel = feedModels.get(position); - if (feedModel != null) { - final RequestManager glideRequestManager = Glide.with(viewHolder.itemView); - - feedModel.setPosition(position); - - viewHolder.viewPost.setTag(feedModel); - viewHolder.profilePic.setTag(feedModel); - viewHolder.btnDownload.setTag(feedModel); - viewHolder.viewerCaption.setTag(feedModel); - - final ProfileModel profileModel = feedModel.getProfileModel(); - if (profileModel != null) { - glideRequestManager.load(profileModel.getSdProfilePic()).into(viewHolder.profilePic); - final int titleLen = profileModel.getUsername().length() + 1; - final SpannableString spannableString = new SpannableString("@"+profileModel.getUsername()); - spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0); - viewHolder.username.setText(spannableString); - viewHolder.username.setMovementMethod(new LinkMovementMethod()); - viewHolder.username.setMentionClickListener((view, text, isHashtag) -> - mentionClickListener.onClick(null, profileModel.getUsername(), false)); + final Context context = parent.getContext(); + final LayoutInflater layoutInflater = LayoutInflater.from(context); + final MediaItemType type = MediaItemType.valueOf(viewType); + switch (type) { + case MEDIA_TYPE_VIDEO: { + final ItemFeedVideoBinding binding = ItemFeedVideoBinding.inflate(layoutInflater, parent, false); + return new FeedVideoViewHolder(binding, mentionClickListener, clickListener, longClickListener); } - - viewHolder.viewPost.setOnClickListener(clickListener); - viewHolder.profilePic.setOnClickListener(clickListener); - viewHolder.btnDownload.setOnClickListener(clickListener); - - viewHolder.tvPostDate.setText(feedModel.getPostDate()); - - final long commentsCount = feedModel.getCommentsCount(); - viewHolder.commentsCount.setText(String.valueOf(commentsCount)); - - viewHolder.btnComments.setTag(feedModel); - viewHolder.btnComments.setOnClickListener(clickListener); - viewHolder.btnComments.setEnabled(true); - - final JSONObject location = feedModel.getLocation(); - - if (location == null) { - viewHolder.location.setVisibility(View.GONE); - viewHolder.username.setLayoutParams(new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT - )); - } - else { - viewHolder.location.setVisibility(View.VISIBLE); - viewHolder.location.setText(location.optString("name")); - viewHolder.username.setLayoutParams(new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT - )); - viewHolder.location.setOnClickListener(v -> - new AlertDialog.Builder(v.getContext()).setTitle(location.optString("name")) - .setMessage(R.string.comment_view_mention_location_search) - .setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, - (dialog, which) -> mentionClickListener.onClick(null, location.optString("id")+"/"+location.optString("slug"), false)).show() - ); - } - - final String thumbnailUrl = feedModel.getThumbnailUrl(); - final String displayUrl = feedModel.getDisplayUrl(); - CharSequence postCaption = feedModel.getPostCaption(); - - final boolean captionEmpty = Utils.isEmpty(postCaption); - - viewHolder.viewerCaption.setOnClickListener(clickListener); - viewHolder.viewerCaption.setOnLongClickListener(longClickListener); - viewHolder.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE); - - if (!captionEmpty && Utils.hasMentions(postCaption)) { - postCaption = Utils.getMentionText(postCaption); - feedModel.setPostCaption(postCaption); - viewHolder.viewerCaption.setText(postCaption, TextView.BufferType.SPANNABLE); - viewHolder.viewerCaption.setMentionClickListener(mentionClickListener); - } else { - viewHolder.viewerCaption.setText(postCaption); - } - - expandCollapseTextView(viewHolder.viewerCaption, feedModel); - - final MediaItemType itemType = feedModel.getItemType(); - final View viewToChangeHeight; - - if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) { - viewToChangeHeight = viewHolder.playerView; - final Player player = viewHolder.playerView.getPlayer(); - if (player != null) { - final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); - player.setPlayWhenReady(shouldAutoplay); - } - viewHolder.videoViewsParent.setVisibility(View.VISIBLE); - viewHolder.videoViews.setText(String.valueOf(feedModel.getViewCount())); - } else { - viewHolder.videoViewsParent.setVisibility(View.GONE); - viewHolder.btnMute.setVisibility(View.GONE); - - if (itemType == MediaItemType.MEDIA_TYPE_SLIDER) { - viewToChangeHeight = viewHolder.mediaList; - - final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); - final int sliderItemLen = sliderItems != null ? sliderItems.length : 0; - - if (sliderItemLen > 0) { - viewHolder.mediaCounter.setText("1/" + sliderItemLen); - viewHolder.mediaList.setOffscreenPageLimit(Math.min(5, sliderItemLen)); - - final ViewPager.SimpleOnPageChangeListener simpleOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { - private int prevPos = 0; - - @Override - public void onPageSelected(final int position) { - ViewerPostModel sliderItem = sliderItems[prevPos]; - if (sliderItem != null) sliderItem.setSelected(false); - sliderItem = sliderItems[position]; - if (sliderItem != null) sliderItem.setSelected(true); - - View childAt = viewHolder.mediaList.getChildAt(prevPos); - if (childAt instanceof PlayerView) { - pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer(); - if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(false); - } - childAt = viewHolder.mediaList.getChildAt(position); - if (childAt instanceof PlayerView) { - pagerPlayer = (SimpleExoPlayer) ((PlayerView) childAt).getPlayer(); - if (pagerPlayer != null) pagerPlayer.setPlayWhenReady(true); - } - prevPos = position; - viewHolder.mediaCounter.setText((position + 1) + "/" + sliderItemLen); - } - }; - - //noinspection deprecation - viewHolder.mediaList.setOnPageChangeListener(simpleOnPageChangeListener); // cause add listeners might add to recycled holders - - final View.OnClickListener muteClickListener = v -> { - Player player = null; - if (v instanceof PlayerView) player = ((PlayerView) v).getPlayer(); - else if (v instanceof ImageView || v == viewHolder.btnMute) { - final int currentItem = viewHolder.mediaList.getCurrentItem(); - if (currentItem < viewHolder.mediaList.getChildCount()) { - final View childAt = viewHolder.mediaList.getChildAt(currentItem); - if (childAt instanceof PlayerView) player = ((PlayerView) childAt).getPlayer(); - } - - } else { - final Object tag = v.getTag(); - if (tag instanceof Player) player = (Player) tag; - } - - if (player instanceof SimpleExoPlayer) { - final SimpleExoPlayer exoPlayer = (SimpleExoPlayer) player; - final float intVol = exoPlayer.getVolume() == 0f ? 1f : 0f; - exoPlayer.setVolume(intVol); - viewHolder.btnMute.setImageResource(intVol == 0f ? R.drawable.mute : R.drawable.vol); - Utils.sessionVolumeFull = intVol == 1f; - } - }; - - viewHolder.btnMute.setOnClickListener(muteClickListener); - viewHolder.mediaList.setAdapter(new ChildMediaItemsAdapter(sliderItems, viewHolder.btnMute, playerChangeListener)); - } - } else { - viewToChangeHeight = viewHolder.imageView; - String url = displayUrl; - if (Utils.isEmpty(url)) url = thumbnailUrl; - glideRequestManager.load(url).into(viewHolder.imageView); - } + case MEDIA_TYPE_SLIDER: { + final ItemFeedSliderBinding binding = ItemFeedSliderBinding.inflate(layoutInflater, parent, false); + return new FeedSliderViewHolder(binding, mentionClickListener, clickListener, longClickListener); } - - if (viewToChangeHeight != null) { - final ViewGroup.LayoutParams layoutParams = viewToChangeHeight.getLayoutParams(); - layoutParams.height = Utils.displayMetrics.widthPixels + 1; - viewToChangeHeight.setLayoutParams(layoutParams); + default: + case MEDIA_TYPE_IMAGE: { + final ItemFeedPhotoBinding binding = ItemFeedPhotoBinding.inflate(layoutInflater, parent, false); + return new FeedPhotoViewHolder(binding, glide, mentionClickListener, clickListener, longClickListener); } } } @Override - public int getItemCount() { - return feedModels == null ? 0 : feedModels.size(); + public void onBindViewHolder(@NonNull final FeedItemViewHolder viewHolder, final int position) { + final FeedModel feedModel = getItem(position); + if (feedModel == null) { + return; + } + feedModel.setPosition(position); + viewHolder.bind(feedModel); } @Override public int getItemViewType(final int position) { - if (feedModels != null) return feedModels.get(position).getItemType().ordinal(); - return MediaItemType.MEDIA_TYPE_IMAGE.ordinal(); + return getItem(position).getItemType().getId(); } - /** - * expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation] - * - * @param textView the {@link RamboTextView} view, to expand and collapse - * @param feedModel the {@link FeedModel} model to check wether model is collapsed to expanded - * - * @return true if expanded/collapsed, false if empty or text size is <= 255 chars - */ - public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, @NonNull final FeedModel feedModel) { - final CharSequence caption = feedModel.getPostCaption(); - if (Utils.isEmpty(caption)) return false; - - final TextView.BufferType bufferType = caption instanceof Spanned ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL; - - if (!feedModel.isCaptionExpanded()) { - int i = Utils.indexOfChar(caption, '\r', 0); - if (i == -1) i = Utils.indexOfChar(caption, '\n', 0); - if (i == -1) i = 255; - - final int captionLen = caption.length(); - final int minTrim = Math.min(255, i); - if (captionLen <= minTrim) return false; - - if (Utils.hasMentions(caption)) - textView.setText(Utils.getMentionText(caption), TextView.BufferType.SPANNABLE); - textView.setCaptionIsExpandable(true); - textView.setCaptionIsExpanded(true); - } else { - textView.setText(caption, bufferType); - textView.setCaptionIsExpanded(false); - } - return true; - } - - private interface PlayerChangeListener { - void playerChanged(final int childPos, final SimpleExoPlayer player); + @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(); } - private static final class ChildMediaItemsAdapter extends PagerAdapter { - private final PlayerChangeListener playerChangeListener; - private final ViewerPostModel[] sliderItems; - private final View btnMute; - private SimpleExoPlayer player; - - private ChildMediaItemsAdapter(final ViewerPostModel[] sliderItems, final View btnMute, - final PlayerChangeListener playerChangeListener) { - this.sliderItems = sliderItems; - this.btnMute = btnMute; - if (BuildConfig.DEBUG) this.playerChangeListener = playerChangeListener; - else this.playerChangeListener = null; - } - - @NonNull - @Override - public Object instantiateItem(@NonNull final ViewGroup container, final int position) { - final Context context = container.getContext(); - final ViewerPostModel sliderItem = sliderItems[position]; - - if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { - if (btnMute != null) btnMute.setVisibility(View.VISIBLE); - final PlayerView playerView = new PlayerView(context); - - player = new SimpleExoPlayer.Builder(context).build(); - playerView.setPlayer(player); - - float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; - if (vol == 0f && Utils.sessionVolumeFull) vol = 1f; - player.setVolume(vol); - player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); - - final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) - .createMediaSource(Uri.parse(sliderItem.getDisplayUrl())); - - player.setRepeatMode(Player.REPEAT_MODE_ALL); - player.prepare(mediaSource); - player.setVolume(vol); - - playerView.setTag(player); - - if (playerChangeListener != null) { - //todo - // playerChangeListener.playerChanged(position, player); - Log.d("AWAISKING_APP", "playerChangeListener: " + playerChangeListener); - } - - container.addView(playerView); - return playerView; - } else { - if (btnMute != null) btnMute.setVisibility(View.GONE); - - final PhotoView photoView = new PhotoView(context); - photoView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - Glide.with(context).load(sliderItem.getDisplayUrl()).into(photoView); - container.addView(photoView); - return photoView; - } - } - - @Override - public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { - final Player player = object instanceof PlayerView ? ((PlayerView) object).getPlayer() : this.player; - - if (player == this.player && this.player != null) { - this.player.stop(true); - this.player.release(); - } else if (player != null) { - player.stop(true); - 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 == object; - } + @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(); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java index 05769834..b9d5c33f 100755 --- a/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java @@ -7,8 +7,6 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; - import awais.instagrabber.R; import awais.instagrabber.adapters.viewholder.HighlightViewHolder; import awais.instagrabber.models.FeedStoryModel; @@ -37,11 +35,9 @@ public final class FeedStoriesAdapter extends RecyclerView.Adapter mentionClickListener.onClick(null, profileModel.getUsername(), false)); + } + bottomBinding.tvPostDate.setText(feedModel.getPostDate()); + final long commentsCount = feedModel.getCommentsCount(); + bottomBinding.commentsCount.setText(String.valueOf(commentsCount)); + + final JSONObject location = feedModel.getLocation(); + setLocation(location); + CharSequence postCaption = feedModel.getPostCaption(); + final boolean captionEmpty = Utils.isEmpty(postCaption); + bottomBinding.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE); + if (!captionEmpty) { + if (Utils.hasMentions(postCaption)) { + postCaption = Utils.getMentionText(postCaption); + feedModel.setPostCaption(postCaption); + bottomBinding.viewerCaption.setText(postCaption, TextView.BufferType.SPANNABLE); + } else { + bottomBinding.viewerCaption.setText(postCaption); + } + } + expandCollapseTextView(bottomBinding.viewerCaption, feedModel.getPostCaption()); + bindItem(feedModel); + } + + private void setLocation(final JSONObject location) { + if (location == null) { + topBinding.location.setVisibility(View.GONE); + topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT + )); + } else { + topBinding.location.setVisibility(View.VISIBLE); + topBinding.location.setText(location.optString("name")); + topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT + )); + topBinding.location.setOnClickListener(v -> { + new AlertDialog.Builder(v.getContext()).setTitle(location.optString("name")) + .setMessage(R.string.comment_view_mention_location_search) + .setNegativeButton(R.string.cancel, null).setPositiveButton(R.string.ok, + (dialog, which) -> mentionClickListener.onClick(null, location.optString("id") + "/" + location.optString("slug"), false)).show(); + }); + } + } + + /** + * expands or collapses {@link RamboTextView} [stg idek why i wrote this documentation] + * + * @param textView the {@link RamboTextView} view, to expand and collapse + * @param caption + * @return isExpanded + */ + public static boolean expandCollapseTextView(@NonNull final RamboTextView textView, final CharSequence caption) { + if (Utils.isEmpty(caption)) return false; + + final TextView.BufferType bufferType = caption instanceof Spanned ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL; + + if (!textView.isCaptionExpanded()) { + int i = Utils.indexOfChar(caption, '\r', 0); + if (i == -1) i = Utils.indexOfChar(caption, '\n', 0); + if (i == -1) i = MAX_CHARS; + + final int captionLen = caption.length(); + final int minTrim = Math.min(MAX_CHARS, i); + if (captionLen <= minTrim) return false; + + if (Utils.hasMentions(caption)) + textView.setText(Utils.getMentionText(caption), TextView.BufferType.SPANNABLE); + textView.setCaptionIsExpandable(true); + textView.setCaptionIsExpanded(true); + } else { + textView.setText(caption, bufferType); + textView.setCaptionIsExpanded(false); + } + return true; + } + + public abstract void bindItem(final FeedModel feedModel); +} \ 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 new file mode 100644 index 00000000..324676e9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java @@ -0,0 +1,87 @@ +package awais.instagrabber.adapters.viewholder.feed; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.RequestManager; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import awais.instagrabber.databinding.ItemFeedPhotoBinding; +import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.utils.Utils; + +public class FeedPhotoViewHolder extends FeedItemViewHolder { + private static final String TAG = "FeedPhotoViewHolder"; + + private final ItemFeedPhotoBinding binding; + private final RequestManager glide; + private final ColorDrawable drawable; + // private final PipelineDraweeControllerBuilder controllerBuilder; + // private final CustomTarget customTarget; + + public FeedPhotoViewHolder(@NonNull final ItemFeedPhotoBinding binding, + final RequestManager glide, + final MentionClickListener mentionClickListener, + final View.OnClickListener clickListener, + final View.OnLongClickListener longClickListener) { + super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); + this.binding = binding; + this.glide = glide; + binding.itemFeedBottom.videoViewsContainer.setVisibility(View.GONE); + binding.itemFeedBottom.btnMute.setVisibility(View.GONE); + drawable = new ColorDrawable(Color.WHITE); + binding.imageViewer.setAllowTouchInterceptionWhileZoomed(false); + final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(itemView.getContext().getResources()) + .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) + .build(); + binding.imageViewer.setHierarchy(hierarchy); + } + + @Override + public void bindItem(final FeedModel feedModel) { + // glide.clear(customTarget); + if (feedModel == null) { + return; + } + final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); + final int requiredWidth = Utils.displayMetrics.widthPixels; + final int resultingHeight = Utils.getResultingHeight(requiredWidth, feedModel.getImageHeight(), feedModel.getImageWidth()); + layoutParams.width = requiredWidth; + layoutParams.height = resultingHeight; + binding.imageViewer.requestLayout(); + final String thumbnailUrl = feedModel.getThumbnailUrl(); + String url = feedModel.getDisplayUrl(); + if (Utils.isEmpty(url)) url = thumbnailUrl; + final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)) + .setLocalThumbnailPreviewsEnabled(true) + .setProgressiveRenderingEnabled(true) + .build(); + binding.imageViewer.setController(Fresco.newDraweeControllerBuilder() + .setImageRequest(requestBuilder) + .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); + + } +} 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 new file mode 100644 index 00000000..572286f2 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java @@ -0,0 +1,339 @@ +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.widget.ViewSwitcher; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +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.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.ui.PlayerView; +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.databinding.ItemFeedSliderBinding; +import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.ViewerPostModel; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class FeedSliderViewHolder extends FeedItemViewHolder { + private static final String TAG = "FeedSliderViewHolder"; + private static final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); + + private final ItemFeedSliderBinding binding; + private final DefaultDataSourceFactory dataSourceFactory; + + private final PlayerChangeListener playerChangeListener = (position, player) -> { + pagerPlayer = player; + playerPosition = position; + }; + + private CacheDataSourceFactory cacheDataSourceFactory; + private SimpleExoPlayer pagerPlayer; + private int playerPosition = 0; + + public FeedSliderViewHolder(@NonNull final ItemFeedSliderBinding binding, + final MentionClickListener mentionClickListener, + final View.OnClickListener clickListener, + final View.OnLongClickListener longClickListener) { + super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); + this.binding = binding; + binding.itemFeedBottom.videoViewsContainer.setVisibility(View.GONE); + binding.itemFeedBottom.btnMute.setVisibility(View.GONE); + final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams(); + layoutParams.height = Utils.displayMetrics.widthPixels + 1; + binding.mediaList.setLayoutParams(layoutParams); + final Context context = binding.getRoot().getContext(); + dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); + final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); + if (simpleCache != null) { + cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); + } + } + + @Override + public void bindItem(final FeedModel feedModel) { + final ViewerPostModel[] sliderItems = feedModel.getSliderItems(); + final int sliderItemLen = sliderItems != null ? sliderItems.length : 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)); + } + } + 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; + + @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); + } + }); + + final View.OnClickListener muteClickListener = v -> { + final int currentItem = binding.mediaList.getCurrentItem(); + if (currentItem < 0 || currentItem >= binding.mediaList.getChildCount()) { + return; + } + final ViewerPostModel sliderItem = sliderItems[currentItem]; + if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) { + return; + } + final View currentView = binding.mediaList.getChildAt(currentItem); + if (!(currentView instanceof ViewSwitcher)) { + return; + } + final ViewSwitcher viewSwitcher = (ViewSwitcher) currentView; + final Object tag = viewSwitcher.getTag(); + if (!(tag instanceof SimpleExoPlayer)) { + return; + } + final SimpleExoPlayer player = (SimpleExoPlayer) tag; + final float intVol = player.getVolume() == 0f ? 1f : 0f; + player.setVolume(intVol); + binding.itemFeedBottom.btnMute.setImageResource(intVol == 0f ? R.drawable.vol : R.drawable.mute); + Utils.sessionVolumeFull = intVol == 1f; + }; + final ViewerPostModel firstItem = sliderItems[0]; + if (firstItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE); + } + binding.itemFeedBottom.btnMute.setImageResource(Utils.sessionVolumeFull ? R.drawable.mute : R.drawable.vol); + binding.itemFeedBottom.btnMute.setOnClickListener(muteClickListener); + } + + 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); + } + + public void startPlayingVideo() { + autoPlay(playerPosition); + } + + public void stopPlayingVideo() { + if (pagerPlayer == null) { + return; + } + pagerPlayer.setPlayWhenReady(false); + } + + private interface PlayerChangeListener { + void playerChanged(final int position, final SimpleExoPlayer player); + } + + private static void loadPlayer(final Context context, + final int position, final String displayUrl, + final ViewSwitcher viewSwitcher, + final DataSource.Factory factory, + final PlayerChangeListener playerChangeListener) { + if (viewSwitcher == null) { + return; + } + SimpleExoPlayer player = (SimpleExoPlayer) viewSwitcher.getTag(); + if (player != null) { + player.setPlayWhenReady(true); + return; + } + player = new SimpleExoPlayer.Builder(context).build(); + final PlayerView playerView = (PlayerView) viewSwitcher.getChildAt(1); + playerView.setPlayer(player); + if (viewSwitcher.getDisplayedChild() == 0) { + viewSwitcher.showNext(); + } + playerView.setControllerShowTimeoutMs(1000); + float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f; + 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)); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + player.prepare(mediaSource); + 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 == 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 new file mode 100644 index 00000000..2df6c499 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java @@ -0,0 +1,156 @@ +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; +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.databinding.ItemFeedVideoBinding; +import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class FeedVideoViewHolder extends FeedItemViewHolder { + private static final String TAG = "FeedVideoViewHolder"; + + private final ItemFeedVideoBinding binding; + private final Handler handler; + private final DefaultDataSourceFactory dataSourceFactory; + + private CacheDataSourceFactory cacheDataSourceFactory; + private FeedModel feedModel; + private SimpleExoPlayer player; + + final Runnable loadRunnable = new Runnable() { + @Override + public void run() { + loadPlayer(feedModel); + } + }; + + public FeedVideoViewHolder(@NonNull final ItemFeedVideoBinding binding, + final MentionClickListener mentionClickListener, + final View.OnClickListener clickListener, + final View.OnLongClickListener longClickListener) { + super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, mentionClickListener, clickListener, longClickListener); + this.binding = binding; + binding.itemFeedBottom.videoViewsContainer.setVisibility(View.VISIBLE); + handler = new Handler(Looper.getMainLooper()); + final Context context = binding.getRoot().getContext(); + dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); + final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); + if (simpleCache != null) { + cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); + } + } + + @Override + public void bindItem(final FeedModel feedModel) { + // Log.d(TAG, "Binding post: " + feedModel.getPostId()); + this.feedModel = feedModel; + setThumbnail(feedModel); + binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(feedModel.getViewCount())); + } + + private void setThumbnail(final FeedModel feedModel) { + final ViewGroup.LayoutParams layoutParams = binding.thumbnailParent.getLayoutParams(); + layoutParams.width = feedModel.getImageWidth(); + layoutParams.height = 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)); + } + + 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 = Utils.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; + binding.itemFeedBottom.btnMute.setOnClickListener(v -> { + final float intVol = finalPlayer.getVolume() == 0f ? 1f : 0f; + finalPlayer.setVolume(intVol); + setMuteIcon(intVol); + Utils.sessionVolumeFull = intVol == 1f; + }); + binding.playerView.setOnClickListener(v -> finalPlayer.setPlayWhenReady(!finalPlayer.getPlayWhenReady())); + } + + private void setMuteIcon(final float vol) { + binding.itemFeedBottom.btnMute.setImageResource(vol == 0f ? R.drawable.vol : R.drawable.mute); + } + + public FeedModel getCurrentFeedModel() { + 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 startPlaying() { + handler.removeCallbacks(loadRunnable); + handler.postDelayed(loadRunnable, 800); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java index 4ebffb6b..441c21e3 100755 --- a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java +++ b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java @@ -1,22 +1,23 @@ package awais.instagrabber.customviews; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapShader; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Outline; import android.graphics.Paint; -import android.graphics.Shader; -import android.graphics.drawable.BitmapDrawable; -import android.os.Build; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewOutlineProvider; -import androidx.appcompat.widget.AppCompatImageView; +import androidx.annotation.Nullable; -public final class CircularImageView extends AppCompatImageView { +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +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(); @@ -24,82 +25,115 @@ public final class CircularImageView extends AppCompatImageView { private BitmapShader shader; private Bitmap bitmap; + 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(); } - 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); - } - }); - } - } - - @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); - } - } + protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) { + Resources resources = context.getResources(); + final RoundingParams roundingParams = RoundingParams.asCircle(); + GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(resources) + .setRoundingParams(roundingParams) + .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER); + GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs); + setAspectRatio(builder.getDesiredAspectRatio()); + setHierarchy(builder.build()); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - setLayerType(LAYER_TYPE_HARDWARE, null); + 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 - protected void onDetachedFromWindow() { - setLayerType(LAYER_TYPE_NONE, null); - super.onDetachedFromWindow(); - } + // @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(); + // invalidate(); + // final RoundingParams roundingParams = RoundingParams.fromCornersRadius(5f); + // + RoundingParams roundingParams = getHierarchy().getRoundingParams(); + if (roundingParams == null) { + roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY); + } + roundingParams.setBorder(color, 5.0f); + getHierarchy().setRoundingParams(roundingParams); } } \ 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 93ea7cf0..ac8b2c9b 100755 --- a/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java +++ b/app/src/main/java/awais/instagrabber/customviews/RamboTextView.java @@ -176,4 +176,8 @@ public final class RamboTextView extends AppCompatTextView { return null; } + + public boolean isCaptionExpanded() { + return isExpanded; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/AbstractAnimatedZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/AbstractAnimatedZoomableController.java new file mode 100644 index 00000000..b473ef35 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/AbstractAnimatedZoomableController.java @@ -0,0 +1,170 @@ +/* + * 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.graphics.Matrix; +import android.graphics.PointF; + +import androidx.annotation.Nullable; + +import com.facebook.common.logging.FLog; + +/** + * Abstract class for ZoomableController that adds animation capabilities to + * DefaultZoomableController. + */ +public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController { + + private boolean mIsAnimating; + private final float[] mStartValues = new float[9]; + private final float[] mStopValues = new float[9]; + private final float[] mCurrentValues = new float[9]; + private final Matrix mNewTransform = new Matrix(); + private final Matrix mWorkingTransform = new Matrix(); + + public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) { + super(transformGestureDetector); + } + + @Override + public void reset() { + FLog.v(getLogTag(), "reset"); + stopAnimation(); + mWorkingTransform.reset(); + mNewTransform.reset(); + super.reset(); + } + + /** + * Returns true if the zoomable transform is identity matrix, and the controller is idle. + */ + @Override + public boolean isIdentity() { + return !isAnimating() && super.isIdentity(); + } + + /** + * Zooms to the desired scale and positions the image so that the given image point corresponds to + * the given view point. + * + *

If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + */ + @Override + public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { + zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null); + } + + /** + * Zooms to the desired scale and positions the image so that the given image point corresponds to + * the given view point. + * + *

If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + * @param limitFlags whether to limit translation and/or scale. + * @param durationMs length of animation of the zoom, or 0 if no animation desired + * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 + */ + public void zoomToPoint( + float scale, + PointF imagePoint, + PointF viewPoint, + @LimitFlag int limitFlags, + long durationMs, + @Nullable Runnable onAnimationComplete) { + FLog.v(getLogTag(), "zoomToPoint: duration %d ms", durationMs); + calculateZoomToPointTransform(mNewTransform, scale, imagePoint, viewPoint, limitFlags); + setTransform(mNewTransform, durationMs, onAnimationComplete); + } + + /** + * Sets a new zoomable transformation and animates to it if desired. + * + *

If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param newTransform new transform to make active + * @param durationMs duration of the animation, or 0 to not animate + * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 + */ + public void setTransform( + Matrix newTransform, long durationMs, @Nullable Runnable onAnimationComplete) { + FLog.v(getLogTag(), "setTransform: duration %d ms", durationMs); + if (durationMs <= 0) { + setTransformImmediate(newTransform); + } else { + setTransformAnimated(newTransform, durationMs, onAnimationComplete); + } + } + + private void setTransformImmediate(final Matrix newTransform) { + FLog.v(getLogTag(), "setTransformImmediate"); + stopAnimation(); + mWorkingTransform.set(newTransform); + super.setTransform(newTransform); + getDetector().restartGesture(); + } + + protected boolean isAnimating() { + return mIsAnimating; + } + + protected void setAnimating(boolean isAnimating) { + mIsAnimating = isAnimating; + } + + protected float[] getStartValues() { + return mStartValues; + } + + protected float[] getStopValues() { + return mStopValues; + } + + protected Matrix getWorkingTransform() { + return mWorkingTransform; + } + + @Override + public void onGestureBegin(TransformGestureDetector detector) { + FLog.v(getLogTag(), "onGestureBegin"); + stopAnimation(); + super.onGestureBegin(detector); + } + + @Override + public void onGestureUpdate(TransformGestureDetector detector) { + FLog.v(getLogTag(), "onGestureUpdate %s", isAnimating() ? "(ignored)" : ""); + if (isAnimating()) { + return; + } + super.onGestureUpdate(detector); + } + + protected void calculateInterpolation(Matrix outMatrix, float fraction) { + for (int i = 0; i < 9; i++) { + mCurrentValues[i] = (1 - fraction) * mStartValues[i] + fraction * mStopValues[i]; + } + outMatrix.setValues(mCurrentValues); + } + + public abstract void setTransformAnimated( + final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete); + + protected abstract void stopAnimation(); + + protected abstract Class getLogTag(); +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/AnimatedZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/AnimatedZoomableController.java new file mode 100644 index 00000000..963ac632 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/AnimatedZoomableController.java @@ -0,0 +1,105 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.view.animation.DecelerateInterpolator; + +import androidx.annotation.Nullable; + +import com.facebook.common.internal.Preconditions; +import com.facebook.common.logging.FLog; + + +/** + * ZoomableController that adds animation capabilities to DefaultZoomableController using standard + * Android animation classes + */ +public class AnimatedZoomableController extends AbstractAnimatedZoomableController { + + private static final Class TAG = AnimatedZoomableController.class; + + private final ValueAnimator mValueAnimator; + + public static AnimatedZoomableController newInstance() { + return new AnimatedZoomableController(TransformGestureDetector.newInstance()); + } + + @SuppressLint("NewApi") + public AnimatedZoomableController(TransformGestureDetector transformGestureDetector) { + super(transformGestureDetector); + mValueAnimator = ValueAnimator.ofFloat(0, 1); + mValueAnimator.setInterpolator(new DecelerateInterpolator()); + } + + @SuppressLint("NewApi") + @Override + public void setTransformAnimated( + final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete) { + FLog.v(getLogTag(), "setTransformAnimated: duration %d ms", durationMs); + stopAnimation(); + Preconditions.checkArgument(durationMs > 0); + Preconditions.checkState(!isAnimating()); + setAnimating(true); + mValueAnimator.setDuration(durationMs); + getTransform().getValues(getStartValues()); + newTransform.getValues(getStopValues()); + mValueAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + calculateInterpolation(getWorkingTransform(), (float) valueAnimator.getAnimatedValue()); + AnimatedZoomableController.super.setTransform(getWorkingTransform()); + } + }); + mValueAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + FLog.v(getLogTag(), "setTransformAnimated: animation cancelled"); + onAnimationStopped(); + } + + @Override + public void onAnimationEnd(Animator animation) { + FLog.v(getLogTag(), "setTransformAnimated: animation finished"); + onAnimationStopped(); + } + + private void onAnimationStopped() { + if (onAnimationComplete != null) { + onAnimationComplete.run(); + } + setAnimating(false); + getDetector().restartGesture(); + } + }); + mValueAnimator.start(); + } + + @SuppressLint("NewApi") + @Override + public void stopAnimation() { + if (!isAnimating()) { + return; + } + FLog.v(getLogTag(), "stopAnimation"); + mValueAnimator.cancel(); + mValueAnimator.removeAllUpdateListeners(); + mValueAnimator.removeAllListeners(); + } + + @Override + protected Class getLogTag() { + return TAG; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java new file mode 100644 index 00000000..0ee5d0cd --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/DefaultZoomableController.java @@ -0,0 +1,720 @@ +/* + * 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.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; +import android.view.MotionEvent; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import com.facebook.common.logging.FLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Zoomable controller that calculates transformation based on touch events. + */ +public class DefaultZoomableController + implements ZoomableController, TransformGestureDetector.Listener { + + /** + * Interface for handling call backs when the image bounds are set. + */ + public interface ImageBoundsListener { + void onImageBoundsSet(RectF imageBounds); + } + + @IntDef( + flag = true, + value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL}) + @Retention(RetentionPolicy.SOURCE) + public @interface LimitFlag {} + + public static final int LIMIT_NONE = 0; + public static final int LIMIT_TRANSLATION_X = 1; + public static final int LIMIT_TRANSLATION_Y = 2; + public static final int LIMIT_SCALE = 4; + public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE; + + private static final float EPS = 1e-3f; + + private static final Class TAG = DefaultZoomableController.class; + + private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1); + + private TransformGestureDetector mGestureDetector; + + private @Nullable + ImageBoundsListener mImageBoundsListener; + + private @Nullable + Listener mListener = null; + + private boolean mIsEnabled = false; + private boolean mIsRotationEnabled = false; + private boolean mIsScaleEnabled = true; + private boolean mIsTranslationEnabled = true; + private boolean mIsGestureZoomEnabled = true; + + private float mMinScaleFactor = 1.0f; + private float mMaxScaleFactor = 2.0f; + + // View bounds, in view-absolute coordinates + private final RectF mViewBounds = new RectF(); + // Non-transformed image bounds, in view-absolute coordinates + private final RectF mImageBounds = new RectF(); + // Transformed image bounds, in view-absolute coordinates + private final RectF mTransformedImageBounds = new RectF(); + + private final Matrix mPreviousTransform = new Matrix(); + private final Matrix mActiveTransform = new Matrix(); + private final Matrix mActiveTransformInverse = new Matrix(); + private final float[] mTempValues = new float[9]; + private final RectF mTempRect = new RectF(); + private boolean mWasTransformCorrected; + + public static DefaultZoomableController newInstance() { + return new DefaultZoomableController(TransformGestureDetector.newInstance()); + } + + public DefaultZoomableController(TransformGestureDetector gestureDetector) { + mGestureDetector = gestureDetector; + mGestureDetector.setListener(this); + } + + /** + * Rests the controller. + */ + public void reset() { + FLog.v(TAG, "reset"); + mGestureDetector.reset(); + mPreviousTransform.reset(); + mActiveTransform.reset(); + onTransformChanged(); + } + + /** + * Sets the zoomable listener. + */ + @Override + public void setListener(Listener listener) { + mListener = listener; + } + + /** + * Sets whether the controller is enabled or not. + */ + @Override + public void setEnabled(boolean enabled) { + mIsEnabled = enabled; + if (!enabled) { + reset(); + } + } + + /** + * Gets whether the controller is enabled or not. + */ + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * Sets whether the rotation gesture is enabled or not. + */ + public void setRotationEnabled(boolean enabled) { + mIsRotationEnabled = enabled; + } + + /** + * Gets whether the rotation gesture is enabled or not. + */ + public boolean isRotationEnabled() { + return mIsRotationEnabled; + } + + /** + * Sets whether the scale gesture is enabled or not. + */ + public void setScaleEnabled(boolean enabled) { + mIsScaleEnabled = enabled; + } + + /** + * Gets whether the scale gesture is enabled or not. + */ + public boolean isScaleEnabled() { + return mIsScaleEnabled; + } + + /** + * Sets whether the translation gesture is enabled or not. + */ + public void setTranslationEnabled(boolean enabled) { + mIsTranslationEnabled = enabled; + } + + /** + * Gets whether the translations gesture is enabled or not. + */ + public boolean isTranslationEnabled() { + return mIsTranslationEnabled; + } + + /** + * Sets the minimum scale factor allowed. + * + *

Hierarchy's scaling (if any) is not taken into account. + */ + public void setMinScaleFactor(float minScaleFactor) { + mMinScaleFactor = minScaleFactor; + } + + /** + * Gets the minimum scale factor allowed. + */ + public float getMinScaleFactor() { + return mMinScaleFactor; + } + + /** + * Sets the maximum scale factor allowed. + * + *

Hierarchy's scaling (if any) is not taken into account. + */ + public void setMaxScaleFactor(float maxScaleFactor) { + mMaxScaleFactor = maxScaleFactor; + } + + /** + * Gets the maximum scale factor allowed. + */ + public float getMaxScaleFactor() { + return mMaxScaleFactor; + } + + /** + * Sets whether gesture zooms are enabled or not. + */ + public void setGestureZoomEnabled(boolean isGestureZoomEnabled) { + mIsGestureZoomEnabled = isGestureZoomEnabled; + } + + /** + * Gets whether gesture zooms are enabled or not. + */ + public boolean isGestureZoomEnabled() { + return mIsGestureZoomEnabled; + } + + /** + * Gets the current scale factor. + */ + @Override + public float getScaleFactor() { + return getMatrixScaleFactor(mActiveTransform); + } + + /** + * Sets the image bounds, in view-absolute coordinates. + */ + @Override + public void setImageBounds(RectF imageBounds) { + if (!imageBounds.equals(mImageBounds)) { + mImageBounds.set(imageBounds); + onTransformChanged(); + if (mImageBoundsListener != null) { + mImageBoundsListener.onImageBoundsSet(mImageBounds); + } + } + } + + /** + * Gets the non-transformed image bounds, in view-absolute coordinates. + */ + public RectF getImageBounds() { + return mImageBounds; + } + + /** + * Gets the transformed image bounds, in view-absolute coordinates + */ + private RectF getTransformedImageBounds() { + return mTransformedImageBounds; + } + + /** + * Sets the view bounds. + */ + @Override + public void setViewBounds(RectF viewBounds) { + mViewBounds.set(viewBounds); + } + + /** + * Gets the view bounds. + */ + public RectF getViewBounds() { + return mViewBounds; + } + + /** + * Sets the image bounds listener. + */ + public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) { + mImageBoundsListener = imageBoundsListener; + } + + /** + * Gets the image bounds listener. + */ + public @Nullable + ImageBoundsListener getImageBoundsListener() { + return mImageBoundsListener; + } + + /** + * Returns true if the zoomable transform is identity matrix. + */ + @Override + public boolean isIdentity() { + return isMatrixIdentity(mActiveTransform, 1e-3f); + } + + /** + * Returns true if the transform was corrected during the last update. + * + *

We should rename this method to `wasTransformedWithoutCorrection` and just return the + * internal flag directly. However, this requires interface change and negation of meaning. + */ + @Override + public boolean wasTransformCorrected() { + return mWasTransformCorrected; + } + + /** + * Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The + * zoomable transformation is taken into account. + * + *

Internal matrix is exposed for performance reasons and is not to be modified by the callers. + */ + @Override + public Matrix getTransform() { + return mActiveTransform; + } + + /** + * Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The + * zoomable transformation is taken into account. + */ + public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) { + outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL); + } + + /** + * Maps point from view-absolute to image-relative coordinates. This takes into account the + * zoomable transformation. + */ + public PointF mapViewToImage(PointF viewPoint) { + float[] points = mTempValues; + points[0] = viewPoint.x; + points[1] = viewPoint.y; + mActiveTransform.invert(mActiveTransformInverse); + mActiveTransformInverse.mapPoints(points, 0, points, 0, 1); + mapAbsoluteToRelative(points, points, 1); + return new PointF(points[0], points[1]); + } + + /** + * Maps point from image-relative to view-absolute coordinates. This takes into account the + * zoomable transformation. + */ + public PointF mapImageToView(PointF imagePoint) { + float[] points = mTempValues; + points[0] = imagePoint.x; + points[1] = imagePoint.y; + mapRelativeToAbsolute(points, points, 1); + mActiveTransform.mapPoints(points, 0, points, 0, 1); + return new PointF(points[0], points[1]); + } + + /** + * Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take + * into account the zoomable transformation. Points are represented by a float array of [x0, y0, + * x1, y1, ...]. + * + * @param destPoints destination array (may be the same as source array) + * @param srcPoints source array + * @param numPoints number of points to map + */ + private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) { + for (int i = 0; i < numPoints; i++) { + destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width(); + destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height(); + } + } + + /** + * Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take + * into account the zoomable transformation. Points are represented by float array of [x0, y0, x1, + * y1, ...]. + * + * @param destPoints destination array (may be the same as source array) + * @param srcPoints source array + * @param numPoints number of points to map + */ + private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) { + for (int i = 0; i < numPoints; i++) { + destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left; + destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top; + } + } + + /** + * Zooms to the desired scale and positions the image so that the given image point corresponds to + * the given view point. + * + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + */ + public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { + FLog.v(TAG, "zoomToPoint"); + calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL); + onTransformChanged(); + } + + /** + * Calculates the zoom transformation that would zoom to the desired scale and position the image + * so that the given image point corresponds to the given view point. + * + * @param outTransform the matrix to store the result to + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + * @param limitFlags whether to limit translation and/or scale. + * @return whether or not the transform has been corrected due to limitation + */ + protected boolean calculateZoomToPointTransform( + Matrix outTransform, + float scale, + PointF imagePoint, + PointF viewPoint, + @LimitFlag int limitFlags) { + float[] viewAbsolute = mTempValues; + viewAbsolute[0] = imagePoint.x; + viewAbsolute[1] = imagePoint.y; + mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1); + float distanceX = viewPoint.x - viewAbsolute[0]; + float distanceY = viewPoint.y - viewAbsolute[1]; + boolean transformCorrected = false; + outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]); + transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags); + outTransform.postTranslate(distanceX, distanceY); + transformCorrected |= limitTranslation(outTransform, limitFlags); + return transformCorrected; + } + + /** + * Sets a new zoom transformation. + */ + public void setTransform(Matrix newTransform) { + FLog.v(TAG, "setTransform"); + mActiveTransform.set(newTransform); + onTransformChanged(); + } + + /** + * Gets the gesture detector. + */ + protected TransformGestureDetector getDetector() { + return mGestureDetector; + } + + /** + * Notifies controller of the received touch event. + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + FLog.v(TAG, "onTouchEvent: action: ", event.getAction()); + if (mIsEnabled && mIsGestureZoomEnabled) { + return mGestureDetector.onTouchEvent(event); + } + return false; + } + + /* TransformGestureDetector.Listener methods */ + + @Override + public void onGestureBegin(TransformGestureDetector detector) { + FLog.v(TAG, "onGestureBegin"); + mPreviousTransform.set(mActiveTransform); + onTransformBegin(); + // We only received a touch down event so far, and so we don't know yet in which direction a + // future move event will follow. Therefore, if we can't scroll in all directions, we have to + // assume the worst case where the user tries to scroll out of edge, which would cause + // transformation to be corrected. + mWasTransformCorrected = !canScrollInAllDirection(); + } + + @Override + public void onGestureUpdate(TransformGestureDetector detector) { + FLog.v(TAG, "onGestureUpdate"); + boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL); + onTransformChanged(); + if (transformCorrected) { + mGestureDetector.restartGesture(); + } + // A transformation happened, but was it without correction? + mWasTransformCorrected = transformCorrected; + } + + @Override + public void onGestureEnd(TransformGestureDetector detector) { + FLog.v(TAG, "onGestureEnd"); + onTransformEnd(); + } + + /** + * Calculates the zoom transformation based on the current gesture. + * + * @param outTransform the matrix to store the result to + * @param limitTypes whether to limit translation and/or scale. + * @return whether or not the transform has been corrected due to limitation + */ + protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) { + TransformGestureDetector detector = mGestureDetector; + boolean transformCorrected = false; + outTransform.set(mPreviousTransform); + if (mIsRotationEnabled) { + float angle = detector.getRotation() * (float) (180 / Math.PI); + outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); + } + if (mIsScaleEnabled) { + float scale = detector.getScale(); + outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); + } + transformCorrected |= + limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes); + if (mIsTranslationEnabled) { + outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); + } + transformCorrected |= limitTranslation(outTransform, limitTypes); + return transformCorrected; + } + + private void onTransformBegin() { + if (mListener != null && isEnabled()) { + mListener.onTransformBegin(mActiveTransform); + } + } + + private void onTransformChanged() { + mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds); + if (mListener != null && isEnabled()) { + mListener.onTransformChanged(mActiveTransform); + } + } + + private void onTransformEnd() { + if (mListener != null && isEnabled()) { + mListener.onTransformEnd(mActiveTransform); + } + } + + /** + * Keeps the scaling factor within the specified limits. + * + * @param pivotX x coordinate of the pivot point + * @param pivotY y coordinate of the pivot point + * @param limitTypes whether to limit scale. + * @return whether limiting has been applied or not + */ + private boolean limitScale( + Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) { + if (!shouldLimit(limitTypes, LIMIT_SCALE)) { + return false; + } + float currentScale = getMatrixScaleFactor(transform); + float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor); + if (targetScale != currentScale) { + float scale = targetScale / currentScale; + transform.postScale(scale, scale, pivotX, pivotY); + return true; + } + return false; + } + + /** + * Limits the translation so that there are no empty spaces on the sides if possible. + * + *

The image is attempted to be centered within the view bounds if the transformed image is + * smaller. There will be no empty spaces within the view bounds if the transformed image is + * bigger. This applies to each dimension (horizontal and vertical) independently. + * + * @param limitTypes whether to limit translation along the specific axis. + * @return whether limiting has been applied or not + */ + private boolean limitTranslation(Matrix transform, @LimitFlag int limitTypes) { + if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) { + return false; + } + 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; + if (offsetLeft != 0 || offsetTop != 0) { + transform.postTranslate(offsetLeft, offsetTop); + return true; + } + return false; + } + + /** + * Checks whether the specified limit flag is present in the limits provided. + * + *

If the flag contains multiple flags together using a bitwise OR, this only checks that at + * least one of the flags is included. + * + * @param limits the limits to apply + * @param flag the limit flag(s) to check for + * @return true if the flag (or one of the flags) is included in the limits + */ + private static boolean shouldLimit(@LimitFlag int limits, @LimitFlag int flag) { + return (limits & flag) != LIMIT_NONE; + } + + /** + * Returns the offset necessary to make sure that: - the image is centered within the limit if the + * image is smaller than the limit - there is no empty space on left/right if the image is bigger + * than the limit + */ + private float getOffset( + float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) { + float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart; + float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2; + // center if smaller than limitInnerWidth + if (imageWidth < limitInnerWidth) { + return limitCenter - (imageEnd + imageStart) / 2; + } + // to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2 + if (imageWidth < limitWidth) { + if (limitCenter < (limitStart + limitEnd) / 2) { + return limitStart - imageStart; + } else { + return limitEnd - imageEnd; + } + } + // to the edge if larger than limitWidth and empty space visible + if (imageStart > limitStart) { + return limitStart - imageStart; + } + if (imageEnd < limitEnd) { + return limitEnd - imageEnd; + } + return 0; + } + + /** + * Limits the value to the given min and max range. + */ + private float limit(float value, float min, float max) { + return Math.min(Math.max(min, value), max); + } + + /** + * Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X + * and Y axis. + */ + private float getMatrixScaleFactor(Matrix transform) { + transform.getValues(mTempValues); + return mTempValues[Matrix.MSCALE_X]; + } + + /** + * Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}. + */ + private boolean isMatrixIdentity(Matrix transform, float eps) { + // Checks whether the given matrix is close enough to the identity matrix: + // 1 0 0 + // 0 1 0 + // 0 0 1 + // Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements: + // 0 0 0 + // 0 0 0 + // 0 0 0 + transform.getValues(mTempValues); + mTempValues[0] -= 1.0f; // m00 + mTempValues[4] -= 1.0f; // m11 + mTempValues[8] -= 1.0f; // m22 + for (int i = 0; i < 9; i++) { + if (Math.abs(mTempValues[i]) > eps) { + return false; + } + } + return true; + } + + /** + * Returns whether the scroll can happen in all directions. I.e. the image is not on any edge. + */ + private boolean canScrollInAllDirection() { + return mTransformedImageBounds.left < mViewBounds.left - EPS + && mTransformedImageBounds.top < mViewBounds.top - EPS + && mTransformedImageBounds.right > mViewBounds.right + EPS + && mTransformedImageBounds.bottom > mViewBounds.bottom + EPS; + } + + @Override + public int computeHorizontalScrollRange() { + return (int) mTransformedImageBounds.width(); + } + + @Override + public int computeHorizontalScrollOffset() { + return (int) (mViewBounds.left - mTransformedImageBounds.left); + } + + @Override + public int computeHorizontalScrollExtent() { + return (int) mViewBounds.width(); + } + + @Override + public int computeVerticalScrollRange() { + return (int) mTransformedImageBounds.height(); + } + + @Override + public int computeVerticalScrollOffset() { + return (int) (mViewBounds.top - mTransformedImageBounds.top); + } + + @Override + public int computeVerticalScrollExtent() { + return (int) mViewBounds.height(); + } + + public Listener getListener() { + return mListener; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/DoubleTapGestureListener.java b/app/src/main/java/awais/instagrabber/customviews/drawee/DoubleTapGestureListener.java new file mode 100644 index 00000000..59b9c7f0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/DoubleTapGestureListener.java @@ -0,0 +1,84 @@ +/* + * 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.graphics.PointF; +import android.view.GestureDetector; +import android.view.MotionEvent; + +/** + * Tap gesture listener for double tap to zoom / unzoom and double-tap-and-drag to zoom. + * + * @see ZoomableDraweeView#setTapListener(GestureDetector.SimpleOnGestureListener) + */ +public class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener { + private static final int DURATION_MS = 300; + private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20; + + private final ZoomableDraweeView mDraweeView; + private final PointF mDoubleTapViewPoint = new PointF(); + private final PointF mDoubleTapImagePoint = new PointF(); + private float mDoubleTapScale = 1; + private boolean mDoubleTapScroll = false; + + public DoubleTapGestureListener(ZoomableDraweeView zoomableDraweeView) { + mDraweeView = zoomableDraweeView; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + AbstractAnimatedZoomableController zc = + (AbstractAnimatedZoomableController) mDraweeView.getZoomableController(); + PointF vp = new PointF(e.getX(), e.getY()); + PointF ip = zc.mapViewToImage(vp); + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDoubleTapViewPoint.set(vp); + mDoubleTapImagePoint.set(ip); + mDoubleTapScale = zc.getScaleFactor(); + break; + case MotionEvent.ACTION_MOVE: + mDoubleTapScroll = mDoubleTapScroll || shouldStartDoubleTapScroll(vp); + if (mDoubleTapScroll) { + float scale = calcScale(vp); + zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint); + } + break; + case MotionEvent.ACTION_UP: + if (mDoubleTapScroll) { + float scale = calcScale(vp); + zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint); + } else { + final float maxScale = zc.getMaxScaleFactor(); + final float minScale = zc.getMinScaleFactor(); + if (zc.getScaleFactor() < (maxScale + minScale) / 2) { + zc.zoomToPoint( + maxScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null); + } else { + zc.zoomToPoint( + minScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null); + } + } + mDoubleTapScroll = false; + break; + } + return true; + } + + private boolean shouldStartDoubleTapScroll(PointF viewPoint) { + double dist = + Math.hypot(viewPoint.x - mDoubleTapViewPoint.x, viewPoint.y - mDoubleTapViewPoint.y); + return dist > DOUBLE_TAP_SCROLL_THRESHOLD; + } + + private float calcScale(PointF currentViewPoint) { + float dy = (currentViewPoint.y - mDoubleTapViewPoint.y); + float t = 1 + Math.abs(dy) * 0.001f; + return (dy < 0) ? mDoubleTapScale / t : mDoubleTapScale * t; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/GestureListenerWrapper.java b/app/src/main/java/awais/instagrabber/customviews/drawee/GestureListenerWrapper.java new file mode 100644 index 00000000..933cf69d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/GestureListenerWrapper.java @@ -0,0 +1,72 @@ +/* + * 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.view.GestureDetector; +import android.view.MotionEvent; + +/** + * Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. + */ +public class GestureListenerWrapper extends GestureDetector.SimpleOnGestureListener { + + private GestureDetector.SimpleOnGestureListener mDelegate; + + public GestureListenerWrapper() { + mDelegate = new GestureDetector.SimpleOnGestureListener(); + } + + public void setListener(GestureDetector.SimpleOnGestureListener listener) { + mDelegate = listener; + } + + @Override + public void onLongPress(MotionEvent e) { + mDelegate.onLongPress(e); + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return mDelegate.onScroll(e1, e2, distanceX, distanceY); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return mDelegate.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public void onShowPress(MotionEvent e) { + mDelegate.onShowPress(e); + } + + @Override + public boolean onDown(MotionEvent e) { + return mDelegate.onDown(e); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mDelegate.onDoubleTap(e); + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + return mDelegate.onDoubleTapEvent(e); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return mDelegate.onSingleTapConfirmed(e); + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mDelegate.onSingleTapUp(e); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/MultiGestureListener.java b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiGestureListener.java new file mode 100644 index 00000000..e19d0f35 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiGestureListener.java @@ -0,0 +1,156 @@ +/* + * 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.view.GestureDetector; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Gesture listener that allows multiple child listeners to be added and notified about gesture + * events. + * + *

NOTE: The order of the listeners is important. Listeners can consume gesture events. For + * example, if one of the child listeners consumes {@link #onLongPress(MotionEvent)} (the listener + * returned true), subsequent listeners will not be notified about the event any more since it has + * been consumed. + */ +public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener { + + private final List mListeners = new ArrayList<>(); + + /** + * Adds a listener to the multi gesture listener. + * + *

NOTE: The order of the listeners is important since gesture events can be consumed. + * + * @param listener the listener to be added + */ + public synchronized void addListener(GestureDetector.SimpleOnGestureListener listener) { + mListeners.add(listener); + } + + /** + * Removes the given listener so that it will not be notified about future events. + * + *

NOTE: The order of the listeners is important since gesture events can be consumed. + * + * @param listener the listener to remove + */ + public synchronized void removeListener(GestureDetector.SimpleOnGestureListener listener) { + mListeners.remove(listener); + } + + @Override + public synchronized boolean onSingleTapUp(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onSingleTapUp(e)) { + return true; + } + } + return false; + } + + @Override + public synchronized void onLongPress(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).onLongPress(e); + } + } + + @Override + public synchronized boolean onScroll( + MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onScroll(e1, e2, distanceX, distanceY)) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean onFling( + MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onFling(e1, e2, velocityX, velocityY)) { + return true; + } + } + return false; + } + + @Override + public synchronized void onShowPress(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).onShowPress(e); + } + } + + @Override + public synchronized boolean onDown(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onDown(e)) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean onDoubleTap(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onDoubleTap(e)) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean onDoubleTapEvent(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onDoubleTapEvent(e)) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean onSingleTapConfirmed(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onSingleTapConfirmed(e)) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean onContextClick(MotionEvent e) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + if (mListeners.get(i).onContextClick(e)) { + return true; + } + } + return false; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/MultiPointerGestureDetector.java b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiPointerGestureDetector.java new file mode 100644 index 00000000..8c453dd7 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiPointerGestureDetector.java @@ -0,0 +1,286 @@ +/* + * 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.view.MotionEvent; + +/** + * Component that detects and tracks multiple pointers based on touch events. + * + *

Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new + * one will be started (if there are still pressed pointers left). It is guaranteed that the number + * of pointers within the single gesture will remain the same during the whole gesture. + */ +public class MultiPointerGestureDetector { + + /** + * The listener for receiving notifications when gestures occur. + */ + public interface Listener { + /** + * A callback called right before the gesture is about to start. + */ + public void onGestureBegin(MultiPointerGestureDetector detector); + + /** + * A callback called each time the gesture gets updated. + */ + public void onGestureUpdate(MultiPointerGestureDetector detector); + + /** + * A callback called right after the gesture has finished. + */ + public void onGestureEnd(MultiPointerGestureDetector detector); + } + + private static final int MAX_POINTERS = 2; + + private boolean mGestureInProgress; + private int mPointerCount; + private int mNewPointerCount; + private final int mId[] = new int[MAX_POINTERS]; + private final float mStartX[] = new float[MAX_POINTERS]; + private final float mStartY[] = new float[MAX_POINTERS]; + private final float mCurrentX[] = new float[MAX_POINTERS]; + private final float mCurrentY[] = new float[MAX_POINTERS]; + + private Listener mListener = null; + + public MultiPointerGestureDetector() { + reset(); + } + + /** + * Factory method that creates a new instance of MultiPointerGestureDetector + */ + public static MultiPointerGestureDetector newInstance() { + return new MultiPointerGestureDetector(); + } + + /** + * Sets the listener. + * + * @param listener listener to set + */ + public void setListener(Listener listener) { + mListener = listener; + } + + /** + * Resets the component to the initial state. + */ + public void reset() { + mGestureInProgress = false; + mPointerCount = 0; + for (int i = 0; i < MAX_POINTERS; i++) { + mId[i] = MotionEvent.INVALID_POINTER_ID; + } + } + + /** + * This method can be overridden in order to perform threshold check or something similar. + * + * @return whether or not to start a new gesture + */ + protected boolean shouldStartGesture() { + return true; + } + + /** + * Starts a new gesture and calls the listener just before starting it. + */ + private void startGesture() { + if (!mGestureInProgress) { + if (mListener != null) { + mListener.onGestureBegin(this); + } + mGestureInProgress = true; + } + } + + /** + * Stops the current gesture and calls the listener right after stopping it. + */ + private void stopGesture() { + if (mGestureInProgress) { + mGestureInProgress = false; + if (mListener != null) { + mListener.onGestureEnd(this); + } + } + } + + /** + * Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in + * the case when the pointer is released. + * + * @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down) + */ + private int getPressedPointerIndex(MotionEvent event, int i) { + final int count = event.getPointerCount(); + final int action = event.getActionMasked(); + final int index = event.getActionIndex(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + if (i >= index) { + i++; + } + } + return (i < count) ? i : -1; + } + + /** + * Gets the number of pressed pointers (fingers down). + */ + private static int getPressedPointerCount(MotionEvent event) { + int count = event.getPointerCount(); + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + count--; + } + return count; + } + + private void updatePointersOnTap(MotionEvent event) { + mPointerCount = 0; + for (int i = 0; i < MAX_POINTERS; i++) { + int index = getPressedPointerIndex(event, i); + if (index == -1) { + mId[i] = MotionEvent.INVALID_POINTER_ID; + } else { + mId[i] = event.getPointerId(index); + mCurrentX[i] = mStartX[i] = event.getX(index); + mCurrentY[i] = mStartY[i] = event.getY(index); + mPointerCount++; + } + } + } + + private void updatePointersOnMove(MotionEvent event) { + for (int i = 0; i < MAX_POINTERS; i++) { + int index = event.findPointerIndex(mId[i]); + if (index != -1) { + mCurrentX[i] = event.getX(index); + mCurrentY[i] = event.getY(index); + } + } + } + + /** + * Handles the given motion event. + * + * @param event event to handle + * @return whether or not the event was handled + */ + public boolean onTouchEvent(final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: { + // update pointers + updatePointersOnMove(event); + // start a new gesture if not already started + if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { + startGesture(); + } + // notify listener + if (mGestureInProgress && mListener != null) { + mListener.onGestureUpdate(this); + } + break; + } + + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: { + // restart gesture whenever the number of pointers changes + mNewPointerCount = getPressedPointerCount(event); + stopGesture(); + updatePointersOnTap(event); + if (mPointerCount > 0 && shouldStartGesture()) { + startGesture(); + } + break; + } + + case MotionEvent.ACTION_CANCEL: { + mNewPointerCount = 0; + stopGesture(); + reset(); + break; + } + } + return true; + } + + /** + * Restarts the current gesture (if any). + */ + public void restartGesture() { + if (!mGestureInProgress) { + return; + } + stopGesture(); + for (int i = 0; i < MAX_POINTERS; i++) { + mStartX[i] = mCurrentX[i]; + mStartY[i] = mCurrentY[i]; + } + startGesture(); + } + + /** + * Gets whether there is a gesture in progress + */ + public boolean isGestureInProgress() { + return mGestureInProgress; + } + + /** + * Gets the number of pointers after the current gesture + */ + public int getNewPointerCount() { + return mNewPointerCount; + } + + /** + * Gets the number of pointers in the current gesture + */ + public int getPointerCount() { + return mPointerCount; + } + + /** + * Gets the start X coordinates for the all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + public float[] getStartX() { + return mStartX; + } + + /** + * Gets the start Y coordinates for the all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + public float[] getStartY() { + return mStartY; + } + + /** + * Gets the current X coordinates for the all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + public float[] getCurrentX() { + return mCurrentX; + } + + /** + * Gets the current Y coordinates for the all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + public float[] getCurrentY() { + return mCurrentY; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java new file mode 100644 index 00000000..b459ece6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/MultiZoomableControllerListener.java @@ -0,0 +1,51 @@ +/* + * 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.graphics.Matrix; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of {@link ZoomableController.Listener} that allows multiple child listeners to + * be added and notified about {@link ZoomableController} events. + */ +public class MultiZoomableControllerListener implements ZoomableController.Listener { + + private final List mListeners = new ArrayList<>(); + + @Override + public synchronized void onTransformBegin(Matrix transform) { + for (ZoomableController.Listener listener : mListeners) { + listener.onTransformBegin(transform); + } + } + + @Override + public synchronized void onTransformChanged(Matrix transform) { + for (ZoomableController.Listener listener : mListeners) { + listener.onTransformChanged(transform); + } + } + + @Override + public synchronized void onTransformEnd(Matrix transform) { + for (ZoomableController.Listener listener : mListeners) { + listener.onTransformEnd(transform); + } + } + + public synchronized void addListener(ZoomableController.Listener listener) { + mListeners.add(listener); + } + + public synchronized void removeListener(ZoomableController.Listener listener) { + mListeners.remove(listener); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/TransformGestureDetector.java b/app/src/main/java/awais/instagrabber/customviews/drawee/TransformGestureDetector.java new file mode 100644 index 00000000..b8a9518c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/TransformGestureDetector.java @@ -0,0 +1,203 @@ +/* + * 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.view.MotionEvent; + +/** + * Component that detects translation, scale and rotation based on touch events. + * + *

This class notifies its listeners whenever a gesture begins, updates or ends. The instance of + * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or + * rotation. + */ +public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { + + /** + * The listener for receiving notifications when gestures occur. + */ + public interface Listener { + /** + * A callback called right before the gesture is about to start. + */ + public void onGestureBegin(TransformGestureDetector detector); + + /** + * A callback called each time the gesture gets updated. + */ + public void onGestureUpdate(TransformGestureDetector detector); + + /** + * A callback called right after the gesture has finished. + */ + public void onGestureEnd(TransformGestureDetector detector); + } + + private final MultiPointerGestureDetector mDetector; + + private Listener mListener = null; + + public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) { + mDetector = multiPointerGestureDetector; + mDetector.setListener(this); + } + + /** + * Factory method that creates a new instance of TransformGestureDetector + */ + public static TransformGestureDetector newInstance() { + return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); + } + + /** + * Sets the listener. + * + * @param listener listener to set + */ + public void setListener(Listener listener) { + mListener = listener; + } + + /** + * Resets the component to the initial state. + */ + public void reset() { + mDetector.reset(); + } + + /** + * Handles the given motion event. + * + * @param event event to handle + * @return whether or not the event was handled + */ + public boolean onTouchEvent(final MotionEvent event) { + return mDetector.onTouchEvent(event); + } + + @Override + public void onGestureBegin(MultiPointerGestureDetector detector) { + if (mListener != null) { + mListener.onGestureBegin(this); + } + } + + @Override + public void onGestureUpdate(MultiPointerGestureDetector detector) { + if (mListener != null) { + mListener.onGestureUpdate(this); + } + } + + @Override + public void onGestureEnd(MultiPointerGestureDetector detector) { + if (mListener != null) { + mListener.onGestureEnd(this); + } + } + + private float calcAverage(float[] arr, int len) { + float sum = 0; + for (int i = 0; i < len; i++) { + sum += arr[i]; + } + return (len > 0) ? sum / len : 0; + } + + /** + * Restarts the current gesture (if any). + */ + public void restartGesture() { + mDetector.restartGesture(); + } + + /** + * Gets whether there is a gesture in progress + */ + public boolean isGestureInProgress() { + return mDetector.isGestureInProgress(); + } + + /** + * Gets the number of pointers after the current gesture + */ + public int getNewPointerCount() { + return mDetector.getNewPointerCount(); + } + + /** + * Gets the number of pointers in the current gesture + */ + public int getPointerCount() { + return mDetector.getPointerCount(); + } + + /** + * Gets the X coordinate of the pivot point + */ + public float getPivotX() { + return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); + } + + /** + * Gets the Y coordinate of the pivot point + */ + public float getPivotY() { + return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); + } + + /** + * Gets the X component of the translation + */ + public float getTranslationX() { + return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) + - calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); + } + + /** + * Gets the Y component of the translation + */ + public float getTranslationY() { + return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) + - calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); + } + + /** + * Gets the scale + */ + public float getScale() { + if (mDetector.getPointerCount() < 2) { + return 1; + } else { + float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; + float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; + float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; + float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; + float startDist = (float) Math.hypot(startDeltaX, startDeltaY); + float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY); + return currentDist / startDist; + } + } + + /** + * Gets the rotation in radians + */ + public float getRotation() { + if (mDetector.getPointerCount() < 2) { + return 0; + } else { + float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; + float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; + float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; + float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; + float startAngle = (float) Math.atan2(startDeltaY, startDeltaX); + float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX); + return currentAngle - startAngle; + } + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java new file mode 100644 index 00000000..93d83586 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableController.java @@ -0,0 +1,134 @@ +/* + * 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.graphics.Matrix; +import android.graphics.RectF; +import android.view.MotionEvent; + +/** + * Interface for implementing a controller that works with {@link ZoomableDraweeView} to control the + * zoom. + */ +public interface ZoomableController { + + /** + * Listener interface. + */ + interface Listener { + + /** + * Notifies the view that the transform began. + * + * @param transform the current transform matrix + */ + void onTransformBegin(Matrix transform); + + /** + * Notifies the view that the transform changed. + * + * @param transform the new matrix + */ + void onTransformChanged(Matrix transform); + + /** + * Notifies the view that the transform ended. + * + * @param transform the current transform matrix + */ + void onTransformEnd(Matrix transform); + } + + /** + * Enables the controller. The controller is enabled when the image has been loaded. + * + * @param enabled whether to enable the controller + */ + void setEnabled(boolean enabled); + + /** + * Gets whether the controller is enabled. This should return the last value passed to {@link + * #setEnabled}. + * + * @return whether the controller is enabled. + */ + boolean isEnabled(); + + /** + * Sets the listener for the controller to call back when the matrix changes. + * + * @param listener the listener + */ + void setListener(Listener listener); + + /** + * Gets the current scale factor. A convenience method for calculating the scale from the + * transform. + * + * @return the current scale factor + */ + float getScaleFactor(); + + /** + * Returns true if the zoomable transform is identity matrix, and the controller is idle. + */ + boolean isIdentity(); + + /** + * Returns true if the transform was corrected during the last update. + * + *

This mainly happens when a gesture would cause the image to get out of limits and the + * transform gets corrected in order to prevent that. + */ + boolean wasTransformCorrected(); + + /** + * See {@link androidx.core.view.ScrollingView}. + */ + int computeHorizontalScrollRange(); + + int computeHorizontalScrollOffset(); + + int computeHorizontalScrollExtent(); + + int computeVerticalScrollRange(); + + int computeVerticalScrollOffset(); + + int computeVerticalScrollExtent(); + + /** + * Gets the current transform. + * + * @return the transform + */ + Matrix getTransform(); + + /** + * Sets the bounds of the image post transform prior to application of the zoomable + * transformation. + * + * @param imageBounds the bounds of the image + */ + void setImageBounds(RectF imageBounds); + + /** + * Sets the bounds of the view. + * + * @param viewBounds the bounds of the view + */ + void setViewBounds(RectF viewBounds); + + /** + * Allows the controller to handle a touch event. + * + * @param event the touch event + * @return whether the controller handled the event + */ + boolean onTouchEvent(MotionEvent event); +} diff --git a/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java new file mode 100644 index 00000000..6a91b2aa --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java @@ -0,0 +1,416 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.drawable.Animatable; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ViewParent; + +import androidx.annotation.Nullable; +import androidx.core.view.ScrollingView; + +import com.facebook.common.internal.Preconditions; +import com.facebook.common.logging.FLog; +import com.facebook.drawee.controller.AbstractDraweeController; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.controller.ControllerListener; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.drawee.generic.GenericDraweeHierarchyInflater; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.drawee.view.DraweeView; + + +/** + * DraweeView that has zoomable capabilities. + * + *

Once the image loads, pinch-to-zoom and translation gestures are enabled. + */ +public class ZoomableDraweeView extends DraweeView + implements ScrollingView { + + private static final Class TAG = ZoomableDraweeView.class; + + private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f; + + private final RectF mImageBounds = new RectF(); + private final RectF mViewBounds = new RectF(); + + private DraweeController mHugeImageController; + private ZoomableController mZoomableController; + private GestureDetector mTapGestureDetector; + private boolean mAllowTouchInterceptionWhileZoomed = true; + + private boolean mIsDialtoneEnabled = false; + private boolean mZoomingEnabled = true; + + private final ControllerListener mControllerListener = + new BaseControllerListener() { + @Override + public void onFinalImageSet( + String id, @Nullable Object imageInfo, @Nullable Animatable animatable) { + ZoomableDraweeView.this.onFinalImageSet(); + } + + @Override + public void onRelease(String id) { + ZoomableDraweeView.this.onRelease(); + } + }; + + private final ZoomableController.Listener mZoomableListener = + new ZoomableController.Listener() { + @Override + public void onTransformBegin(Matrix transform) {} + + @Override + public void onTransformChanged(Matrix transform) { + ZoomableDraweeView.this.onTransformChanged(transform); + } + + @Override + public void onTransformEnd(Matrix transform) {} + }; + + private final GestureListenerWrapper mTapListenerWrapper = new GestureListenerWrapper(); + + public ZoomableDraweeView(Context context, GenericDraweeHierarchy hierarchy) { + super(context); + setHierarchy(hierarchy); + init(); + } + + public ZoomableDraweeView(Context context) { + super(context); + inflateHierarchy(context, null); + init(); + } + + public ZoomableDraweeView(Context context, AttributeSet attrs) { + super(context, attrs); + inflateHierarchy(context, attrs); + init(); + } + + public ZoomableDraweeView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + inflateHierarchy(context, attrs); + init(); + } + + protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) { + Resources resources = context.getResources(); + GenericDraweeHierarchyBuilder builder = + new GenericDraweeHierarchyBuilder(resources) + .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER); + GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs); + setAspectRatio(builder.getDesiredAspectRatio()); + setHierarchy(builder.build()); + } + + private void init() { + mZoomableController = createZoomableController(); + mZoomableController.setListener(mZoomableListener); + mTapGestureDetector = new GestureDetector(getContext(), mTapListenerWrapper); + } + + public void setIsDialtoneEnabled(boolean isDialtoneEnabled) { + mIsDialtoneEnabled = isDialtoneEnabled; + } + + /** + * Gets the original image bounds, in view-absolute coordinates. + * + *

The original image bounds are those reported by the hierarchy. The hierarchy itself may + * apply scaling on its own (e.g. due to scale type) so the reported bounds are not necessarily + * the same as the actual bitmap dimensions. In other words, the original image bounds correspond + * to the image bounds within this view when no zoomable transformation is applied, but including + * the potential scaling of the hierarchy. Having the actual bitmap dimensions abstracted away + * from this view greatly simplifies implementation because the actual bitmap may change (e.g. + * when a high-res image arrives and replaces the previously set low-res image). With proper + * hierarchy scaling (e.g. FIT_CENTER), this underlying change will not affect this view nor the + * zoomable transformation in any way. + */ + protected void getImageBounds(RectF outBounds) { + getHierarchy().getActualImageBounds(outBounds); + } + + /** + * Gets the bounds used to limit the translation, in view-absolute coordinates. + * + *

These bounds are passed to the zoomable controller in order to limit the translation. The + * image is attempted to be centered within the limit bounds if the transformed image is smaller. + * There will be no empty spaces within the limit bounds if the transformed image is bigger. This + * applies to each dimension (horizontal and vertical) independently. + * + *

Unless overridden by a subclass, these bounds are same as the view bounds. + */ + protected void getLimitBounds(RectF outBounds) { + outBounds.set(0, 0, getWidth(), getHeight()); + } + + /** + * Sets a custom zoomable controller, instead of using the default one. + */ + public void setZoomableController(ZoomableController zoomableController) { + Preconditions.checkNotNull(zoomableController); + mZoomableController.setListener(null); + mZoomableController = zoomableController; + mZoomableController.setListener(mZoomableListener); + } + + /** + * Gets the zoomable controller. + * + *

Zoomable controller can be used to zoom to point, or to map point from view to image + * coordinates for instance. + */ + public ZoomableController getZoomableController() { + return mZoomableController; + } + + /** + * Check whether the parent view can intercept touch events while zoomed. This can be used, for + * example, to swipe between images in a view pager while zoomed. + * + * @return true if touch events can be intercepted + */ + public boolean allowsTouchInterceptionWhileZoomed() { + return mAllowTouchInterceptionWhileZoomed; + } + + /** + * If this is set to true, parent views can intercept touch events while the view is zoomed. For + * example, this can be used to swipe between images in a view pager while zoomed. + * + * @param allowTouchInterceptionWhileZoomed true if the parent needs to intercept touches + */ + public void setAllowTouchInterceptionWhileZoomed(boolean allowTouchInterceptionWhileZoomed) { + mAllowTouchInterceptionWhileZoomed = allowTouchInterceptionWhileZoomed; + } + + /** + * Sets the tap listener. + */ + public void setTapListener(GestureDetector.SimpleOnGestureListener tapListener) { + mTapListenerWrapper.setListener(tapListener); + } + + /** + * Sets whether long-press tap detection is enabled. Unfortunately, long-press conflicts with + * onDoubleTapEvent. + */ + public void setIsLongpressEnabled(boolean enabled) { + mTapGestureDetector.setIsLongpressEnabled(enabled); + } + + public void setZoomingEnabled(boolean zoomingEnabled) { + mZoomingEnabled = zoomingEnabled; + mZoomableController.setEnabled(false); + } + + /** + * Sets the image controller. + */ + @Override + public void setController(@Nullable DraweeController controller) { + setControllers(controller, null); + } + + /** + * Sets the controllers for the normal and huge image. + * + *

The huge image controller is used after the image gets scaled above a certain threshold. + * + *

IMPORTANT: in order to avoid a flicker when switching to the huge image, the huge image + * controller should have the normal-image-uri set as its low-res-uri. + * + * @param controller controller to be initially used + * @param hugeImageController controller to be used after the client starts zooming-in + */ + public void setControllers( + @Nullable DraweeController controller, @Nullable DraweeController hugeImageController) { + setControllersInternal(null, null); + mZoomableController.setEnabled(false); + setControllersInternal(controller, hugeImageController); + } + + private void setControllersInternal( + @Nullable DraweeController controller, @Nullable DraweeController hugeImageController) { + removeControllerListener(getController()); + addControllerListener(controller); + mHugeImageController = hugeImageController; + super.setController(controller); + } + + private void maybeSetHugeImageController() { + if (mHugeImageController != null + && mZoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD) { + setControllersInternal(mHugeImageController, null); + } + } + + private void removeControllerListener(DraweeController controller) { + if (controller instanceof AbstractDraweeController) { + ((AbstractDraweeController) controller).removeControllerListener(mControllerListener); + } + } + + private void addControllerListener(DraweeController controller) { + if (controller instanceof AbstractDraweeController) { + ((AbstractDraweeController) controller).addControllerListener(mControllerListener); + } + } + + @Override + protected void onDraw(Canvas canvas) { + int saveCount = canvas.save(); + canvas.concat(mZoomableController.getTransform()); + try { + super.onDraw(canvas); + } catch (Exception e) { + DraweeController controller = getController(); + if (controller != null && controller instanceof AbstractDraweeController) { + Object callerContext = ((AbstractDraweeController) controller).getCallerContext(); + if (callerContext != null) { + throw new RuntimeException( + String.format("Exception in onDraw, callerContext=%s", callerContext.toString()), e); + } + } + throw e; + } + canvas.restoreToCount(saveCount); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + 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()); + return true; + } + + if (!mIsDialtoneEnabled && mZoomableController.onTouchEvent(event)) { + FLog.v( + getLogTag(), + "onTouchEvent: %d, view %x, handled by zoomable controller", + a, + this.hashCode()); + if (!mAllowTouchInterceptionWhileZoomed && !mZoomableController.isIdentity()) { + final ViewParent parent = getParent(); + parent.requestDisallowInterceptTouchEvent(true); + } + return true; + } + if (super.onTouchEvent(event)) { + FLog.v(getLogTag(), "onTouchEvent: %d, view %x, handled by the super", a, this.hashCode()); + return true; + } + // None of our components reported that they handled the touch event. Upon returning false + // from this method, our parent won't send us any more events for this gesture. Unfortunately, + // some components may have started a delayed action, such as a long-press timer, and since we + // won't receive an ACTION_UP that would cancel that timer, a false event may be triggered. + // To prevent that we explicitly send one last cancel event when returning false. + MotionEvent cancelEvent = MotionEvent.obtain(event); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + mTapGestureDetector.onTouchEvent(cancelEvent); + mZoomableController.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + return false; + } + + @Override + public int computeHorizontalScrollRange() { + return mZoomableController.computeHorizontalScrollRange(); + } + + @Override + public int computeHorizontalScrollOffset() { + return mZoomableController.computeHorizontalScrollOffset(); + } + + @Override + public int computeHorizontalScrollExtent() { + return mZoomableController.computeHorizontalScrollExtent(); + } + + @Override + public int computeVerticalScrollRange() { + return mZoomableController.computeVerticalScrollRange(); + } + + @Override + public int computeVerticalScrollOffset() { + return mZoomableController.computeVerticalScrollOffset(); + } + + @Override + public int computeVerticalScrollExtent() { + return mZoomableController.computeVerticalScrollExtent(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + FLog.v(getLogTag(), "onLayout: view %x", this.hashCode()); + super.onLayout(changed, left, top, right, bottom); + updateZoomableControllerBounds(); + } + + private void onFinalImageSet() { + FLog.v(getLogTag(), "onFinalImageSet: view %x", this.hashCode()); + if (!mZoomableController.isEnabled() && mZoomingEnabled) { + mZoomableController.setEnabled(true); + updateZoomableControllerBounds(); + } + } + + private void onRelease() { + FLog.v(getLogTag(), "onRelease: view %x", this.hashCode()); + mZoomableController.setEnabled(false); + } + + protected void onTransformChanged(Matrix transform) { + FLog.v(getLogTag(), "onTransformChanged: view %x, transform: %s", this.hashCode(), transform); + maybeSetHugeImageController(); + invalidate(); + } + + protected void updateZoomableControllerBounds() { + getImageBounds(mImageBounds); + getLimitBounds(mViewBounds); + mZoomableController.setImageBounds(mImageBounds); + mZoomableController.setViewBounds(mViewBounds); + FLog.v( + getLogTag(), + "updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s", + this.hashCode(), + mViewBounds, + mImageBounds); + } + + protected Class getLogTag() { + return TAG; + } + + protected ZoomableController createZoomableController() { + return AnimatedZoomableController.newInstance(); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/ImageResizingControllerListener.java b/app/src/main/java/awais/instagrabber/customviews/helpers/ImageResizingControllerListener.java new file mode 100644 index 00000000..2e9a0516 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/ImageResizingControllerListener.java @@ -0,0 +1,43 @@ +package awais.instagrabber.customviews.helpers; + +import android.graphics.drawable.Animatable; +import android.view.ViewGroup; + +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.generic.GenericDraweeHierarchy; +import com.facebook.drawee.view.DraweeView; +import com.facebook.imagepipeline.image.ImageInfo; + +import awais.instagrabber.utils.Utils; + +public class ImageResizingControllerListener> extends BaseControllerListener { + private static final String TAG = "ImageResizingController"; + + private T imageView; + private final int requiredWidth; + + public ImageResizingControllerListener(final T imageView, final int requiredWidth) { + this.imageView = imageView; + this.requiredWidth = requiredWidth; + } + + @Override + public void onIntermediateImageSet(final String id, final ImageInfo imageInfo) { + super.onIntermediateImageSet(id, imageInfo); + } + + public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) { + if (imageInfo != null) { + // updateViewSize(imageInfo); + final int height = imageInfo.getHeight(); + final int width = imageInfo.getWidth(); + // final float aspectRatio = ((float) width) / height; + final ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams(); + // final int deviceWidth = Utils.displayMetrics.widthPixels; + final int resultingHeight = Utils.getResultingHeight(requiredWidth, height, width); + layoutParams.width = requiredWidth; + layoutParams.height = resultingHeight; + imageView.requestLayout(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/PauseGlideOnFlingScrollListener.java b/app/src/main/java/awais/instagrabber/customviews/helpers/PauseGlideOnFlingScrollListener.java new file mode 100644 index 00000000..0249c1fa --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/PauseGlideOnFlingScrollListener.java @@ -0,0 +1,45 @@ +package awais.instagrabber.customviews.helpers; + +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestManager; + +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; + +public class PauseGlideOnFlingScrollListener extends RecyclerView.OnScrollListener { + private static final int FLING_JUMP_LOW_THRESHOLD = 80; + private static final int FLING_JUMP_HIGH_THRESHOLD = 120; + + private final RequestManager glide; + private boolean dragging = false; + + public PauseGlideOnFlingScrollListener(final RequestManager glide) { + this.glide = glide; + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + dragging = newState == SCROLL_STATE_DRAGGING; + if (glide.isPaused()) { + if (newState == SCROLL_STATE_DRAGGING || newState == SCROLL_STATE_IDLE) { + // user is touchy or the scroll finished, show images + glide.resumeRequests(); + } // settling means the user let the screen go, but it can still be flinging + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (!dragging) { + // TODO can be made better by a rolling average of last N calls to smooth out patterns like a,b,a + int currentSpeed = Math.abs(dy); + boolean paused = glide.isPaused(); + if (paused && currentSpeed < FLING_JUMP_LOW_THRESHOLD) { + glide.resumeRequests(); + } else if (!paused && FLING_JUMP_HIGH_THRESHOLD < currentSpeed) { + glide.pauseRequests(); + } + } + } +} 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 e1086d54..ae237d60 100755 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/VideoAwareRecyclerScroller.java @@ -1,297 +1,318 @@ package awais.instagrabber.customviews.helpers; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; +import android.graphics.Point; import android.graphics.Rect; -import android.net.Uri; import android.view.View; -import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.ui.PlayerView; -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 java.util.List; import awais.instagrabber.R; -import awais.instagrabber.activities.CommentsViewer; -import awais.instagrabber.adapters.FeedAdapter; +import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder; import awais.instagrabber.models.FeedModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.Utils; -import static awais.instagrabber.utils.Utils.settingsHelper; +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; public class VideoAwareRecyclerScroller extends RecyclerView.OnScrollListener { private static final String TAG = "VideoAwareRecScroll"; + private static final int FLING_JUMP_LOW_THRESHOLD = 80; + private static final int FLING_JUMP_HIGH_THRESHOLD = 120; private static final Object LOCK = new Object(); private LinearLayoutManager layoutManager; - private View firstItemView, lastItemView; - private int videoPosShown = -1, lastVideoPos = -1, lastChangedVideoPos, lastStoppedVideoPos, lastPlayedVideoPos; - private boolean videoAttached = false; - private SimpleExoPlayer player; - private ImageView btnMute; - private CacheDataSourceFactory cacheDataSourceFactory; - - private final List feedModels; - private final Context context; - private final VideoChangeCallback videoChangeCallback; - private final DefaultDataSourceFactory dataSourceFactory; - - private final View.OnClickListener commentClickListener = new View.OnClickListener() { - @Override - public void onClick(@NonNull final View v) { - final Object tag = v.getTag(); - if (tag instanceof FeedModel && context instanceof Activity) { - if (player != null) player.setPlayWhenReady(false); - ((Activity) context).startActivityForResult(new Intent(context, CommentsViewer.class) - .putExtra(Constants.EXTRAS_SHORTCODE, ((FeedModel) tag).getShortCode()) - .putExtra(Constants.EXTRAS_POST, ((FeedModel) tag).getPostId()) - .putExtra(Constants.EXTRAS_POST, ((FeedModel) tag).getProfileModel().getId()), 6969); - } - } - }; - - private final View.OnClickListener muteClickListener = v -> { - if (player == null) return; - final float intVol = player.getVolume() == 0f ? 1f : 0f; - player.setVolume(intVol); - if (btnMute != null) - btnMute.setImageResource(intVol == 0f ? R.drawable.mute : R.drawable.vol); - Utils.sessionVolumeFull = intVol == 1f; - }; + private boolean dragging; + private boolean isLoadingPaused = false; + private FeedVideoViewHolder currentlyPlayingViewHolder; - public VideoAwareRecyclerScroller(final Context context, final List feedModels, - final VideoChangeCallback videoChangeCallback) { - this.context = context; - this.feedModels = feedModels; - this.videoChangeCallback = videoChangeCallback; - dataSourceFactory = new DefaultDataSourceFactory(context, "instagram"); - final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context); - if (simpleCache != null) { - cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory); + @Override + public void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) { + dragging = newState == SCROLL_STATE_DRAGGING; + if (isLoadingPaused) { + if (newState == SCROLL_STATE_DRAGGING || newState == SCROLL_STATE_IDLE) { + // user is touchy or the scroll finished, show videos + isLoadingPaused = false; + } // settling means the user let the screen go, but it can still be flinging } } @Override public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { + if (!dragging) { + // TODO can be made better by a rolling average of last N calls to smooth out patterns like a,b,a + int currentSpeed = Math.abs(dy); + if (isLoadingPaused && currentSpeed < FLING_JUMP_LOW_THRESHOLD) { + isLoadingPaused = false; + } else if (!isLoadingPaused && FLING_JUMP_HIGH_THRESHOLD < currentSpeed) { + isLoadingPaused = true; + // stop playing video + } + } + if (isLoadingPaused) return; if (layoutManager == null) { final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) this.layoutManager = (LinearLayoutManager) layoutManager; } - if (feedModels.size() == 0 || layoutManager == null) { + if (layoutManager == null) { return; } int firstVisibleItemPos = layoutManager.findFirstCompletelyVisibleItemPosition(); int lastVisibleItemPos = layoutManager.findLastCompletelyVisibleItemPosition(); - if (firstVisibleItemPos == -1 && lastVisibleItemPos == -1) { firstVisibleItemPos = layoutManager.findFirstVisibleItemPosition(); lastVisibleItemPos = layoutManager.findLastVisibleItemPosition(); } - - boolean processFirstItem = false, processLastItem = false; - View currView; - if (firstVisibleItemPos != -1) { - currView = layoutManager.findViewByPosition(firstVisibleItemPos); - if (currView != null && currView.getId() == R.id.videoHolder) { - firstItemView = currView; - processFirstItem = true; - } - } - if (lastVisibleItemPos != -1) { - currView = layoutManager.findViewByPosition(lastVisibleItemPos); - if (currView != null && currView.getId() == R.id.videoHolder) { - lastItemView = currView; - processLastItem = true; - } - } - - final Rect visibleItemRect = new Rect(); - - int firstVisibleItemHeight = 0, lastVisibleItemHeight = 0; - - final boolean isFirstItemVideoHolder = firstItemView != null && firstItemView.getId() == R.id.videoHolder; - if (isFirstItemVideoHolder) { - firstItemView.getGlobalVisibleRect(visibleItemRect); - firstVisibleItemHeight = visibleItemRect.height(); - } - final boolean isLastItemVideoHolder = lastItemView != null && lastItemView.getId() == R.id.videoHolder; - if (isLastItemVideoHolder) { - lastItemView.getGlobalVisibleRect(visibleItemRect); - lastVisibleItemHeight = visibleItemRect.height(); - } - - if (processFirstItem && firstVisibleItemHeight > lastVisibleItemHeight) - videoPosShown = firstVisibleItemPos; - else if (processLastItem && lastVisibleItemHeight != 0) videoPosShown = lastVisibleItemPos; - - if (firstItemView != lastItemView) { - final int mox = lastVisibleItemHeight - firstVisibleItemHeight; - if (processLastItem && lastVisibleItemHeight > firstVisibleItemHeight) - videoPosShown = lastVisibleItemPos; - if ((processFirstItem || processLastItem) && mox >= 0) - videoPosShown = lastVisibleItemPos; - } - - if (lastChangedVideoPos != -1 && lastVideoPos != -1) { - currView = layoutManager.findViewByPosition(lastChangedVideoPos); - if (currView != null && currView.getId() == R.id.videoHolder && - lastStoppedVideoPos != lastChangedVideoPos && lastPlayedVideoPos != lastChangedVideoPos) { - lastStoppedVideoPos = lastChangedVideoPos; - stopVideo(lastChangedVideoPos, recyclerView, currView); - } - - currView = layoutManager.findViewByPosition(lastVideoPos); - if (currView != null && currView.getId() == R.id.videoHolder) { - final Rect rect = new Rect(); - currView.getGlobalVisibleRect(rect); - - final int holderTop = currView.getTop(); - final int holderHeight = currView.getBottom() - holderTop; - final int halfHeight = holderHeight / 2; - //halfHeight -= halfHeight / 5; - - if (rect.height() < halfHeight) { - if (lastStoppedVideoPos != lastVideoPos) { - lastStoppedVideoPos = lastVideoPos; - stopVideo(lastVideoPos, recyclerView, currView); - } - } else if (lastPlayedVideoPos != lastVideoPos) { - lastPlayedVideoPos = lastVideoPos; - playVideo(lastVideoPos, recyclerView, currView); + synchronized (LOCK) { + final FeedVideoViewHolder videoHolder = getFirstVideoHolder(recyclerView, firstVisibleItemPos, lastVisibleItemPos); + if (videoHolder == null || videoHolder.getCurrentFeedModel() == null) { + if (currentlyPlayingViewHolder != null) { + currentlyPlayingViewHolder.stopPlaying(); + currentlyPlayingViewHolder = null; } + return; } - - if (lastChangedVideoPos != lastVideoPos) lastChangedVideoPos = lastVideoPos; - } - - if (lastVideoPos != -1 && lastVideoPos != videoPosShown) { - if (videoAttached) { - //if ((currView = layoutManager.findViewByPosition(lastVideoPos)) != null && currView.getId() == R.id.videoHolder) - releaseVideo(lastVideoPos, recyclerView, null); - videoAttached = false; + if (currentlyPlayingViewHolder != null && currentlyPlayingViewHolder.getCurrentFeedModel().getPostId() + .equals(videoHolder.getCurrentFeedModel().getPostId())) { + return; } - } - if (videoPosShown != -1) { - lastVideoPos = videoPosShown; - if (!videoAttached) { - if ((currView = layoutManager.findViewByPosition(videoPosShown)) != null && currView.getId() == R.id.videoHolder) - attachVideo(videoPosShown, recyclerView, currView); - videoAttached = true; + if (currentlyPlayingViewHolder != null) { + currentlyPlayingViewHolder.stopPlaying(); } + videoHolder.startPlaying(); + currentlyPlayingViewHolder = videoHolder; } + // boolean processFirstItem = false, processLastItem = false; + // View currView; + // if (firstVisibleItemPos != -1) { + // currView = layoutManager.findViewByPosition(firstVisibleItemPos); + // if (currView != null && currView.getId() == R.id.videoHolder) { + // firstItemView = currView; + // // processFirstItem = true; + // } + // } + // if (lastVisibleItemPos != -1) { + // currView = layoutManager.findViewByPosition(lastVisibleItemPos); + // if (currView != null && currView.getId() == R.id.videoHolder) { + // lastItemView = currView; + // // processLastItem = true; + // } + // } + // if (firstItemView == null && lastItemView == null) { + // return; + // } + // if (firstItemView != null) { + // + // Log.d(TAG, "view" + viewHolder); + // } + // if (lastItemView != null) { + // final FeedVideoViewHolder viewHolder = (FeedVideoViewHolder) recyclerView.getChildViewHolder(lastItemView); + // Log.d(TAG, "view" + viewHolder); + // } + // Log.d(TAG, firstItemView + " " + lastItemView); + + // final Rect visibleItemRect = new Rect(); + + // int firstVisibleItemHeight = 0, lastVisibleItemHeight = 0; + + // final boolean isFirstItemVideoHolder = firstItemView != null && firstItemView.getId() == R.id.videoHolder; + // if (isFirstItemVideoHolder) { + // firstItemView.getGlobalVisibleRect(visibleItemRect); + // firstVisibleItemHeight = visibleItemRect.height(); + // } + // final boolean isLastItemVideoHolder = lastItemView != null && lastItemView.getId() == R.id.videoHolder; + // if (isLastItemVideoHolder) { + // lastItemView.getGlobalVisibleRect(visibleItemRect); + // lastVisibleItemHeight = visibleItemRect.height(); + // } + // + // if (processFirstItem && firstVisibleItemHeight > lastVisibleItemHeight) + // videoPosShown = firstVisibleItemPos; + // else if (processLastItem && lastVisibleItemHeight != 0) videoPosShown = lastVisibleItemPos; + // + // if (firstItemView != lastItemView) { + // final int mox = lastVisibleItemHeight - firstVisibleItemHeight; + // if (processLastItem && lastVisibleItemHeight > firstVisibleItemHeight) + // videoPosShown = lastVisibleItemPos; + // if ((processFirstItem || processLastItem) && mox >= 0) + // videoPosShown = lastVisibleItemPos; + // } + // + // if (lastChangedVideoPos != -1 && lastVideoPos != -1) { + // currView = layoutManager.findViewByPosition(lastChangedVideoPos); + // if (currView != null && currView.getId() == R.id.videoHolder && + // lastStoppedVideoPos != lastChangedVideoPos && lastPlayedVideoPos != lastChangedVideoPos) { + // lastStoppedVideoPos = lastChangedVideoPos; + // stopVideo(lastChangedVideoPos, recyclerView, currView); + // } + // + // currView = layoutManager.findViewByPosition(lastVideoPos); + // if (currView != null && currView.getId() == R.id.videoHolder) { + // final Rect rect = new Rect(); + // currView.getGlobalVisibleRect(rect); + // + // final int holderTop = currView.getTop(); + // final int holderHeight = currView.getBottom() - holderTop; + // final int halfHeight = holderHeight / 2; + // //halfHeight -= halfHeight / 5; + // + // if (rect.height() < halfHeight) { + // if (lastStoppedVideoPos != lastVideoPos) { + // lastStoppedVideoPos = lastVideoPos; + // stopVideo(lastVideoPos, recyclerView, currView); + // } + // } else if (lastPlayedVideoPos != lastVideoPos) { + // lastPlayedVideoPos = lastVideoPos; + // playVideo(lastVideoPos, recyclerView, currView); + // } + // } + // + // if (lastChangedVideoPos != lastVideoPos) lastChangedVideoPos = lastVideoPos; + // } + // + // if (lastVideoPos != -1 && lastVideoPos != videoPosShown) { + // if (videoAttached) { + // //if ((currView = layoutManager.findViewByPosition(lastVideoPos)) != null && currView.getId() == R.id.videoHolder) + // releaseVideo(lastVideoPos, recyclerView, null); + // videoAttached = false; + // } + // } + // if (videoPosShown != -1) { + // lastVideoPos = videoPosShown; + // if (!videoAttached) { + // if ((currView = layoutManager.findViewByPosition(videoPosShown)) != null && currView.getId() == R.id.videoHolder) + // attachVideo(videoPosShown, recyclerView, currView); + // videoAttached = true; + // } + // } } - 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 (player != null) { - player.stop(true); - player.release(); - player = null; - } - - final boolean shouldAutoplay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); - player = new SimpleExoPlayer.Builder(context) - .setUseLazyPreparation(!shouldAutoplay) - .build(); - player.setPlayWhenReady(shouldAutoplay); - - if (itemView != null) { - final Object tag = itemView.getTag(); - - final View btnComments = itemView.findViewById(R.id.btnComments); - if (btnComments != null && tag instanceof FeedModel) { - final FeedModel feedModel = (FeedModel) tag; - - if (feedModel.getCommentsCount() <= 0) btnComments.setEnabled(false); - else { - btnComments.setTag(feedModel); - btnComments.setEnabled(true); - btnComments.setOnClickListener(commentClickListener); - } + private FeedVideoViewHolder getFirstVideoHolder(final RecyclerView recyclerView, final int firstVisibleItemPos, final int lastVisibleItemPos) { + final Rect visibleItemRect = new Rect(); + final Point offset = new Point(); + 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); + if (viewSwitcher == null) { + continue; } - - final PlayerView playerView = itemView.findViewById(R.id.playerView); - if (playerView == null) return; - 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 boolean result = viewSwitcher.getGlobalVisibleRect(visibleItemRect, offset); + if (!result) continue; + final FeedVideoViewHolder viewHolder = (FeedVideoViewHolder) recyclerView.getChildViewHolder(view); + final FeedModel currentFeedModel = viewHolder.getCurrentFeedModel(); + visibleItemRect.offset(-offset.x, -offset.y); + final int visibleHeight = visibleItemRect.height(); + if (visibleHeight < currentFeedModel.getImageHeight()) { + continue; } - final DataSource.Factory factory = cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory; - final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(factory); - final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(Uri.parse(feedModels.get(itemPos).getDisplayUrl())); - - player.setRepeatMode(Player.REPEAT_MODE_ALL); - player.prepare(mediaSource); - player.setVolume(vol); - - playerView.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); + // Log.d(TAG, "post:" + currentFeedModel.getPostId() + ", visibleHeight: " + visibleHeight + ", post height: " + currentFeedModel.getImageHeight()); + return viewHolder; } - - if (videoChangeCallback != null) videoChangeCallback.playerChanged(itemPos, player); } + return null; } - private void releaseVideo(final int itemPos, final RecyclerView recyclerView, final View itemView) { -// Log.d("AWAISKING_APP", "release: " + itemPos); +// 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 = 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(); - } - } +// +// 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/models/FeedModel.java b/app/src/main/java/awais/instagrabber/models/FeedModel.java index 9eeeda1a..1a63233f 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedModel.java @@ -1,15 +1,17 @@ package awais.instagrabber.models; -import awais.instagrabber.models.enums.MediaItemType; - import org.json.JSONObject; +import awais.instagrabber.models.enums.MediaItemType; + public final class FeedModel extends PostModel { private final ProfileModel profileModel; private final long commentsCount, viewCount; private boolean captionExpanded = false, mentionClicked = false; private final JSONObject location; private ViewerPostModel[] sliderItems; + private int imageWidth; + private int imageHeight; 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, @@ -61,4 +63,20 @@ public final class FeedModel extends PostModel { public void toggleCaption() { captionExpanded = !captionExpanded; } + + public int getImageWidth() { + return imageWidth; + } + + public void setImageWidth(final int imageWidth) { + this.imageWidth = imageWidth; + } + + public void setImageHeight(final int imageHeight) { + this.imageHeight = imageHeight; + } + + public int getImageHeight() { + return imageHeight; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java index 97433550..727b0a1b 100755 --- a/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java @@ -1,10 +1,33 @@ package awais.instagrabber.models.enums; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; public enum MediaItemType implements Serializable { - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, - MEDIA_TYPE_SLIDER, - MEDIA_TYPE_VOICE, + MEDIA_TYPE_IMAGE(1), + MEDIA_TYPE_VIDEO(2), + MEDIA_TYPE_SLIDER(3), + MEDIA_TYPE_VOICE(4); + + private final int id; + private static Map map = new HashMap<>(); + + static { + for (MediaItemType type : MediaItemType.values()) { + map.put(type.id, type); + } + } + + MediaItemType(final int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static MediaItemType valueOf(final int id) { + return map.get(id); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index 22af0b1b..f96920e3 100755 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -245,8 +245,7 @@ public final class Utils { || nextChar == '\r' || nextChar == '\n') { break; } - } - else if (currChar == '.') + } else if (currChar == '.') break; // for merged hashtags @@ -293,8 +292,7 @@ public final class Utils { if (currRes > lastResMain && !low) { lastResMain = currRes; lastIndexMain = i; - } - else if (currRes < lastResMain && low) { + } else if (currRes < lastResMain && low) { lastResMain = currRes; lastIndexMain = i; } @@ -302,8 +300,7 @@ public final class Utils { if (currRes > lastResBase && !low) { lastResBase = currRes; lastIndexBase = i; - } - else if (currRes < lastResBase && low) { + } else if (currRes < lastResBase && low) { lastResBase = currRes; lastIndexBase = i; } @@ -326,7 +323,8 @@ public final class Utils { public static String getHighQualityImage(final JSONObject resources) { String src = null; try { - if (resources.has("display_resources")) src = getHighQualityPost(resources.getJSONArray("display_resources"), false, false, false); + if (resources.has("display_resources")) + src = getHighQualityPost(resources.getJSONArray("display_resources"), false, false, false); else if (resources.has("image_versions2")) src = getHighQualityPost(resources.getJSONObject("image_versions2").getJSONArray("candidates"), false, true, false); if (src == null) return resources.getString("display_url"); @@ -390,8 +388,9 @@ public final class Utils { } else if (mediaType == MediaItemType.MEDIA_TYPE_SLIDER) { final JSONArray carouselMedia = mediaObj.optJSONArray("carousel_media"); - if (carouselMedia != null) thumbnail = Utils.getItemThumbnail(carouselMedia.getJSONObject(0) - .getJSONObject("image_versions2").getJSONArray("candidates")); + if (carouselMedia != null) + thumbnail = Utils.getItemThumbnail(carouselMedia.getJSONObject(0) + .getJSONObject("image_versions2").getJSONArray("candidates")); } return thumbnail; @@ -624,13 +623,15 @@ public final class Utils { final JSONArray seenUserIdsArray = visualMedia.getJSONArray("seen_user_ids"); final int seenUsersLen = seenUserIdsArray.length(); final String[] seenUserIds = new String[seenUsersLen]; - for (int j = 0; j < seenUsersLen; j++) seenUserIds[j] = seenUserIdsArray.getString(j); + for (int j = 0; j < seenUsersLen; j++) + seenUserIds[j] = seenUserIdsArray.getString(j); RavenExpiringMediaActionSummaryModel expiringSummaryModel = null; final JSONObject actionSummary = visualMedia.optJSONObject("expiring_media_action_summary"); - if (actionSummary != null) expiringSummaryModel = new RavenExpiringMediaActionSummaryModel( - actionSummary.getLong("timestamp"), actionSummary.getInt("count"), - getExpiringMediaType(actionSummary.getString("type"))); + if (actionSummary != null) + expiringSummaryModel = new RavenExpiringMediaActionSummaryModel( + actionSummary.getLong("timestamp"), actionSummary.getInt("count"), + getExpiringMediaType(actionSummary.getString("type"))); final RavenMediaViewType viewType; final String viewMode = visualMedia.getString("view_mode"); @@ -692,11 +693,11 @@ public final class Utils { final JSONObject actionLog = itemObject.getJSONObject("action_log"); String desc = actionLog.getString("description"); JSONArray bold = actionLog.getJSONArray("bold"); - for (int q=0; q < bold.length(); ++q) { + for (int q = 0; q < bold.length(); ++q) { JSONObject boldItem = bold.getJSONObject(q); - desc = desc.substring(0, boldItem.getInt("start") + q*7) + "" - + desc.substring(boldItem.getInt("start") + q*7, boldItem.getInt("end") + q*7) - + "" + desc.substring(boldItem.getInt("end") + q*7); + desc = desc.substring(0, boldItem.getInt("start") + q * 7) + "" + + desc.substring(boldItem.getInt("start") + q * 7, boldItem.getInt("end") + q * 7) + + "" + desc.substring(boldItem.getInt("end") + q * 7); } actionLogModel = new DirectItemActionLogModel(desc); break; @@ -972,14 +973,17 @@ public final class Utils { saveFile, file -> { model.setDownloaded(true); - if (saved != null) saved.deselectSelection(selectedItem); - else if (mainActivity != null) mainActivity.mainHelper.deselectSelection(selectedItem); + if (saved != null) + saved.deselectSelection(selectedItem); + else if (mainActivity != null) + mainActivity.mainHelper.deselectSelection(selectedItem); else if (pv != null) pv.deselectSelection(selectedItem); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } else { if (saved != null) saved.deselectSelection(selectedItem); - else if (mainActivity != null) mainActivity.mainHelper.deselectSelection(selectedItem); + else if (mainActivity != null) + mainActivity.mainHelper.deselectSelection(selectedItem); else if (pv != null) pv.deselectSelection(selectedItem); } }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -990,7 +994,7 @@ public final class Utils { } public static void dmDownload(@NonNull final Context context, @Nullable final String username, final DownloadMethod method, - final List itemsToDownload) { + final List itemsToDownload) { if (settingsHelper == null) settingsHelper = new SettingsHelper(context); if (itemsToDownload == null || itemsToDownload.size() < 1) return; @@ -1002,7 +1006,7 @@ public final class Utils { } private static void dmDownloadImpl(@NonNull final Context context, @Nullable final String username, - final DownloadMethod method, final List itemsToDownload) { + final DownloadMethod method, final List itemsToDownload) { File dir = new File(Environment.getExternalStorageDirectory(), "Download"); if (settingsHelper.getBoolean(FOLDER_SAVE_TO)) { @@ -1129,10 +1133,12 @@ public final class Utils { if (v == passwordParent) importExportBinding.cbPassword.performClick(); else if (v == exportLoginsParent) importExportBinding.cbExportLogins.performClick(); - else if (v == exportFavoritesParent) importExportBinding.cbExportFavorites.performClick(); + else if (v == exportFavoritesParent) + importExportBinding.cbExportFavorites.performClick(); else if (v == importLoginsParent) importExportBinding.cbImportLogins.performClick(); - else if (v == importFavoritesParent) importExportBinding.cbImportFavorites.performClick(); + else if (v == importFavoritesParent) + importExportBinding.cbImportFavorites.performClick(); else if (v == exportSettingsParent) importExportBinding.cbExportSettings.performClick(); else if (v == importSettingsParent) importExportBinding.cbImportSettings.performClick(); @@ -1151,9 +1157,12 @@ public final class Utils { final File file = new File(path, "InstaGrabber_Settings_" + System.currentTimeMillis() + ".zaai"); final String password = passwordChecked ? text.toString() : null; int flags = 0; - if (importExportBinding.cbExportFavorites.isChecked()) flags |= ExportImportUtils.FLAG_FAVORITES; - if (importExportBinding.cbExportSettings.isChecked()) flags |= ExportImportUtils.FLAG_SETTINGS; - if (importExportBinding.cbExportLogins.isChecked()) flags |= ExportImportUtils.FLAG_COOKIES; + if (importExportBinding.cbExportFavorites.isChecked()) + flags |= ExportImportUtils.FLAG_FAVORITES; + if (importExportBinding.cbExportSettings.isChecked()) + flags |= ExportImportUtils.FLAG_SETTINGS; + if (importExportBinding.cbExportLogins.isChecked()) + flags |= ExportImportUtils.FLAG_COOKIES; ExportImportUtils.Export(password, flags, file, result -> { Toast.makeText(context, result ? R.string.dialog_export_success : R.string.dialog_export_failed, Toast.LENGTH_SHORT).show(); @@ -1166,9 +1175,12 @@ public final class Utils { } else if (v == importExportBinding.btnImport) { new DirectoryChooser().setInitialDirectory(folderPath).setShowZaAiConfigFiles(true).setInteractionListener(path -> { int flags = 0; - if (importExportBinding.cbImportFavorites.isChecked()) flags |= ExportImportUtils.FLAG_FAVORITES; - if (importExportBinding.cbImportSettings.isChecked()) flags |= ExportImportUtils.FLAG_SETTINGS; - if (importExportBinding.cbImportLogins.isChecked()) flags |= ExportImportUtils.FLAG_COOKIES; + if (importExportBinding.cbImportFavorites.isChecked()) + flags |= ExportImportUtils.FLAG_FAVORITES; + if (importExportBinding.cbImportSettings.isChecked()) + flags |= ExportImportUtils.FLAG_SETTINGS; + if (importExportBinding.cbImportLogins.isChecked()) + flags |= ExportImportUtils.FLAG_COOKIES; ExportImportUtils.Import(context, flags, new File(path), result -> { ((AppCompatActivity) context).recreate(); @@ -1205,9 +1217,8 @@ public final class Utils { if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } - return "ig_sig_key_version="+Constants.SIGNATURE_VERSION+"&signed_body=" + hexString.toString() + "." + message; - } - catch (Exception e) { + return "ig_sig_key_version=" + Constants.SIGNATURE_VERSION + "&signed_body=" + hexString.toString() + "." + message; + } catch (Exception e) { Log.e(TAG, "Error signing", e); return null; } @@ -1251,9 +1262,10 @@ public final class Utils { final String scheme = itemUri.getScheme(); if (isEmpty(scheme)) mimeType = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(itemUri.toString()).toLowerCase()); - else mimeType = scheme.equals(ContentResolver.SCHEME_CONTENT) ? contentResolver.getType(itemUri) - : mimeTypeMap.getMimeTypeFromExtension - (MimeTypeMap.getFileExtensionFromUrl(itemUri.toString()).toLowerCase()); + else + mimeType = scheme.equals(ContentResolver.SCHEME_CONTENT) ? contentResolver.getType(itemUri) + : mimeTypeMap.getMimeTypeFromExtension + (MimeTypeMap.getFileExtensionFromUrl(itemUri.toString()).toLowerCase()); if (isEmpty(mimeType)) return true; mimeType = mimeType.toLowerCase(); @@ -1418,4 +1430,12 @@ public final class Utils { } return simpleCache; } + + public static int getResultingHeight(final int requiredWidth, final int height, final int width) { + return requiredWidth * height / width; + } + + public static int getResultingWidth(final int requiredHeight, final int height, final int width) { + return requiredHeight * width / height; + } } \ 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 new file mode 100644 index 00000000..a0706c60 --- /dev/null +++ b/app/src/main/res/drawable/rounder_corner_semi_black_bg.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 74b7e606..cbefb539 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -71,7 +71,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - tools:listitem="@layout/item_feed" /> + tools:listitem="@layout/item_feed_photo" /> diff --git a/app/src/main/res/layout/item_feed.xml b/app/src/main/res/layout/item_feed.xml deleted file mode 100755 index e48d0cbb..00000000 --- a/app/src/main/res/layout/item_feed.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_feed_bottom.xml b/app/src/main/res/layout/item_feed_bottom.xml index 2e5e104b..5f37390d 100755 --- a/app/src/main/res/layout/item_feed_bottom.xml +++ b/app/src/main/res/layout/item_feed_bottom.xml @@ -16,8 +16,9 @@ + android:layout_toEndOf="@id/videoViewsContainer" + android:layout_toRightOf="@id/videoViewsContainer"> + tools:text="BOTTOM TEXT" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_feed_photo.xml b/app/src/main/res/layout/item_feed_photo.xml new file mode 100644 index 00000000..f52b2483 --- /dev/null +++ b/app/src/main/res/layout/item_feed_photo.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_feed_slider.xml b/app/src/main/res/layout/item_feed_slider.xml index 9da36039..782c8bfa 100755 --- a/app/src/main/res/layout/item_feed_slider.xml +++ b/app/src/main/res/layout/item_feed_slider.xml @@ -1,13 +1,13 @@ + android:orientation="vertical"> - + + android:layout_height="wrap_content" /> + android:padding="5dp" + android:textColor="@android:color/white" + android:background="@drawable/rounder_corner_semi_black_bg" + android:textAppearance="@style/TextAppearance.AppCompat.Caption"/> - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_feed_top.xml b/app/src/main/res/layout/item_feed_top.xml index 0e0991d1..84211c59 100755 --- a/app/src/main/res/layout/item_feed_top.xml +++ b/app/src/main/res/layout/item_feed_top.xml @@ -1,48 +1,45 @@ - + android:paddingRight="4dp" + android:weightSum="2"> + android:textSize="18sp" /> diff --git a/app/src/main/res/layout/item_feed_video.xml b/app/src/main/res/layout/item_feed_video.xml index dd11ce5e..fa25c3a5 100755 --- a/app/src/main/res/layout/item_feed_video.xml +++ b/app/src/main/res/layout/item_feed_video.xml @@ -1,23 +1,52 @@ + android:orientation="vertical"> - + - + android:layout_height="wrap_content"> - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_feed_view.xml b/app/src/main/res/layout/layout_feed_view.xml index d77333e0..adcf4f50 100644 --- a/app/src/main/res/layout/layout_feed_view.xml +++ b/app/src/main/res/layout/layout_feed_view.xml @@ -44,6 +44,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - tools:listitem="@layout/item_feed" /> + tools:listitem="@layout/item_feed_photo" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_profile_view.xml b/app/src/main/res/layout/layout_profile_view.xml index 15554aef..1aefe2b5 100644 --- a/app/src/main/res/layout/layout_profile_view.xml +++ b/app/src/main/res/layout/layout_profile_view.xml @@ -1,16 +1,15 @@ - + android:layout_height="wrap_content" + android:background="@null"> + android:visibility="gone" + tools:text="https://austinhuang.me/" /> + android:visibility="gone" + app:backgroundTint="@color/btn_pink_background" /> + android:visibility="gone" + app:backgroundTint="@color/btn_orange_background" /> + android:visibility="gone" + app:backgroundTint="@color/btn_red_background" /> + android:visibility="gone" + app:backgroundTint="@color/btn_blue_background" /> + android:visibility="gone" + app:backgroundTint="@color/btn_orange_background" /> + android:visibility="gone" + app:backgroundTint="@color/btn_lightpink_background" /> @@ -219,9 +218,7 @@ + android:layout_height="@dimen/profile_picture_size" /> + android:visibility="gone" + tools:text="https://austinhuang.me/" /> diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index 113cdba4..28705129 100755 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -31,6 +31,7 @@ @color/text_color_light #efefef + #80000000 #000000