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.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<BottomNavigationView>? = null
var currentTabs: List<Tab> = emptyList()
private set
private var showBottomViewDestinations: List<Int> = emptyList<Int>()
private var showBottomViewDestinations: List<Int> = 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<Media> = object : ServiceCallback<Media> {
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) {

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.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<User>) users);
}), Dispatchers.getIO())
);
}
}
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.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<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)) {
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();
}

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.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<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 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.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);
}

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.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<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);

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.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<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;
}
})
@ -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())
);
});
}

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.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<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) {
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<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) {
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<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(
data: MutableLiveData<Resource<Any?>>,
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(
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) }
}
}

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.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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
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<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?>> {
@ -180,79 +185,87 @@ class PostViewV2ViewModel : ViewModel() {
fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
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<Resource<Any?>>,
result: 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?>> {
val data = MutableLiveData<Resource<Any?>>()
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) {
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<Resource<String?>> {
val data = MutableLiveData<Resource<String?>>()
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()) {
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<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()))
} 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
}

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