From b997504602785368be51ed4e65a3aa85b746bfd2 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Fri, 4 Jun 2021 07:17:24 +0900 Subject: [PATCH] Convert MediaRepository and MediaService to kotlin. --- .../instagrabber/activities/MainActivity.kt | 36 +- .../fragments/LikesViewerFragment.java | 27 +- .../NotificationsViewerFragment.java | 45 +-- .../fragments/StoryViewerFragment.java | 65 ++-- .../fragments/imageedit/FiltersFragment.java | 53 +-- .../fragments/main/DiscoverFragment.java | 57 ++-- .../fragments/main/ProfileFragment.java | 47 +-- .../instagrabber/managers/ThreadManager.kt | 107 +----- .../repositories/MediaRepository.java | 55 --- .../repositories/MediaRepository.kt | 57 ++++ .../awais/instagrabber/utils/MediaUploader.kt | 40 +-- .../viewmodels/PostViewV2ViewModel.kt | 182 +++++----- .../webservices/MediaService.java | 317 ------------------ .../instagrabber/webservices/MediaService.kt | 169 ++++++++++ 14 files changed, 546 insertions(+), 711 deletions(-) delete mode 100644 app/src/main/java/awais/instagrabber/repositories/MediaRepository.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt delete mode 100644 app/src/main/java/awais/instagrabber/webservices/MediaService.java create mode 100644 app/src/main/java/awais/instagrabber/webservices/MediaService.kt diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt index 960d897d..1c185133 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavController.OnDestinationChangedListener import androidx.navigation.NavDestination @@ -68,6 +69,8 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.textfield.TextInputLayout import com.google.common.collect.ImmutableList import com.google.common.collect.Iterators +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.* import java.util.stream.Collectors @@ -81,11 +84,14 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private var isActivityCheckerServiceBound = false private var isBackStackEmpty = false private var isLoggedIn = false + private var deviceUuid: String? = null + private var csrfToken: String? = null + private var userId: Long = 0 // private var behavior: HideBottomViewOnScrollBehavior? = null var currentTabs: List = emptyList() private set - private var showBottomViewDestinations: List = emptyList() + private var showBottomViewDestinations: List = emptyList() private var graphQLService: GraphQLService? = null private var mediaService: MediaService? = null @@ -157,17 +163,17 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private fun setupCookie() { val cookie = Utils.settingsHelper.getString(Constants.COOKIE) - var userId: Long = 0 - var csrfToken: String? = null - if (!isEmpty(cookie)) { + userId = 0 + csrfToken = null + if (cookie.isNotBlank()) { userId = getUserIdFromCookie(cookie) csrfToken = getCsrfTokenFromCookie(cookie) } - if (isEmpty(cookie) || userId == 0L || isEmpty(csrfToken)) { + if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) { isLoggedIn = false return } - val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) + deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) if (isEmpty(deviceUuid)) { Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString()) } @@ -175,6 +181,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL isLoggedIn = true } + @Suppress("unused") private fun initDmService() { if (!isLoggedIn) return val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH) @@ -628,7 +635,9 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL .setView(R.layout.dialog_opening_post) .create() if (graphQLService == null) graphQLService = GraphQLService.getInstance() - if (mediaService == null) mediaService = MediaService.getInstance(null, null, 0L) + if (mediaService == null) { + mediaService = deviceUuid?.let { csrfToken?.let { it1 -> MediaService.getInstance(it, it1, userId) } } + } val postCb: ServiceCallback = object : ServiceCallback { override fun onSuccess(feedModel: Media?) { if (feedModel != null) { @@ -650,7 +659,18 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL } } alertDialog.show() - if (isLoggedIn) mediaService?.fetch(shortcodeToId(shortCode), postCb) else graphQLService?.fetchPost(shortCode, postCb) + if (isLoggedIn) { + lifecycleScope.launch(Dispatchers.IO) { + try { + val media = mediaService?.fetch(shortcodeToId(shortCode)) + postCb.onSuccess(media) + } catch (e: Exception) { + postCb.onFailure(e) + } + } + } else { + graphQLService?.fetchPost(shortCode, postCb) + } } private fun showLocationView(intentModel: IntentModel) { diff --git a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java index c1f41568..a58016f3 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -25,12 +25,15 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentLikesBinding; import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -104,9 +107,12 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); final String cookie = settingsHelper.getString(Constants.COOKIE); - isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; - // final AppCompatActivity fragmentActivity = (AppCompatActivity) getActivity(); - mediaService = isLoggedIn ? MediaService.getInstance(null, null, 0) : null; + final long userId = CookieUtils.getUserIdFromCookie(cookie); + isLoggedIn = !TextUtils.isEmpty(cookie) && userId != 0; + final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + if (csrfToken == null) return; + mediaService = isLoggedIn ? MediaService.getInstance(deviceUuid, csrfToken, userId) : null; graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); // setHasOptionsMenu(true); } @@ -130,7 +136,20 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme if (isComment && !isLoggedIn) { lazyLoader.resetState(); graphQLService.fetchCommentLikers(postId, null, anonCb); - } else mediaService.fetchLikes(postId, isComment, cb); + } else { + mediaService.fetchLikes( + postId, + isComment, + CoroutineUtilsKt.getContinuation((users, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + //noinspection unchecked + cb.onSuccess((List) users); + }), Dispatchers.getIO()) + ); + } } private void init() { diff --git a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java index c6ccff9f..8271f9af 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -35,12 +35,13 @@ import awais.instagrabber.databinding.FragmentNotificationsViewerBinding; import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.notification.Notification; import awais.instagrabber.repositories.responses.notification.NotificationArgs; import awais.instagrabber.repositories.responses.notification.NotificationImage; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.NotificationViewModel; @@ -48,6 +49,7 @@ import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -106,26 +108,25 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(mediaId, new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(NotificationsViewerFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "onSuccess: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - }); + mediaService.fetch( + 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(NotificationsViewerFragment.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, "onSuccess: ", e); + } + }), Dispatchers.getIO()) + ); } } @@ -218,11 +219,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe if (TextUtils.isEmpty(cookie)) { Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show(); } - mediaService = MediaService.getInstance(null, null, 0); final long userId = CookieUtils.getUserIdFromCookie(cookie); deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId); + mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); newsService = NewsService.getInstance(); } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index 8ce173de..ff1af8c6 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -85,8 +85,8 @@ 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.ThreadIdOrUserIds; -import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.StoryStickerResponse; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CoroutineUtilsKt; @@ -159,7 +159,7 @@ public class StoryViewerFragment extends Fragment { final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID); fragmentActivity = (AppCompatActivity) requireActivity(); storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId); - mediaService = MediaService.getInstance(null, null, 0); + mediaService = MediaService.getInstance(deviceId, csrfToken, userIdFromCookie); directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); setHasOptionsMenu(true); } @@ -220,7 +220,7 @@ public class StoryViewerFragment extends Fragment { .setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread( Collections.singletonList(currentStory.getUserId()), null, - CoroutineUtilsKt.getContinuation((thread, throwable) -> { + CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { if (throwable != null) { Log.e(TAG, "onOptionsItemSelected: ", throwable); Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); @@ -231,17 +231,19 @@ public class StoryViewerFragment extends Fragment { input.getText().toString(), currentStory.getStoryMediaId(), String.valueOf(currentStory.getUserId()), - CoroutineUtilsKt.getContinuation((directThreadBroadcastResponse, throwable1) -> { - 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()) + 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() + ) ); - }, Dispatchers.getIO()) + }), Dispatchers.getIO()) )) .setNegativeButton(R.string.cancel, null) .show(); @@ -451,26 +453,25 @@ public class StoryViewerFragment extends Fragment { .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(Long.parseLong(mediaId), new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "openPostDialog: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - }); + mediaService.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(); diff --git a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java index fd29212f..4c83b007 100644 --- a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java @@ -55,6 +55,7 @@ import awais.instagrabber.viewmodels.ImageEditViewModel; import jp.co.cyberagent.android.gpuimage.GPUImage; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup; +import kotlinx.coroutines.Dispatchers; public class FiltersFragment extends Fragment { private static final String TAG = FiltersFragment.class.getSimpleName(); @@ -461,31 +462,33 @@ public class FiltersFragment extends Fragment { filtersAdapter.setSelected(position); appliedFilter = filter; }; - BitmapUtils.getThumbnail(context, sourceUri, CoroutineUtilsKt.getContinuation((bitmapResult, throwable) -> { - if (throwable != null) { - Log.e(TAG, "setupFilters: ", throwable); - return; - } - if (bitmapResult == null || bitmapResult.getBitmap() == null) { - return; - } - filtersAdapter = new FiltersAdapter( - tuningFilters.values() - .stream() - .map(Filter::getInstance) - .collect(Collectors.toList()), - sourceUri.toString(), - bitmapResult.getBitmap(), - onFilterClickListener - ); - appExecutors.getMainThread().execute(() -> { - binding.filters.setAdapter(filtersAdapter); - filtersAdapter.submitList(FiltersHelper.getFilters(), () -> { - if (appliedFilter == null) return; - filtersAdapter.setSelectedFilter(appliedFilter.getInstance()); - }); - }); - })); + BitmapUtils.getThumbnail( + context, + sourceUri, + CoroutineUtilsKt.getContinuation((bitmapResult, throwable) -> appExecutors.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "setupFilters: ", throwable); + return; + } + if (bitmapResult == null || bitmapResult.getBitmap() == null) { + return; + } + filtersAdapter = new FiltersAdapter( + tuningFilters.values() + .stream() + .map(Filter::getInstance) + .collect(Collectors.toList()), + sourceUri.toString(), + bitmapResult.getBitmap(), + onFilterClickListener + ); + binding.filters.setAdapter(filtersAdapter); + filtersAdapter.submitList(FiltersHelper.getFilters(), () -> { + if (appliedFilter == null) return; + filtersAdapter.setSelectedFilter(appliedFilter.getInstance()); + }); + }), Dispatchers.getIO()) + ); addInitialFilter(); binding.preview.setFilter(filterGroup); } diff --git a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java index 26ee6ce9..6e115d70 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java @@ -30,11 +30,16 @@ import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.discover.TopicCluster; import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.TopicClusterViewModel; import awais.instagrabber.webservices.DiscoverService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "DiscoverFragment"; @@ -52,7 +57,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); discoverService = DiscoverService.getInstance(); - mediaService = MediaService.getInstance(null, null, 0); + final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); + final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + final long userId = CookieUtils.getUserIdFromCookie(cookie); + mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); } @Override @@ -104,29 +113,29 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(Long.valueOf(coverMedia.getPk()), new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(DiscoverFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "onSuccess: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - try { - Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - catch (Throwable e) {} - } - }); + final String pk = coverMedia.getPk(); + if (pk == null) return; + mediaService.fetch( + Long.parseLong(pk), + CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + alertDialog.dismiss(); + try { + Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Throwable ignored) {} + return; + } + final NavController navController = NavHostFragment.findNavController(DiscoverFragment.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, "onTopicLongClick: ", e); + } + }), Dispatchers.getIO()) + ); } }; final DiscoverTopicsAdapter adapter = new DiscoverTopicsAdapter(otcl); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 7e892024..10365ef1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -84,6 +84,7 @@ import awais.instagrabber.repositories.responses.FriendshipStatus; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.UserProfileContextLink; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CoroutineUtilsKt; @@ -335,7 +336,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe friendshipService = isLoggedIn ? FriendshipService.getInstance(deviceUuid, csrfToken, myId) : null; directMessagesService = isLoggedIn ? DirectMessagesService.getInstance(csrfToken, myId, deviceUuid) : null; storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null; - mediaService = isLoggedIn ? MediaService.getInstance(null, null, 0) : null; + mediaService = isLoggedIn ? MediaService.getInstance(deviceUuid, csrfToken, myId) : null; userService = isLoggedIn ? UserService.getInstance() : null; graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); final Context context = getContext(); @@ -821,26 +822,26 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Utils.copyText(context, biography); break; case 1: - mediaService.translate(String.valueOf(profileModel.getPk()), "3", new ServiceCallback() { - @Override - public void onSuccess(final String result) { - if (TextUtils.isEmpty(result)) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - new AlertDialog.Builder(context) - .setTitle(profileModel.getUsername()) - .setMessage(result) - .setPositiveButton(R.string.ok, null) - .show(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error translating bio", t); - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); + mediaService.translate(String.valueOf(profileModel.getPk()), "3", CoroutineUtilsKt.getContinuation( + (result, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error translating bio", throwable); + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + return; + } + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT) + .show(); + return; + } + new AlertDialog.Builder(context) + .setTitle(profileModel.getUsername()) + .setMessage(result) + .setPositiveButton(R.string.ok, null) + .show(); + }), + Dispatchers.getIO() + )); break; } }) @@ -1079,7 +1080,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe directMessagesService.createThread( Collections.singletonList(profileModel.getPk()), null, - CoroutineUtilsKt.getContinuation((thread, throwable) -> { + CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { if (throwable != null) { Log.e(TAG, "setupCommonListeners: ", throwable); Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); @@ -1092,7 +1093,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe inboxManager.addThread(thread, 0); } fragmentActivity.navigateToThread(thread.getThreadId(), profileModel.getUsername()); - }, Dispatchers.getIO()) + }), Dispatchers.getIO()) ); }); } diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt index 8a1953db..c23707f4 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -41,8 +41,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import java.io.File import java.io.IOException import java.net.HttpURLConnection @@ -457,40 +455,15 @@ class ThreadManager private constructor( "4", null ) - val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) - uploadFinishRequest.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - scope.launch(Dispatchers.IO) { - try { - val request = service.broadcastVoice( - clientContext, - threadIdOrUserIds, - uploadDmVoiceOptions.uploadId, - waveform, - samplingFreq - ) - parseResponse(request, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, directItem)) - Log.e(TAG, "sendVoice: ", e) - } - } - return - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data) - return - } - data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") - } - - override fun onFailure(call: Call, t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "sendVoice: ", t) - } - }) + mediaService.uploadFinish(uploadFinishOptions) + val broadcastResponse = service.broadcastVoice( + clientContext, + threadIdOrUserIds, + uploadDmVoiceOptions.uploadId, + waveform, + samplingFreq + ) + parseResponse(broadcastResponse, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendVoice: ", e) @@ -806,39 +779,15 @@ class ThreadManager private constructor( "2", VideoOptions(duration / 1000f, emptyList(), 0, false) ) - val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) - uploadFinishRequest.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - scope.launch(Dispatchers.IO) { - try { - val response1 = service.broadcastVideo( - clientContext, - threadIdOrUserIds, - uploadDmVideoOptions.uploadId, - "", - true - ) - parseResponse(response1, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, null)) - Log.e(TAG, "sendVideo: ", e) - } - } - return - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data) - return - } - data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") - } - override fun onFailure(call: Call, t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "sendVideo: ", t) - } - }) + mediaService.uploadFinish(uploadFinishOptions) + val broadcastResponse = service.broadcastVideo( + clientContext, + threadIdOrUserIds, + uploadDmVideoOptions.uploadId, + "", + true + ) + parseResponse(broadcastResponse, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, directItem)) Log.e(TAG, "sendVideo: ", e) @@ -900,26 +849,6 @@ class ThreadManager private constructor( } } - private fun handleErrorBody( - call: Call<*>, - response: Response<*>, - data: MutableLiveData>?, - ) { - try { - val string = response.errorBody()?.string() ?: "" - val msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string) - data?.postValue(error(msg, null)) - Log.e(TAG, msg) - } catch (e: IOException) { - data?.postValue(error(e.message, null)) - Log.e(TAG, "onResponse: ", e) - } - } - private fun handleInvalidResponse( data: MutableLiveData>, response: MediaUploadResponse, diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java deleted file mode 100644 index f2c78b11..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package awais.instagrabber.repositories; - -import java.util.Map; - -import awais.instagrabber.repositories.responses.LikersResponse; -import awais.instagrabber.repositories.responses.MediaInfoResponse; -import retrofit2.Call; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.POST; -import retrofit2.http.Path; -import retrofit2.http.Query; -import retrofit2.http.QueryMap; - -public interface MediaRepository { - @GET("/api/v1/media/{mediaId}/info/") - Call fetch(@Path("mediaId") final long mediaId); - - @GET("/api/v1/media/{mediaId}/{action}/") - Call fetchLikes(@Path("mediaId") final String mediaId, - @Path("action") final String action); // one of "likers" or "comment_likers" - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/{action}/") - Call action(@Path("action") final String action, - @Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/edit_media/") - Call editCaption(@Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); - - @GET("/api/v1/language/translate/") - Call translate(@QueryMap final Map form); - - @FormUrlEncoded - @POST("/api/v1/media/upload_finish/") - Call uploadFinish(@Header("retry_context") final String retryContext, - @QueryMap Map queryParams, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/delete/") - Call delete(@Path("mediaId") final String mediaId, - @Query("media_type") final String mediaType, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/archive/") - Call archive(@Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt new file mode 100644 index 00000000..ba0708e3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt @@ -0,0 +1,57 @@ +package awais.instagrabber.repositories + +import awais.instagrabber.repositories.responses.LikersResponse +import awais.instagrabber.repositories.responses.MediaInfoResponse +import retrofit2.http.* + +interface MediaRepository { + @GET("/api/v1/media/{mediaId}/info/") + suspend fun fetch(@Path("mediaId") mediaId: Long): MediaInfoResponse + + @GET("/api/v1/media/{mediaId}/{action}/") + suspend fun fetchLikes( + @Path("mediaId") mediaId: String, // one of "likers" or "comment_likers" + @Path("action") action: String, + ): LikersResponse + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/{action}/") + suspend fun action( + @Path("action") action: String, + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/edit_media/") + suspend fun editCaption( + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String + + @GET("/api/v1/language/translate/") + suspend fun translate(@QueryMap form: Map): String + + @FormUrlEncoded + @POST("/api/v1/media/upload_finish/") + suspend fun uploadFinish( + @Header("retry_context") retryContext: String, + @QueryMap queryParams: Map, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/delete/") + suspend fun delete( + @Path("mediaId") mediaId: String, + @Query("media_type") mediaType: String, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/archive/") + suspend fun archive( + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt index 6ae575a7..911b30c8 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -26,12 +26,10 @@ object MediaUploader { suspend fun uploadPhoto( uri: Uri, contentResolver: ContentResolver, - ): MediaUploadResponse { - return withContext(Dispatchers.IO) { - val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) - val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") - uploadPhoto(bitmap) - } + ): MediaUploadResponse = withContext(Dispatchers.IO) { + val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) + val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") + uploadPhoto(bitmap) } @Suppress("BlockingMethodInNonBlockingContext") @@ -98,25 +96,23 @@ object MediaUploader { } } - private fun create(mediaType: MediaType, inputStream: InputStream): RequestBody { - return object : RequestBody() { - override fun contentType(): MediaType { - return mediaType - } + private fun create(mediaType: MediaType, inputStream: InputStream): RequestBody = object : RequestBody() { + override fun contentType(): MediaType { + return mediaType + } - override fun contentLength(): Long { - return try { - inputStream.available().toLong() - } catch (e: IOException) { - 0 - } + override fun contentLength(): Long { + return try { + inputStream.available().toLong() + } catch (e: IOException) { + 0 } + } - @Throws(IOException::class) - @Suppress("DEPRECATION_ERROR") - override fun writeTo(sink: BufferedSink) { - Okio.source(inputStream).use { sink.writeAll(it) } - } + @Throws(IOException::class) + @Suppress("DEPRECATION_ERROR") + override fun writeTo(sink: BufferedSink) { + Okio.source(inputStream).use { sink.writeAll(it) } } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt index f70aac34..543c9db0 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt @@ -23,11 +23,9 @@ import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.getCsrfTokenFromCookie import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.webservices.MediaService -import awais.instagrabber.webservices.ServiceCallback import com.google.common.collect.ImmutableList -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.* class PostViewV2ViewModel : ViewModel() { @@ -127,44 +125,51 @@ class PostViewV2ViewModel : ViewModel() { fun like(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.like(media.pk, getLikeUnlikeCallback(data)) + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val liked = mediaService?.like(mediaId) + updateMediaLikeUnlike(data, liked ?: false) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } fun unlike(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.unlike(media.pk, getLikeUnlikeCallback(data)) + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val unliked = mediaService?.unlike(mediaId) + updateMediaLikeUnlike(data, unliked ?: false) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } - private fun getLikeUnlikeCallback(data: MutableLiveData>): ServiceCallback { - return object : ServiceCallback { - override fun onSuccess(result: Boolean?) { - if (result != null && !result) { - data.postValue(error("", null)) - return - } - data.postValue(success(true)) - val currentLikesCount = media.likeCount - val updatedCount: Long - if (!media.hasLiked) { - updatedCount = currentLikesCount + 1 - media.hasLiked = true - } else { - updatedCount = currentLikesCount - 1 - media.hasLiked = false - } - media.likeCount = updatedCount - likeCount.postValue(updatedCount) - liked.postValue(media.hasLiked) - } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, null)) - Log.e(TAG, "Error during like/unlike", t) - } + private fun updateMediaLikeUnlike(data: MutableLiveData>, result: Boolean) { + if (!result) { + data.postValue(error("", null)) + return + } + data.postValue(success(true)) + val currentLikesCount = media.likeCount + val updatedCount: Long + if (!media.hasLiked) { + updatedCount = currentLikesCount + 1 + media.hasLiked = true + } else { + updatedCount = currentLikesCount - 1 + media.hasLiked = false } + media.likeCount = updatedCount + likeCount.postValue(updatedCount) + liked.postValue(media.hasLiked) } fun toggleSave(): LiveData> { @@ -180,79 +185,87 @@ class PostViewV2ViewModel : ViewModel() { fun save(collection: String?, ignoreSaveState: Boolean): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.save(media.pk, collection, getSaveUnsaveCallback(data, ignoreSaveState)) + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val saved = mediaService?.save(mediaId, collection) + getSaveUnsaveCallback(data, saved ?: false, ignoreSaveState) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } fun unsave(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.unsave(media.pk, getSaveUnsaveCallback(data, false)) + viewModelScope.launch(Dispatchers.IO) { + val mediaId = media.pk ?: return@launch + val unsaved = mediaService?.unsave(mediaId) + getSaveUnsaveCallback(data, unsaved ?: false, false) + } return data } private fun getSaveUnsaveCallback( data: MutableLiveData>, + result: Boolean, ignoreSaveState: Boolean, - ): ServiceCallback { - return object : ServiceCallback { - override fun onSuccess(result: Boolean?) { - if (result != null && !result) { - data.postValue(error("", null)) - return - } - data.postValue(success(true)) - if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved - saved.postValue(media.hasViewerSaved) - } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, null)) - Log.e(TAG, "Error during save/unsave", t) - } + ) { + if (!result) { + data.postValue(error("", null)) + return } + data.postValue(success(true)) + if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved + saved.postValue(media.hasViewerSaved) } fun updateCaption(caption: String): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.editCaption(media.pk, caption, object : ServiceCallback { - override fun onSuccess(result: Boolean?) { + viewModelScope.launch(Dispatchers.IO) { + try { + val postId = media.pk ?: return@launch + val result = mediaService?.editCaption(postId, caption) if (result != null && result) { data.postValue(success("")) media.setPostCaption(caption) this@PostViewV2ViewModel.caption.postValue(media.caption) - return + return@launch } data.postValue(error("", null)) + } catch (e: Exception) { + Log.e(TAG, "Error editing caption", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "Error editing caption", t) - data.postValue(error(t.message, null)) - } - }) + } return data } fun translateCaption(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - val value = caption.value ?: return data - mediaService?.translate(value.pk, "1", object : ServiceCallback { - override fun onSuccess(result: String?) { + val value = caption.value + val pk = value?.pk + if (pk == null) { + data.postValue(error("caption is null", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + try { + val result = mediaService?.translate(pk, "1") if (result.isNullOrBlank()) { data.postValue(error("", null)) - return + return@launch } data.postValue(success(result)) + } catch (e: Exception) { + Log.e(TAG, "Error translating comment", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "Error translating comment", t) - data.postValue(error(t.message, null)) - } - }) + } return data } @@ -273,30 +286,19 @@ class PostViewV2ViewModel : ViewModel() { data.postValue(error("media id or type is null", null)) return data } - val request = mediaService?.delete(mediaId, mediaType) - if (request == null) { - data.postValue(success(Any())) - return data - } - request.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - data.postValue(error(R.string.generic_null_response, null)) - return - } - val body = response.body() - if (body == null) { - data.postValue(error(R.string.generic_null_response, null)) - return + viewModelScope.launch(Dispatchers.IO) { + try { + val response = mediaService?.delete(mediaId, mediaType) + if (response == null) { + data.postValue(success(Any())) + return@launch } data.postValue(success(Any())) + } catch (e: Exception) { + Log.e(TAG, "delete: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(call: Call, t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java deleted file mode 100644 index 65aef412..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ /dev/null @@ -1,317 +0,0 @@ -package awais.instagrabber.webservices; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.repositories.MediaRepository; -import awais.instagrabber.repositories.requests.Clip; -import awais.instagrabber.repositories.requests.UploadFinishOptions; -import awais.instagrabber.repositories.requests.VideoOptions; -import awais.instagrabber.repositories.responses.LikersResponse; -import awais.instagrabber.repositories.responses.Media; -import awais.instagrabber.repositories.responses.MediaInfoResponse; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.DateUtils; -import awais.instagrabber.utils.MediaUploadHelper; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class MediaService extends BaseService { - private static final String TAG = "MediaService"; - private static final List DELETABLE_ITEMS_TYPES = ImmutableList.of(MediaItemType.MEDIA_TYPE_IMAGE, - MediaItemType.MEDIA_TYPE_VIDEO, - MediaItemType.MEDIA_TYPE_SLIDER); - - private final MediaRepository repository; - private final String deviceUuid, csrfToken; - private final long userId; - - private static MediaService instance; - - private MediaService(final String deviceUuid, - final String csrfToken, - final long userId) { - this.deviceUuid = deviceUuid; - this.csrfToken = csrfToken; - this.userId = userId; - repository = RetrofitFactory.INSTANCE - .getRetrofit() - .create(MediaRepository.class); - } - - public String getCsrfToken() { - return csrfToken; - } - - public String getDeviceUuid() { - return deviceUuid; - } - - public long getUserId() { - return userId; - } - - public static MediaService getInstance(final String deviceUuid, final String csrfToken, final long userId) { - if (instance == null - || !Objects.equals(instance.getCsrfToken(), csrfToken) - || !Objects.equals(instance.getDeviceUuid(), deviceUuid) - || !Objects.equals(instance.getUserId(), userId)) { - instance = new MediaService(deviceUuid, csrfToken, userId); - } - return instance; - } - - public void fetch(final long mediaId, - final ServiceCallback callback) { - final Call request = repository.fetch(mediaId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback == null) return; - final MediaInfoResponse mediaInfoResponse = response.body(); - if (mediaInfoResponse == null || mediaInfoResponse.getItems() == null || mediaInfoResponse.getItems().isEmpty()) { - callback.onSuccess(null); - return; - } - callback.onSuccess(mediaInfoResponse.getItems().get(0)); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void like(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "like", null, callback); - } - - public void unlike(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "unlike", null, callback); - } - - public void save(final String mediaId, - final String collection, - final ServiceCallback callback) { - action(mediaId, "save", collection, callback); - } - - public void unsave(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "unsave", null, callback); - } - - private void action(final String mediaId, - final String action, - final String collection, - final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("media_id", mediaId); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - // form.put("radio_type", "wifi-none"); - if (action.equals("save") && !TextUtils.isEmpty(collection)) form.put("added_collection_ids", "[" + collection + "]"); - // there also exists "removed_collection_ids" which can be used with "save" and "unsave" - final Map signedForm = Utils.sign(form); - final Call request = repository.action(action, mediaId, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback == null) return; - final String body = response.body(); - if (body == null) { - callback.onFailure(new RuntimeException("Returned body is null")); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); - } catch (JSONException e) { - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void editCaption(final String postId, - final String newCaption, - @NonNull final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("igtv_feed_preview", "false"); - form.put("media_id", postId); - form.put("caption_text", newCaption); - final Map signedForm = Utils.sign(form); - final Call request = repository.editCaption(postId, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "Error occurred while editing caption"); - callback.onSuccess(false); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); - } catch (JSONException e) { - // Log.e(TAG, "Error parsing body", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error editing caption", t); - callback.onFailure(t); - } - }); - } - - public void fetchLikes(final String mediaId, - final boolean isComment, - @NonNull final ServiceCallback> callback) { - final Call likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers"); - likesRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final LikersResponse likersResponse = response.body(); - if (likersResponse == null) { - Log.e(TAG, "Error occurred while fetching likes of " + mediaId); - callback.onSuccess(null); - return; - } - callback.onSuccess(likersResponse.getUsers()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error getting likes", t); - callback.onFailure(t); - } - }); - } - - public void translate(final String id, - final String type, // 1 caption 2 comment 3 bio - @NonNull final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("id", String.valueOf(id)); - form.put("type", type); - final Call request = repository.translate(form); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "Error occurred while translating"); - callback.onSuccess(null); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String translation = jsonObject.optString("translation"); - callback.onSuccess(translation); - } catch (JSONException e) { - // Log.e(TAG, "Error parsing body", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error translating", t); - callback.onFailure(t); - } - }); - } - - public Call uploadFinish(@NonNull final UploadFinishOptions options) { - if (options.getVideoOptions() != null) { - final VideoOptions videoOptions = options.getVideoOptions(); - if (videoOptions.getClips().isEmpty()) { - videoOptions.setClips(Collections.singletonList(new Clip(videoOptions.getLength(), options.getSourceType()))); - } - } - final String timezoneOffset = String.valueOf(DateUtils.getTimezoneOffset()); - final ImmutableMap.Builder formBuilder = ImmutableMap.builder() - .put("timezone_offset", timezoneOffset) - .put("_csrftoken", csrfToken) - .put("source_type", options.getSourceType()) - .put("_uid", String.valueOf(userId)) - .put("_uuid", deviceUuid) - .put("upload_id", options.getUploadId()); - if (options.getVideoOptions() != null) { - formBuilder.putAll(options.getVideoOptions().getMap()); - } - final Map queryMap = options.getVideoOptions() != null ? ImmutableMap.of("video", "1") : Collections.emptyMap(); - final Map signedForm = Utils.sign(formBuilder.build()); - return repository.uploadFinish(MediaUploadHelper.getRetryContextString(), queryMap, signedForm); - } - - public Call delete(@NonNull final String postId, - @NonNull final MediaItemType type) { - if (!DELETABLE_ITEMS_TYPES.contains(type)) return null; - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("igtv_feed_preview", "false"); - form.put("media_id", postId); - final Map signedForm = Utils.sign(form); - final String mediaType; - switch (type) { - case MEDIA_TYPE_IMAGE: - mediaType = "PHOTO"; - break; - case MEDIA_TYPE_VIDEO: - mediaType = "VIDEO"; - break; - case MEDIA_TYPE_SLIDER: - mediaType = "CAROUSEL"; - break; - default: - return null; - } - return repository.delete(postId, mediaType, signedForm); - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.kt b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt new file mode 100644 index 00000000..ee128d73 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt @@ -0,0 +1,169 @@ +package awais.instagrabber.webservices + +import awais.instagrabber.models.enums.MediaItemType +import awais.instagrabber.repositories.MediaRepository +import awais.instagrabber.repositories.requests.Clip +import awais.instagrabber.repositories.requests.UploadFinishOptions +import awais.instagrabber.repositories.responses.Media +import awais.instagrabber.repositories.responses.User +import awais.instagrabber.utils.DateUtils +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.retryContextString +import awais.instagrabber.webservices.RetrofitFactory.retrofit +import org.json.JSONObject + +class MediaService private constructor( + val deviceUuid: String, + val csrfToken: String, + val userId: Long, +) : BaseService() { + private val repository: MediaRepository = retrofit.create(MediaRepository::class.java) + + suspend fun fetch( + mediaId: Long, + ): Media? { + val response = repository.fetch(mediaId) + return if (response.items.isNullOrEmpty()) { + null + } else response.items[0] + } + + suspend fun like(mediaId: String): Boolean = action(mediaId, "like", null) + + suspend fun unlike(mediaId: String): Boolean = action(mediaId, "unlike", null) + + suspend fun save(mediaId: String, collection: String?): Boolean = action(mediaId, "save", collection) + + suspend fun unsave(mediaId: String): Boolean = action(mediaId, "unsave", null) + + private suspend fun action( + mediaId: String, + action: String, + collection: String?, + ): Boolean { + val form: MutableMap = mutableMapOf( + "media_id" to mediaId, + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + ) + // form.put("radio_type", "wifi-none"); + if (action == "save" && !collection.isNullOrBlank()) { + form["added_collection_ids"] = "[$collection]" + } + // there also exists "removed_collection_ids" which can be used with "save" and "unsave" + val signedForm = Utils.sign(form) + val response = repository.action(action, mediaId, signedForm) + val jsonObject = JSONObject(response) + val status = jsonObject.optString("status") + return status == "ok" + } + + suspend fun editCaption( + postId: String, + newCaption: String, + ): Boolean { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "igtv_feed_preview" to "false", + "media_id" to postId, + "caption_text" to newCaption, + ) + val signedForm = Utils.sign(form) + val response = repository.editCaption(postId, signedForm) + val jsonObject = JSONObject(response) + val status = jsonObject.optString("status") + return status == "ok" + } + + suspend fun fetchLikes( + mediaId: String, + isComment: Boolean, + ): List { + val response = repository.fetchLikes(mediaId, if (isComment) "comment_likers" else "likers") + return response.users + } + + suspend fun translate( + id: String, + type: String, // 1 caption 2 comment 3 bio + ): String { + val form = mapOf( + "id" to id, + "type" to type, + ) + val response = repository.translate(form) + val jsonObject = JSONObject(response) + return jsonObject.optString("translation") + } + + suspend fun uploadFinish(options: UploadFinishOptions): String { + if (options.videoOptions != null) { + val videoOptions = options.videoOptions + if (videoOptions.clips.isEmpty()) { + videoOptions.clips = listOf(Clip(videoOptions.length, options.sourceType)) + } + } + val timezoneOffset = DateUtils.getTimezoneOffset().toString() + val form = mutableMapOf( + "timezone_offset" to timezoneOffset, + "_csrftoken" to csrfToken, + "source_type" to options.sourceType, + "_uid" to userId.toString(), + "_uuid" to deviceUuid, + "upload_id" to options.uploadId, + ) + if (options.videoOptions != null) { + form.putAll(options.videoOptions.map) + } + val queryMap = if (options.videoOptions != null) mapOf("video" to "1") else emptyMap() + val signedForm = Utils.sign(form) + return repository.uploadFinish(retryContextString, queryMap, signedForm) + } + + suspend fun delete( + postId: String, + type: MediaItemType, + ): String? { + if (!DELETABLE_ITEMS_TYPES.contains(type)) return null + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "igtv_feed_preview" to "false", + "media_id" to postId, + ) + val signedForm = Utils.sign(form) + val mediaType: String = when (type) { + MediaItemType.MEDIA_TYPE_IMAGE -> "PHOTO" + MediaItemType.MEDIA_TYPE_VIDEO -> "VIDEO" + MediaItemType.MEDIA_TYPE_SLIDER -> "CAROUSEL" + else -> return null + } + return repository.delete(postId, mediaType, signedForm) + } + + companion object { + private val DELETABLE_ITEMS_TYPES = listOf( + MediaItemType.MEDIA_TYPE_IMAGE, + MediaItemType.MEDIA_TYPE_VIDEO, + MediaItemType.MEDIA_TYPE_SLIDER + ) + private lateinit var instance: MediaService + + @JvmStatic + fun getInstance(deviceUuid: String, csrfToken: String, userId: Long): MediaService { + if (!this::instance.isInitialized + || instance.csrfToken != csrfToken + || instance.deviceUuid != deviceUuid + || instance.userId != userId + ) { + instance = MediaService(deviceUuid, csrfToken, userId) + } + return instance + } + } + +} \ No newline at end of file