Browse Source

Convert MediaRepository and MediaService to kotlin.

renovate/org.robolectric-robolectric-4.x
Ammar Githam 4 years ago
parent
commit
b997504602
  1. 36
      app/src/main/java/awais/instagrabber/activities/MainActivity.kt
  2. 27
      app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
  3. 45
      app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java
  4. 65
      app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
  5. 53
      app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java
  6. 57
      app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java
  7. 47
      app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
  8. 107
      app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
  9. 55
      app/src/main/java/awais/instagrabber/repositories/MediaRepository.java
  10. 57
      app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt
  11. 40
      app/src/main/java/awais/instagrabber/utils/MediaUploader.kt
  12. 182
      app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt
  13. 317
      app/src/main/java/awais/instagrabber/webservices/MediaService.java
  14. 169
      app/src/main/java/awais/instagrabber/webservices/MediaService.kt

36
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.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination 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.android.material.textfield.TextInputLayout
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterators import com.google.common.collect.Iterators
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.stream.Collectors import java.util.stream.Collectors
@ -81,11 +84,14 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private var isActivityCheckerServiceBound = false private var isActivityCheckerServiceBound = false
private var isBackStackEmpty = false private var isBackStackEmpty = false
private var isLoggedIn = false private var isLoggedIn = false
private var deviceUuid: String? = null
private var csrfToken: String? = null
private var userId: Long = 0
// private var behavior: HideBottomViewOnScrollBehavior<BottomNavigationView>? = null // private var behavior: HideBottomViewOnScrollBehavior<BottomNavigationView>? = null
var currentTabs: List<Tab> = emptyList() var currentTabs: List<Tab> = emptyList()
private set private set
private var showBottomViewDestinations: List<Int> = emptyList<Int>()
private var showBottomViewDestinations: List<Int> = emptyList()
private var graphQLService: GraphQLService? = null private var graphQLService: GraphQLService? = null
private var mediaService: MediaService? = null private var mediaService: MediaService? = null
@ -157,17 +163,17 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun setupCookie() { private fun setupCookie() {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE) 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) userId = getUserIdFromCookie(cookie)
csrfToken = getCsrfTokenFromCookie(cookie) csrfToken = getCsrfTokenFromCookie(cookie)
} }
if (isEmpty(cookie) || userId == 0L || isEmpty(csrfToken)) {
if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
isLoggedIn = false isLoggedIn = false
return return
} }
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
if (isEmpty(deviceUuid)) { if (isEmpty(deviceUuid)) {
Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString()) Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
} }
@ -175,6 +181,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
isLoggedIn = true isLoggedIn = true
} }
@Suppress("unused")
private fun initDmService() { private fun initDmService() {
if (!isLoggedIn) return if (!isLoggedIn) return
val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH) 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) .setView(R.layout.dialog_opening_post)
.create() .create()
if (graphQLService == null) graphQLService = GraphQLService.getInstance() 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<Media> = object : ServiceCallback<Media> { val postCb: ServiceCallback<Media> = object : ServiceCallback<Media> {
override fun onSuccess(feedModel: Media?) { override fun onSuccess(feedModel: Media?) {
if (feedModel != null) { if (feedModel != null) {
@ -650,7 +659,18 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
} }
} }
alertDialog.show() 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) { private fun showLocationView(intentModel: IntentModel) {

27
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.databinding.FragmentLikesBinding;
import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; 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) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final String cookie = settingsHelper.getString(Constants.COOKIE); 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(); graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
// setHasOptionsMenu(true); // setHasOptionsMenu(true);
} }
@ -130,7 +136,20 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
if (isComment && !isLoggedIn) { if (isComment && !isLoggedIn) {
lazyLoader.resetState(); lazyLoader.resetState();
graphQLService.fetchCommentLikers(postId, null, anonCb); 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<User>) users);
}), Dispatchers.getIO())
);
}
} }
private void init() { private void init() {

45
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.models.enums.NotificationType;
import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.FriendshipChangeResponse; 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.Notification;
import awais.instagrabber.repositories.responses.notification.NotificationArgs; import awais.instagrabber.repositories.responses.notification.NotificationArgs;
import awais.instagrabber.repositories.responses.notification.NotificationImage; import awais.instagrabber.repositories.responses.notification.NotificationImage;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.NotificationViewModel; import awais.instagrabber.viewmodels.NotificationViewModel;
@ -48,6 +49,7 @@ import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; 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) .setView(R.layout.dialog_opening_post)
.create(); .create();
alertDialog.show(); alertDialog.show();
mediaService.fetch(mediaId, new ServiceCallback<Media>() {
@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)) { if (TextUtils.isEmpty(cookie)) {
Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show();
} }
mediaService = MediaService.getInstance(null, null, 0);
final long userId = CookieUtils.getUserIdFromCookie(cookie); final long userId = CookieUtils.getUserIdFromCookie(cookie);
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId); friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId);
mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId);
newsService = NewsService.getInstance(); newsService = NewsService.getInstance();
} }

