BarInsta/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java

1262 lines
63 KiB
Java

package awais.instagrabber.fragments;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.GestureDetectorCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.adapters.StoriesAdapter;
import awais.instagrabber.customviews.helpers.SwipeGestureListener;
import awais.instagrabber.databinding.FragmentStoryViewerBinding;
import awais.instagrabber.fragments.main.ProfileFragmentDirections;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.interfaces.SwipeEvent;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.stickers.PollModel;
import awais.instagrabber.models.stickers.QuestionModel;
import awais.instagrabber.models.stickers.QuizModel;
import awais.instagrabber.models.stickers.SliderModel;
import awais.instagrabber.models.stickers.SwipeUpModel;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.requests.StoryViewerOptions.Type;
import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds;
import awais.instagrabber.repositories.responses.stories.Broadcast;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.ArchivesViewModel;
import awais.instagrabber.viewmodels.FeedStoriesViewModel;
import awais.instagrabber.viewmodels.HighlightsViewModel;
import awais.instagrabber.viewmodels.StoriesViewModel;
import awais.instagrabber.webservices.DirectMessagesRepository;
import awais.instagrabber.webservices.MediaRepository;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesRepository;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_THRESHOLD;
import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD;
import static awais.instagrabber.fragments.settings.PreferenceKeys.MARK_AS_SEEN;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class StoryViewerFragment extends Fragment {
private static final String TAG = "StoryViewerFragment";
private final String cookie = settingsHelper.getString(Constants.COOKIE);
private AppCompatActivity fragmentActivity;
private View root;
private FragmentStoryViewerBinding binding;
private String currentStoryUsername;
private String highlightTitle;
private StoriesAdapter storiesAdapter;
private SwipeEvent swipeEvent;
private GestureDetectorCompat gestureDetector;
private StoriesRepository storiesRepository;
private MediaRepository mediaRepository;
private StoryModel currentStory;
private Broadcast live;
private int slidePos;
private int lastSlidePos;
private String url;
private PollModel poll;
private QuestionModel question;
private String[] mentions;
private QuizModel quiz;
private SliderModel slider;
private MenuItem menuDownload, menuDm, menuProfile;
private SimpleExoPlayer player;
// private boolean isHashtag;
// private boolean isLoc;
// private String highlight;
private String actionBarTitle, actionBarSubtitle;
private boolean fetching = false, sticking = false, shouldRefresh = true;
private boolean downloadVisible = false, dmVisible = false, profileVisible = true;
private int currentFeedStoryIndex;
private double sliderValue;
private StoriesViewModel storiesViewModel;
private ViewModel viewModel;
// private boolean isHighlight;
// private boolean isArchive;
// private boolean isNotification;
private DirectMessagesRepository directMessagesRepository;
private StoryViewerOptions options;
private String csrfToken;
private String deviceId;
private long userId;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (csrfToken == null) return;
userId = CookieUtils.getUserIdFromCookie(cookie);
deviceId = settingsHelper.getString(Constants.DEVICE_UUID);
fragmentActivity = (AppCompatActivity) requireActivity();
storiesRepository = StoriesRepository.Companion.getInstance();
mediaRepository = MediaRepository.Companion.getInstance();
directMessagesRepository = DirectMessagesRepository.Companion.getInstance();
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentStoryViewerBinding.inflate(inflater, container, false);
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
init();
shouldRefresh = false;
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater menuInflater) {
menuInflater.inflate(R.menu.story_menu, menu);
menuDownload = menu.findItem(R.id.action_download);
menuDm = menu.findItem(R.id.action_dms);
menuProfile = menu.findItem(R.id.action_profile);
menuDownload.setVisible(downloadVisible);
menuDm.setVisible(dmVisible);
menuProfile.setVisible(profileVisible);
}
@Override
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
// hide menu items from activity
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final Context context = getContext();
if (context == null) return false;
int itemId = item.getItemId();
if (itemId == R.id.action_download) {
downloadStory();
return true;
}
if (itemId == R.id.action_dms) {
final EditText input = new EditText(context);
input.setHint(R.string.reply_hint);
final AlertDialog ad = new AlertDialog.Builder(context)
.setTitle(R.string.reply_story)
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> directMessagesRepository.broadcastStoryReply(
csrfToken,
userId,
deviceId,
ThreadIdsOrUserIds.Companion.ofOneUser(String.valueOf(currentStory.getUserId())),
input.getText().toString(),
currentStory.getStoryMediaId(),
String.valueOf(currentStory.getUserId()),
CoroutineUtilsKt.getContinuation(
(directThreadBroadcastResponse, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
Log.e(TAG, "onFailure: ", throwable1);
return;
}
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
}), Dispatchers.getIO()
)
))
.setNegativeButton(R.string.cancel, null)
.show();
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s));
}
@Override
public void afterTextChanged(final Editable s) {}
});
return true;
}
if (itemId == R.id.action_profile) {
openProfile("@" + currentStory.getUsername());
}
return false;
}
@Override
public void onPause() {
super.onPause();
if (player != null) {
player.pause();
}
}
@Override
public void onResume() {
super.onResume();
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(actionBarTitle);
actionBar.setSubtitle(actionBarSubtitle);
}
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
releasePlayer();
// reset subtitle
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(null);
}
super.onDestroy();
}
private void init() {
if (getArguments() == null) return;
final StoryViewerFragmentArgs fragmentArgs = StoryViewerFragmentArgs.fromBundle(getArguments());
options = fragmentArgs.getOptions();
currentFeedStoryIndex = options.getCurrentFeedStoryIndex();
// highlight = fragmentArgs.getHighlight();
// isHighlight = !TextUtils.isEmpty(highlight);
// isArchive = fragmentArgs.getIsArchive();
// isNotification = fragmentArgs.getIsNotification();
final Type type = options.getType();
if (currentFeedStoryIndex >= 0) {
switch (type) {
case HIGHLIGHT:
viewModel = new ViewModelProvider(fragmentActivity).get(HighlightsViewModel.class);
break;
case STORY_ARCHIVE:
viewModel = new ViewModelProvider(fragmentActivity).get(ArchivesViewModel.class);
break;
default:
case FEED_STORY_POSITION:
viewModel = new ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel.class);
break;
}
}
setupStories();
}
private void setupStories() {
storiesViewModel = new ViewModelProvider(this).get(StoriesViewModel.class);
setupListeners();
final Context context = getContext();
if (context == null) return;
binding.storiesList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
storiesAdapter = new StoriesAdapter((model, position) -> {
currentStory = model;
slidePos = position;
refreshStory();
});
binding.storiesList.setAdapter(storiesAdapter);
storiesViewModel.getList().observe(fragmentActivity, storiesAdapter::submitList);
resetView();
}
@SuppressLint("ClickableViewAccessibility")
private void setupListeners() {
final boolean hasFeedStories;
List<?> models = null;
if (currentFeedStoryIndex >= 0) {
final Type type = options.getType();
switch (type) {
case HIGHLIGHT:
final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel;
models = highlightsViewModel.getList().getValue();
break;
case FEED_STORY_POSITION:
final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel;
models = feedStoriesViewModel.getList().getValue();
break;
case STORY_ARCHIVE:
final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel;
models = archivesViewModel.getList().getValue();
break;
}
}
hasFeedStories = models != null && !models.isEmpty();
final List<?> finalModels = models;
final Context context = getContext();
if (context == null) return;
swipeEvent = isRightSwipe -> {
final List<StoryModel> storyModels = storiesViewModel.getList().getValue();
final int storiesLen = storyModels == null ? 0 : storyModels.size();
if (sticking) {
Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_SHORT).show();
return;
}
if (storiesLen <= 0) return;
final boolean isLeftSwipe = !isRightSwipe;
final boolean endOfCurrentStories = slidePos + 1 >= storiesLen;
final boolean swipingBeyondCurrentStories = (endOfCurrentStories && isLeftSwipe) || (slidePos == 0 && isRightSwipe);
if (swipingBeyondCurrentStories && hasFeedStories) {
final int index = currentFeedStoryIndex;
if ((isRightSwipe && index == 0) || (isLeftSwipe && index == finalModels.size() - 1)) {
Toast.makeText(context, R.string.no_more_stories, Toast.LENGTH_SHORT).show();
return;
}
final Object feedStoryModel = isRightSwipe
? finalModels.get(index - 1)
: finalModels.size() == index + 1 ? null : finalModels.get(index + 1);
paginateStories(feedStoryModel, finalModels.get(index), context, isRightSwipe, currentFeedStoryIndex == finalModels.size() - 2);
return;
}
if (isRightSwipe) {
if (--slidePos <= 0) {
slidePos = 0;
}
} else if (++slidePos >= storiesLen) {
slidePos = storiesLen - 1;
}
currentStory = storyModels.get(slidePos);
refreshStory();
};
gestureDetector = new GestureDetectorCompat(context, new SwipeGestureListener(swipeEvent));
binding.playerView.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
final GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) {
final float diffX = e2.getX() - e1.getX();
try {
if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && Math.abs(diffX) > SWIPE_THRESHOLD
&& Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
swipeEvent.onSwipe(diffX > 0);
return true;
}
} catch (final Exception e) {
// if (logCollector != null)
// logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "setupListeners",
// new Pair<>("swipeEvent", swipeEvent),
// new Pair<>("diffX", diffX));
if (BuildConfig.DEBUG) Log.e(TAG, "Error", e);
}
return false;
}
};
if (hasFeedStories) {
binding.btnBackward.setVisibility(currentFeedStoryIndex == 0 ? View.INVISIBLE : View.VISIBLE);
binding.btnForward.setVisibility(currentFeedStoryIndex == finalModels.size() - 1 ? View.INVISIBLE : View.VISIBLE);
binding.btnBackward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex - 1),
finalModels.get(currentFeedStoryIndex),
context, true, false));
binding.btnForward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex + 1),
finalModels.get(currentFeedStoryIndex),
context, false,
currentFeedStoryIndex == finalModels.size() - 2));
}
binding.imageViewer.setTapListener(simpleOnGestureListener);
binding.spotify.setOnClickListener(v -> {
final Object tag = v.getTag();
if (tag instanceof CharSequence) {
Utils.openURL(context, tag.toString());
}
});
binding.swipeUp.setOnClickListener(v -> {
final Object tag = v.getTag();
if (tag instanceof CharSequence) {
new AlertDialog.Builder(context)
.setTitle(R.string.swipe_up_confirmation)
.setMessage(tag.toString()).setPositiveButton(R.string.yes, (d, w) -> Utils.openURL(context, tag.toString()))
.setNegativeButton(R.string.no, (d, w) -> d.dismiss()).show();
}
});
binding.viewStoryPost.setOnClickListener(v -> {
final Object tag = v.getTag();
if (!(tag instanceof CharSequence)) return;
final String mediaId = tag.toString();
final AlertDialog alertDialog = new AlertDialog.Builder(context)
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
mediaRepository.fetch(
Long.parseLong(mediaId),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
alertDialog.dismiss();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "openPostDialog: ", e);
}
}), Dispatchers.getIO())
);
});
final View.OnClickListener storyActionListener = v -> {
final Object tag = v.getTag();
if (tag instanceof PollModel) {
poll = (PollModel) tag;
if (poll.getMyChoice() > -1) {
new AlertDialog.Builder(context)
.setTitle(R.string.voted_story_poll)
.setAdapter(new ArrayAdapter<>(
context,
android.R.layout.simple_list_item_1,
new String[]{
(poll.getMyChoice() == 0 ? "" : "") + poll.getLeftChoice() + " (" + poll.getLeftCount() + ")",
(poll.getMyChoice() == 1 ? "" : "") + poll.getRightChoice() + " (" + poll.getRightCount() + ")"
}),
null)
.setPositiveButton(R.string.ok, null)
.show();
} else {
new AlertDialog.Builder(context)
.setTitle(poll.getQuestion())
.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[]{
poll.getLeftChoice() + " (" + poll.getLeftCount() + ")",
poll.getRightChoice() + " (" + poll.getRightCount() + ")"
}), (d, w) -> {
sticking = true;
storiesRepository.respondToPoll(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
poll.getId(),
w,
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
})
.setPositiveButton(R.string.cancel, null)
.show();
}
} else if (tag instanceof QuestionModel) {
question = (QuestionModel) tag;
final EditText input = new EditText(context);
input.setHint(R.string.answer_hint);
final AlertDialog ad = new AlertDialog.Builder(context)
.setTitle(question.getQuestion())
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true;
storiesRepository.respondToQuestion(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
question.getId(),
input.getText().toString(),
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s));
}
@Override
public void afterTextChanged(final Editable s) {}
});
} else if (tag instanceof String[]) {
mentions = (String[]) tag;
new AlertDialog.Builder(context)
.setTitle(R.string.story_mentions)
.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, mentions), (d, w) -> openProfile(mentions[w]))
.setPositiveButton(R.string.cancel, null)
.show();
} else if (tag instanceof QuizModel) {
String[] choices = new String[quiz.getChoices().length];
for (int q = 0; q < choices.length; ++q) {
choices[q] = (quiz.getMyChoice() == q ? "" : "") + quiz.getChoices()[q] + " (" + quiz.getCounts()[q] + ")";
}
new AlertDialog.Builder(context)
.setTitle(quiz.getMyChoice() > -1 ? getString(R.string.story_quizzed) : quiz.getQuestion())
.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, choices), (d, w) -> {
if (quiz.getMyChoice() == -1) {
sticking = true;
storiesRepository.respondToQuiz(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
quiz.getId(),
w,
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
}
})
.setPositiveButton(R.string.cancel, null)
.show();
} else if (tag instanceof SliderModel) {
slider = (SliderModel) tag;
NumberFormat percentage = NumberFormat.getPercentInstance();
percentage.setMaximumFractionDigits(2);
LinearLayout sliderView = new LinearLayout(context);
sliderView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
sliderView.setOrientation(LinearLayout.VERTICAL);
TextView tv = new TextView(context);
tv.setGravity(Gravity.CENTER_HORIZONTAL);
final SeekBar input = new SeekBar(context);
double avg = slider.getAverage() * 100;
input.setProgress((int) avg);
sliderView.addView(input);
sliderView.addView(tv);
if (slider.getMyChoice().isNaN() && slider.canVote()) {
input.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
sliderValue = progress / 100.0;
tv.setText(percentage.format(sliderValue));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
new AlertDialog.Builder(context)
.setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion())
.setMessage(getResources().getQuantityString(R.plurals.slider_info,
slider.getVoteCount(),
slider.getVoteCount(),
percentage.format(slider.getAverage())))
.setView(sliderView)
.setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true;
storiesRepository.respondToSlider(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
slider.getId(),
sliderValue,
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}), Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
} else {
input.setEnabled(false);
tv.setText(getString(R.string.slider_answer, percentage.format(slider.getMyChoice())));
new AlertDialog.Builder(context)
.setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion())
.setMessage(getResources().getQuantityString(R.plurals.slider_info,
slider.getVoteCount(),
slider.getVoteCount(),
percentage.format(slider.getAverage())))
.setView(sliderView)
.setPositiveButton(R.string.ok, null)
.show();
}
}
};
binding.poll.setOnClickListener(storyActionListener);
binding.answer.setOnClickListener(storyActionListener);
binding.mention.setOnClickListener(storyActionListener);
binding.quiz.setOnClickListener(storyActionListener);
binding.slider.setOnClickListener(storyActionListener);
}
private void resetView() {
final Context context = getContext();
if (context == null) return;
live = null;
slidePos = 0;
lastSlidePos = 0;
if (menuDownload != null) menuDownload.setVisible(false);
if (menuDm != null) menuDm.setVisible(false);
if (menuProfile != null) menuProfile.setVisible(false);
downloadVisible = false;
dmVisible = false;
profileVisible = false;
binding.imageViewer.setController(null);
releasePlayer();
String currentStoryMediaId = null;
final Type type = options.getType();
StoryViewerOptions fetchOptions = null;
switch (type) {
case HIGHLIGHT: {
final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel;
final List<Story> models = highlightsViewModel.getList().getValue();
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
final Story model = models.get(currentFeedStoryIndex);
currentStoryMediaId = model.getId();
fetchOptions = StoryViewerOptions.forHighlight(model.getId());
highlightTitle = model.getTitle();
break;
}
case FEED_STORY_POSITION: {
final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel;
final List<Story> models = feedStoriesViewModel.getList().getValue();
if (models == null || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) return;
final Story model = models.get(currentFeedStoryIndex);
currentStoryMediaId = model.getId();
currentStoryUsername = model.getUser().getUsername();
fetchOptions = StoryViewerOptions.forUser(Long.parseLong(currentStoryMediaId), currentStoryUsername);
live = model.getBroadcast();
break;
}
case STORY_ARCHIVE: {
final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel;
final List<Story> models = archivesViewModel.getList().getValue();
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
final Story model = models.get(currentFeedStoryIndex);
currentStoryMediaId = parseStoryMediaId(model.getId());
currentStoryUsername = model.getTitle();
fetchOptions = StoryViewerOptions.forStoryArchive(model.getId());
break;
}
}
if (type == Type.USER) {
currentStoryMediaId = String.valueOf(options.getId());
currentStoryUsername = options.getName();
fetchOptions = StoryViewerOptions.forUser(options.getId(), currentStoryUsername);
}
setTitle(type);
storiesViewModel.getList().setValue(Collections.emptyList());
if (type == Type.STORY) {
storiesRepository.fetch(
options.getId(),
CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", throwable);
return;
}
fetching = false;
binding.storiesList.setVisibility(View.GONE);
if (storyModel == null) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
return;
}
storiesViewModel.getList().setValue(Collections.singletonList(storyModel));
currentStory = storyModel;
refreshStory();
}), Dispatchers.getIO())
);
return;
}
if (currentStoryMediaId == null) return;
if (live != null) {
currentStory = null;
refreshLive();
return;
}
final ServiceCallback<List<StoryModel>> storyCallback = new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
fetching = false;
if (storyModels == null || storyModels.isEmpty()) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
binding.storiesList.setVisibility(View.GONE);
return;
}
binding.storiesList.setVisibility((storyModels.size() == 1 && currentFeedStoryIndex == -1) ? View.GONE : View.VISIBLE);
if (currentFeedStoryIndex == -1) {
binding.btnBackward.setVisibility(View.GONE);
binding.btnForward.setVisibility(View.GONE);
}
storiesViewModel.getList().setValue(storyModels);
currentStory = storyModels.get(0);
refreshStory();
}
@Override
public void onFailure(final Throwable t) {
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", t);
}
};
storiesRepository.getUserStory(
fetchOptions,
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
storyCallback.onFailure(throwable);
return;
}
//noinspection unchecked
storyCallback.onSuccess((List<StoryModel>) storyModels);
}), Dispatchers.getIO())
);
}
private void setTitle(final Type type) {
final boolean hasUsername = !TextUtils.isEmpty(currentStoryUsername);
if (type == Type.HIGHLIGHT) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBarTitle = highlightTitle;
actionBar.setTitle(highlightTitle);
}
} else if (hasUsername) {
currentStoryUsername = currentStoryUsername.replace("@", "");
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBarTitle = currentStoryUsername;
actionBar.setTitle(currentStoryUsername);
}
}
}
private synchronized void refreshLive() {
binding.storiesList.setVisibility(View.INVISIBLE);
binding.viewStoryPost.setVisibility(View.GONE);
binding.spotify.setVisibility(View.GONE);
binding.poll.setVisibility(View.GONE);
binding.answer.setVisibility(View.GONE);
binding.mention.setVisibility(View.GONE);
binding.quiz.setVisibility(View.GONE);
binding.slider.setVisibility(View.GONE);
lastSlidePos = slidePos;
releasePlayer();
url = live.getDashPlaybackUrl();
setupLive();
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
actionBarSubtitle = TextUtils.epochSecondToString(live.getPublishedTime());
if (actionBar != null) {
try {
actionBar.setSubtitle(actionBarSubtitle);
} catch (Exception e) {
Log.e(TAG, "refreshLive: ", e);
}
}
}
private synchronized void refreshStory() {
if (binding.storiesList.getVisibility() == View.VISIBLE) {
final List<StoryModel> storyModels = storiesViewModel.getList().getValue();
if (storyModels != null && storyModels.size() > 0) {
StoryModel item = storyModels.get(lastSlidePos);
if (item != null) {
item.setCurrentSlide(false);
storiesAdapter.notifyItemChanged(lastSlidePos, item);
}
item = storyModels.get(slidePos);
if (item != null) {
item.setCurrentSlide(true);
storiesAdapter.notifyItemChanged(slidePos, item);
}
}
}
lastSlidePos = slidePos;
final MediaItemType itemType = currentStory.getItemType();
url = itemType == MediaItemType.MEDIA_TYPE_IMAGE ? currentStory.getStoryUrl() : currentStory.getVideoUrl();
final String shortCode = currentStory.getTappableShortCode();
binding.viewStoryPost.setVisibility(shortCode != null ? View.VISIBLE : View.GONE);
binding.viewStoryPost.setTag(shortCode);
final String spotify = currentStory.getSpotify();
binding.spotify.setVisibility(spotify != null ? View.VISIBLE : View.GONE);
binding.spotify.setTag(spotify);
poll = currentStory.getPoll();
binding.poll.setVisibility(poll != null ? View.VISIBLE : View.GONE);
binding.poll.setTag(poll);
question = currentStory.getQuestion();
binding.answer.setVisibility((question != null) ? View.VISIBLE : View.GONE);
binding.answer.setTag(question);
mentions = currentStory.getMentions();
binding.mention.setVisibility((mentions != null && mentions.length > 0) ? View.VISIBLE : View.GONE);
binding.mention.setTag(mentions);
quiz = currentStory.getQuiz();
binding.quiz.setVisibility(quiz != null ? View.VISIBLE : View.GONE);
binding.quiz.setTag(quiz);
slider = currentStory.getSlider();
binding.slider.setVisibility(slider != null ? View.VISIBLE : View.GONE);
binding.slider.setTag(slider);
final SwipeUpModel swipeUp = currentStory.getSwipeUp();
if (swipeUp != null) {
binding.swipeUp.setVisibility(View.VISIBLE);
binding.swipeUp.setText(swipeUp.getText());
binding.swipeUp.setTag(swipeUp.getUrl());
} else binding.swipeUp.setVisibility(View.GONE);
releasePlayer();
final Type type = options.getType();
if (type == Type.HASHTAG || type == Type.LOCATION) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBarTitle = currentStory.getUsername();
actionBar.setTitle(currentStory.getUsername());
}
}
if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) setupVideo();
else setupImage();
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
actionBarSubtitle = TextUtils.epochSecondToString(currentStory.getTimestamp());
if (actionBar != null) {
try {
actionBar.setSubtitle(actionBarSubtitle);
} catch (Exception e) {
Log.e(TAG, "refreshStory: ", e);
}
}
if (settingsHelper.getBoolean(MARK_AS_SEEN))
storiesRepository.seen(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO())
);
}
private void downloadStory() {
final Context context = getContext();
if (context == null) return;
if (currentStory == null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
DownloadUtils.download(context, currentStory);
}
private void setupImage() {
binding.progressView.setVisibility(View.VISIBLE);
binding.playerView.setVisibility(View.GONE);
binding.imageViewer.setVisibility(View.VISIBLE);
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
.setLocalThumbnailPreviewsEnabled(true)
.setProgressiveRenderingEnabled(true)
.build();
final DraweeController controller = Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setOldController(binding.imageViewer.getController())
.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFailure(final String id, final Throwable throwable) {
binding.progressView.setVisibility(View.GONE);
}
@Override
public void onFinalImageSet(final String id,
final ImageInfo imageInfo,
final Animatable animatable) {
if (menuDownload != null) {
downloadVisible = true;
menuDownload.setVisible(true);
}
if (currentStory.getCanReply() && menuDm != null) {
dmVisible = true;
menuDm.setVisible(true);
}
if (!TextUtils.isEmpty(currentStory.getUsername())) {
profileVisible = true;
menuProfile.setVisible(true);
}
binding.progressView.setVisibility(View.GONE);
}
})
.build();
binding.imageViewer.setController(controller);
}
private void setupVideo() {
binding.playerView.setVisibility(View.VISIBLE);
binding.progressView.setVisibility(View.GONE);
binding.imageViewer.setVisibility(View.GONE);
binding.imageViewer.setController(null);
final Context context = getContext();
if (context == null) return;
player = new SimpleExoPlayer.Builder(context).build();
binding.playerView.setPlayer(player);
player.setPlayWhenReady(settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES));
final Uri uri = Uri.parse(url);
final MediaItem mediaItem = MediaItem.fromUri(uri);
final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(mediaItem);
mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() {
@Override
public void onLoadCompleted(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
if (menuDownload != null) {
downloadVisible = true;
menuDownload.setVisible(true);
}
if (currentStory.getCanReply() && menuDm != null) {
dmVisible = true;
menuDm.setVisible(true);
}
if (!TextUtils.isEmpty(currentStory.getUsername()) && menuProfile != null) {
profileVisible = true;
menuProfile.setVisible(true);
}
binding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadStarted(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
if (menuDownload != null) {
downloadVisible = true;
menuDownload.setVisible(true);
}
if (currentStory.getCanReply() && menuDm != null) {
dmVisible = true;
menuDm.setVisible(true);
}
if (!TextUtils.isEmpty(currentStory.getUsername()) && menuProfile != null) {
profileVisible = true;
menuProfile.setVisible(true);
}
binding.progressView.setVisibility(View.VISIBLE);
}
@Override
public void onLoadCanceled(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
binding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadError(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData,
@NonNull final IOException error,
final boolean wasCanceled) {
if (menuDownload != null) {
downloadVisible = false;
menuDownload.setVisible(false);
}
if (menuDm != null) {
dmVisible = false;
menuDm.setVisible(false);
}
if (menuProfile != null) {
profileVisible = false;
menuProfile.setVisible(false);
}
binding.progressView.setVisibility(View.GONE);
}
});
player.setMediaSource(mediaSource);
player.prepare();
binding.playerView.setOnClickListener(v -> {
if (player != null) {
if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0);
player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying());
}
});
}
private void setupLive() {
binding.playerView.setVisibility(View.VISIBLE);
binding.progressView.setVisibility(View.GONE);
binding.imageViewer.setVisibility(View.GONE);
binding.imageViewer.setController(null);
if (menuDownload != null) menuDownload.setVisible(false);
if (menuDm != null) menuDm.setVisible(false);
final Context context = getContext();
if (context == null) return;
player = new SimpleExoPlayer.Builder(context).build();
binding.playerView.setPlayer(player);
player.setPlayWhenReady(settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES));
final Uri uri = Uri.parse(url);
final MediaItem mediaItem = MediaItem.fromUri(uri);
final DashMediaSource mediaSource = new DashMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(mediaItem);
mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() {
@Override
public void onLoadCompleted(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
binding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadStarted(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
binding.progressView.setVisibility(View.VISIBLE);
}
@Override
public void onLoadCanceled(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData) {
binding.progressView.setVisibility(View.GONE);
}
@Override
public void onLoadError(final int windowIndex,
@Nullable final MediaSource.MediaPeriodId mediaPeriodId,
@NonNull final LoadEventInfo loadEventInfo,
@NonNull final MediaLoadData mediaLoadData,
@NonNull final IOException error,
final boolean wasCanceled) {
binding.progressView.setVisibility(View.GONE);
}
});
player.setMediaSource(mediaSource);
player.prepare();
binding.playerView.setOnClickListener(v -> {
if (player != null) {
if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0);
player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying());
}
});
}
private void openProfile(final String username) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(null);
}
final char t = username.charAt(0);
if (t == '@') {
final NavDirections action = HashTagFragmentDirections.actionGlobalProfileFragment(username);
NavHostFragment.findNavController(this).navigate(action);
} else if (t == '#') {
final NavDirections action = HashTagFragmentDirections.actionGlobalHashTagFragment(username.substring(1));
NavHostFragment.findNavController(this).navigate(action);
} else {
final NavDirections action = ProfileFragmentDirections
.actionGlobalLocationFragment(Long.parseLong(username.split(" \\(")[1].replace(")", "")));
NavHostFragment.findNavController(this).navigate(action);
}
}
private void releasePlayer() {
if (player == null) return;
try { player.stop(true); } catch (Exception ignored) { }
try { player.release(); } catch (Exception ignored) { }
player = null;
}
private void paginateStories(Object newFeedStory, Object oldFeedStory, Context context, boolean backward, boolean last) {
if (newFeedStory != null) {
if (fetching) {
Toast.makeText(context, R.string.be_patient, Toast.LENGTH_SHORT).show();
return;
}
if (settingsHelper.getBoolean(MARK_AS_SEEN)
&& oldFeedStory instanceof Story
&& viewModel instanceof FeedStoriesViewModel) {
final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel;
final Story oldFeedStoryModel = (Story) oldFeedStory;
if (oldFeedStoryModel.getSeen() == null || !oldFeedStoryModel.getSeen().equals(oldFeedStoryModel.getLatestReelMedia())) {
oldFeedStoryModel.setSeen(oldFeedStoryModel.getLatestReelMedia());
final List<Story> models = feedStoriesViewModel.getList().getValue();
final List<Story> modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models);
modelsCopy.set(currentFeedStoryIndex, oldFeedStoryModel);
feedStoriesViewModel.getList().postValue(models);
}
}
fetching = true;
binding.btnBackward.setVisibility(currentFeedStoryIndex == 1 && backward ? View.INVISIBLE : View.VISIBLE);
binding.btnForward.setVisibility(last ? View.INVISIBLE : View.VISIBLE);
currentFeedStoryIndex = backward ? (currentFeedStoryIndex - 1) : (currentFeedStoryIndex + 1);
resetView();
}
}
/**
* Parses the Story's media ID. For user stories this is a number, but for archive stories
* this is "archiveDay:" plus a number.
*/
private static String parseStoryMediaId(String rawId) {
final String regex = "(?:archiveDay:)?(.+)";
final Pattern pattern = Pattern.compile(regex);
final Matcher matcher = pattern.matcher(rawId);
if (matcher.matches() && matcher.groupCount() >= 1) {
return matcher.group(1);
}
return rawId;
}
}