65
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;
import awais.instagrabber.repositories.requests.StoryViewerOptions.Type; import awais.instagrabber.repositories.requests.StoryViewerOptions.Type;
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.StoryStickerResponse; import awais.instagrabber.repositories.responses.StoryStickerResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.CoroutineUtilsKt;
@ -159,7 +159,7 @@ public class StoryViewerFragment extends Fragment {
final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID); final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID);
fragmentActivity = (AppCompatActivity) requireActivity(); fragmentActivity = (AppCompatActivity) requireActivity();
storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId); storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId);
mediaService = MediaService.getInstance(null, null, 0);
mediaService = MediaService.getInstance(deviceId, csrfToken, userIdFromCookie);
directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId);
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -220,7 +220,7 @@ public class StoryViewerFragment extends Fragment {
.setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread( .setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread(
Collections.singletonList(currentStory.getUserId()), Collections.singletonList(currentStory.getUserId()),
null, null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> {
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) { if (throwable != null) {
Log.e(TAG, "onOptionsItemSelected: ", throwable); Log.e(TAG, "onOptionsItemSelected: ", throwable);
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
@ -231,17 +231,19 @@ public class StoryViewerFragment extends Fragment {
input.getText().toString(), input.getText().toString(),
currentStory.getStoryMediaId(), currentStory.getStoryMediaId(),
String.valueOf(currentStory.getUserId()), 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) .setNegativeButton(R.string.cancel, null)
.show(); .show();
@ -451,26 +453,25 @@ public class StoryViewerFragment extends Fragment {
.setView(R.layout.dialog_opening_post) .setView(R.layout.dialog_opening_post)
.create(); .create();
alertDialog.show(); alertDialog.show();
mediaService.fetch(Long.parseLong(mediaId), new ServiceCallback<Media>() {
@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 View.OnClickListener storyActionListener = v -> {
final Object tag = v.getTag(); final Object tag = v.getTag();

53
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.GPUImage;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup;
import kotlinx.coroutines.Dispatchers;
public class FiltersFragment extends Fragment { public class FiltersFragment extends Fragment {
private static final String TAG = FiltersFragment.class.getSimpleName(); private static final String TAG = FiltersFragment.class.getSimpleName();
@ -461,31 +462,33 @@ public class FiltersFragment extends Fragment {
filtersAdapter.setSelected(position); filtersAdapter.setSelected(position);
appliedFilter = filter; 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(); addInitialFilter();
binding.preview.setFilter(filterGroup); binding.preview.setFilter(filterGroup);
} }

57
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.Media;
import awais.instagrabber.repositories.responses.discover.TopicCluster; import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse; 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.utils.Utils;
import awais.instagrabber.viewmodels.TopicClusterViewModel; import awais.instagrabber.viewmodels.TopicClusterViewModel;
import awais.instagrabber.webservices.DiscoverService; import awais.instagrabber.webservices.DiscoverService;
import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "DiscoverFragment"; private static final String TAG = "DiscoverFragment";
@ -52,7 +57,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity(); fragmentActivity = (MainActivity) requireActivity();
discoverService = DiscoverService.getInstance(); 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 @Override
@ -104,29 +113,29 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR
.setView(R.layout.dialog_opening_post) .setView(R.layout.dialog_opening_post)
.create(); .create();
alertDialog.show(); alertDialog.show();
mediaService.fetch(Long.valueOf(coverMedia.getPk()), new ServiceCallback<Media>() {
@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); final DiscoverTopicsAdapter adapter = new DiscoverTopicsAdapter(otcl);

47
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.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.UserProfileContextLink; import awais.instagrabber.repositories.responses.UserProfileContextLink;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt; 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; friendshipService = isLoggedIn ? FriendshipService.getInstance(deviceUuid, csrfToken, myId) : null;
directMessagesService = isLoggedIn ? DirectMessagesService.getInstance(csrfToken, myId, deviceUuid) : null; directMessagesService = isLoggedIn ? DirectMessagesService.getInstance(csrfToken, myId, deviceUuid) : null;
storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : 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; userService = isLoggedIn ? UserService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
final Context context = getContext(); final Context context = getContext();
@ -821,26 +822,26 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
Utils.copyText(context, biography); Utils.copyText(context, biography);
break; break;
case 1: case 1:
mediaService.translate(String.valueOf(profileModel.getPk()), "3", new ServiceCallback<String>() {
@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; break;
} }
}) })
@ -1079,7 +1080,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
directMessagesService.createThread( directMessagesService.createThread(
Collections.singletonList(profileModel.getPk()), Collections.singletonList(profileModel.getPk()),
null, null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> {
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) { if (throwable != null) {
Log.e(TAG, "setupCommonListeners: ", throwable); Log.e(TAG, "setupCommonListeners: ", throwable);
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); 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); inboxManager.addThread(thread, 0);
} }
fragmentActivity.navigateToThread(thread.getThreadId(), profileModel.getUsername()); fragmentActivity.navigateToThread(thread.getThreadId(), profileModel.getUsername());
}, Dispatchers.getIO())
}), Dispatchers.getIO())
); );
}); });
} }

107
app/src/main/java/awais/instagrabber/managers/ThreadManager.kt

@ -41,8 +41,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
@ -457,40 +455,15 @@ class ThreadManager private constructor(
"4", "4",
null null
) )
val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions)
uploadFinishRequest.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
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<String?>, 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) { } catch (e: Exception) {
data.postValue(error(e.message, directItem)) data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVoice: ", e) Log.e(TAG, "sendVoice: ", e)
@ -806,39 +779,15 @@ class ThreadManager private constructor(
"2", "2",
VideoOptions(duration / 1000f, emptyList(), 0, false) VideoOptions(duration / 1000f, emptyList(), 0, false)
) )
val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions)
uploadFinishRequest.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
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<String?>, 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) { } catch (e: Exception) {
data.postValue(error(e.message, directItem)) data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVideo: ", e) Log.e(TAG, "sendVideo: ", e)
@ -900,26 +849,6 @@ class ThreadManager private constructor(
} }
} }
private fun handleErrorBody(
call: Call<*>,
response: Response<*>,
data: MutableLiveData<Resource<Any?>>?,
) {
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( private fun handleInvalidResponse(
data: MutableLiveData<Resource<Any?>>, data: MutableLiveData<Resource<Any?>>,
response: MediaUploadResponse, response: MediaUploadResponse,

55
app/src/main/java/awais/instagrabber/repositories/MediaRepository.java

@ -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<MediaInfoResponse> fetch(@Path("mediaId") final long mediaId);
@GET("/api/v1/media/{mediaId}/{action}/")
Call<LikersResponse> 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<String> action(@Path("action") final String action,
@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/edit_media/")
Call<String> editCaption(@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
@GET("/api/v1/language/translate/")
Call<String> translate(@QueryMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/media/upload_finish/")
Call<String> uploadFinish(@Header("retry_context") final String retryContext,
@QueryMap Map<String, String> queryParams,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/delete/")
Call<String> delete(@Path("mediaId") final String mediaId,
@Query("media_type") final String mediaType,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/archive/")
Call<String> archive(@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
}

57
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, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/edit_media/")
suspend fun editCaption(
@Path("mediaId") mediaId: String,
@FieldMap signedForm: Map<String, String>,
): String
@GET("/api/v1/language/translate/")
suspend fun translate(@QueryMap form: Map<String, String>): String
@FormUrlEncoded
@POST("/api/v1/media/upload_finish/")
suspend fun uploadFinish(
@Header("retry_context") retryContext: String,
@QueryMap queryParams: Map<String, String>,
@FieldMap signedForm: Map<String, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/delete/")
suspend fun delete(
@Path("mediaId") mediaId: String,
@Query("media_type") mediaType: String,
@FieldMap signedForm: Map<String, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/archive/")
suspend fun archive(
@Path("mediaId") mediaId: String,
@FieldMap signedForm: Map<String, String>,
): String
}

40
app/src/main/java/awais/instagrabber/utils/MediaUploader.kt

@ -26,12 +26,10 @@ object MediaUploader {
suspend fun uploadPhoto( suspend fun uploadPhoto(
uri: Uri, uri: Uri,
contentResolver: ContentResolver, 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") @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) }
} }
} }

182
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.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.MediaService import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList 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.* import java.util.*
class PostViewV2ViewModel : ViewModel() { class PostViewV2ViewModel : ViewModel() {
@ -127,44 +125,51 @@ class PostViewV2ViewModel : ViewModel() {
fun like(): LiveData<Resource<Any?>> { fun like(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) 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 return data
} }
fun unlike(): LiveData<Resource<Any?>> { fun unlike(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) 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 return data
} }
private fun getLikeUnlikeCallback(data: MutableLiveData<Resource<Any?>>): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
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<Resource<Any?>>, 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<Resource<Any?>> { fun toggleSave(): LiveData<Resource<Any?>> {
@ -180,79 +185,87 @@ class PostViewV2ViewModel : ViewModel() {
fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> { fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) 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 return data
} }
fun unsave(): LiveData<Resource<Any?>> { fun unsave(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) 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 return data
} }
private fun getSaveUnsaveCallback( private fun getSaveUnsaveCallback(
data: MutableLiveData<Resource<Any?>>, data: MutableLiveData<Resource<Any?>>,
result: Boolean,
ignoreSaveState: Boolean, ignoreSaveState: Boolean,
): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
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<Resource<Any?>> { fun updateCaption(caption: String): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null)) data.postValue(loading(null))
mediaService?.editCaption(media.pk, caption, object : ServiceCallback<Boolean?> {
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) { if (result != null && result) {
data.postValue(success("")) data.postValue(success(""))
media.setPostCaption(caption) media.setPostCaption(caption)
this@PostViewV2ViewModel.caption.postValue(media.caption) this@PostViewV2ViewModel.caption.postValue(media.caption)
return
return@launch
} }
data.postValue(error("", null)) 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 return data
} }
fun translateCaption(): LiveData<Resource<String?>> { fun translateCaption(): LiveData<Resource<String?>> {
val data = MutableLiveData<Resource<String?>>() val data = MutableLiveData<Resource<String?>>()
data.postValue(loading(null)) data.postValue(loading(null))
val value = caption.value ?: return data
mediaService?.translate(value.pk, "1", object : ServiceCallback<String?> {
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()) { if (result.isNullOrBlank()) {
data.postValue(error("", null)) data.postValue(error("", null))
return
return@launch
} }
data.postValue(success(result)) 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 return data
} }
@ -273,30 +286,19 @@ class PostViewV2ViewModel : ViewModel() {
data.postValue(error("media id or type is null", null)) data.postValue(error("media id or type is null", null))
return data return data
} }
val request = mediaService?.delete(mediaId, mediaType)
if (request == null) {
data.postValue(success(Any()))
return data
}
request.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
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())) data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "delete: ", e)
data.postValue(error(e.message, null))
} }
override fun onFailure(call: Call<String?>, t: Throwable) {
Log.e(TAG, "onFailure: ", t)
data.postValue(error(t.message, null))
}
})
}
return data return data
} }

317
app/src/main/java/awais/instagrabber/webservices/MediaService.java

@ -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<MediaItemType> 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<Media> callback) {
final Call<MediaInfoResponse> request = repository.fetch(mediaId);
request.enqueue(new Callback<MediaInfoResponse>() {
@Override
public void onResponse(@NonNull final Call<MediaInfoResponse> call,
@NonNull final Response<MediaInfoResponse> 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<MediaInfoResponse> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void like(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "like", null, callback);
}
public void unlike(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unlike", null, callback);
}
public void save(final String mediaId,
final String collection,
final ServiceCallback<Boolean> callback) {
action(mediaId, "save", collection, callback);
}
public void unsave(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unsave", null, callback);
}
private void action(final String mediaId,
final String action,
final String collection,
final ServiceCallback<Boolean> callback) {
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.action(action, mediaId, signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> 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<String> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void editCaption(final String postId,
final String newCaption,
@NonNull final ServiceCallback<Boolean> callback) {
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.editCaption(postId, signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> 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<List<User>> callback) {
final Call<LikersResponse> likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers");
likesRequest.enqueue(new Callback<LikersResponse>() {
@Override
public void onResponse(@NonNull final Call<LikersResponse> call, @NonNull final Response<LikersResponse> 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<LikersResponse> 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<String> callback) {
final Map<String, String> form = new HashMap<>();
form.put("id", String.valueOf(id));
form.put("type", type);
final Call<String> request = repository.translate(form);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> call, @NonNull final Throwable t) {
Log.e(TAG, "Error translating", t);
callback.onFailure(t);
}
});
}
public Call<String> 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<String, Object> formBuilder = ImmutableMap.<String, Object>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<String, String> queryMap = options.getVideoOptions() != null ? ImmutableMap.of("video", "1") : Collections.emptyMap();
final Map<String, String> signedForm = Utils.sign(formBuilder.build());
return repository.uploadFinish(MediaUploadHelper.getRetryContextString(), queryMap, signedForm);
}
public Call<String> delete(@NonNull final String postId,
@NonNull final MediaItemType type) {
if (!DELETABLE_ITEMS_TYPES.contains(type)) return null;
final Map<String, Object> 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<String, String> 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);
}
}

169
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<String, Any> = 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<User> {
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<String, Any>(
"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
}
}
}
Loading…
Cancel
Save