diff --git a/app/build.gradle b/app/build.gradle index d1cab37b..284981a4 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -147,6 +147,11 @@ android { exclude 'META-INF/LICENSE.md' exclude 'META-INF/LICENSE-notice.md' } + + testOptions.unitTests { + includeAndroidResources = true + } + } configurations.all { @@ -172,7 +177,6 @@ dependencies { implementation "androidx.navigation:navigation-ui:$nav_version" implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.preference:preference:1.1.1" - implementation "androidx.work:work-runtime:2.5.0" implementation 'androidx.palette:palette:1.0.0' implementation 'com.google.guava:guava:27.0.1-android' @@ -191,6 +195,7 @@ dependencies { def room_version = "2.3.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-guava:$room_version" + implementation "androidx.room:room-ktx:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" // CameraX @@ -204,6 +209,13 @@ dependencies { implementation "androidx.emoji:emoji:$emoji_compat_version" implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version" + // Work + def work_version = '2.5.0' + implementation "androidx.work:work-runtime:$work_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + + implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0" + implementation 'com.facebook.fresco:fresco:2.3.0' implementation 'com.facebook.fresco:animated-webp:2.3.0' implementation 'com.facebook.fresco:webpsupport:2.3.0' @@ -225,6 +237,9 @@ dependencies { githubImplementation 'io.sentry:sentry-android:4.3.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' + testImplementation "androidx.test.ext:junit-ktx:1.1.2" + testImplementation "androidx.test:core-ktx:1.3.0" + testImplementation "org.robolectric:robolectric:4.5.1" androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' androidTestImplementation 'androidx.test:core:1.3.0' diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt index 960d897d..10ac6278 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavController.OnDestinationChangedListener import androidx.navigation.NavDestination @@ -48,7 +49,6 @@ import awais.instagrabber.models.IntentModel import awais.instagrabber.models.Resource import awais.instagrabber.models.Tab import awais.instagrabber.models.enums.IntentModelType -import awais.instagrabber.repositories.responses.Media import awais.instagrabber.services.ActivityCheckerService import awais.instagrabber.services.DMSyncAlarmReceiver import awais.instagrabber.utils.* @@ -60,7 +60,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel import awais.instagrabber.viewmodels.DirectInboxViewModel import awais.instagrabber.webservices.GraphQLService import awais.instagrabber.webservices.MediaService -import awais.instagrabber.webservices.ServiceCallback import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior import com.google.android.material.appbar.CollapsingToolbarLayout @@ -68,6 +67,9 @@ 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 kotlinx.coroutines.withContext import java.util.* import java.util.stream.Collectors @@ -81,13 +83,14 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private var isActivityCheckerServiceBound = false private var isBackStackEmpty = false private var isLoggedIn = false + private var deviceUuid: String? = null + private var csrfToken: String? = null + private var userId: Long = 0 // private var behavior: HideBottomViewOnScrollBehavior? = null var currentTabs: List = emptyList() private set - private var showBottomViewDestinations: List = emptyList() - private var graphQLService: GraphQLService? = null - private var mediaService: MediaService? = null + private var showBottomViewDestinations: List = emptyList() private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { @@ -157,17 +160,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 +178,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) @@ -369,7 +373,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL .collect(Collectors.toList()) showBottomViewDestinations = currentTabs.asSequence().map { it.startDestinationFragmentId - }.toMutableList().apply { add(R.id.postViewFragment) } + }.toMutableList().apply { + add(R.id.postViewFragment) + add(R.id.favoritesFragment) + } if (setDefaultTabFromSettings) { setSelectedTab(currentTabs) } else { @@ -627,30 +634,33 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL .setCancelable(false) .setView(R.layout.dialog_opening_post) .create() - if (graphQLService == null) graphQLService = GraphQLService.getInstance() - if (mediaService == null) mediaService = MediaService.getInstance(null, null, 0L) - val postCb: ServiceCallback = object : ServiceCallback { - override fun onSuccess(feedModel: Media?) { - if (feedModel != null) { - val currentNavControllerLiveData = currentNavControllerLiveData ?: return + alertDialog.show() + lifecycleScope.launch(Dispatchers.IO) { + try { + val media = if (isLoggedIn) MediaService.fetch(shortcodeToId(shortCode)) else GraphQLService.fetchPost(shortCode) + withContext(Dispatchers.Main) { + if (media == null) { + Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show() + return@withContext + } + val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext val navController = currentNavControllerLiveData.value val bundle = Bundle() - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel) + bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media) try { navController?.navigate(R.id.action_global_post_view, bundle) } catch (e: Exception) { Log.e(TAG, "showPostView: ", e) } - } else Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show() - alertDialog.dismiss() - } - - override fun onFailure(t: Throwable) { - alertDialog.dismiss() + } + } catch (e: Exception) { + Log.e(TAG, "showPostView: ", e) + } finally { + withContext(Dispatchers.Main) { + alertDialog.dismiss() + } } } - alertDialog.show() - if (isLoggedIn) mediaService?.fetch(shortcodeToId(shortCode), postCb) else graphQLService?.fetchPost(shortCode, postCb) } private fun showLocationView(intentModel: IntentModel) { diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java index 66eab9aa..610acc7c 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableList; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; @@ -164,7 +165,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple binding.ivProfilePic.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE); binding.tvUsername.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE); if (messageDirection == MessageDirection.INCOMING && thread.isGroup()) { - final User user = getUser(item.getUserId(), thread.getUsers()); + final List allUsers = new LinkedList(thread.getUsers()); + allUsers.addAll(thread.getLeftUsers()); + final User user = getUser(item.getUserId(), allUsers); if (user != null) { binding.tvUsername.setText(user.getUsername()); binding.ivProfilePic.setImageURI(user.getProfilePicUrl()); @@ -220,7 +223,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple private void setupReply(final DirectItem item, final MessageDirection messageDirection) { if (item.getRepliedToMessage() != null) { - setReply(item, messageDirection, thread.getUsers()); + final List allUsers = new LinkedList(thread.getUsers()); + allUsers.addAll(thread.getLeftUsers()); + setReply(item, messageDirection, allUsers); } else { binding.quoteLine.setVisibility(View.GONE); binding.replyContainer.setVisibility(View.GONE); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java index aab59a58..eb39f405 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java @@ -37,7 +37,7 @@ public class RecipientThreadViewHolder extends RecyclerView.ViewHolder { final DirectThread thread, final boolean showSelection, final boolean isSelected) { - if (thread == null) return; + if (thread == null || thread.getUsers().size() == 0) return; binding.getRoot().setOnClickListener(v -> { if (onThreadClickListener == null) return; onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected); diff --git a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java index fa9f331e..0a47e895 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java @@ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.repositories.responses.Hashtag; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.TagsService; +import kotlinx.coroutines.Dispatchers; public class HashtagPostFetchService implements PostFetcher.PostFetchService { private final TagsService tagsService; @@ -23,7 +25,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { this.hashtagModel = hashtagModel; this.isLoggedIn = isLoggedIn; tagsService = isLoggedIn ? TagsService.getInstance() : null; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; } @Override @@ -48,7 +50,17 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { } }; if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); - else graphQLService.fetchHashtagPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); + else graphQLService.fetchHashtagPosts( + hashtagModel.getName().toLowerCase(), + nextMaxId, + CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(postsFetchResponse); + }, Dispatchers.getIO()) + ); } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java index 274b2314..e7410a64 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java @@ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.LocationService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; public class LocationPostFetchService implements PostFetcher.PostFetchService { private final LocationService locationService; @@ -23,7 +25,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { this.locationModel = locationModel; this.isLoggedIn = isLoggedIn; locationService = isLoggedIn ? LocationService.getInstance() : null; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; } @Override @@ -48,7 +50,17 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { } }; if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb); - else graphQLService.fetchLocationPosts(locationModel.getPk(), nextMaxId, cb); + else graphQLService.fetchLocationPosts( + locationModel.getPk(), + nextMaxId, + CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(postsFetchResponse); + }, Dispatchers.getIO()) + ); } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java index 02e0e27a..f6bc3423 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java @@ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ProfileService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; public class ProfilePostFetchService implements PostFetcher.PostFetchService { private static final String TAG = "ProfilePostFetchService"; @@ -23,7 +25,7 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) { this.profileModel = profileModel; this.isLoggedIn = isLoggedIn; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; profileService = isLoggedIn ? ProfileService.getInstance() : null; } @@ -49,7 +51,19 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { } }; if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb); - else graphQLService.fetchProfilePosts(profileModel.getPk(), 30, nextMaxId, profileModel, cb); + else graphQLService.fetchProfilePosts( + profileModel.getPk(), + 30, + nextMaxId, + profileModel, + CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(postsFetchResponse); + }, Dispatchers.getIO()) + ); } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java index 9b3511a9..efc4eaa0 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java @@ -7,9 +7,11 @@ import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.enums.PostItemType; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ProfileService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; public class SavedPostFetchService implements PostFetcher.PostFetchService { private final ProfileService profileService; @@ -27,7 +29,7 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService { this.type = type; this.isLoggedIn = isLoggedIn; this.collectionId = collectionId; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; profileService = isLoggedIn ? ProfileService.getInstance() : null; } @@ -58,7 +60,18 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService { break; case TAGGED: if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback); - else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback); + else graphQLService.fetchTaggedPosts( + profileId, + 30, + nextMaxId, + CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> { + if (throwable != null) { + callback.onFailure(throwable); + return; + } + callback.onSuccess(postsFetchResponse); + }, Dispatchers.getIO()) + ); break; case COLLECTION: case SAVED: diff --git a/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java b/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java deleted file mode 100644 index 8524f192..00000000 --- a/app/src/main/java/awais/instagrabber/db/dao/AccountDao.java +++ /dev/null @@ -1,34 +0,0 @@ -package awais.instagrabber.db.dao; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Update; - -import java.util.List; - -import awais.instagrabber.db.entities.Account; - -@Dao -public interface AccountDao { - - @Query("SELECT * FROM accounts") - List getAllAccounts(); - - @Query("SELECT * FROM accounts WHERE uid = :uid") - Account findAccountByUid(String uid); - - @Insert(onConflict = OnConflictStrategy.REPLACE) - List insertAccounts(Account... accounts); - - @Update - void updateAccounts(Account... accounts); - - @Delete - void deleteAccounts(Account... accounts); - - @Query("DELETE from accounts") - void deleteAllAccounts(); -} diff --git a/app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt b/app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt new file mode 100644 index 00000000..5661de80 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt @@ -0,0 +1,25 @@ +package awais.instagrabber.db.dao + +import androidx.room.* +import awais.instagrabber.db.entities.Account + +@Dao +interface AccountDao { + @Query("SELECT * FROM accounts") + suspend fun getAllAccounts(): List + + @Query("SELECT * FROM accounts WHERE uid = :uid") + suspend fun findAccountByUid(uid: String): Account? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAccounts(vararg accounts: Account) + + @Update + suspend fun updateAccounts(vararg accounts: Account) + + @Delete + suspend fun deleteAccounts(vararg accounts: Account) + + @Query("DELETE from accounts") + suspend fun deleteAllAccounts() +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java deleted file mode 100644 index 4bf0f2ad..00000000 --- a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java +++ /dev/null @@ -1,35 +0,0 @@ -package awais.instagrabber.db.dao; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Update; - -import java.util.List; - -import awais.instagrabber.db.entities.Favorite; -import awais.instagrabber.models.enums.FavoriteType; - -@Dao -public interface FavoriteDao { - - @Query("SELECT * FROM favorites") - List getAllFavorites(); - - @Query("SELECT * FROM favorites WHERE query_text = :query and type = :type") - Favorite findFavoriteByQueryAndType(String query, FavoriteType type); - - @Insert - List insertFavorites(Favorite... favorites); - - @Update - void updateFavorites(Favorite... favorites); - - @Delete - void deleteFavorites(Favorite... favorites); - - @Query("DELETE from favorites") - void deleteAllFavorites(); -} diff --git a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.kt b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.kt new file mode 100644 index 00000000..f78e531a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.kt @@ -0,0 +1,26 @@ +package awais.instagrabber.db.dao + +import androidx.room.* +import awais.instagrabber.db.entities.Favorite +import awais.instagrabber.models.enums.FavoriteType + +@Dao +interface FavoriteDao { + @Query("SELECT * FROM favorites") + suspend fun getAllFavorites(): List + + @Query("SELECT * FROM favorites WHERE query_text = :query and type = :type") + suspend fun findFavoriteByQueryAndType(query: String, type: FavoriteType): Favorite? + + @Insert + suspend fun insertFavorites(vararg favorites: Favorite) + + @Update + suspend fun updateFavorites(vararg favorites: Favorite) + + @Delete + suspend fun deleteFavorites(vararg favorites: Favorite) + + @Query("DELETE from favorites") + suspend fun deleteAllFavorites() +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java deleted file mode 100644 index a2ffe515..00000000 --- a/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.java +++ /dev/null @@ -1,68 +0,0 @@ -package awais.instagrabber.db.datasources; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.List; - -import awais.instagrabber.db.AppDatabase; -import awais.instagrabber.db.dao.AccountDao; -import awais.instagrabber.db.entities.Account; - -public class AccountDataSource { - private static final String TAG = AccountDataSource.class.getSimpleName(); - - private static AccountDataSource INSTANCE; - - private final AccountDao accountDao; - - private AccountDataSource(final AccountDao accountDao) { - this.accountDao = accountDao; - } - - public static AccountDataSource getInstance(@NonNull Context context) { - if (INSTANCE == null) { - synchronized (AccountDataSource.class) { - if (INSTANCE == null) { - final AppDatabase database = AppDatabase.getDatabase(context); - INSTANCE = new AccountDataSource(database.accountDao()); - } - } - } - return INSTANCE; - } - - @Nullable - public final Account getAccount(final String uid) { - return accountDao.findAccountByUid(uid); - } - - @NonNull - public final List getAllAccounts() { - return accountDao.getAllAccounts(); - } - - public final void insertOrUpdateAccount(final String uid, - final String username, - final String cookie, - final String fullName, - final String profilePicUrl) { - final Account account = getAccount(uid); - final Account toUpdate = new Account(account == null ? 0 : account.getId(), uid, username, cookie, fullName, profilePicUrl); - if (account != null) { - accountDao.updateAccounts(toUpdate); - return; - } - accountDao.insertAccounts(toUpdate); - } - - public final void deleteAccount(@NonNull final Account account) { - accountDao.deleteAccounts(account); - } - - public final void deleteAllAccounts() { - accountDao.deleteAllAccounts(); - } -} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt b/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt new file mode 100644 index 00000000..3324a807 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/AccountDataSource.kt @@ -0,0 +1,49 @@ +package awais.instagrabber.db.datasources + +import android.content.Context +import awais.instagrabber.db.AppDatabase +import awais.instagrabber.db.dao.AccountDao +import awais.instagrabber.db.entities.Account + +class AccountDataSource private constructor(private val accountDao: AccountDao) { + suspend fun getAccount(uid: String): Account? = accountDao.findAccountByUid(uid) + + suspend fun getAllAccounts(): List = accountDao.getAllAccounts() + + suspend fun insertOrUpdateAccount( + uid: String, + username: String, + cookie: String, + fullName: String, + profilePicUrl: String?, + ) { + val account = getAccount(uid) + val toUpdate = Account(account?.id ?: 0, uid, username, cookie, fullName, profilePicUrl) + if (account != null) { + accountDao.updateAccounts(toUpdate) + return + } + accountDao.insertAccounts(toUpdate) + } + + suspend fun deleteAccount(account: Account) = accountDao.deleteAccounts(account) + + suspend fun deleteAllAccounts() = accountDao.deleteAllAccounts() + + companion object { + private lateinit var INSTANCE: AccountDataSource + + @JvmStatic + fun getInstance(context: Context): AccountDataSource { + if (!this::INSTANCE.isInitialized) { + synchronized(AccountDataSource::class.java) { + if (!this::INSTANCE.isInitialized) { + val database = AppDatabase.getDatabase(context) + INSTANCE = AccountDataSource(database.accountDao()) + } + } + } + return INSTANCE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java deleted file mode 100644 index bc013926..00000000 --- a/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.java +++ /dev/null @@ -1,62 +0,0 @@ -package awais.instagrabber.db.datasources; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.List; - -import awais.instagrabber.db.AppDatabase; -import awais.instagrabber.db.dao.FavoriteDao; -import awais.instagrabber.db.entities.Favorite; -import awais.instagrabber.models.enums.FavoriteType; - -public class FavoriteDataSource { - private static final String TAG = FavoriteDataSource.class.getSimpleName(); - - private static FavoriteDataSource INSTANCE; - - private final FavoriteDao favoriteDao; - - private FavoriteDataSource(final FavoriteDao favoriteDao) { - this.favoriteDao = favoriteDao; - } - - public static synchronized FavoriteDataSource getInstance(@NonNull Context context) { - if (INSTANCE == null) { - synchronized (FavoriteDataSource.class) { - if (INSTANCE == null) { - final AppDatabase database = AppDatabase.getDatabase(context); - INSTANCE = new FavoriteDataSource(database.favoriteDao()); - } - } - } - return INSTANCE; - } - - - @Nullable - public final Favorite getFavorite(@NonNull final String query, @NonNull final FavoriteType type) { - return favoriteDao.findFavoriteByQueryAndType(query, type); - } - - @NonNull - public final List getAllFavorites() { - return favoriteDao.getAllFavorites(); - } - - public final void insertOrUpdateFavorite(@NonNull final Favorite favorite) { - if (favorite.getId() != 0) { - favoriteDao.updateFavorites(favorite); - return; - } - favoriteDao.insertFavorites(favorite); - } - - public final void deleteFavorite(@NonNull final String query, @NonNull final FavoriteType type) { - final Favorite favorite = getFavorite(query, type); - if (favorite == null) return; - favoriteDao.deleteFavorites(favorite); - } -} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.kt b/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.kt new file mode 100644 index 00000000..e14a9409 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/FavoriteDataSource.kt @@ -0,0 +1,44 @@ +package awais.instagrabber.db.datasources + +import android.content.Context +import awais.instagrabber.db.AppDatabase +import awais.instagrabber.db.dao.FavoriteDao +import awais.instagrabber.db.entities.Favorite +import awais.instagrabber.models.enums.FavoriteType + +class FavoriteDataSource private constructor(private val favoriteDao: FavoriteDao) { + suspend fun getFavorite(query: String, type: FavoriteType): Favorite? = favoriteDao.findFavoriteByQueryAndType(query, type) + + suspend fun getAllFavorites(): List = favoriteDao.getAllFavorites() + + suspend fun insertOrUpdateFavorite(favorite: Favorite) { + if (favorite.id != 0) { + favoriteDao.updateFavorites(favorite) + return + } + favoriteDao.insertFavorites(favorite) + } + + suspend fun deleteFavorite(query: String, type: FavoriteType) { + val favorite = getFavorite(query, type) ?: return + favoriteDao.deleteFavorites(favorite) + } + + companion object { + private lateinit var INSTANCE: FavoriteDataSource + + @JvmStatic + @Synchronized + fun getInstance(context: Context): FavoriteDataSource { + if (!this::INSTANCE.isInitialized) { + synchronized(FavoriteDataSource::class.java) { + if (!this::INSTANCE.isInitialized) { + val database = AppDatabase.getDatabase(context) + INSTANCE = FavoriteDataSource(database.favoriteDao()) + } + } + } + return INSTANCE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java deleted file mode 100644 index 0dc1031c..00000000 --- a/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.java +++ /dev/null @@ -1,131 +0,0 @@ -package awais.instagrabber.db.repositories; - -import java.util.List; - -import awais.instagrabber.db.datasources.AccountDataSource; -import awais.instagrabber.db.entities.Account; -import awais.instagrabber.utils.AppExecutors; - -public class AccountRepository { - private static final String TAG = AccountRepository.class.getSimpleName(); - - private static AccountRepository instance; - - private final AppExecutors appExecutors; - private final AccountDataSource accountDataSource; - - // private List cachedAccounts; - - private AccountRepository(final AppExecutors appExecutors, final AccountDataSource accountDataSource) { - this.appExecutors = appExecutors; - this.accountDataSource = accountDataSource; - } - - public static AccountRepository getInstance(final AccountDataSource accountDataSource) { - if (instance == null) { - instance = new AccountRepository(AppExecutors.INSTANCE, accountDataSource); - } - return instance; - } - - public void getAccount(final long uid, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - final Account account = accountDataSource.getAccount(String.valueOf(uid)); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - if (account == null) { - callback.onDataNotAvailable(); - return; - } - callback.onSuccess(account); - }); - }); - } - - public void getAllAccounts(final RepositoryCallback> callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - final List accounts = accountDataSource.getAllAccounts(); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - if (accounts == null) { - callback.onDataNotAvailable(); - return; - } - // cachedAccounts = accounts; - callback.onSuccess(accounts); - }); - }); - } - - public void insertOrUpdateAccounts(final List accounts, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - for (final Account account : accounts) { - accountDataSource.insertOrUpdateAccount(account.getUid(), - account.getUsername(), - account.getCookie(), - account.getFullName(), - account.getProfilePic()); - } - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - callback.onSuccess(null); - }); - }); - } - - public void insertOrUpdateAccount(final long uid, - final String username, - final String cookie, - final String fullName, - final String profilePicUrl, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - accountDataSource.insertOrUpdateAccount(String.valueOf(uid), username, cookie, fullName, profilePicUrl); - final Account updated = accountDataSource.getAccount(String.valueOf(uid)); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - if (updated == null) { - callback.onDataNotAvailable(); - return; - } - callback.onSuccess(updated); - }); - }); - } - - public void deleteAccount(final Account account, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - accountDataSource.deleteAccount(account); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - callback.onSuccess(null); - }); - }); - } - - public void deleteAllAccounts(final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - accountDataSource.deleteAllAccounts(); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - callback.onSuccess(null); - }); - }); - } - -} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt b/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt new file mode 100644 index 00000000..53d21f1f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/AccountRepository.kt @@ -0,0 +1,49 @@ +package awais.instagrabber.db.repositories + +import awais.instagrabber.db.datasources.AccountDataSource +import awais.instagrabber.db.entities.Account + +class AccountRepository private constructor(private val accountDataSource: AccountDataSource) { + suspend fun getAccount(uid: Long): Account? = accountDataSource.getAccount(uid.toString()) + + suspend fun getAllAccounts(): List = accountDataSource.getAllAccounts() + + suspend fun insertOrUpdateAccounts(accounts: List) { + for (account in accounts) { + accountDataSource.insertOrUpdateAccount( + account.uid, + account.username, + account.cookie, + account.fullName, + account.profilePic + ) + } + } + + suspend fun insertOrUpdateAccount( + uid: Long, + username: String, + cookie: String, + fullName: String, + profilePicUrl: String?, + ): Account? { + accountDataSource.insertOrUpdateAccount(uid.toString(), username, cookie, fullName, profilePicUrl) + return accountDataSource.getAccount(uid.toString()) + } + + suspend fun deleteAccount(account: Account) = accountDataSource.deleteAccount(account) + + suspend fun deleteAllAccounts() = accountDataSource.deleteAllAccounts() + + companion object { + private lateinit var instance: AccountRepository + + @JvmStatic + fun getInstance(accountDataSource: AccountDataSource): AccountRepository { + if (!this::instance.isInitialized) { + instance = AccountRepository(accountDataSource) + } + return instance + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java deleted file mode 100644 index eb130e22..00000000 --- a/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.java +++ /dev/null @@ -1,88 +0,0 @@ -package awais.instagrabber.db.repositories; - -import java.util.List; - -import awais.instagrabber.db.datasources.FavoriteDataSource; -import awais.instagrabber.db.entities.Favorite; -import awais.instagrabber.models.enums.FavoriteType; -import awais.instagrabber.utils.AppExecutors; - -public class FavoriteRepository { - private static final String TAG = FavoriteRepository.class.getSimpleName(); - - private static FavoriteRepository instance; - - private final AppExecutors appExecutors; - private final FavoriteDataSource favoriteDataSource; - - private FavoriteRepository(final AppExecutors appExecutors, final FavoriteDataSource favoriteDataSource) { - this.appExecutors = appExecutors; - this.favoriteDataSource = favoriteDataSource; - } - - public static FavoriteRepository getInstance(final FavoriteDataSource favoriteDataSource) { - if (instance == null) { - instance = new FavoriteRepository(AppExecutors.INSTANCE, favoriteDataSource); - } - return instance; - } - - public void getFavorite(final String query, final FavoriteType type, final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - final Favorite favorite = favoriteDataSource.getFavorite(query, type); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - if (favorite == null) { - callback.onDataNotAvailable(); - return; - } - callback.onSuccess(favorite); - }); - }); - } - - public void getAllFavorites(final RepositoryCallback> callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - final List favorites = favoriteDataSource.getAllFavorites(); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - if (favorites == null) { - callback.onDataNotAvailable(); - return; - } - callback.onSuccess(favorites); - }); - }); - } - - public void insertOrUpdateFavorite(final Favorite favorite, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - favoriteDataSource.insertOrUpdateFavorite(favorite); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - callback.onSuccess(null); - }); - }); - } - - public void deleteFavorite(final String query, - final FavoriteType type, - final RepositoryCallback callback) { - // request on the I/O thread - appExecutors.getDiskIO().execute(() -> { - favoriteDataSource.deleteFavorite(query, type); - // notify on the main thread - appExecutors.getMainThread().execute(() -> { - if (callback == null) return; - callback.onSuccess(null); - }); - }); - } -} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.kt b/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.kt new file mode 100644 index 00000000..5a1b2572 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/FavoriteRepository.kt @@ -0,0 +1,28 @@ +package awais.instagrabber.db.repositories + +import awais.instagrabber.db.datasources.FavoriteDataSource +import awais.instagrabber.db.entities.Favorite +import awais.instagrabber.models.enums.FavoriteType + +class FavoriteRepository private constructor(private val favoriteDataSource: FavoriteDataSource) { + + suspend fun getFavorite(query: String, type: FavoriteType): Favorite? = favoriteDataSource.getFavorite(query, type) + + suspend fun getAllFavorites(): List = favoriteDataSource.getAllFavorites() + + suspend fun insertOrUpdateFavorite(favorite: Favorite) = favoriteDataSource.insertOrUpdateFavorite(favorite) + + suspend fun deleteFavorite(query: String, type: FavoriteType) = favoriteDataSource.deleteFavorite(query, type) + + companion object { + private lateinit var instance: FavoriteRepository + + @JvmStatic + fun getInstance(favoriteDataSource: FavoriteDataSource): FavoriteRepository { + if (!this::instance.isInitialized) { + instance = FavoriteRepository(favoriteDataSource) + } + return instance + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java index 25251c84..23d89959 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java @@ -3,6 +3,7 @@ package awais.instagrabber.dialogs; import android.app.Dialog; import android.content.Context; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,6 +15,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,30 +25,29 @@ import awais.instagrabber.databinding.DialogAccountSwitcherBinding; import awais.instagrabber.db.datasources.AccountDataSource; import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.repositories.AccountRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.ProcessPhoenix; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; public class AccountSwitcherDialogFragment extends DialogFragment { + private static final String TAG = AccountSwitcherDialogFragment.class.getSimpleName(); private AccountRepository accountRepository; private OnAddAccountClickListener onAddAccountClickListener; private DialogAccountSwitcherBinding binding; - public AccountSwitcherDialogFragment() { - accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); - } + public AccountSwitcherDialogFragment() {} public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) { this.onAddAccountClickListener = onAddAccountClickListener; - accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); } private final AccountSwitcherAdapter.OnAccountClickListener accountClickListener = (model, isCurrent) -> { @@ -80,17 +81,15 @@ public class AccountSwitcherDialogFragment extends DialogFragment { .setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername())) .setPositiveButton(R.string.yes, (dialog, which) -> { if (accountRepository == null) return; - accountRepository.deleteAccount(model, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - dismiss(); - } - - @Override - public void onDataNotAvailable() { - dismiss(); - } - }); + accountRepository.deleteAccount( + model, + CoroutineUtilsKt.getContinuation((unit, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + dismiss(); + if (throwable != null) { + Log.e(TAG, "deleteAccount: ", throwable); + } + }), Dispatchers.getIO()) + ); }) .setNegativeButton(R.string.cancel, null) .show(); @@ -113,6 +112,12 @@ public class AccountSwitcherDialogFragment extends DialogFragment { init(); } + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); + } + @Override public void onStart() { super.onStart(); @@ -129,18 +134,19 @@ public class AccountSwitcherDialogFragment extends DialogFragment { final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener); binding.accounts.setAdapter(adapter); if (accountRepository == null) return; - accountRepository.getAllAccounts(new RepositoryCallback>() { - @Override - public void onSuccess(final List accounts) { - if (accounts == null) return; - final String cookie = settingsHelper.getString(Constants.COOKIE); - sortUserList(cookie, accounts); - adapter.submitList(accounts); - } - - @Override - public void onDataNotAvailable() {} - }); + accountRepository.getAllAccounts( + CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "init: ", throwable); + return; + } + if (accounts == null) return; + final String cookie = settingsHelper.getString(Constants.COOKIE); + final List copy = new ArrayList<>(accounts); + sortUserList(cookie, copy); + adapter.submitList(copy); + }), Dispatchers.getIO()) + ); binding.addAccountBtn.setOnClickListener(v -> { if (onAddAccountClickListener == null) return; onAddAccountClickListener.onAddAccountClick(this); diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java index 84a829cb..e1915f8d 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java @@ -28,13 +28,14 @@ import java.io.File; import awais.instagrabber.R; import awais.instagrabber.databinding.DialogProfilepicBinding; -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.DownloadUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.UserService; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -129,33 +130,29 @@ public class ProfilePicDialogFragment extends DialogFragment { private void fetchAvatar() { if (isLoggedIn) { - final UserService userService = UserService.getInstance(); - userService.getUserInfo(id, new ServiceCallback() { - @Override - public void onSuccess(final User result) { - if (result != null) { - final String url = result.getHDProfilePicUrl(); - if (url == null) { - final Context context = getContext(); - if (context == null) return; - Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show(); - return; - } - setupPhoto(url); - } - } - - @Override - public void onFailure(final Throwable t) { + final UserService userService = UserService.INSTANCE; + userService.getUserInfo(id, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { final Context context = getContext(); if (context == null) { dismiss(); return; } - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); dismiss(); + return; } - }); + if (user != null) { + final String url = user.getHDProfilePicUrl(); + if (TextUtils.isEmpty(url)) { + final Context context = getContext(); + if (context == null) return; + Toast.makeText(context, R.string.no_profile_pic_found, Toast.LENGTH_LONG).show(); + return; + } + setupPhoto(url); + } + }), Dispatchers.getIO())); } else setupPhoto(fallbackUrl); } diff --git a/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java index 52b4fed7..19fcf309 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java @@ -30,9 +30,12 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentFollowersViewerBinding; import awais.instagrabber.models.FollowModel; import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; import thoughtbot.expandableadapter.ExpandableGroup; public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { @@ -68,10 +71,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh if (!isFollowersList) followModels.addAll(result.getItems()); if (result.isMoreAvailable()) { endCursor = result.getNextMaxId(); - friendshipService.getList(false, profileId, endCursor, this); + friendshipService.getList( + false, + profileId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + onFailure(throwable); + return; + } + onSuccess(response); + }), Dispatchers.getIO()) + ); } else if (followersModels.size() == 0) { if (!isFollowersList) moreAvailable = false; - friendshipService.getList(true, profileId, null, followingFetchCb); + friendshipService.getList( + true, + profileId, + null, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + followingFetchCb.onFailure(throwable); + return; + } + followingFetchCb.onSuccess(response); + }), Dispatchers.getIO()) + ); } else { if (!isFollowersList) moreAvailable = false; showCompare(); @@ -84,8 +109,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh try { binding.swipeRefreshLayout.setRefreshing(false); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); - } - catch(Throwable e) {} + } catch (Throwable ignored) {} Log.e(TAG, "Error fetching list (double, following)", t); } }; @@ -97,10 +121,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh if (isFollowersList) followModels.addAll(result.getItems()); if (result.isMoreAvailable()) { endCursor = result.getNextMaxId(); - friendshipService.getList(true, profileId, endCursor, this); + friendshipService.getList( + true, + profileId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + onFailure(throwable); + return; + } + onSuccess(response); + }), Dispatchers.getIO()) + ); } else if (followingModels.size() == 0) { if (isFollowersList) moreAvailable = false; - friendshipService.getList(false, profileId, null, followingFetchCb); + friendshipService.getList( + false, + profileId, + null, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + followingFetchCb.onFailure(throwable); + return; + } + followingFetchCb.onSuccess(response); + }), Dispatchers.getIO()) + ); } else { if (isFollowersList) moreAvailable = false; showCompare(); @@ -113,8 +159,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh try { binding.swipeRefreshLayout.setRefreshing(false); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); - } - catch(Throwable e) {} + } catch (Throwable ignored) {} Log.e(TAG, "Error fetching list (double, follower)", t); } }; @@ -122,7 +167,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - friendshipService = FriendshipService.getInstance(null, null, 0); + friendshipService = FriendshipService.INSTANCE; fragmentActivity = (AppCompatActivity) getActivity(); setHasOptionsMenu(true); } @@ -174,6 +219,13 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh onRefresh(); } + @Override + public void onResume() { + super.onResume(); + setTitle(username); + setSubtitle(type); + } + private void setTitle(final String title) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar == null) return; @@ -228,8 +280,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh try { binding.swipeRefreshLayout.setRefreshing(false); Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); - } - catch(Throwable e) {} + } catch (Throwable ignored) {} Log.e(TAG, "Error fetching list (single)", t); } }; @@ -238,7 +289,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh if (!TextUtils.isEmpty(endCursor) && !searching) { binding.swipeRefreshLayout.setRefreshing(true); layoutManager.setStackFromEnd(true); - friendshipService.getList(isFollowersList, profileId, endCursor, cb); + friendshipService.getList( + isFollowersList, + profileId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(response); + }), Dispatchers.getIO()) + ); endCursor = null; } }); @@ -246,7 +308,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh binding.rvFollow.setLayoutManager(layoutManager); if (moreAvailable) { binding.swipeRefreshLayout.setRefreshing(true); - friendshipService.getList(isFollowersList, profileId, endCursor, cb); + friendshipService.getList( + isFollowersList, + profileId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(response); + }), Dispatchers.getIO()) + ); } else { refreshAdapter(followModels, null, null, null); layoutManager.scrollToPosition(0); @@ -262,17 +335,34 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh if (moreAvailable) { binding.swipeRefreshLayout.setRefreshing(true); Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); - friendshipService.getList(isFollowersList, - profileId, - endCursor, - isFollowersList ? followersFetchCb : followingFetchCb); + friendshipService.getList( + isFollowersList, + profileId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + final ServiceCallback callback = isFollowersList ? followersFetchCb : followingFetchCb; + if (throwable != null) { + callback.onFailure(throwable); + return; + } + callback.onSuccess(response); + }), Dispatchers.getIO()) + ); } else if (followersModels.size() == 0 || followingModels.size() == 0) { binding.swipeRefreshLayout.setRefreshing(true); Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); - friendshipService.getList(!isFollowersList, - profileId, - null, - isFollowersList ? followingFetchCb : followersFetchCb); + friendshipService.getList( + !isFollowersList, + profileId, + null, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + final ServiceCallback callback = isFollowersList ? followingFetchCb : followersFetchCb; + if (throwable != null) { + callback.onFailure(throwable); + return; + } + callback.onSuccess(response); + }), Dispatchers.getIO())); } else showCompare(); } @@ -330,10 +420,10 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh final Context context = getContext(); if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show(); else if (isCompare) { - isCompare = !isCompare; + isCompare = false; listFollows(); } else { - isCompare = !isCompare; + isCompare = true; listCompare(); } return true; @@ -347,16 +437,15 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh final ArrayList groups = new ArrayList<>(1); if (isCompare && followingModels != null && followersModels != null && allFollowing != null) { - if (followingModels != null && followingModels.size() > 0) + if (followingModels.size() > 0) groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_following, username), followingModels)); - if (followersModels != null && followersModels.size() > 0) + if (followersModels.size() > 0) groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_follower, namePost), followersModels)); - if (allFollowing != null && allFollowing.size() > 0) + if (allFollowing.size() > 0) groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing)); } else if (followModels != null) { groups.add(new ExpandableGroup(type, followModels)); - } - else return; + } else return; adapter = new FollowAdapter(clickListener, groups); adapter.toggleGroup(0); binding.rvFollow.setAdapter(adapter); diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 6aa2ae20..441bf324 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -39,7 +39,6 @@ import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; import java.time.LocalDateTime; -import java.util.List; import java.util.Set; import awais.instagrabber.R; @@ -52,10 +51,8 @@ import awais.instagrabber.databinding.LayoutHashtagDetailsBinding; import awais.instagrabber.db.datasources.FavoriteDataSource; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.repositories.FavoriteRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.models.PostsLayoutPreferences; -import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FollowingType; import awais.instagrabber.repositories.requests.StoryViewerOptions; @@ -63,8 +60,10 @@ import awais.instagrabber.repositories.responses.Hashtag; import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Media; 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.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -72,6 +71,7 @@ import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.TagsService; +import kotlinx.coroutines.Dispatchers; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; @@ -218,20 +218,15 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe if (TextUtils.isEmpty(user.getUsername())) { // this only happens for anons opening = true; - graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback() { - @Override - public void onSuccess(final Media newFeedModel) { - opening = false; - if (newFeedModel == null) return; - openPostDialog(newFeedModel, profilePicView, mainPostImage, position); + graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> { + opening = false; + if (throwable != null) { + Log.e(TAG, "Error", throwable); + return; } - - @Override - public void onFailure(final Throwable t) { - opening = false; - Log.e(TAG, "Error", t); - } - }); + if (media == null) return; + AppExecutors.INSTANCE.getMainThread().execute(() -> openPostDialog(media, profilePicView, mainPostImage, position)); + }, Dispatchers.getIO())); return; } opening = true; @@ -303,8 +298,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe final String cookie = settingsHelper.getString(Constants.COOKIE); isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; tagsService = isLoggedIn ? TagsService.getInstance() : null; - storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + storiesService = isLoggedIn ? StoriesService.INSTANCE : null; + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; setHasOptionsMenu(true); } @@ -385,7 +380,13 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe private void fetchHashtagModel() { binding.swipeRefreshLayout.setRefreshing(true); if (isLoggedIn) tagsService.fetch(hashtag, cb); - else graphQLService.fetchTag(hashtag, cb); + else graphQLService.fetchTag(hashtag, CoroutineUtilsKt.getContinuation((hashtag1, throwable) -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + AppExecutors.INSTANCE.getMainThread().execute(() -> cb.onSuccess(hashtag1)); + }, Dispatchers.getIO())); } private void setupPosts() { @@ -478,73 +479,81 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe final Context context = getContext(); if (context == null) return; final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); - favoriteRepository.getFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - favoriteRepository.insertOrUpdateFavorite(new Favorite( - result.getId(), - hashtag, - FavoriteType.HASHTAG, - hashtagModel.getName(), - "res:/" + R.drawable.ic_hashtag, - result.getDateAdded() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.getFavorite( + hashtag, + FavoriteType.HASHTAG, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null || favorite == null) { + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); + return; } - - @Override - public void onDataNotAvailable() {} - }); - } - - @Override - public void onDataNotAvailable() { - hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); - } - }); - hashtagDetailsBinding.favChip.setOnClickListener( - v -> favoriteRepository.getFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - favoriteRepository.deleteFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + favorite.getId(), + hashtag, + FavoriteType.HASHTAG, + hashtagModel.getName(), + "res:/" + R.drawable.ic_hashtag, + favorite.getDateAdded() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + return; + } + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + }), Dispatchers.getIO()) + ); + }), Dispatchers.getIO()) + ); + hashtagDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite( + hashtag, + FavoriteType.HASHTAG, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "setHashtagDetails: ", throwable); + return; + } + if (favorite == null) { + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + 0, + hashtag, + FavoriteType.HASHTAG, + hashtagModel.getName(), + "res:/" + R.drawable.ic_hashtag, + LocalDateTime.now() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onDataNotAvailable: ", throwable1); + return; + } + hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + }), Dispatchers.getIO()) + ); + return; + } + favoriteRepository.deleteFavorite( + hashtag, + FavoriteType.HASHTAG, + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + return; + } hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites); hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); showSnackbar(getString(R.string.removed_from_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - - @Override - public void onDataNotAvailable() { - favoriteRepository.insertOrUpdateFavorite(new Favorite( - 0, - hashtag, - FavoriteType.HASHTAG, - hashtagModel.getName(), - "res:/" + R.drawable.ic_hashtag, - LocalDateTime.now() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - hashtagDetailsBinding.favChip.setText(R.string.favorite_short); - hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - showSnackbar(getString(R.string.added_to_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - })); + }), Dispatchers.getIO()) + ); + }), Dispatchers.getIO()) + ) + ); hashtagDetailsBinding.mainHashtagImage.setImageURI("res:/" + R.drawable.ic_hashtag); final String postCount = String.valueOf(hashtagModel.getMediaCount()); final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, @@ -578,24 +587,21 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe storiesFetching = true; storiesService.getUserStory( StoryViewerOptions.forHashtag(hashtagModel.getName()), - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1); - hasStories = true; - } else { - hasStories = false; - } + CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error", throwable); storiesFetching = false; + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - storiesFetching = false; + if (storyModels != null && !storyModels.isEmpty()) { + hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1); + hasStories = true; + } else { + hasStories = false; } - }); + storiesFetching = false; + }), Dispatchers.getIO()) + ); } private void setTitle() { diff --git a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java index c1f41568..57ca0761 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -25,12 +25,15 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentLikesBinding; import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -104,10 +107,13 @@ 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; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + 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.INSTANCE : null; + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; // setHasOptionsMenu(true); } @@ -129,8 +135,31 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme public void onRefresh() { if (isComment && !isLoggedIn) { lazyLoader.resetState(); - graphQLService.fetchCommentLikers(postId, null, anonCb); - } else mediaService.fetchLikes(postId, isComment, cb); + graphQLService.fetchCommentLikers( + postId, + null, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + anonCb.onFailure(throwable); + return; + } + anonCb.onSuccess(response); + }), Dispatchers.getIO()) + ); + } else { + mediaService.fetchLikes( + postId, + isComment, + CoroutineUtilsKt.getContinuation((users, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + //noinspection unchecked + cb.onSuccess((List) users); + }), Dispatchers.getIO()) + ); + } } private void init() { @@ -145,8 +174,19 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme binding.rvLikes.setLayoutManager(layoutManager); binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL)); lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (!TextUtils.isEmpty(endCursor)) - graphQLService.fetchCommentLikers(postId, endCursor, anonCb); + if (!TextUtils.isEmpty(endCursor)) { + graphQLService.fetchCommentLikers( + postId, + endCursor, + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + anonCb.onFailure(throwable); + return; + } + anonCb.onSuccess(response); + }), Dispatchers.getIO()) + ); + } endCursor = null; }); binding.rvLikes.addOnScrollListener(lazyLoader); diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index fd050345..58d4c2e0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -37,7 +37,6 @@ import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; import java.time.LocalDateTime; -import java.util.List; import java.util.Set; import awais.instagrabber.R; @@ -50,17 +49,17 @@ import awais.instagrabber.databinding.LayoutLocationDetailsBinding; import awais.instagrabber.db.datasources.FavoriteDataSource; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.repositories.FavoriteRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.models.PostsLayoutPreferences; -import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Media; 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.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -68,6 +67,7 @@ import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.LocationService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; +import kotlinx.coroutines.Dispatchers; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; @@ -208,20 +208,18 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR if (user == null) return; if (TextUtils.isEmpty(user.getUsername())) { opening = true; - graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback() { - @Override - public void onSuccess(final Media newFeedModel) { - opening = false; - if (newFeedModel == null) return; - openPostDialog(newFeedModel, profilePicView, mainPostImage, position); - } - - @Override - public void onFailure(final Throwable t) { - opening = false; - Log.e(TAG, "Error", t); - } - }); + graphQLService.fetchPost( + feedModel.getCode(), + CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + opening = false; + if (throwable != null) { + Log.e(TAG, "Error", throwable); + return; + } + if (media == null) return; + openPostDialog(media, profilePicView, mainPostImage, position); + })) + ); return; } opening = true; @@ -293,8 +291,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR final String cookie = settingsHelper.getString(Constants.COOKIE); isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; locationService = isLoggedIn ? LocationService.getInstance() : null; - storiesService = StoriesService.getInstance(null, 0L, null); - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + storiesService = StoriesService.INSTANCE; + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; setHasOptionsMenu(true); } @@ -402,7 +400,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR private void fetchLocationModel() { binding.swipeRefreshLayout.setRefreshing(true); if (isLoggedIn) locationService.fetch(locationId, cb); - else graphQLService.fetchLocation(locationId, cb); + else graphQLService.fetchLocation( + locationId, + CoroutineUtilsKt.getContinuation((location, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(location); + })) + ); } private void setupLocationDetails() { @@ -485,75 +492,82 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR final FavoriteDataSource dataSource = FavoriteDataSource.getInstance(context); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(dataSource); locationDetailsBinding.favChip.setVisibility(View.VISIBLE); - favoriteRepository.getFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - locationDetailsBinding.favChip.setText(R.string.favorite_short); - favoriteRepository.insertOrUpdateFavorite(new Favorite( - result.getId(), - String.valueOf(locationId), - FavoriteType.LOCATION, - locationModel.getName(), - "res:/" + R.drawable.ic_location, - result.getDateAdded() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) {} - - @Override - public void onDataNotAvailable() {} - }); - } - - @Override - public void onDataNotAvailable() { - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - locationDetailsBinding.favChip.setText(R.string.add_to_favorites); - } - }); - locationDetailsBinding.favChip.setOnClickListener(v -> { - favoriteRepository.getFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - favoriteRepository.deleteFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - locationDetailsBinding.favChip.setText(R.string.add_to_favorites); - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - showSnackbar(getString(R.string.removed_from_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - - @Override - public void onDataNotAvailable() { - favoriteRepository.insertOrUpdateFavorite(new Favorite( - 0, + favoriteRepository.getFavorite( + String.valueOf(locationId), + FavoriteType.LOCATION, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null || favorite == null) { + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + locationDetailsBinding.favChip.setText(R.string.add_to_favorites); + Log.e(TAG, "setupLocationDetails: ", throwable); + return; + } + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + locationDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + favorite.getId(), + String.valueOf(locationId), + FavoriteType.LOCATION, + locationModel.getName(), + "res:/" + R.drawable.ic_location, + favorite.getDateAdded() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + } + }), Dispatchers.getIO()) + ); + }), Dispatchers.getIO()) + ); + locationDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite( + String.valueOf(locationId), + FavoriteType.LOCATION, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "setupLocationDetails: ", throwable); + return; + } + if (favorite == null) { + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + 0, + String.valueOf(locationId), + FavoriteType.LOCATION, + locationModel.getName(), + "res:/" + R.drawable.ic_location, + LocalDateTime.now() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onDataNotAvailable: ", throwable1); + return; + } + locationDetailsBinding.favChip.setText(R.string.favorite_short); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + }), Dispatchers.getIO()) + ); + return; + } + favoriteRepository.deleteFavorite( String.valueOf(locationId), FavoriteType.LOCATION, - locationModel.getName(), - "res:/" + R.drawable.ic_location, - LocalDateTime.now() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - locationDetailsBinding.favChip.setText(R.string.favorite_short); - locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - showSnackbar(getString(R.string.added_to_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - }); - }); + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + return; + } + locationDetailsBinding.favChip.setText(R.string.add_to_favorites); + locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + showSnackbar(getString(R.string.removed_from_favs)); + }), Dispatchers.getIO()) + ); + }), Dispatchers.getIO()) + )); locationDetailsBinding.mainLocationImage.setOnClickListener(v -> { if (hasStories) { // show stories @@ -577,22 +591,19 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR storiesFetching = true; storiesService.getUserStory( StoryViewerOptions.forLocation(locationId, locationModel.getName()), - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - locationDetailsBinding.mainLocationImage.setStoriesBorder(1); - hasStories = true; - } + CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error", throwable); storiesFetching = false; + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - storiesFetching = false; + if (storyModels != null && !storyModels.isEmpty()) { + locationDetailsBinding.mainLocationImage.setStoriesBorder(1); + hasStories = true; } - }); + storiesFetching = false; + }), Dispatchers.getIO()) + ); } } diff --git a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java index c6ccff9f..52570afe 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -34,13 +34,13 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe 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 +48,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; @@ -66,6 +67,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe private String type; private long targetId; private Context context; + private long userId; private final ServiceCallback> cb = new ServiceCallback>() { @Override @@ -106,26 +108,25 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(mediaId, new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(NotificationsViewerFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "onSuccess: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - }); + mediaService.fetch( + mediaId, + CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + alertDialog.dismiss(); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + final NavController navController = NavHostFragment.findNavController(NotificationsViewerFragment.this); + final Bundle bundle = new Bundle(); + bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media); + try { + navController.navigate(R.id.action_global_post_view, bundle); + alertDialog.dismiss(); + } catch (Exception e) { + Log.e(TAG, "onSuccess: ", e); + } + }), Dispatchers.getIO()) + ); } } @@ -167,34 +168,40 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe break; case 1: if (model.getType() == NotificationType.REQUEST) { - friendshipService.approve(args.getUserId(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - onRefresh(); - Log.e(TAG, "approve: status was not ok!"); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "approve: onFailure: ", t); - } - }); + friendshipService.approve( + csrfToken, + userId, + deviceUuid, + args.getUserId(), + CoroutineUtilsKt.getContinuation( + (response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "approve: onFailure: ", throwable); + return; + } + onRefresh(); + }), + Dispatchers.getIO() + ) + ); return; } clickListener.onPreviewClick(model); break; case 2: - friendshipService.ignore(args.getUserId(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - onRefresh(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "ignore: onFailure: ", t); - } - }); + friendshipService.ignore( + csrfToken, + userId, + deviceUuid, + args.getUserId(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "approve: onFailure: ", throwable); + return; + } + onRefresh(); + }), Dispatchers.getIO()) + ); break; } }; @@ -218,11 +225,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); + userId = CookieUtils.getUserIdFromCookie(cookie); deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId); + friendshipService = FriendshipService.INSTANCE; + mediaService = MediaService.INSTANCE; newsService = NewsService.getInstance(); } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java index 6521e735..b21a77e8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java @@ -41,12 +41,15 @@ import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.HighlightModel; import awais.instagrabber.repositories.requests.StoryViewerOptions; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.viewmodels.ArchivesViewModel; import awais.instagrabber.viewmodels.FeedStoriesViewModel; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService.ArchiveFetchResponse; +import kotlinx.coroutines.Dispatchers; public final class StoryListViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "StoryListViewerFragment"; @@ -133,7 +136,7 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr context = getContext(); if (context == null) return; setHasOptionsMenu(true); - storiesService = StoriesService.getInstance(null, 0L, null); + storiesService = StoriesService.INSTANCE; } @NonNull @@ -239,22 +242,31 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr } firstRefresh = false; } else if (type.equals("feed")) { - storiesService.getFeedStories(new ServiceCallback>() { - @Override - public void onSuccess(final List result) { - feedStoriesViewModel.getList().postValue(result); - adapter.submitList(result); - binding.swipeRefreshLayout.setRefreshing(false); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "failed", t); - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); + storiesService.getFeedStories( + CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "failed", throwable); + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + return; + } + //noinspection unchecked + feedStoriesViewModel.getList().postValue((List) feedStoryModels); + //noinspection unchecked + adapter.submitList((List) feedStoryModels); + binding.swipeRefreshLayout.setRefreshing(false); + }), Dispatchers.getIO()) + ); } else if (type.equals("archive")) { - storiesService.fetchArchive(endCursor, cb); + storiesService.fetchArchive( + endCursor, + CoroutineUtilsKt.getContinuation((archiveFetchResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + cb.onFailure(throwable); + return; + } + cb.onSuccess(archiveFetchResponse); + }), Dispatchers.getIO()) + ); } } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index 8ce173de..9a6ab1f0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -7,6 +7,8 @@ import android.graphics.drawable.Animatable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; import android.view.GestureDetector; import android.view.Gravity; @@ -85,8 +87,7 @@ 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; @@ -111,6 +112,8 @@ import static awais.instagrabber.utils.Utils.settingsHelper; public class StoryViewerFragment extends Fragment { private static final String TAG = "StoryViewerFragment"; + private final String cookie = settingsHelper.getString(Constants.COOKIE); + private AppCompatActivity fragmentActivity; private View root; private FragmentStoryViewerBinding binding; @@ -146,21 +149,22 @@ public class StoryViewerFragment extends Fragment { // private boolean isArchive; // private boolean isNotification; private DirectMessagesService directMessagesService; - - private final String cookie = settingsHelper.getString(Constants.COOKIE); private StoryViewerOptions options; + private String csrfToken; + private String deviceId; + private long userId; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); if (csrfToken == null) return; - final long userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); - final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID); + userId = CookieUtils.getUserIdFromCookie(cookie); + deviceId = settingsHelper.getString(Constants.DEVICE_UUID); fragmentActivity = (AppCompatActivity) requireActivity(); - storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId); - mediaService = MediaService.getInstance(null, null, 0); - directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); + storiesService = StoriesService.INSTANCE; + mediaService = MediaService.INSTANCE; + directMessagesService = DirectMessagesService.INSTANCE; setHasOptionsMenu(true); } @@ -214,37 +218,58 @@ public class StoryViewerFragment extends Fragment { if (itemId == R.id.action_dms) { final EditText input = new EditText(context); input.setHint(R.string.reply_hint); - new AlertDialog.Builder(context) + final AlertDialog ad = new AlertDialog.Builder(context) .setTitle(R.string.reply_story) .setView(input) .setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread( + csrfToken, + userId, + deviceId, 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(); return; } directMessagesService.broadcastStoryReply( + csrfToken, + userId, + deviceId, ThreadIdOrUserIds.of(thread.getThreadId()), 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(); + ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + input.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s)); + } + + @Override + public void afterTextChanged(final Editable s) {} + }); return true; } if (itemId == R.id.action_profile) { @@ -451,26 +476,25 @@ public class StoryViewerFragment extends Fragment { .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(Long.parseLong(mediaId), new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "openPostDialog: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - }); + mediaService.fetch( + Long.parseLong(mediaId), + CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + alertDialog.dismiss(); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this); + final Bundle bundle = new Bundle(); + bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media); + try { + navController.navigate(R.id.action_global_post_view, bundle); + alertDialog.dismiss(); + } catch (Exception e) { + Log.e(TAG, "openPostDialog: ", e); + } + }), Dispatchers.getIO()) + ); }); final View.OnClickListener storyActionListener = v -> { final Object tag = v.getTag(); @@ -498,28 +522,31 @@ public class StoryViewerFragment extends Fragment { }), (d, w) -> { sticking = true; storiesService.respondToPoll( + csrfToken, + userId, + deviceId, currentStory.getStoryMediaId().split("_")[0], poll.getId(), w, - new ServiceCallback() { - @Override - public void onSuccess(final StoryStickerResponse result) { - sticking = false; - try { - poll.setMyChoice(w); - Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - - @Override - public void onFailure(final Throwable t) { - sticking = false; - Log.e(TAG, "Error responding", t); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - }); + CoroutineUtilsKt.getContinuation( + (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + sticking = false; + Log.e(TAG, "Error responding", throwable); + try { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + return; + } + sticking = false; + try { + poll.setMyChoice(w); + Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + }), + Dispatchers.getIO() + ) + ); }) .setPositiveButton(R.string.cancel, null) .show(); @@ -528,36 +555,52 @@ public class StoryViewerFragment extends Fragment { question = (QuestionModel) tag; final EditText input = new EditText(context); input.setHint(R.string.answer_hint); - new AlertDialog.Builder(context) + final AlertDialog ad = new AlertDialog.Builder(context) .setTitle(question.getQuestion()) .setView(input) .setPositiveButton(R.string.confirm, (d, w) -> { sticking = true; storiesService.respondToQuestion( + csrfToken, + userId, + deviceId, currentStory.getStoryMediaId().split("_")[0], question.getId(), input.getText().toString(), - new ServiceCallback() { - @Override - public void onSuccess(final StoryStickerResponse result) { - sticking = false; - try { - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - - @Override - public void onFailure(final Throwable t) { - sticking = false; - Log.e(TAG, "Error responding", t); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - }); + CoroutineUtilsKt.getContinuation( + (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + sticking = false; + Log.e(TAG, "Error responding", throwable); + try { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + return; + } + sticking = false; + try { + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + }), + Dispatchers.getIO() + ) + ); }) .setNegativeButton(R.string.cancel, null) .show(); + ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + input.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s)); + } + + @Override + public void afterTextChanged(final Editable s) {} + }); } else if (tag instanceof String[]) { mentions = (String[]) tag; new AlertDialog.Builder(context) @@ -576,28 +619,31 @@ public class StoryViewerFragment extends Fragment { if (quiz.getMyChoice() == -1) { sticking = true; storiesService.respondToQuiz( + csrfToken, + userId, + deviceId, currentStory.getStoryMediaId().split("_")[0], quiz.getId(), w, - new ServiceCallback() { - @Override - public void onSuccess(final StoryStickerResponse result) { - sticking = false; - try { - quiz.setMyChoice(w); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - - @Override - public void onFailure(final Throwable t) { - sticking = false; - Log.e(TAG, "Error responding", t); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - }); + CoroutineUtilsKt.getContinuation( + (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + sticking = false; + Log.e(TAG, "Error responding", throwable); + try { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + return; + } + sticking = false; + try { + quiz.setMyChoice(w); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + }), + Dispatchers.getIO() + ) + ); } }) .setPositiveButton(R.string.cancel, null) @@ -644,28 +690,30 @@ public class StoryViewerFragment extends Fragment { .setPositiveButton(R.string.confirm, (d, w) -> { sticking = true; storiesService.respondToSlider( + csrfToken, + userId, + deviceId, currentStory.getStoryMediaId().split("_")[0], slider.getId(), sliderValue, - new ServiceCallback() { - @Override - public void onSuccess(final StoryStickerResponse result) { - sticking = false; - try { - slider.setMyChoice(sliderValue); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - - @Override - public void onFailure(final Throwable t) { - sticking = false; - Log.e(TAG, "Error responding", t); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - } - }); + CoroutineUtilsKt.getContinuation( + (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + sticking = false; + Log.e(TAG, "Error responding", throwable); + try { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + return; + } + sticking = false; + try { + slider.setMyChoice(sliderValue); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } catch (Exception ignored) {} + }), Dispatchers.getIO() + ) + ); }) .setNegativeButton(R.string.cancel, null) .show(); @@ -757,27 +805,26 @@ public class StoryViewerFragment extends Fragment { setTitle(type); storiesViewModel.getList().setValue(Collections.emptyList()); if (type == Type.STORY) { - storiesService.fetch(options.getId(), new ServiceCallback() { - @Override - public void onSuccess(final StoryModel storyModel) { - fetching = false; - binding.storiesList.setVisibility(View.GONE); - if (storyModel == null) { - storiesViewModel.getList().setValue(Collections.emptyList()); - currentStory = null; - return; - } - storiesViewModel.getList().setValue(Collections.singletonList(storyModel)); - currentStory = storyModel; - refreshStory(); - } - - @Override - public void onFailure(final Throwable t) { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Error", t); - } - }); + storiesService.fetch( + options.getId(), + CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Error", throwable); + return; + } + fetching = false; + binding.storiesList.setVisibility(View.GONE); + if (storyModel == null) { + storiesViewModel.getList().setValue(Collections.emptyList()); + currentStory = null; + return; + } + storiesViewModel.getList().setValue(Collections.singletonList(storyModel)); + currentStory = storyModel; + refreshStory(); + }), Dispatchers.getIO()) + ); return; } if (currentStoryMediaId == null) return; @@ -811,7 +858,17 @@ public class StoryViewerFragment extends Fragment { storyCallback.onSuccess(Collections.singletonList(live)); return; } - storiesService.getUserStory(fetchOptions, storyCallback); + storiesService.getUserStory( + fetchOptions, + CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + storyCallback.onFailure(throwable); + return; + } + //noinspection unchecked + storyCallback.onSuccess((List) storyModels); + }), Dispatchers.getIO()) + ); } private void setTitle(final Type type) { @@ -915,10 +972,15 @@ public class StoryViewerFragment extends Fragment { } if (settingsHelper.getBoolean(MARK_AS_SEEN)) - storiesService.seen(currentStory.getStoryMediaId(), - currentStory.getTimestamp(), - System.currentTimeMillis() / 1000, - null); + storiesService.seen( + csrfToken, + userId, + deviceId, + currentStory.getStoryMediaId(), + currentStory.getTimestamp(), + System.currentTimeMillis() / 1000, + CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO()) + ); } private void downloadStory() { diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java deleted file mode 100644 index ea895eba..00000000 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java +++ /dev/null @@ -1,531 +0,0 @@ -package awais.instagrabber.fragments.directmessages; - -import android.content.Context; -import android.os.Bundle; -import android.text.Editable; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavBackStackEntry; -import androidx.navigation.NavController; -import androidx.navigation.NavDestination; -import androidx.navigation.NavDirections; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.google.android.material.snackbar.Snackbar; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import awais.instagrabber.ProfileNavGraphDirections; -import awais.instagrabber.R; -import awais.instagrabber.UserSearchNavGraphDirections; -import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.DirectPendingUsersAdapter; -import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser; -import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback; -import awais.instagrabber.adapters.DirectUsersAdapter; -import awais.instagrabber.customviews.helpers.TextWatcherAdapter; -import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; -import awais.instagrabber.dialogs.ConfirmDialogFragment; -import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback; -import awais.instagrabber.dialogs.MultiOptionDialogFragment; -import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; -import awais.instagrabber.fragments.UserSearchFragment; -import awais.instagrabber.fragments.UserSearchFragmentDirections; -import awais.instagrabber.models.Resource; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.AppStateViewModel; -import awais.instagrabber.viewmodels.DirectSettingsViewModel; -import awais.instagrabber.viewmodels.factories.DirectSettingsViewModelFactory; - -public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback { - private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName(); - private static final int APPROVAL_REQUIRED_REQUEST_CODE = 200; - private static final int LEAVE_THREAD_REQUEST_CODE = 201; - private static final int END_THREAD_REQUEST_CODE = 202; - - private FragmentDirectMessagesSettingsBinding binding; - private DirectSettingsViewModel viewModel; - private DirectUsersAdapter usersAdapter; - private boolean isPendingRequestsSetupDone = false; - private DirectPendingUsersAdapter pendingUsersAdapter; - private Set approvalRequiredUsers; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle arguments = getArguments(); - if (arguments == null) return; - final DirectMessageSettingsFragmentArgs args = DirectMessageSettingsFragmentArgs.fromBundle(arguments); - final MainActivity fragmentActivity = (MainActivity) requireActivity(); - final AppStateViewModel appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); - final DirectSettingsViewModelFactory viewModelFactory = new DirectSettingsViewModelFactory( - fragmentActivity.getApplication(), - args.getThreadId(), - args.getPending(), - appStateViewModel.getCurrentUser() - ); - viewModel = new ViewModelProvider(this, viewModelFactory).get(DirectSettingsViewModel.class); - } - - @NonNull - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - final ViewGroup container, - final Bundle savedInstanceState) { - binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false); - // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - final Bundle arguments = getArguments(); - if (arguments == null) return; - init(); - setupObservers(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - isPendingRequestsSetupDone = false; - } - - private void setupObservers() { - viewModel.getInputMode().observe(getViewLifecycleOwner(), inputMode -> { - if (inputMode == null || inputMode == 0) return; - if (inputMode == 1) { - binding.groupSettings.setVisibility(View.GONE); - binding.pendingMembersGroup.setVisibility(View.GONE); - binding.approvalRequired.setVisibility(View.GONE); - binding.approvalRequiredLabel.setVisibility(View.GONE); - binding.muteMessagesLabel.setVisibility(View.GONE); - binding.muteMessages.setVisibility(View.GONE); - } - }); - // Need to observe, so that getValue is correct - viewModel.getUsers().observe(getViewLifecycleOwner(), users -> {}); - viewModel.getLeftUsers().observe(getViewLifecycleOwner(), users -> {}); - viewModel.getUsersAndLeftUsers().observe(getViewLifecycleOwner(), usersPair -> { - if (usersAdapter == null) return; - usersAdapter.submitUsers(usersPair.first, usersPair.second); - }); - viewModel.getTitle().observe(getViewLifecycleOwner(), title -> binding.titleEdit.setText(title)); - viewModel.getAdminUserIds().observe(getViewLifecycleOwner(), adminUserIds -> { - if (usersAdapter == null) return; - usersAdapter.setAdminUserIds(adminUserIds); - }); - viewModel.isMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); - viewModel.isPending().observe(getViewLifecycleOwner(), pending -> binding.muteMessages.setVisibility(pending ? View.GONE : View.VISIBLE)); - viewModel.isViewerAdmin().observe(getViewLifecycleOwner(), this::setApprovalRelatedUI); - viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required)); - viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests); - viewModel.isGroup().observe(getViewLifecycleOwner(), this::setupSettings); - final NavController navController = NavHostFragment.findNavController(this); - final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); - if (backStackEntry != null) { - final MutableLiveData resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); - resultLiveData.observe(getViewLifecycleOwner(), result -> { - if ((result instanceof RankedRecipient)) { - final RankedRecipient recipient = (RankedRecipient) result; - final User user = getUser(recipient); - // Log.d(TAG, "result: " + user); - if (user != null) { - addMembers(Collections.singleton(recipient.getUser())); - } - } else if ((result instanceof Set)) { - try { - //noinspection unchecked - final Set recipients = (Set) result; - final Set users = recipients.stream() - .filter(Objects::nonNull) - .map(this::getUser) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - // Log.d(TAG, "result: " + users); - addMembers(users); - } catch (Exception e) { - Log.e(TAG, "search users result: ", e); - Snackbar.make(binding.getRoot(), e.getMessage() != null ? e.getMessage() : "", Snackbar.LENGTH_LONG).show(); - } - } - }); - } - } - - private void addMembers(final Set users) { - final Boolean approvalRequired = viewModel.getApprovalRequiredToJoin().getValue(); - Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue(); - if (isViewerAdmin == null) { - isViewerAdmin = false; - } - if (!isViewerAdmin && approvalRequired != null && approvalRequired) { - approvalRequiredUsers = users; - final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( - APPROVAL_REQUIRED_REQUEST_CODE, - R.string.admin_approval_required, - R.string.admin_approval_required_description, - R.string.ok, - R.string.cancel, - 0 - ); - confirmDialogFragment.show(getChildFragmentManager(), "approval_required_dialog"); - return; - } - final LiveData> detailsChangeResourceLiveData = viewModel.addMembers(users); - observeDetailsChange(detailsChangeResourceLiveData); - } - - @Nullable - private User getUser(@NonNull final RankedRecipient recipient) { - User user = null; - if (recipient.getUser() != null) { - user = recipient.getUser(); - } else if (recipient.getThread() != null && !recipient.getThread().isGroup()) { - user = recipient.getThread().getUsers().get(0); - } - return user; - } - - private void init() { - // setupSettings(); - setupMembers(); - } - - private void setupSettings(final boolean isGroup) { - binding.groupSettings.setVisibility(isGroup ? View.VISIBLE : View.GONE); - binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle()); - binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> { - final LiveData> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute(); - handleSwitchChangeResource(resourceLiveData, buttonView); - }); - if (!isGroup) return; - binding.titleEdit.addTextChangedListener(new TextWatcherAdapter() { - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - if (s.toString().trim().equals(viewModel.getTitle().getValue())) { - binding.titleEditInputLayout.setSuffixText(null); - return; - } - binding.titleEditInputLayout.setSuffixText(getString(R.string.save)); - } - }); - binding.titleEditInputLayout.getSuffixTextView().setOnClickListener(v -> { - final Editable text = binding.titleEdit.getText(); - if (text == null) return; - final String newTitle = text.toString().trim(); - if (newTitle.equals(viewModel.getTitle().getValue())) return; - observeDetailsChange(viewModel.updateTitle(newTitle)); - }); - binding.addMembers.setOnClickListener(v -> { - if (!isAdded()) return; - final NavController navController = NavHostFragment.findNavController(this); - final NavDestination currentDestination = navController.getCurrentDestination(); - if (currentDestination == null) return; - if (currentDestination.getId() != R.id.directMessagesSettingsFragment) return; - final List users = viewModel.getUsers().getValue(); - final long[] currentUserIds; - if (users != null) { - currentUserIds = users.stream() - .mapToLong(User::getPk) - .sorted() - .toArray(); - } else { - currentUserIds = new long[0]; - } - final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections - .actionGlobalUserSearch() - .setTitle(getString(R.string.add_members)) - .setActionLabel(getString(R.string.add)) - .setHideUserIds(currentUserIds) - .setSearchMode(UserSearchFragment.SearchMode.RAVEN) - .setMultiple(true); - navController.navigate(actionGlobalUserSearch); - }); - binding.muteMentionsLabel.setOnClickListener(v -> binding.muteMentions.toggle()); - binding.muteMentions.setOnCheckedChangeListener((buttonView, isChecked) -> { - final LiveData> resourceLiveData = isChecked ? viewModel.muteMentions() : viewModel.unmuteMentions(); - handleSwitchChangeResource(resourceLiveData, buttonView); - }); - binding.leave.setOnClickListener(v -> { - final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( - LEAVE_THREAD_REQUEST_CODE, - R.string.dms_action_leave_question, - 0, - R.string.yes, - R.string.no, - 0 - ); - confirmDialogFragment.show(getChildFragmentManager(), "leave_thread_confirmation_dialog"); - }); - Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue(); - if (isViewerAdmin == null) isViewerAdmin = false; - if (isViewerAdmin) { - binding.end.setVisibility(View.VISIBLE); - binding.end.setOnClickListener(v -> { - final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( - END_THREAD_REQUEST_CODE, - R.string.dms_action_end_question, - R.string.dms_action_end_description, - R.string.yes, - R.string.no, - 0 - ); - confirmDialogFragment.show(getChildFragmentManager(), "end_thread_confirmation_dialog"); - }); - } else { - binding.end.setVisibility(View.GONE); - } - } - - private void setApprovalRelatedUI(final boolean isViewerAdmin) { - if (!isViewerAdmin) { - binding.pendingMembersGroup.setVisibility(View.GONE); - binding.approvalRequired.setVisibility(View.GONE); - binding.approvalRequiredLabel.setVisibility(View.GONE); - return; - } - binding.approvalRequired.setVisibility(View.VISIBLE); - binding.approvalRequiredLabel.setVisibility(View.VISIBLE); - binding.approvalRequiredLabel.setOnClickListener(v -> binding.approvalRequired.toggle()); - binding.approvalRequired.setOnCheckedChangeListener((buttonView, isChecked) -> { - final LiveData> resourceLiveData = isChecked ? viewModel.approvalRequired() : viewModel.approvalNotRequired(); - handleSwitchChangeResource(resourceLiveData, buttonView); - }); - } - - private void handleSwitchChangeResource(final LiveData> resourceLiveData, final CompoundButton buttonView) { - resourceLiveData.observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - switch (resource.status) { - case SUCCESS: - buttonView.setEnabled(true); - break; - case ERROR: - buttonView.setEnabled(true); - buttonView.setChecked(!buttonView.isChecked()); - if (resource.message != null) { - Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); - } - if (resource.resId != 0) { - Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); - } - break; - case LOADING: - buttonView.setEnabled(false); - break; - } - }); - } - - private void setupMembers() { - final Context context = getContext(); - if (context == null) return; - binding.users.setLayoutManager(new LinearLayoutManager(context)); - final User inviter = viewModel.getInviter().getValue(); - usersAdapter = new DirectUsersAdapter( - inviter != null ? inviter.getPk() : -1, - (position, user, selected) -> { - if (TextUtils.isEmpty(user.getUsername()) && !TextUtils.isEmpty(user.getFbId())) { - Utils.openURL(context, "https://facebook.com/" + user.getFbId()); - return; - } - if (TextUtils.isEmpty(user.getUsername())) return; - final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections - .actionGlobalProfileFragment("@" + user.getUsername()); - NavHostFragment.findNavController(this).navigate(directions); - }, - (position, user) -> { - final ArrayList> options = viewModel.createUserOptions(user); - if (options == null || options.isEmpty()) return true; - final MultiOptionDialogFragment fragment = MultiOptionDialogFragment.newInstance(-1, options); - fragment.setSingleCallback(new MultiOptionDialogFragment.MultiOptionDialogSingleCallback() { - @Override - public void onSelect(final String action) { - if (action == null) return; - observeDetailsChange(viewModel.doAction(user, action)); - } - - @Override - public void onCancel() {} - }); - final FragmentManager fragmentManager = getChildFragmentManager(); - fragment.show(fragmentManager, "actions"); - return true; - } - ); - binding.users.setAdapter(usersAdapter); - } - - private void setPendingRequests(final DirectThreadParticipantRequestsResponse requests) { - if (requests == null || requests.getUsers() == null || requests.getUsers().isEmpty()) { - binding.pendingMembersGroup.setVisibility(View.GONE); - return; - } - if (!isPendingRequestsSetupDone) { - final Context context = getContext(); - if (context == null) return; - binding.pendingMembers.setLayoutManager(new LinearLayoutManager(context)); - pendingUsersAdapter = new DirectPendingUsersAdapter(new PendingUserCallback() { - @Override - public void onClick(final int position, final PendingUser pendingUser) { - final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections - .actionGlobalProfileFragment("@" + pendingUser.getUser().getUsername()); - NavHostFragment.findNavController(DirectMessageSettingsFragment.this).navigate(directions); - } - - @Override - public void onApprove(final int position, final PendingUser pendingUser) { - final LiveData> resourceLiveData = viewModel.approveUsers(Collections.singletonList(pendingUser.getUser())); - observeApprovalChange(resourceLiveData, position, pendingUser); - } - - @Override - public void onDeny(final int position, final PendingUser pendingUser) { - final LiveData> resourceLiveData = viewModel.denyUsers(Collections.singletonList(pendingUser.getUser())); - observeApprovalChange(resourceLiveData, position, pendingUser); - } - }); - binding.pendingMembers.setAdapter(pendingUsersAdapter); - binding.pendingMembersGroup.setVisibility(View.VISIBLE); - isPendingRequestsSetupDone = true; - } - if (pendingUsersAdapter != null) { - pendingUsersAdapter.submitPendingRequests(requests); - } - } - - private void observeDetailsChange(@NonNull final LiveData> resourceLiveData) { - resourceLiveData.observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - switch (resource.status) { - case SUCCESS: - case LOADING: - break; - case ERROR: - if (resource.message != null) { - Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); - } - if (resource.resId != 0) { - Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); - } - break; - } - }); - } - - private void observeApprovalChange(@NonNull final LiveData> detailsChangeResourceLiveData, - final int position, - @NonNull final PendingUser pendingUser) { - detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - switch (resource.status) { - case SUCCESS: - // pending user will be removed from the list, so no need to set the progress to false - // pendingUser.setInProgress(false); - break; - case LOADING: - pendingUser.setInProgress(true); - break; - case ERROR: - pendingUser.setInProgress(false); - if (resource.message != null) { - Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); - } - if (resource.resId != 0) { - Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); - } - break; - } - pendingUsersAdapter.notifyItemChanged(position); - }); - } - - @Override - public void onPositiveButtonClicked(final int requestCode) { - if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE && approvalRequiredUsers != null) { - final LiveData> detailsChangeResourceLiveData = viewModel.addMembers(approvalRequiredUsers); - observeDetailsChange(detailsChangeResourceLiveData); - return; - } - if (requestCode == LEAVE_THREAD_REQUEST_CODE) { - final LiveData> resourceLiveData = viewModel.leave(); - resourceLiveData.observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - switch (resource.status) { - case SUCCESS: - final NavDirections directions = DirectMessageSettingsFragmentDirections.actionSettingsToInbox(); - NavHostFragment.findNavController(this).navigate(directions); - break; - case ERROR: - binding.leave.setEnabled(true); - if (resource.message != null) { - Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); - } - if (resource.resId != 0) { - Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); - } - break; - case LOADING: - binding.leave.setEnabled(false); - break; - } - }); - return; - } - if (requestCode == END_THREAD_REQUEST_CODE) { - final LiveData> resourceLiveData = viewModel.end(); - resourceLiveData.observe(getViewLifecycleOwner(), resource -> { - if (resource == null) return; - switch (resource.status) { - case SUCCESS: - break; - case ERROR: - binding.end.setEnabled(true); - if (resource.message != null) { - Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); - } - if (resource.resId != 0) { - Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); - } - break; - case LOADING: - binding.end.setEnabled(false); - break; - } - }); - } - } - - @Override - public void onNegativeButtonClicked(final int requestCode) { - if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) { - approvalRequiredUsers = null; - } - } - - @Override - public void onNeutralButtonClicked(final int requestCode) {} -} diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt new file mode 100644 index 00000000..74b56d7a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt @@ -0,0 +1,475 @@ +package awais.instagrabber.fragments.directmessages + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.LinearLayoutManager +import awais.instagrabber.ProfileNavGraphDirections +import awais.instagrabber.R +import awais.instagrabber.activities.MainActivity +import awais.instagrabber.adapters.DirectPendingUsersAdapter +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback +import awais.instagrabber.adapters.DirectUsersAdapter +import awais.instagrabber.customviews.helpers.TextWatcherAdapter +import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding +import awais.instagrabber.dialogs.ConfirmDialogFragment +import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback +import awais.instagrabber.dialogs.MultiOptionDialogFragment +import awais.instagrabber.dialogs.MultiOptionDialogFragment.MultiOptionDialogSingleCallback +import awais.instagrabber.fragments.UserSearchFragment +import awais.instagrabber.fragments.UserSearchFragmentDirections +import awais.instagrabber.models.Resource +import awais.instagrabber.repositories.responses.User +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.utils.TextUtils.isEmpty +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.extensions.TAG +import awais.instagrabber.viewmodels.AppStateViewModel +import awais.instagrabber.viewmodels.DirectSettingsViewModel +import awais.instagrabber.viewmodels.factories.DirectSettingsViewModelFactory +import com.google.android.material.snackbar.Snackbar +import java.util.* + +class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback { + private lateinit var viewModel: DirectSettingsViewModel + private lateinit var binding: FragmentDirectMessagesSettingsBinding + + private var usersAdapter: DirectUsersAdapter? = null + private var isPendingRequestsSetupDone = false + private var pendingUsersAdapter: DirectPendingUsersAdapter? = null + private var approvalRequiredUsers: Set? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val arguments = arguments ?: return + val args = DirectMessageSettingsFragmentArgs.fromBundle(arguments) + val fragmentActivity = requireActivity() as MainActivity + val appStateViewModel: AppStateViewModel by activityViewModels() + val currentUser = appStateViewModel.currentUser ?: return + val viewModelFactory = DirectSettingsViewModelFactory( + fragmentActivity.application, + args.threadId, + args.pending, + currentUser + ) + viewModel = ViewModelProvider(this, viewModelFactory).get(DirectSettingsViewModel::class.java) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false) + // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + init() + setupObservers() + } + + override fun onDestroyView() { + super.onDestroyView() + isPendingRequestsSetupDone = false + } + + private fun setupObservers() { + viewModel.inputMode.observe(viewLifecycleOwner, { inputMode: Int? -> + if (inputMode == null || inputMode == 0) return@observe + if (inputMode == 1) { + binding.groupSettings.visibility = View.GONE + binding.pendingMembersGroup.visibility = View.GONE + binding.approvalRequired.visibility = View.GONE + binding.approvalRequiredLabel.visibility = View.GONE + binding.muteMessagesLabel.visibility = View.GONE + binding.muteMessages.visibility = View.GONE + } + }) + // Need to observe, so that getValue is correct + viewModel.getUsers().observe(viewLifecycleOwner, { }) + viewModel.getLeftUsers().observe(viewLifecycleOwner, { }) + viewModel.getUsersAndLeftUsers().observe(viewLifecycleOwner, { usersAdapter?.submitUsers(it.first, it.second) }) + viewModel.getTitle().observe(viewLifecycleOwner, { binding.titleEdit.setText(it) }) + viewModel.getAdminUserIds().observe(viewLifecycleOwner, { usersAdapter?.setAdminUserIds(it) }) + viewModel.isMuted().observe(viewLifecycleOwner, { binding.muteMessages.isChecked = it }) + viewModel.isPending().observe(viewLifecycleOwner, { binding.muteMessages.visibility = if (it) View.GONE else View.VISIBLE }) + viewModel.isViewerAdmin().observe(viewLifecycleOwner, { setApprovalRelatedUI(it) }) + viewModel.getApprovalRequiredToJoin().observe(viewLifecycleOwner, { binding.approvalRequired.isChecked = it }) + viewModel.getPendingRequests().observe(viewLifecycleOwner, { setPendingRequests(it) }) + viewModel.isGroup().observe(viewLifecycleOwner, { isGroup: Boolean -> setupSettings(isGroup) }) + val navController = NavHostFragment.findNavController(this) + val backStackEntry = navController.currentBackStackEntry + if (backStackEntry != null) { + val resultLiveData = backStackEntry.savedStateHandle.getLiveData("result") + resultLiveData.observe(viewLifecycleOwner, { result: Any? -> + if (result == null) return@observe + if (result is RankedRecipient) { + val user = getUser(result) + // Log.d(TAG, "result: " + user); + if (user != null) { + addMembers(setOf(user)) + } + } else if (result is Set<*>) { + try { + @Suppress("UNCHECKED_CAST") val recipients = result as Set + val users: Set = recipients.asSequence() + .filterNotNull() + .map { getUser(it) } + .filterNotNull() + .toSet() + // Log.d(TAG, "result: " + users); + addMembers(users) + } catch (e: Exception) { + Log.e(TAG, "search users result: ", e) + Snackbar.make(binding.root, e.message ?: "", Snackbar.LENGTH_LONG).show() + } + } + }) + } + } + + private fun addMembers(users: Set) { + val approvalRequired = viewModel.getApprovalRequiredToJoin().value + var isViewerAdmin = viewModel.isViewerAdmin().value + if (isViewerAdmin == null) { + isViewerAdmin = false + } + if (!isViewerAdmin && approvalRequired != null && approvalRequired) { + approvalRequiredUsers = users + val confirmDialogFragment = ConfirmDialogFragment.newInstance( + APPROVAL_REQUIRED_REQUEST_CODE, + R.string.admin_approval_required, + R.string.admin_approval_required_description, + R.string.ok, + R.string.cancel, + 0 + ) + confirmDialogFragment.show(childFragmentManager, "approval_required_dialog") + return + } + val detailsChangeResourceLiveData = viewModel.addMembers(users) + observeDetailsChange(detailsChangeResourceLiveData) + } + + private fun getUser(recipient: RankedRecipient): User? { + var user: User? = null + if (recipient.user != null) { + user = recipient.user + } else if (recipient.thread != null && !recipient.thread.isGroup) { + user = recipient.thread.users?.get(0) + } + return user + } + + private fun init() { + // setupSettings(); + setupMembers() + } + + private fun setupSettings(isGroup: Boolean) { + binding.groupSettings.visibility = if (isGroup) View.VISIBLE else View.GONE + binding.muteMessagesLabel.setOnClickListener { binding.muteMessages.toggle() } + binding.muteMessages.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> + val resourceLiveData = if (isChecked) viewModel.mute() else viewModel.unmute() + handleSwitchChangeResource(resourceLiveData, buttonView) + } + if (!isGroup) return + binding.titleEdit.addTextChangedListener(object : TextWatcherAdapter() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString().trim { it <= ' ' } == viewModel.getTitle().value) { + binding.titleEditInputLayout.suffixText = null + return + } + binding.titleEditInputLayout.suffixText = getString(R.string.save) + } + }) + binding.titleEditInputLayout.suffixTextView.setOnClickListener { + val text = binding.titleEdit.text ?: return@setOnClickListener + val newTitle = text.toString().trim { it <= ' ' } + if (newTitle == viewModel.getTitle().value) return@setOnClickListener + observeDetailsChange(viewModel.updateTitle(newTitle)) + } + binding.addMembers.setOnClickListener { + if (!isAdded) return@setOnClickListener + val navController = NavHostFragment.findNavController(this) + val currentDestination = navController.currentDestination ?: return@setOnClickListener + if (currentDestination.id != R.id.directMessagesSettingsFragment) return@setOnClickListener + val users = viewModel.getUsers().value + val currentUserIds: LongArray = users?.asSequence()?.map { obj: User -> obj.pk }?.sorted()?.toList()?.toLongArray() ?: LongArray(0) + val actionGlobalUserSearch = UserSearchFragmentDirections + .actionGlobalUserSearch() + .setTitle(getString(R.string.add_members)) + .setActionLabel(getString(R.string.add)) + .setHideUserIds(currentUserIds) + .setSearchMode(UserSearchFragment.SearchMode.RAVEN) + .setMultiple(true) + navController.navigate(actionGlobalUserSearch) + } + binding.muteMentionsLabel.setOnClickListener { binding.muteMentions.toggle() } + binding.muteMentions.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> + val resourceLiveData = if (isChecked) viewModel.muteMentions() else viewModel.unmuteMentions() + handleSwitchChangeResource(resourceLiveData, buttonView) + } + binding.leave.setOnClickListener { + val confirmDialogFragment = ConfirmDialogFragment.newInstance( + LEAVE_THREAD_REQUEST_CODE, + R.string.dms_action_leave_question, + 0, + R.string.yes, + R.string.no, + 0 + ) + confirmDialogFragment.show(childFragmentManager, "leave_thread_confirmation_dialog") + } + var isViewerAdmin = viewModel.isViewerAdmin().value + if (isViewerAdmin == null) isViewerAdmin = false + if (isViewerAdmin) { + binding.end.visibility = View.VISIBLE + binding.end.setOnClickListener { + val confirmDialogFragment = ConfirmDialogFragment.newInstance( + END_THREAD_REQUEST_CODE, + R.string.dms_action_end_question, + R.string.dms_action_end_description, + R.string.yes, + R.string.no, + 0 + ) + confirmDialogFragment.show(childFragmentManager, "end_thread_confirmation_dialog") + } + } else { + binding.end.visibility = View.GONE + } + } + + private fun setApprovalRelatedUI(isViewerAdmin: Boolean) { + if (!isViewerAdmin) { + binding.pendingMembersGroup.visibility = View.GONE + binding.approvalRequired.visibility = View.GONE + binding.approvalRequiredLabel.visibility = View.GONE + return + } + binding.approvalRequired.visibility = View.VISIBLE + binding.approvalRequiredLabel.visibility = View.VISIBLE + binding.approvalRequiredLabel.setOnClickListener { binding.approvalRequired.toggle() } + binding.approvalRequired.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean -> + val resourceLiveData = if (isChecked) viewModel.approvalRequired() else viewModel.approvalNotRequired() + handleSwitchChangeResource(resourceLiveData, buttonView) + } + } + + private fun handleSwitchChangeResource(resourceLiveData: LiveData>, buttonView: CompoundButton) { + resourceLiveData.observe(viewLifecycleOwner, { resource: Resource? -> + if (resource == null) return@observe + when (resource.status) { + Resource.Status.SUCCESS -> buttonView.isEnabled = true + Resource.Status.ERROR -> { + buttonView.isEnabled = true + buttonView.isChecked = !buttonView.isChecked + if (resource.message != null) { + Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show() + } + if (resource.resId != 0) { + Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show() + } + } + Resource.Status.LOADING -> buttonView.isEnabled = false + } + }) + } + + private fun setupMembers() { + val context = context ?: return + binding.users.layoutManager = LinearLayoutManager(context) + val inviter = viewModel.getInviter().value + usersAdapter = DirectUsersAdapter( + inviter?.pk ?: -1, + { _: Int, user: User, _: Boolean -> + if (user.username.isBlank() && !user.interopMessagingUserFbid.isNullOrBlank()) { + Utils.openURL(context, "https://facebook.com/" + user.interopMessagingUserFbid) + return@DirectUsersAdapter + } + if (isEmpty(user.username)) return@DirectUsersAdapter + val directions = ProfileNavGraphDirections + .actionGlobalProfileFragment("@" + user.username) + NavHostFragment.findNavController(this).navigate(directions) + }, + { _: Int, user: User? -> + val options = viewModel.createUserOptions(user) + if (options.isEmpty()) return@DirectUsersAdapter true + val fragment = MultiOptionDialogFragment.newInstance(-1, options) + fragment.setSingleCallback(object : MultiOptionDialogSingleCallback { + override fun onSelect(action: String?) { + if (action == null) return + val resourceLiveData = viewModel.doAction(user, action) + if (resourceLiveData != null) { + observeDetailsChange(resourceLiveData) + } + } + + override fun onCancel() {} + }) + val fragmentManager = childFragmentManager + fragment.show(fragmentManager, "actions") + true + } + ) + binding.users.adapter = usersAdapter + } + + private fun setPendingRequests(requests: DirectThreadParticipantRequestsResponse?) { + val nullOrEmpty: Boolean = requests?.users?.isNullOrEmpty() ?: true + if (nullOrEmpty) { + binding.pendingMembersGroup.visibility = View.GONE + return + } + if (!isPendingRequestsSetupDone) { + val context = context ?: return + binding.pendingMembers.layoutManager = LinearLayoutManager(context) + pendingUsersAdapter = DirectPendingUsersAdapter(object : PendingUserCallback { + override fun onClick(position: Int, pendingUser: PendingUser) { + val directions = ProfileNavGraphDirections + .actionGlobalProfileFragment("@" + pendingUser.user.username) + NavHostFragment.findNavController(this@DirectMessageSettingsFragment).navigate(directions) + } + + override fun onApprove(position: Int, pendingUser: PendingUser) { + val resourceLiveData = viewModel.approveUsers(listOf(pendingUser.user)) + observeApprovalChange(resourceLiveData, position, pendingUser) + } + + override fun onDeny(position: Int, pendingUser: PendingUser) { + val resourceLiveData = viewModel.denyUsers(listOf(pendingUser.user)) + observeApprovalChange(resourceLiveData, position, pendingUser) + } + }) + binding.pendingMembers.adapter = pendingUsersAdapter + binding.pendingMembersGroup.visibility = View.VISIBLE + isPendingRequestsSetupDone = true + } + pendingUsersAdapter?.submitPendingRequests(requests) + } + + private fun observeDetailsChange(resourceLiveData: LiveData>) { + resourceLiveData.observe(viewLifecycleOwner, { resource: Resource? -> + if (resource == null) return@observe + when (resource.status) { + Resource.Status.SUCCESS, + Resource.Status.LOADING, + -> { + } + Resource.Status.ERROR -> { + if (resource.message != null) { + Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show() + } + if (resource.resId != 0) { + Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show() + } + } + } + }) + } + + private fun observeApprovalChange( + detailsChangeResourceLiveData: LiveData>, + position: Int, + pendingUser: PendingUser, + ) { + detailsChangeResourceLiveData.observe(viewLifecycleOwner, { resource: Resource? -> + if (resource == null) return@observe + when (resource.status) { + Resource.Status.SUCCESS -> { + } + Resource.Status.LOADING -> pendingUser.isInProgress = true + Resource.Status.ERROR -> { + pendingUser.isInProgress = false + if (resource.message != null) { + Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show() + } + if (resource.resId != 0) { + Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show() + } + } + } + pendingUsersAdapter?.notifyItemChanged(position) + }) + } + + override fun onPositiveButtonClicked(requestCode: Int) { + if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) { + approvalRequiredUsers?.let { + val detailsChangeResourceLiveData = viewModel.addMembers(it) + observeDetailsChange(detailsChangeResourceLiveData) + } + return + } + if (requestCode == LEAVE_THREAD_REQUEST_CODE) { + val resourceLiveData = viewModel.leave() + resourceLiveData.observe(viewLifecycleOwner, { resource: Resource? -> + if (resource == null) return@observe + when (resource.status) { + Resource.Status.SUCCESS -> { + val directions = DirectMessageSettingsFragmentDirections.actionSettingsToInbox() + NavHostFragment.findNavController(this).navigate(directions) + } + Resource.Status.ERROR -> { + binding.leave.isEnabled = true + if (resource.message != null) { + Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show() + } + if (resource.resId != 0) { + Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show() + } + } + Resource.Status.LOADING -> binding.leave.isEnabled = false + } + }) + return + } + if (requestCode == END_THREAD_REQUEST_CODE) { + val resourceLiveData = viewModel.end() + resourceLiveData.observe(viewLifecycleOwner, { resource: Resource? -> + if (resource == null) return@observe + when (resource.status) { + Resource.Status.SUCCESS -> { + } + Resource.Status.ERROR -> { + binding.end.isEnabled = true + if (resource.message != null) { + Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show() + } + if (resource.resId != 0) { + Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show() + } + } + Resource.Status.LOADING -> binding.end.isEnabled = false + } + }) + } + } + + override fun onNegativeButtonClicked(requestCode: Int) { + if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) { + approvalRequiredUsers = null + } + } + + override fun onNeutralButtonClicked(requestCode: Int) {} + + companion object { + private const val APPROVAL_REQUIRED_REQUEST_CODE = 200 + private const val LEAVE_THREAD_REQUEST_CODE = 201 + private const val END_THREAD_REQUEST_CODE = 202 + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java index 6cd0de96..4c83b007 100644 --- a/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/imageedit/FiltersFragment.java @@ -47,6 +47,7 @@ import awais.instagrabber.fragments.imageedit.filters.properties.FloatProperty; import awais.instagrabber.fragments.imageedit.filters.properties.Property; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.BitmapUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.SerializablePair; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FiltersFragmentViewModel; @@ -54,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(); @@ -460,32 +462,33 @@ public class FiltersFragment extends Fragment { filtersAdapter.setSelected(position); appliedFilter = filter; }; - BitmapUtils.getThumbnail(context, sourceUri, new BitmapUtils.ThumbnailLoadCallback() { - @Override - public void onLoad(@Nullable final Bitmap bitmap, final int width, final int height) { - filtersAdapter = new FiltersAdapter( - tuningFilters.values() - .stream() - .map(Filter::getInstance) - .collect(Collectors.toList()), - sourceUri.toString(), - bitmap, - onFilterClickListener - ); - appExecutors.getMainThread().execute(() -> { + 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()); }); - }); - } - - @Override - public void onFailure(@NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + }), Dispatchers.getIO()) + ); addInitialFilter(); binding.preview.setFilter(filterGroup); } diff --git a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java index 26ee6ce9..16e12d5d 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java @@ -30,11 +30,14 @@ 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.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 +55,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.INSTANCE; } @Override @@ -104,29 +111,29 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR .setView(R.layout.dialog_opening_post) .create(); alertDialog.show(); - mediaService.fetch(Long.valueOf(coverMedia.getPk()), new ServiceCallback() { - @Override - public void onSuccess(final Media feedModel) { - final NavController navController = NavHostFragment.findNavController(DiscoverFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "onSuccess: ", e); - } - } - - @Override - public void onFailure(final Throwable t) { - alertDialog.dismiss(); - try { - Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } - catch (Throwable e) {} - } - }); + final String pk = coverMedia.getPk(); + if (pk == null) return; + mediaService.fetch( + Long.parseLong(pk), + CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + alertDialog.dismiss(); + try { + Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } catch (Throwable ignored) {} + return; + } + final NavController navController = NavHostFragment.findNavController(DiscoverFragment.this); + final Bundle bundle = new Bundle(); + bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media); + try { + navController.navigate(R.id.action_global_post_view, bundle); + alertDialog.dismiss(); + } catch (Exception e) { + Log.e(TAG, "onTopicLongClick: ", e); + } + }), Dispatchers.getIO()) + ); } }; final DiscoverTopicsAdapter adapter = new DiscoverTopicsAdapter(otcl); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index d22b5a51..d1d36562 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -48,12 +48,14 @@ import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Media; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FeedStoriesViewModel; -import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; +import kotlinx.coroutines.Dispatchers; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; @@ -274,7 +276,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); - storiesService = StoriesService.getInstance(null, 0L, null); + storiesService = StoriesService.INSTANCE; setHasOptionsMenu(true); } @@ -428,23 +430,23 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre // final String cookie = settingsHelper.getString(Constants.COOKIE); storiesFetching = true; updateSwipeRefreshState(); - storiesService.getFeedStories(new ServiceCallback>() { - @Override - public void onSuccess(final List result) { - storiesFetching = false; - feedStoriesViewModel.getList().postValue(result); - feedStoriesAdapter.submitList(result); - if (storyListMenu != null) storyListMenu.setVisible(true); - updateSwipeRefreshState(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "failed", t); - storiesFetching = false; - updateSwipeRefreshState(); - } - }); + storiesService.getFeedStories( + CoroutineUtilsKt.getContinuation((feedStoryModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "failed", throwable); + storiesFetching = false; + updateSwipeRefreshState(); + return; + } + storiesFetching = false; + //noinspection unchecked + feedStoriesViewModel.getList().postValue((List) feedStoryModels); + //noinspection unchecked + feedStoriesAdapter.submitList((List) feedStoryModels); + if (storyListMenu != null) storyListMenu.setVisible(true); + updateSwipeRefreshState(); + }), Dispatchers.getIO()) + ); } private void showPostsLayoutPreferences() { diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 7e892024..4c762e64 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -62,11 +62,9 @@ import awais.instagrabber.databinding.FragmentProfileBinding; import awais.instagrabber.databinding.LayoutProfileDetailsBinding; import awais.instagrabber.db.datasources.AccountDataSource; import awais.instagrabber.db.datasources.FavoriteDataSource; -import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.db.repositories.FavoriteRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.dialogs.ProfilePicDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; @@ -74,16 +72,15 @@ import awais.instagrabber.managers.DirectMessagesManager; import awais.instagrabber.managers.InboxManager; import awais.instagrabber.models.HighlightModel; import awais.instagrabber.models.PostsLayoutPreferences; -import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.PostItemType; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; 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; @@ -92,6 +89,7 @@ import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.HighlightsViewModel; +import awais.instagrabber.viewmodels.ProfileFragmentViewModel; import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.GraphQLService; @@ -138,6 +136,14 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe private int downloadChildPosition = -1; private long myId; private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_PROFILE_POSTS_LAYOUT); + private LayoutProfileDetailsBinding profileDetailsBinding; + private AccountRepository accountRepository; + private FavoriteRepository favoriteRepository; + private AppStateViewModel appStateViewModel; + private boolean disableDm = false; + private ProfileFragmentViewModel viewModel; + private String csrfToken; + private String deviceUuid; private final ServiceCallback changeCb = new ServiceCallback() { @Override @@ -155,7 +161,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Log.e(TAG, "Error editing relationship", t); } }; - private final Runnable usernameSettingRunnable = () -> { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null && !TextUtils.isEmpty(username)) { @@ -317,11 +322,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } } }; - private LayoutProfileDetailsBinding profileDetailsBinding; - private AccountRepository accountRepository; - private FavoriteRepository favoriteRepository; - private AppStateViewModel appStateViewModel; - private boolean disableDm = false; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -329,20 +329,21 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe cookie = Utils.settingsHelper.getString(Constants.COOKIE); isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; myId = CookieUtils.getUserIdFromCookie(cookie); - final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); + csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); fragmentActivity = (MainActivity) requireActivity(); - 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; - userService = isLoggedIn ? UserService.getInstance() : null; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + friendshipService = isLoggedIn ? FriendshipService.INSTANCE : null; + directMessagesService = isLoggedIn ? DirectMessagesService.INSTANCE : null; + storiesService = isLoggedIn ? StoriesService.INSTANCE : null; + mediaService = isLoggedIn ? MediaService.INSTANCE : null; + userService = isLoggedIn ? UserService.INSTANCE : null; + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; final Context context = getContext(); if (context == null) return; accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); + viewModel = new ViewModelProvider(this).get(ProfileFragmentViewModel.class); setHasOptionsMenu(true); } @@ -372,6 +373,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe shouldRefresh = false; return root; } + // appStateViewModel.getCurrentUserLiveData().observe(getViewLifecycleOwner(), user -> viewModel.setCurrentUser(user)); binding = FragmentProfileBinding.inflate(inflater, container, false); root = binding.getRoot(); profileDetailsBinding = binding.header; @@ -429,7 +431,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } chainingMenuItem = menu.findItem(R.id.chaining); if (chainingMenuItem != null) { - chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining()); + chainingMenuItem.setVisible(isNotMe && profileModel.getHasChaining()); } removeFollowerMenuItem = menu.findItem(R.id.remove_follower); if (removeFollowerMenuItem != null) { @@ -447,25 +449,38 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (!isLoggedIn) return false; final String action = profileModel.getFriendshipStatus().isRestricted() ? "Unrestrict" : "Restrict"; friendshipService.toggleRestrict( + csrfToken, + deviceUuid, profileModel.getPk(), !profileModel.getFriendshipStatus().isRestricted(), - new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRestrictResponse result) { - Log.d(TAG, action + " success: " + result); - fetchProfileDetails(); + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error while performing " + action, throwable); + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error while performing " + action, t); - } - }); + // Log.d(TAG, action + " success: " + response); + fetchProfileDetails(); + }), Dispatchers.getIO()) + ); return true; } if (item.getItemId() == R.id.block) { if (!isLoggedIn) return false; - friendshipService.changeBlock(profileModel.getFriendshipStatus().getBlocking(), profileModel.getPk(), changeCb); + // changeCb + friendshipService.changeBlock( + csrfToken, + myId, + deviceUuid, + profileModel.getFriendshipStatus().getBlocking(), + profileModel.getPk(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); return true; } if (item.getItemId() == R.id.chaining) { @@ -480,25 +495,57 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (!isLoggedIn) return false; final String action = profileModel.getFriendshipStatus().isMutingReel() ? "Unmute stories" : "Mute stories"; friendshipService.changeMute( + csrfToken, + myId, + deviceUuid, profileModel.getFriendshipStatus().isMutingReel(), profileModel.getPk(), true, - changeCb); + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); return true; } if (item.getItemId() == R.id.mute_posts) { if (!isLoggedIn) return false; final String action = profileModel.getFriendshipStatus().getMuting() ? "Unmute stories" : "Mute stories"; friendshipService.changeMute( + csrfToken, + myId, + deviceUuid, profileModel.getFriendshipStatus().getMuting(), profileModel.getPk(), false, - changeCb); + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); return true; } if (item.getItemId() == R.id.remove_follower) { if (!isLoggedIn) return false; - friendshipService.removeFollower(profileModel.getPk(), changeCb); + friendshipService.removeFollower( + csrfToken, + myId, + deviceUuid, + profileModel.getPk(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); return true; } return super.onOptionsItemSelected(item); @@ -582,65 +629,51 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe return; } if (isLoggedIn) { - userService.getUsernameInfo(usernameTemp, new ServiceCallback() { - @Override - public void onSuccess(final User user) { - userService.getUserFriendship(user.getPk(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipStatus status) { - user.setFriendshipStatus(status); - profileModel = user; - setProfileDetails(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error fetching profile relationship", t); + userService.getUsernameInfo( + usernameTemp, + CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error fetching profile", throwable); final Context context = getContext(); - try { - if (t == null) - Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show(); - else - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) { - } + if (context == null) return; + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + return; } - }); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error fetching profile", t); - final Context context = getContext(); - try { - if (t == null) - Toast.makeText(context, R.string.error_loading_profile_loggedin, Toast.LENGTH_LONG).show(); - else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) { - } - } - }); + userService.getUserFriendship( + user.getPk(), + CoroutineUtilsKt.getContinuation( + (friendshipStatus, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "Error fetching profile relationship", throwable1); + final Context context = getContext(); + if (context == null) return; + Toast.makeText(context, throwable1.getMessage(), + Toast.LENGTH_SHORT).show(); + return; + } + user.setFriendshipStatus(friendshipStatus); + profileModel = user; + setProfileDetails(); + }), Dispatchers.getIO() + ) + ); + }), Dispatchers.getIO()) + ); return; } - graphQLService.fetchUser(usernameTemp, new ServiceCallback() { - @Override - public void onSuccess(final User user) { - profileModel = user; - setProfileDetails(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error fetching profile", t); - final Context context = getContext(); - try { - if (t == null) - Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_LONG).show(); - else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) { - } - } - }); + graphQLService.fetchUser( + usernameTemp, + CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error fetching profile", throwable); + final Context context = getContext(); + if (context == null) return; + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + } + profileModel = user; + setProfileDetails(); + })) + ); } private void setProfileDetails() { @@ -668,76 +701,80 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe setupButtons(profileId); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); - favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - profileDetailsBinding.favChip.setText(R.string.favorite_short); - favoriteRepository.insertOrUpdateFavorite(new Favorite( - result.getId(), - profileModel.getUsername(), - FavoriteType.USER, - profileModel.getFullName(), - profileModel.getProfilePicUrl(), - result.getDateAdded() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { + favoriteRepository.getFavorite( + profileModel.getUsername(), + FavoriteType.USER, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null || favorite == null) { + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + profileDetailsBinding.favChip.setText(R.string.add_to_favorites); + Log.e(TAG, "setProfileDetails: ", throwable); + return; } - - @Override - public void onDataNotAvailable() { + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + profileDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + favorite.getId(), + profileModel.getUsername(), + FavoriteType.USER, + profileModel.getFullName(), + profileModel.getProfilePicUrl(), + favorite.getDateAdded() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + } + }), Dispatchers.getIO()) + ); + })) + ); + profileDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite( + profileModel.getUsername(), + FavoriteType.USER, + CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "setProfileDetails: ", throwable); + return; } - }); - } - - @Override - public void onDataNotAvailable() { - profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); - profileDetailsBinding.favChip.setText(R.string.add_to_favorites); - } - }); - profileDetailsBinding.favChip.setOnClickListener( - v -> favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - favoriteRepository.deleteFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { + if (favorite == null) { + favoriteRepository.insertOrUpdateFavorite( + new Favorite( + 0, + profileModel.getUsername(), + FavoriteType.USER, + profileModel.getFullName(), + profileModel.getProfilePicUrl(), + LocalDateTime.now() + ), + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onDataNotAvailable: ", throwable1); + return; + } + profileDetailsBinding.favChip.setText(R.string.favorite_short); + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + }), Dispatchers.getIO()) + ); + return; + } + favoriteRepository.deleteFavorite( + profileModel.getUsername(), + FavoriteType.USER, + CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onSuccess: ", throwable1); + return; + } profileDetailsBinding.favChip.setText(R.string.add_to_favorites); profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); showSnackbar(getString(R.string.removed_from_favs)); - } - - @Override - public void onDataNotAvailable() { - } - }); - } - - @Override - public void onDataNotAvailable() { - favoriteRepository.insertOrUpdateFavorite(new Favorite( - 0, - profileModel.getUsername(), - FavoriteType.USER, - profileModel.getFullName(), - profileModel.getProfilePicUrl(), - LocalDateTime.now() - ), new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - profileDetailsBinding.favChip.setText(R.string.favorite_short); - profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - showSnackbar(getString(R.string.added_to_favs)); - } - - @Override - public void onDataNotAvailable() { - } - }); - } - })); + }), Dispatchers.getIO()) + ); + }), Dispatchers.getIO()) + )); profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getProfilePicUrl()); profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE); @@ -821,26 +858,26 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Utils.copyText(context, biography); break; case 1: - mediaService.translate(String.valueOf(profileModel.getPk()), "3", new ServiceCallback() { - @Override - public void onSuccess(final String result) { - if (TextUtils.isEmpty(result)) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - new AlertDialog.Builder(context) - .setTitle(profileModel.getUsername()) - .setMessage(result) - .setPositiveButton(R.string.ok, null) - .show(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error translating bio", t); - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); + mediaService.translate(String.valueOf(profileModel.getPk()), "3", CoroutineUtilsKt.getContinuation( + (result, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error translating bio", throwable); + Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + return; + } + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT) + .show(); + return; + } + new AlertDialog.Builder(context) + .setTitle(profileModel.getUsername()) + .setMessage(result) + .setPositiveButton(R.string.ok, null) + .show(); + }), + Dispatchers.getIO() + )); break; } }) @@ -855,7 +892,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.profileContext.setVisibility(View.GONE); } else { profileDetailsBinding.profileContext.setVisibility(View.VISIBLE); - final List userProfileContextLinks = profileModel.getProfileContextLinks(); + final List userProfileContextLinks = profileModel.getProfileContextLinksWithUserIds(); for (int i = 0; i < userProfileContextLinks.size(); i++) { final UserProfileContextLink link = userProfileContextLinks.get(i); if (link.getUsername() != null) @@ -976,7 +1013,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().getMuting() ? R.string.unmute_posts : R.string.mute_posts); } if (chainingMenuItem != null) { - chainingMenuItem.setVisible(profileModel.hasChaining()); + chainingMenuItem.setVisible(profileModel.getHasChaining()); } if (removeFollowerMenuItem != null) { removeFollowerMenuItem.setVisible(profileModel.getFriendshipStatus().getFollowedBy()); @@ -992,69 +1029,100 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe cookie, profileModel.getFullName(), profileModel.getProfilePicUrl(), - new RepositoryCallback() { - @Override - public void onSuccess(final Account result) { - accountIsUpdated = true; + CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "updateAccountInfo: ", throwable); + return; } - - @Override - public void onDataNotAvailable() { - Log.e(TAG, "onDataNotAvailable: insert failed"); - } - }); + accountIsUpdated = true; + }), Dispatchers.getIO()) + ); } private void fetchStoryAndHighlights(final long profileId) { storiesService.getUserStory( StoryViewerOptions.forUser(profileId, profileModel.getFullName()), - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - profileDetailsBinding.mainProfileImage.setStoriesBorder(1); - hasStories = true; - } + CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error", throwable); + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); + if (storyModels != null && !storyModels.isEmpty()) { + profileDetailsBinding.mainProfileImage.setStoriesBorder(1); + hasStories = true; } - }); - storiesService.fetchHighlights(profileId, - new ServiceCallback>() { - @Override - public void onSuccess(final List result) { - if (result != null) { - profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); - highlightsViewModel.getList().postValue(result); - } else profileDetailsBinding.highlightsList.setVisibility(View.GONE); - } - - @Override - public void onFailure(final Throwable t) { - profileDetailsBinding.highlightsList.setVisibility(View.GONE); - Log.e(TAG, "Error", t); - } - }); + }), Dispatchers.getIO()) + ); + storiesService.fetchHighlights( + profileId, + CoroutineUtilsKt.getContinuation((highlightModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + profileDetailsBinding.highlightsList.setVisibility(View.GONE); + Log.e(TAG, "Error", throwable); + return; + } + if (highlightModels != null) { + profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); + //noinspection unchecked + highlightsViewModel.getList().postValue((List) highlightModels); + } else { + profileDetailsBinding.highlightsList.setVisibility(View.GONE); + } + }), Dispatchers.getIO()) + ); } private void setupCommonListeners() { final Context context = getContext(); + if (context == null) return; profileDetailsBinding.btnFollow.setOnClickListener(v -> { if (profileModel.getFriendshipStatus().getFollowing() && profileModel.isPrivate()) { new AlertDialog.Builder(context) .setTitle(R.string.priv_acc) .setMessage(R.string.priv_acc_confirm) - .setPositiveButton(R.string.confirm, (d, w) -> - friendshipService.unfollow(profileModel.getPk(), changeCb)) + .setPositiveButton(R.string.confirm, (d, w) -> friendshipService.unfollow( + csrfToken, + myId, + deviceUuid, + profileModel.getPk(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + )) .setNegativeButton(R.string.cancel, null) .show(); } else if (profileModel.getFriendshipStatus().getFollowing() || profileModel.getFriendshipStatus().getOutgoingRequest()) { - friendshipService.unfollow(profileModel.getPk(), changeCb); + friendshipService.unfollow( + csrfToken, + myId, + deviceUuid, + profileModel.getPk(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); } else { - friendshipService.follow(profileModel.getPk(), changeCb); + friendshipService.follow( + csrfToken, + myId, + deviceUuid, + profileModel.getPk(), + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + changeCb.onFailure(throwable); + return; + } + changeCb.onSuccess(response); + }), Dispatchers.getIO()) + ); } }); profileDetailsBinding.btnSaved.setOnClickListener(v -> { @@ -1077,9 +1145,12 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.btnDM.setOnClickListener(v -> { profileDetailsBinding.btnDM.setEnabled(false); directMessagesService.createThread( + csrfToken, + myId, + deviceUuid, 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 +1163,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe inboxManager.addThread(thread, 0); } fragmentActivity.navigateToThread(thread.getThreadId(), profileModel.getUsername()); - }, Dispatchers.getIO()) + }), Dispatchers.getIO()) ); }); } @@ -1119,7 +1190,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } showProfilePicDialog(); }; - if (context == null) return; new AlertDialog.Builder(context) .setItems(options, profileDialogListener) .setNegativeButton(R.string.cancel, null) diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index dc8861db..315b8bb3 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -11,7 +11,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentManager; @@ -24,28 +23,24 @@ import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; -import java.util.List; - import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.activities.Login; import awais.instagrabber.activities.MainActivity; import awais.instagrabber.databinding.PrefAccountSwitcherBinding; import awais.instagrabber.db.datasources.AccountDataSource; -import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.repositories.AccountRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.AccountSwitcherDialogFragment; -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.FlavorTown; import awais.instagrabber.utils.ProcessPhoenix; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.UserService; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -98,75 +93,77 @@ public class MorePreferencesFragment extends BasePreferencesFragment { return true; })); } - accountRepository.getAllAccounts(new RepositoryCallback>() { - @Override - public void onSuccess(@NonNull final List accounts) { - if (!isLoggedIn) { - if (accounts.size() > 0) { - final Context context1 = getContext(); - final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1); - if (preference == null) return; - accountCategory.addPreference(preference); - } - // Need to show something to trigger login activity - final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> { - final Context context1 = getContext(); - if (context1 == null) return false; - startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE); - return true; - }); - if (preference1 == null) return; - accountCategory.addPreference(preference1); - } - if (accounts.size() > 0) { - final Preference preference1 = getPreference( - R.string.remove_all_acc, - null, - R.drawable.ic_account_multiple_remove_24, - preference -> { - if (getContext() == null) return false; - new AlertDialog.Builder(getContext()) - .setTitle(R.string.logout) - .setMessage(R.string.remove_all_acc_warning) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final Context context1 = getContext(); - if (context1 == null) return; - CookieUtils.removeAllAccounts(context1, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - // shouldRecreate(); - final Context context1 = getContext(); - if (context1 == null) return; - Toast.makeText(context1, R.string.logout_success, Toast.LENGTH_SHORT).show(); - settingsHelper.putString(Constants.COOKIE, ""); - AppExecutors.INSTANCE.getMainThread().execute(() -> ProcessPhoenix.triggerRebirth(context1), 200); - } - - @Override - public void onDataNotAvailable() {} - }); - }) - .setNegativeButton(R.string.cancel, null) - .show(); + accountRepository.getAllAccounts( + CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.d(TAG, "getAllAccounts", throwable); + if (!isLoggedIn) { + // Need to show something to trigger login activity + accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { + startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); return true; - }); - if (preference1 == null) return; - accountCategory.addPreference(preference1); - } - } - - @Override - public void onDataNotAvailable() { - Log.d(TAG, "onDataNotAvailable"); - if (!isLoggedIn) { - // Need to show something to trigger login activity - accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> { - startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE); - return true; - })); - } - } - }); + })); + } + return; + } + if (!isLoggedIn) { + if (accounts.size() > 0) { + final Context context1 = getContext(); + final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1); + if (preference == null) return; + accountCategory.addPreference(preference); + } + // Need to show something to trigger login activity + final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> { + final Context context1 = getContext(); + if (context1 == null) return false; + startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE); + return true; + }); + if (preference1 == null) return; + accountCategory.addPreference(preference1); + } + if (accounts.size() > 0) { + final Preference preference1 = getPreference( + R.string.remove_all_acc, + null, + R.drawable.ic_account_multiple_remove_24, + preference -> { + if (getContext() == null) return false; + new AlertDialog.Builder(getContext()) + .setTitle(R.string.logout) + .setMessage(R.string.remove_all_acc_warning) + .setPositiveButton(R.string.yes, (dialog, which) -> { + final Context context1 = getContext(); + if (context1 == null) return; + CookieUtils.removeAllAccounts( + context1, + CoroutineUtilsKt.getContinuation( + (unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + return; + } + final Context context2 = getContext(); + if (context2 == null) return; + Toast.makeText(context2, R.string.logout_success, Toast.LENGTH_SHORT).show(); + settingsHelper.putString(Constants.COOKIE, ""); + AppExecutors.INSTANCE + .getMainThread() + .execute(() -> ProcessPhoenix.triggerRebirth(context1), 200); + }), + Dispatchers.getIO() + ) + ); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + return true; + }); + if (preference1 == null) return; + accountCategory.addPreference(preference1); + } + }), Dispatchers.getIO()) + ); // final PreferenceCategory generalCategory = new PreferenceCategory(context); // generalCategory.setTitle(R.string.pref_category_general); @@ -288,44 +285,33 @@ public class MorePreferencesFragment extends BasePreferencesFragment { // adds cookies to database for quick access final long uid = CookieUtils.getUserIdFromCookie(cookie); - final UserService userService = UserService.getInstance(); - userService.getUserInfo(uid, new ServiceCallback() { - @Override - public void onSuccess(final User result) { - // Log.d(TAG, "adding userInfo: " + result); - if (result != null) { - accountRepository.insertOrUpdateAccount( - uid, - result.getUsername(), - cookie, - result.getFullName(), - result.getProfilePicUrl(), - new RepositoryCallback() { - @Override - public void onSuccess(final Account result) { - // final FragmentActivity activity = getActivity(); - // if (activity == null) return; - // activity.recreate(); - AppExecutors.INSTANCE.getMainThread().execute(() -> { - final Context context = getContext(); - if (context == null) return; - ProcessPhoenix.triggerRebirth(context); - }, 200); - } - - @Override - public void onDataNotAvailable() { - Log.e(TAG, "onDataNotAvailable: insert failed"); - } - }); - } + final UserService userService = UserService.INSTANCE; + userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error fetching user info", throwable); + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error fetching user info", t); + if (user != null) { + accountRepository.insertOrUpdateAccount( + uid, + user.getUsername(), + cookie, + user.getFullName(), + user.getProfilePicUrl(), + CoroutineUtilsKt.getContinuation((account, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable1 != null) { + Log.e(TAG, "onActivityResult: ", throwable1); + return; + } + AppExecutors.INSTANCE.getMainThread().execute(() -> { + final Context context = getContext(); + if (context == null) return; + ProcessPhoenix.triggerRebirth(context); + }, 200); + }), Dispatchers.getIO()) + ); } - }); + }), Dispatchers.getIO())); } } @@ -419,20 +405,21 @@ public class MorePreferencesFragment extends BasePreferencesFragment { final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.bind(root); final long uid = CookieUtils.getUserIdFromCookie(cookie); if (uid <= 0) return; - accountRepository.getAccount(uid, new RepositoryCallback() { - @Override - public void onSuccess(final Account account) { - binding.getRoot().post(() -> { - binding.fullName.setText(account.getFullName()); - binding.username.setText("@" + account.getUsername()); - binding.profilePic.setImageURI(account.getProfilePic()); - binding.getRoot().requestLayout(); - }); - } - - @Override - public void onDataNotAvailable() {} - }); + accountRepository.getAccount( + uid, + CoroutineUtilsKt.getContinuation((account, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "onBindViewHolder: ", throwable); + return; + } + binding.getRoot().post(() -> { + binding.fullName.setText(account.getFullName()); + binding.username.setText("@" + account.getUsername()); + binding.profilePic.setImageURI(account.getProfilePic()); + binding.getRoot().requestLayout(); + }); + }), Dispatchers.getIO()) + ); } } } diff --git a/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt index fafc6e6e..99c099c0 100644 --- a/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt @@ -4,7 +4,6 @@ import android.content.ContentResolver import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import awais.instagrabber.managers.ThreadManager.Companion.getInstance import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.loading @@ -18,21 +17,19 @@ import awais.instagrabber.utils.Utils import awais.instagrabber.utils.getCsrfTokenFromCookie import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.webservices.DirectMessagesService -import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.* object DirectMessagesManager { - val inboxManager: InboxManager by lazy { InboxManager.getInstance(false) } - val pendingInboxManager: InboxManager by lazy { InboxManager.getInstance(true) } + val inboxManager: InboxManager by lazy { InboxManager(false) } + val pendingInboxManager: InboxManager by lazy { InboxManager(true) } private val TAG = DirectMessagesManager::class.java.simpleName private val viewerId: Long private val deviceUuid: String private val csrfToken: String - private val service: DirectMessagesService fun moveThreadFromPending(threadId: String) { val pendingThreads = pendingInboxManager.threads.value ?: return @@ -65,10 +62,10 @@ object DirectMessagesManager { currentUser: User, contentResolver: ContentResolver, ): ThreadManager { - return getInstance(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) + return ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) } - suspend fun createThread(userPk: Long): DirectThread = service.createThread(listOf(userPk), null) + suspend fun createThread(userPk: Long): DirectThread = DirectMessagesService.createThread(csrfToken, viewerId, deviceUuid, listOf(userPk), null) fun sendMedia(recipients: Set, mediaId: String, scope: CoroutineScope) { val resultsCount = intArrayOf(0) @@ -134,7 +131,10 @@ object DirectMessagesManager { data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - service.broadcastMediaShare( + DirectMessagesService.broadcastMediaShare( + csrfToken, + viewerId, + deviceUuid, UUID.randomUUID().toString(), of(threadId), mediaId @@ -157,6 +157,5 @@ object DirectMessagesManager { val csrfToken = getCsrfTokenFromCookie(cookie) require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } this.csrfToken = csrfToken - service = getInstance(csrfToken, viewerId, deviceUuid) } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/managers/InboxManager.kt b/app/src/main/java/awais/instagrabber/managers/InboxManager.kt index 2ea2448b..9769ec93 100644 --- a/app/src/main/java/awais/instagrabber/managers/InboxManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.kt @@ -12,8 +12,8 @@ import awais.instagrabber.models.Resource.Companion.success import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.utils.* +import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.webservices.DirectMessagesService -import awais.instagrabber.webservices.DirectMessagesService.Companion.getInstance import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.collect.ImmutableList @@ -24,14 +24,13 @@ import retrofit2.Call import java.util.* import java.util.concurrent.TimeUnit -class InboxManager private constructor(private val pending: Boolean) { +class InboxManager(private val pending: Boolean) { // private val fetchInboxControlledRunner: ControlledRunner> = ControlledRunner() // private val fetchPendingInboxControlledRunner: ControlledRunner> = ControlledRunner() private val inbox = MutableLiveData>(success(null)) private val unseenCount = MutableLiveData>() private val pendingRequestsTotal = MutableLiveData(0) val threads: LiveData> - private val service: DirectMessagesService private var inboxRequest: Call? = null private var unseenCountRequest: Call? = null private var seqId: Long = 0 @@ -58,7 +57,11 @@ class InboxManager private constructor(private val pending: Boolean) { inbox.postValue(loading(currentDirectInbox)) scope.launch(Dispatchers.IO) { try { - val inboxValue = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId) + val inboxValue = if (pending) { + DirectMessagesService.fetchPendingInbox(cursor, seqId) + } else { + DirectMessagesService.fetchInbox(cursor, seqId) + } parseInboxResponse(inboxValue) } catch (e: Exception) { inbox.postValue(error(e.message, currentDirectInbox)) @@ -74,7 +77,7 @@ class InboxManager private constructor(private val pending: Boolean) { unseenCount.postValue(loading(currentUnseenCount)) scope.launch(Dispatchers.IO) { try { - val directBadgeCount = service.fetchUnseenCount() + val directBadgeCount = DirectMessagesService.fetchUnseenCount() unseenCount.postValue(success(directBadgeCount.badgeCount)) } catch (e: Exception) { Log.e(TAG, "Failed fetching unseen count", e) @@ -117,7 +120,7 @@ class InboxManager private constructor(private val pending: Boolean) { val threads = it.threads val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) threadsCopy.addAll(inbox.threads ?: emptyList()) - inbox.threads = threads + inbox.threads = threadsCopy } } this.inbox.postValue(success(inbox)) @@ -286,7 +289,6 @@ class InboxManager private constructor(private val pending: Boolean) { } companion object { - private val TAG = InboxManager::class.java.simpleName private val THREAD_LOCKS = CacheBuilder .newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected @@ -299,10 +301,6 @@ class InboxManager private constructor(private val pending: Boolean) { if (t2FirstDirectItem == null) return@Comparator -1 t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp()) } - - fun getInstance(pending: Boolean): InboxManager { - return InboxManager(pending) - } } init { @@ -311,7 +309,6 @@ class InboxManager private constructor(private val pending: Boolean) { val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) val csrfToken = getCsrfTokenFromCookie(cookie) require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } - service = getInstance(csrfToken, viewerId, deviceUuid) // Transformations threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource -> diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt index 11c51abd..52bff758 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -19,46 +19,40 @@ import awais.instagrabber.repositories.requests.UploadFinishOptions import awais.instagrabber.repositories.requests.VideoOptions import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of -import awais.instagrabber.repositories.responses.FriendshipChangeResponse -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.utils.* import awais.instagrabber.utils.MediaUploader.MediaUploadResponse -import awais.instagrabber.utils.MediaUploader.OnMediaUploadCompleteListener import awais.instagrabber.utils.MediaUploader.uploadPhoto import awais.instagrabber.utils.MediaUploader.uploadVideo import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.TextUtils.isEmpty +import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.FriendshipService import awais.instagrabber.webservices.MediaService -import awais.instagrabber.webservices.ServiceCallback import com.google.common.collect.ImmutableList import com.google.common.collect.Iterables 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 import java.util.* -import java.util.concurrent.ConcurrentHashMap import java.util.stream.Collectors -class ThreadManager private constructor( +class ThreadManager( private val threadId: String, pending: Boolean, - currentUser: User, - contentResolver: ContentResolver, - viewerId: Long, - csrfToken: String, - deviceUuid: String, + private val currentUser: User?, + private val contentResolver: ContentResolver, + private val viewerId: Long, + private val csrfToken: String, + private val deviceUuid: String, ) { private val _fetching = MutableLiveData>() val fetching: LiveData> = _fetching @@ -67,13 +61,7 @@ class ThreadManager private constructor( private val _pendingRequests = MutableLiveData(null) val pendingRequests: LiveData = _pendingRequests private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager - private val viewerId: Long private val threadIdOrUserIds: ThreadIdOrUserIds = of(threadId) - private val currentUser: User? - private val contentResolver: ContentResolver - private val service: DirectMessagesService - private val mediaService: MediaService - private val friendshipService: FriendshipService val thread: LiveData by lazy { distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource? -> @@ -138,7 +126,7 @@ class ThreadManager private constructor( _fetching.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val threadFeedResponse = service.fetchThread(threadId, cursor) + val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor) if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") { _fetching.postValue(error(R.string.generic_not_ok_response, null)) return@launch @@ -166,7 +154,7 @@ class ThreadManager private constructor( if (isGroup == null || !isGroup) return scope.launch(Dispatchers.IO) { try { - val response = service.participantRequests(threadId, 1) + val response = DirectMessagesService.participantRequests(threadId, 1) _pendingRequests.postValue(response) } catch (e: Exception) { Log.e(TAG, "fetchPendingRequests: ", e) @@ -358,7 +346,10 @@ class ThreadManager private constructor( val repliedToClientContext = replyToItemValue?.clientContext scope.launch(Dispatchers.IO) { try { - val response = service.broadcastText( + val response = DirectMessagesService.broadcastText( + csrfToken, + viewerId, + deviceUuid, clientContext, threadIdOrUserIds, text, @@ -413,7 +404,10 @@ class ThreadManager private constructor( data.postValue(loading(directItem)) scope.launch(Dispatchers.IO) { try { - val request = service.broadcastAnimatedMedia( + val request = DirectMessagesService.broadcastAnimatedMedia( + csrfToken, + userId, + deviceUuid, clientContext, threadIdOrUserIds, giphyGif @@ -448,56 +442,33 @@ class ThreadManager private constructor( addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVoiceOptions = createUploadDmVoiceOptions(byteLength, duration) - uploadVideo(uri, contentResolver, uploadDmVoiceOptions, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { + scope.launch(Dispatchers.IO) { + try { + val response = uploadVideo(uri, contentResolver, uploadDmVoiceOptions) // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return + if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVoiceOptions.uploadId, "4", null ) - val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) - uploadFinishRequest.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - scope.launch(Dispatchers.IO) { - try { - val request = service.broadcastVoice( - clientContext, - threadIdOrUserIds, - uploadDmVoiceOptions.uploadId, - waveform, - samplingFreq - ) - parseResponse(request, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, directItem)) - Log.e(TAG, "sendVoice: ", e) - } - } - return - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data) - return - } - data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") - } - - override fun onFailure(call: Call, t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) + val broadcastResponse = DirectMessagesService.broadcastVoice( + csrfToken, + viewerId, + deviceUuid, + clientContext, + threadIdOrUserIds, + uploadDmVoiceOptions.uploadId, + waveform, + samplingFreq + ) + parseResponse(broadcastResponse, data, directItem) + } catch (e: Exception) { + data.postValue(error(e.message, directItem)) + Log.e(TAG, "sendVoice: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } fun sendReaction( @@ -526,7 +497,10 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.broadcastReaction( + DirectMessagesService.broadcastReaction( + csrfToken, + userId, + deviceUuid, clientContext, threadIdOrUserIds, itemId, @@ -563,7 +537,16 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true) + DirectMessagesService.broadcastReaction( + csrfToken, + viewerId, + deviceUuid, + clientContext, + threadIdOrUserIds, + itemId1, + null, + true + ) } catch (e: Exception) { data.postValue(error(e.message, null)) Log.e(TAG, "sendDeleteReaction: ", e) @@ -582,7 +565,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.deleteItem(threadId, itemId) + DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId) } catch (e: Exception) { // add the item back if unsuccessful addItems(index, listOf(item)) @@ -658,7 +641,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.forward( + DirectMessagesService.forward( thread.threadId, itemTypeName, threadId, @@ -677,7 +660,7 @@ class ThreadManager private constructor( val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { - service.approveRequest(threadId) + DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "acceptRequest: ", e) @@ -691,7 +674,7 @@ class ThreadManager private constructor( val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { - service.declineRequest(threadId) + DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) } catch (e: Exception) { Log.e(TAG, "declineRequest: ", e) @@ -736,33 +719,24 @@ class ThreadManager private constructor( height: Int, scope: CoroutineScope, ) { - val userId = getCurrentUserId(data) ?: return val clientContext = UUID.randomUUID().toString() - val directItem = createImageOrVideo(userId, clientContext, uri, width, height, false) + val directItem = createImageOrVideo(viewerId, clientContext, uri, width, height, false) directItem.isPending = true addItems(0, listOf(directItem)) data.postValue(loading(directItem)) - uploadPhoto(uri, contentResolver, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { - if (handleInvalidResponse(data, response)) return - val response1 = response.response ?: return + scope.launch(Dispatchers.IO) { + try { + val response = uploadPhoto(uri, contentResolver) + if (handleInvalidResponse(data, response)) return@launch + val response1 = response.response ?: return@launch val uploadId = response1.optString("upload_id") - scope.launch(Dispatchers.IO) { - try { - val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) - parseResponse(response2, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, null)) - Log.e(TAG, "sendPhoto: ", e) - } - } + val response2 = DirectMessagesService.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdOrUserIds, uploadId) + parseResponse(response2, data, directItem) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + Log.e(TAG, "sendPhoto: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } private fun sendVideo( @@ -806,56 +780,33 @@ class ThreadManager private constructor( addItems(0, listOf(directItem)) data.postValue(loading(directItem)) val uploadDmVideoOptions = createUploadDmVideoOptions(byteLength, duration, width, height) - uploadVideo(uri, contentResolver, uploadDmVideoOptions, object : OnMediaUploadCompleteListener { - override fun onUploadComplete(response: MediaUploadResponse) { + scope.launch(Dispatchers.IO) { + try { + val response = uploadVideo(uri, contentResolver, uploadDmVideoOptions) // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response)) return + if (handleInvalidResponse(data, response)) return@launch val uploadFinishOptions = UploadFinishOptions( uploadDmVideoOptions.uploadId, "2", VideoOptions(duration / 1000f, emptyList(), 0, false) ) - val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions) - uploadFinishRequest.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - scope.launch(Dispatchers.IO) { - try { - val response1 = service.broadcastVideo( - clientContext, - threadIdOrUserIds, - uploadDmVideoOptions.uploadId, - "", - true - ) - parseResponse(response1, data, directItem) - } catch (e: Exception) { - data.postValue(error(e.message, null)) - Log.e(TAG, "sendVideo: ", e) - } - } - return - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data) - return - } - data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem)) - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null") - } - - override fun onFailure(call: Call, t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) + val broadcastResponse = DirectMessagesService.broadcastVideo( + csrfToken, + viewerId, + deviceUuid, + clientContext, + threadIdOrUserIds, + uploadDmVideoOptions.uploadId, + "", + true + ) + parseResponse(broadcastResponse, data, directItem) + } catch (e: Exception) { + data.postValue(error(e.message, directItem)) + Log.e(TAG, "sendVideo: ", e) } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, directItem)) - Log.e(TAG, "onFailure: ", t) - } - }) + } } private fun parseResponse( @@ -912,26 +863,6 @@ class ThreadManager private constructor( } } - private fun handleErrorBody( - call: Call<*>, - response: Response<*>, - data: MutableLiveData>?, - ) { - try { - val string = response.errorBody()?.string() ?: "" - val msg = String.format(Locale.US, - "onResponse: url: %s, responseCode: %d, errorBody: %s", - call.request().url().toString(), - response.code(), - string) - data?.postValue(error(msg, null)) - Log.e(TAG, msg) - } catch (e: IOException) { - data?.postValue(error(e.message, null)) - Log.e(TAG, "onResponse: ", e) - } - } - private fun handleInvalidResponse( data: MutableLiveData>, response: MediaUploadResponse, @@ -990,7 +921,7 @@ class ThreadManager private constructor( val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { - val response = service.updateTitle(threadId, newTitle.trim()) + val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim()) handleDetailsChangeResponse(data, response) } catch (e: Exception) { } @@ -1002,7 +933,9 @@ class ThreadManager private constructor( val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { - val response = service.addUsers( + val response = DirectMessagesService.addUsers( + csrfToken, + deviceUuid, threadId, users.map { obj: User -> obj.pk } ) @@ -1019,7 +952,7 @@ class ThreadManager private constructor( val data = MutableLiveData>() scope.launch(Dispatchers.IO) { try { - service.removeUsers(threadId, setOf(user.pk)) + DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk)) data.postValue(success(Any())) var activeUsers = users.value var leftUsersValue = leftUsers.value @@ -1054,7 +987,7 @@ class ThreadManager private constructor( if (isAdmin(user)) return data scope.launch(Dispatchers.IO) { try { - service.addAdmins(threadId, setOf(user.pk)) + DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) val currentAdminIds = adminUserIds.value val updatedAdminIds = ImmutableList.builder() .addAll(currentAdminIds ?: emptyList()) @@ -1082,7 +1015,7 @@ class ThreadManager private constructor( if (!isAdmin(user)) return data scope.launch(Dispatchers.IO) { try { - service.removeAdmins(threadId, setOf(user.pk)) + DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) val currentAdmins = adminUserIds.value ?: return@launch val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk } val currentThread = thread.value ?: return@launch @@ -1112,7 +1045,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.mute(threadId) + DirectMessagesService.mute(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { @@ -1140,7 +1073,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.unmute(threadId) + DirectMessagesService.unmute(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { @@ -1168,7 +1101,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.muteMentions(threadId) + DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { @@ -1196,7 +1129,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.unmuteMentions(threadId) + DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId) data.postValue(success(Any())) val currentThread = thread.value ?: return@launch try { @@ -1216,61 +1149,57 @@ class ThreadManager private constructor( fun blockUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() - friendshipService.changeBlock(false, user.pk, object : ServiceCallback { - override fun onSuccess(result: FriendshipChangeResponse?) { + scope.launch(Dispatchers.IO) { + try { + FriendshipService.changeBlock(csrfToken, viewerId, deviceUuid, false, user.pk) refreshChats(scope) + } catch (e: Exception) { + Log.e(TAG, "onFailure: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } fun unblockUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() - friendshipService.changeBlock(true, user.pk, object : ServiceCallback { - override fun onSuccess(result: FriendshipChangeResponse?) { + scope.launch(Dispatchers.IO) { + try { + FriendshipService.changeBlock(csrfToken, viewerId, deviceUuid, true, user.pk) refreshChats(scope) + } catch (e: Exception) { + Log.e(TAG, "onFailure: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } fun restrictUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() - friendshipService.toggleRestrict(user.pk, true, object : ServiceCallback { - override fun onSuccess(result: FriendshipRestrictResponse?) { + scope.launch(Dispatchers.IO) { + try { + FriendshipService.toggleRestrict(csrfToken, deviceUuid, user.pk, true) refreshChats(scope) + } catch (e: Exception) { + Log.e(TAG, "onFailure: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } fun unRestrictUser(user: User, scope: CoroutineScope): LiveData> { val data = MutableLiveData>() - friendshipService.toggleRestrict(user.pk, false, object : ServiceCallback { - override fun onSuccess(result: FriendshipRestrictResponse?) { + scope.launch(Dispatchers.IO) { + try { + FriendshipService.toggleRestrict(csrfToken, deviceUuid, user.pk, false) refreshChats(scope) + } catch (e: Exception) { + Log.e(TAG, "onFailure: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } @@ -1279,7 +1208,9 @@ class ThreadManager private constructor( data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val response = service.approveParticipantRequests( + val response = DirectMessagesService.approveParticipantRequests( + csrfToken, + deviceUuid, threadId, users.map { obj: User -> obj.pk } ) @@ -1298,7 +1229,9 @@ class ThreadManager private constructor( data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val response = service.declineParticipantRequests( + val response = DirectMessagesService.declineParticipantRequests( + csrfToken, + deviceUuid, threadId, users.map { obj: User -> obj.pk } ) @@ -1338,7 +1271,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - val response = service.approvalRequired(threadId) + val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, response) val currentThread = thread.value ?: return@launch try { @@ -1366,7 +1299,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - val request = service.approvalNotRequired(threadId) + val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) val currentThread = thread.value ?: return@launch try { @@ -1389,7 +1322,7 @@ class ThreadManager private constructor( data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val request = service.leave(threadId) + val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) } catch (e: Exception) { Log.e(TAG, "leave: ", e) @@ -1404,7 +1337,7 @@ class ThreadManager private constructor( data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val request = service.end(threadId) + val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId) handleDetailsChangeResponse(data, request) val currentThread = thread.value ?: return@launch try { @@ -1441,7 +1374,7 @@ class ThreadManager private constructor( data.postValue(loading(null)) scope.launch(Dispatchers.IO) { try { - val response = service.markAsSeen(threadId, directItem) + val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem) if (response == null) { data.postValue(error(R.string.generic_null_response, null)) return@launch @@ -1464,43 +1397,4 @@ class ThreadManager private constructor( } return data } - - companion object { - private val TAG = ThreadManager::class.java.simpleName - private val LOCK = Any() - private val INSTANCE_MAP: MutableMap = ConcurrentHashMap() - - @JvmStatic - fun getInstance( - threadId: String, - pending: Boolean, - currentUser: User, - contentResolver: ContentResolver, - viewerId: Long, - csrfToken: String, - deviceUuid: String, - ): ThreadManager { - var instance = INSTANCE_MAP[threadId] - if (instance == null) { - synchronized(LOCK) { - instance = INSTANCE_MAP[threadId] - if (instance == null) { - instance = ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) - INSTANCE_MAP[threadId] = instance!! - } - } - } - return instance!! - } - } - - init { - this.currentUser = currentUser - this.contentResolver = contentResolver - this.viewerId = viewerId - service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid) - mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId) - friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, viewerId) - // fetchChats(); - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/HighlightModel.kt b/app/src/main/java/awais/instagrabber/models/HighlightModel.kt index b74fab01..79398bcb 100644 --- a/app/src/main/java/awais/instagrabber/models/HighlightModel.kt +++ b/app/src/main/java/awais/instagrabber/models/HighlightModel.kt @@ -4,7 +4,7 @@ import awais.instagrabber.utils.TextUtils import java.util.* data class HighlightModel( - val title: String, + val title: String?, val id: String, val thumbnailUrl: String, val timestamp: Long, diff --git a/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java b/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java deleted file mode 100644 index b3da4772..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package awais.instagrabber.repositories; - -import java.util.Map; - -import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; -import retrofit2.Call; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Path; -import retrofit2.http.QueryMap; - -public interface FriendshipRepository { - - @FormUrlEncoded - @POST("/api/v1/friendships/{action}/{id}/") - Call change(@Path("action") String action, - @Path("id") long id, - @FieldMap Map form); - - @FormUrlEncoded - @POST("/api/v1/restrict_action/{action}/") - Call toggleRestrict(@Path("action") String action, - @FieldMap Map form); - - @GET("/api/v1/friendships/{userId}/{type}/") - Call getList(@Path("userId") long userId, - @Path("type") String type, // following or followers - @QueryMap(encoded = true) Map queryParams); - - @FormUrlEncoded - @POST("/api/v1/friendships/{action}/") - Call changeMute(@Path("action") String action, - @FieldMap Map form); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt b/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt new file mode 100644 index 00000000..73cd81b0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/FriendshipRepository.kt @@ -0,0 +1,36 @@ +package awais.instagrabber.repositories + +import awais.instagrabber.repositories.responses.FriendshipChangeResponse +import awais.instagrabber.repositories.responses.FriendshipRestrictResponse +import retrofit2.http.* + +interface FriendshipRepository { + @FormUrlEncoded + @POST("/api/v1/friendships/{action}/{id}/") + suspend fun change( + @Path("action") action: String, + @Path("id") id: Long, + @FieldMap form: Map, + ): FriendshipChangeResponse + + @FormUrlEncoded + @POST("/api/v1/restrict_action/{action}/") + suspend fun toggleRestrict( + @Path("action") action: String, + @FieldMap form: Map, + ): FriendshipRestrictResponse + + @GET("/api/v1/friendships/{userId}/{type}/") + suspend fun getList( + @Path("userId") userId: Long, + @Path("type") type: String, // following or followers + @QueryMap(encoded = true) queryParams: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/friendships/{action}/") + suspend fun changeMute( + @Path("action") action: String, + @FieldMap form: Map, + ): FriendshipChangeResponse +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java b/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java deleted file mode 100644 index 199e44b4..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package awais.instagrabber.repositories; - -import java.util.Map; - -import retrofit2.Call; -import retrofit2.http.GET; -import retrofit2.http.Path; -import retrofit2.http.QueryMap; - -public interface GraphQLRepository { - @GET("/graphql/query/") - Call fetch(@QueryMap(encoded = true) Map queryParams); - - @GET("/{username}/?__a=1") - Call getUser(@Path("username") String username); - - @GET("/p/{shortcode}/?__a=1") - Call getPost(@Path("shortcode") String shortcode); - - @GET("/explore/tags/{tag}/?__a=1") - Call getTag(@Path("tag") String tag); - - @GET("/explore/locations/{locationId}/?__a=1") - Call getLocation(@Path("locationId") long locationId); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt b/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt new file mode 100644 index 00000000..483537e2 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.kt @@ -0,0 +1,22 @@ +package awais.instagrabber.repositories + +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface GraphQLRepository { + @GET("/graphql/query/") + suspend fun fetch(@QueryMap(encoded = true) queryParams: Map): String + + @GET("/{username}/?__a=1") + suspend fun getUser(@Path("username") username: String): String + + @GET("/p/{shortcode}/?__a=1") + suspend fun getPost(@Path("shortcode") shortcode: String): String + + @GET("/explore/tags/{tag}/?__a=1") + suspend fun getTag(@Path("tag") tag: String): String + + @GET("/explore/locations/{locationId}/?__a=1") + suspend fun getLocation(@Path("locationId") locationId: Long): String +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java deleted file mode 100644 index f2c78b11..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -package awais.instagrabber.repositories; - -import java.util.Map; - -import awais.instagrabber.repositories.responses.LikersResponse; -import awais.instagrabber.repositories.responses.MediaInfoResponse; -import retrofit2.Call; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.POST; -import retrofit2.http.Path; -import retrofit2.http.Query; -import retrofit2.http.QueryMap; - -public interface MediaRepository { - @GET("/api/v1/media/{mediaId}/info/") - Call fetch(@Path("mediaId") final long mediaId); - - @GET("/api/v1/media/{mediaId}/{action}/") - Call fetchLikes(@Path("mediaId") final String mediaId, - @Path("action") final String action); // one of "likers" or "comment_likers" - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/{action}/") - Call action(@Path("action") final String action, - @Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/edit_media/") - Call editCaption(@Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); - - @GET("/api/v1/language/translate/") - Call translate(@QueryMap final Map form); - - @FormUrlEncoded - @POST("/api/v1/media/upload_finish/") - Call uploadFinish(@Header("retry_context") final String retryContext, - @QueryMap Map queryParams, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/delete/") - Call delete(@Path("mediaId") final String mediaId, - @Query("media_type") final String mediaType, - @FieldMap final Map signedForm); - - @FormUrlEncoded - @POST("/api/v1/media/{mediaId}/archive/") - Call archive(@Path("mediaId") final String mediaId, - @FieldMap final Map signedForm); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt new file mode 100644 index 00000000..ba0708e3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.kt @@ -0,0 +1,57 @@ +package awais.instagrabber.repositories + +import awais.instagrabber.repositories.responses.LikersResponse +import awais.instagrabber.repositories.responses.MediaInfoResponse +import retrofit2.http.* + +interface MediaRepository { + @GET("/api/v1/media/{mediaId}/info/") + suspend fun fetch(@Path("mediaId") mediaId: Long): MediaInfoResponse + + @GET("/api/v1/media/{mediaId}/{action}/") + suspend fun fetchLikes( + @Path("mediaId") mediaId: String, // one of "likers" or "comment_likers" + @Path("action") action: String, + ): LikersResponse + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/{action}/") + suspend fun action( + @Path("action") action: String, + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/edit_media/") + suspend fun editCaption( + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String + + @GET("/api/v1/language/translate/") + suspend fun translate(@QueryMap form: Map): String + + @FormUrlEncoded + @POST("/api/v1/media/upload_finish/") + suspend fun uploadFinish( + @Header("retry_context") retryContext: String, + @QueryMap queryParams: Map, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/delete/") + suspend fun delete( + @Path("mediaId") mediaId: String, + @Query("media_type") mediaType: String, + @FieldMap signedForm: Map, + ): String + + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/archive/") + suspend fun archive( + @Path("mediaId") mediaId: String, + @FieldMap signedForm: Map, + ): String +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java deleted file mode 100644 index 766649c4..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package awais.instagrabber.repositories; - -import java.util.Map; - -import awais.instagrabber.repositories.responses.StoryStickerResponse; -import retrofit2.Call; -import retrofit2.http.FieldMap; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Path; -import retrofit2.http.QueryMap; -import retrofit2.http.Url; - -public interface StoriesRepository { - @GET("/api/v1/media/{mediaId}/info/") - Call fetch(@Path("mediaId") final long mediaId); - // this one is the same as MediaRepository.fetch BUT you need to make sure it's a story - - @GET("/api/v1/feed/reels_tray/") - Call getFeedStories(); - - @GET("/api/v1/highlights/{uid}/highlights_tray/") - Call fetchHighlights(@Path("uid") final long uid); - - @GET("/api/v1/archive/reel/day_shells/") - Call fetchArchive(@QueryMap Map queryParams); - - @GET - Call getUserStory(@Url String url); - - @FormUrlEncoded - @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") - Call respondToSticker(@Path("storyId") String storyId, - @Path("stickerId") String stickerId, - @Path("action") String action, - // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer - @FieldMap Map form); - - @FormUrlEncoded - @POST("/api/v2/media/seen/") - Call seen(@QueryMap Map queryParams, @FieldMap Map form); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt new file mode 100644 index 00000000..28f540e5 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.kt @@ -0,0 +1,38 @@ +package awais.instagrabber.repositories + +import awais.instagrabber.repositories.responses.StoryStickerResponse +import retrofit2.http.* + +interface StoriesRepository { + // this one is the same as MediaRepository.fetch BUT you need to make sure it's a story + @GET("/api/v1/media/{mediaId}/info/") + suspend fun fetch(@Path("mediaId") mediaId: Long): String + + @GET("/api/v1/feed/reels_tray/") + suspend fun getFeedStories(): String + + @GET("/api/v1/highlights/{uid}/highlights_tray/") + suspend fun fetchHighlights(@Path("uid") uid: Long): String + + @GET("/api/v1/archive/reel/day_shells/") + suspend fun fetchArchive(@QueryMap queryParams: Map): String + + @GET + suspend fun getUserStory(@Url url: String): String + + @FormUrlEncoded + @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") + suspend fun respondToSticker( + @Path("storyId") storyId: String, + @Path("stickerId") stickerId: String, + @Path("action") action: String, // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer + @FieldMap form: Map, + ): StoryStickerResponse + + @FormUrlEncoded + @POST("/api/v2/media/seen/") + suspend fun seen( + @QueryMap queryParams: Map, + @FieldMap form: Map, + ): String +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/UserRepository.java b/app/src/main/java/awais/instagrabber/repositories/UserRepository.java deleted file mode 100644 index 4b14034b..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/UserRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package awais.instagrabber.repositories; - -import awais.instagrabber.repositories.responses.FriendshipStatus; -import awais.instagrabber.repositories.responses.UserSearchResponse; -import awais.instagrabber.repositories.responses.WrappedUser; -import retrofit2.Call; -import retrofit2.http.GET; -import retrofit2.http.Path; -import retrofit2.http.Query; - -public interface UserRepository { - - @GET("/api/v1/users/{uid}/info/") - Call getUserInfo(@Path("uid") final long uid); - - @GET("/api/v1/users/{username}/usernameinfo/") - Call getUsernameInfo(@Path("username") final String username); - - @GET("/api/v1/friendships/show/{uid}/") - Call getUserFriendship(@Path("uid") final long uid); - - @GET("/api/v1/users/search/") - Call search(@Query("timezone_offset") float timezoneOffset, - @Query("q") String query); -} diff --git a/app/src/main/java/awais/instagrabber/repositories/UserRepository.kt b/app/src/main/java/awais/instagrabber/repositories/UserRepository.kt new file mode 100644 index 00000000..7264505b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/UserRepository.kt @@ -0,0 +1,25 @@ +package awais.instagrabber.repositories + +import awais.instagrabber.repositories.responses.FriendshipStatus +import awais.instagrabber.repositories.responses.UserSearchResponse +import awais.instagrabber.repositories.responses.WrappedUser +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface UserRepository { + @GET("/api/v1/users/{uid}/info/") + suspend fun getUserInfo(@Path("uid") uid: Long): WrappedUser + + @GET("/api/v1/users/{username}/usernameinfo/") + suspend fun getUsernameInfo(@Path("username") username: String): WrappedUser + + @GET("/api/v1/friendships/show/{uid}/") + suspend fun getUserFriendship(@Path("uid") uid: Long): FriendshipStatus + + @GET("/api/v1/users/search/") + suspend fun search( + @Query("timezone_offset") timezoneOffset: Float, + @Query("q") query: String, + ): UserSearchResponse +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.java b/app/src/main/java/awais/instagrabber/repositories/responses/User.java deleted file mode 100644 index 0d707f72..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ /dev/null @@ -1,296 +0,0 @@ -package awais.instagrabber.repositories.responses; - -import java.io.Serializable; -import java.util.List; -import java.util.Objects; - -public class User implements Serializable { - private final long pk; - private final String username; - private final String fullName; - private final boolean isPrivate; - private final String profilePicUrl; - private final String profilePicId; - private FriendshipStatus friendshipStatus; - private final boolean isVerified; - private final boolean hasAnonymousProfilePicture; - private final boolean isUnpublished; - private final boolean isFavorite; - private final boolean isDirectappInstalled; - private final boolean hasChaining; - private final String reelAutoArchive; - private final String allowedCommenterType; - private final long mediaCount; - private final long followerCount; - private final long followingCount; - private final long followingTagCount; - private final String biography; - private final String externalUrl; - private final long usertagsCount; - private final String publicEmail; - private final HdProfilePicUrlInfo hdProfilePicUrlInfo; - private final String profileContext; // "also followed by" your friends - private final List profileContextLinksWithUserIds; // ^ - private final String socialContext; // AYML - private final String interopMessagingUserFbid; // in DMs only: Facebook user ID - - public User(final long pk, - final String username, - final String fullName, - final boolean isPrivate, - final String profilePicUrl, - final String profilePicId, - final FriendshipStatus friendshipStatus, - final boolean isVerified, - final boolean hasAnonymousProfilePicture, - final boolean isUnpublished, - final boolean isFavorite, - final boolean isDirectappInstalled, - final boolean hasChaining, - final String reelAutoArchive, - final String allowedCommenterType, - final long mediaCount, - final long followerCount, - final long followingCount, - final long followingTagCount, - final String biography, - final String externalUrl, - final long usertagsCount, - final String publicEmail, - final HdProfilePicUrlInfo hdProfilePicUrlInfo, - final String profileContext, - final List profileContextLinksWithUserIds, - final String socialContext, - final String interopMessagingUserFbid) { - this.pk = pk; - this.username = username; - this.fullName = fullName; - this.isPrivate = isPrivate; - this.profilePicUrl = profilePicUrl; - this.profilePicId = profilePicId; - this.friendshipStatus = friendshipStatus; - this.isVerified = isVerified; - this.hasAnonymousProfilePicture = hasAnonymousProfilePicture; - this.isUnpublished = isUnpublished; - this.isFavorite = isFavorite; - this.isDirectappInstalled = isDirectappInstalled; - this.hasChaining = hasChaining; - this.reelAutoArchive = reelAutoArchive; - this.allowedCommenterType = allowedCommenterType; - this.mediaCount = mediaCount; - this.followerCount = followerCount; - this.followingCount = followingCount; - this.followingTagCount = followingTagCount; - this.biography = biography; - this.externalUrl = externalUrl; - this.usertagsCount = usertagsCount; - this.publicEmail = publicEmail; - this.hdProfilePicUrlInfo = hdProfilePicUrlInfo; - this.profileContext = profileContext; - this.profileContextLinksWithUserIds = profileContextLinksWithUserIds; - this.socialContext = socialContext; - this.interopMessagingUserFbid = interopMessagingUserFbid; - } - - public User(final long pk, - final String username, - final String fullName, - final boolean isPrivate, - final String profilePicUrl, - final boolean isVerified) { - this.pk = pk; - this.username = username; - this.fullName = fullName; - this.isPrivate = isPrivate; - this.profilePicUrl = profilePicUrl; - this.profilePicId = null; - this.friendshipStatus = new FriendshipStatus( - false, - false, - false, - false, - false, - false, - false, - false, - false, - false - ); - this.isVerified = isVerified; - this.hasAnonymousProfilePicture = false; - this.isUnpublished = false; - this.isFavorite = false; - this.isDirectappInstalled = false; - this.hasChaining = false; - this.reelAutoArchive = null; - this.allowedCommenterType = null; - this.mediaCount = 0; - this.followerCount = 0; - this.followingCount = 0; - this.followingTagCount = 0; - this.biography = null; - this.externalUrl = null; - this.usertagsCount = 0; - this.publicEmail = null; - this.hdProfilePicUrlInfo = null; - this.profileContext = null; - this.profileContextLinksWithUserIds = null; - this.socialContext = null; - this.interopMessagingUserFbid = null; - } - - public long getPk() { - return pk; - } - - public String getUsername() { - return username; - } - - public String getFullName() { - return fullName; - } - - public boolean isPrivate() { - return isPrivate; - } - - public String getProfilePicUrl() { - return profilePicUrl; - } - - public String getHDProfilePicUrl() { - if (hdProfilePicUrlInfo == null) { - return getProfilePicUrl(); - } - return hdProfilePicUrlInfo.getUrl(); - } - - public String getProfilePicId() { - return profilePicId; - } - - public FriendshipStatus getFriendshipStatus() { - return friendshipStatus; - } - - public void setFriendshipStatus(final FriendshipStatus friendshipStatus) { - this.friendshipStatus = friendshipStatus; - } - - public boolean isVerified() { - return isVerified; - } - - public boolean hasAnonymousProfilePicture() { - return hasAnonymousProfilePicture; - } - - public boolean isUnpublished() { - return isUnpublished; - } - - public boolean isFavorite() { - return isFavorite; - } - - public boolean isDirectappInstalled() { - return isDirectappInstalled; - } - - public boolean hasChaining() { - return hasChaining; - } - - public String getReelAutoArchive() { - return reelAutoArchive; - } - - public String getAllowedCommenterType() { - return allowedCommenterType; - } - - public long getMediaCount() { - return mediaCount; - } - - public long getFollowerCount() { - return followerCount; - } - - public long getFollowingCount() { - return followingCount; - } - - public long getFollowingTagCount() { - return followingTagCount; - } - - public String getBiography() { - return biography; - } - - public String getExternalUrl() { - return externalUrl; - } - - public long getUsertagsCount() { - return usertagsCount; - } - - public String getPublicEmail() { - return publicEmail; - } - - public String getProfileContext() { - return profileContext; - } - - public String getSocialContext() { - return socialContext; - } - - public List getProfileContextLinks() { - return profileContextLinksWithUserIds; - } - - public String getFbId() { - return interopMessagingUserFbid; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final User user = (User) o; - return pk == user.pk && - isPrivate == user.isPrivate && - isVerified == user.isVerified && - hasAnonymousProfilePicture == user.hasAnonymousProfilePicture && - isUnpublished == user.isUnpublished && - isFavorite == user.isFavorite && - isDirectappInstalled == user.isDirectappInstalled && - mediaCount == user.mediaCount && - followerCount == user.followerCount && - followingCount == user.followingCount && - followingTagCount == user.followingTagCount && - usertagsCount == user.usertagsCount && - Objects.equals(username, user.username) && - Objects.equals(fullName, user.fullName) && - Objects.equals(profilePicUrl, user.profilePicUrl) && - Objects.equals(profilePicId, user.profilePicId) && - Objects.equals(friendshipStatus, user.friendshipStatus) && - Objects.equals(reelAutoArchive, user.reelAutoArchive) && - Objects.equals(allowedCommenterType, user.allowedCommenterType) && - Objects.equals(biography, user.biography) && - Objects.equals(externalUrl, user.externalUrl) && - Objects.equals(publicEmail, user.publicEmail); - } - - @Override - public int hashCode() { - return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, - isUnpublished, isFavorite, isDirectappInstalled, hasChaining, reelAutoArchive, allowedCommenterType, mediaCount, - followerCount, followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); - } -} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.kt b/app/src/main/java/awais/instagrabber/repositories/responses/User.kt new file mode 100644 index 00000000..6b82b1df --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.kt @@ -0,0 +1,38 @@ +package awais.instagrabber.repositories.responses + +import java.io.Serializable + + +data class User @JvmOverloads constructor( + val pk: Long = 0, + val username: String = "", + val fullName: String = "", + val isPrivate: Boolean = false, + val profilePicUrl: String? = null, + val isVerified: Boolean = false, + val profilePicId: String? = null, + var friendshipStatus: FriendshipStatus? = null, + val hasAnonymousProfilePicture: Boolean = false, + val isUnpublished: Boolean = false, + val isFavorite: Boolean = false, + val isDirectappInstalled: Boolean = false, + val hasChaining: Boolean = false, + val reelAutoArchive: String? = null, + val allowedCommenterType: String? = null, + val mediaCount: Long = 0, + val followerCount: Long = 0, + val followingCount: Long = 0, + val followingTagCount: Long = 0, + val biography: String? = null, + val externalUrl: String? = null, + val usertagsCount: Long = 0, + val publicEmail: String? = null, + val hdProfilePicUrlInfo: HdProfilePicUrlInfo? = null, + val profileContext: String? = null, // "also followed by" your friends + val profileContextLinksWithUserIds: List? = null, // ^ + val socialContext: String? = null, // AYML + val interopMessagingUserFbid: String? = null, // in DMs only: Facebook user ID +) : Serializable { + val hDProfilePicUrl: String + get() = hdProfilePicUrlInfo?.url ?: profilePicUrl ?: "" +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java deleted file mode 100644 index 49137b71..00000000 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java +++ /dev/null @@ -1,280 +0,0 @@ -package awais.instagrabber.utils; - -import android.content.ContentResolver; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.util.Log; -import android.util.LruCache; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public final class BitmapUtils { - private static final String TAG = BitmapUtils.class.getSimpleName(); - private static final LruCache bitmapMemoryCache; - private static final AppExecutors appExecutors = AppExecutors.INSTANCE; - private static final ExecutorService callbackHandlers = Executors - .newCachedThreadPool(r -> new Thread(r, "bm-load-callback-handler#" + NumberUtils.random(0, 100))); - public static final float THUMBNAIL_SIZE = 200f; - - static { - // Get max available VM memory, exceeding this amount will throw an - // OutOfMemory exception. Stored in kilobytes as LruCache takes an - // int in its constructor. - final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - // Use 1/8th of the available memory for this memory cache. - final int cacheSize = maxMemory / 8; - bitmapMemoryCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, Bitmap bitmap) { - // The cache size will be measured in kilobytes rather than - // number of items. - return bitmap.getByteCount() / 1024; - } - }; - - } - - public static void addBitmapToMemoryCache(final String key, final Bitmap bitmap, final boolean force) { - if (force || getBitmapFromMemCache(key) == null) { - bitmapMemoryCache.put(key, bitmap); - } - } - - public static Bitmap getBitmapFromMemCache(final String key) { - return bitmapMemoryCache.get(key); - } - - public static void getThumbnail(final Context context, final Uri uri, final ThumbnailLoadCallback callback) { - if (context == null || uri == null || callback == null) return; - final String key = uri.toString(); - final Bitmap cachedBitmap = getBitmapFromMemCache(key); - if (cachedBitmap != null) { - callback.onLoad(cachedBitmap, -1, -1); - return; - } - loadBitmap(context.getContentResolver(), uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true, callback); - } - - /** - * Loads bitmap from given Uri - * - * @param contentResolver {@link ContentResolver} to resolve the uri - * @param uri Uri from where Bitmap will be loaded - * @param reqWidth Required width - * @param reqHeight Required height - * @param addToCache true if the loaded bitmap should be added to the mem cache - * @param callback Bitmap load callback - */ - public static void loadBitmap(final ContentResolver contentResolver, - final Uri uri, - final float reqWidth, - final float reqHeight, - final boolean addToCache, - final ThumbnailLoadCallback callback) { - loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1, addToCache, callback); - } - - /** - * Loads bitmap from given Uri - * - * @param contentResolver {@link ContentResolver} to resolve the uri - * @param uri Uri from where Bitmap will be loaded - * @param maxDimenSize Max size of the largest side of the image - * @param addToCache true if the loaded bitmap should be added to the mem cache - * @param callback Bitmap load callback - */ - public static void loadBitmap(final ContentResolver contentResolver, - final Uri uri, - final float maxDimenSize, - final boolean addToCache, - final ThumbnailLoadCallback callback) { - loadBitmap(contentResolver, uri, -1, -1, maxDimenSize, addToCache, callback); - } - - /** - * Loads bitmap from given Uri - * - * @param contentResolver {@link ContentResolver} to resolve the uri - * @param uri Uri from where {@link Bitmap} will be loaded - * @param reqWidth Required width (set to -1 if maxDimenSize provided) - * @param reqHeight Required height (set to -1 if maxDimenSize provided) - * @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight) - * @param addToCache true if the loaded bitmap should be added to the mem cache - * @param callback Bitmap load callback - */ - private static void loadBitmap(final ContentResolver contentResolver, - final Uri uri, - final float reqWidth, - final float reqHeight, - final float maxDimenSize, - final boolean addToCache, - final ThumbnailLoadCallback callback) { - if (contentResolver == null || uri == null || callback == null) return; - final ListenableFuture future = appExecutors - .getTasksThread() - .submit(() -> getBitmapResult(contentResolver, uri, reqWidth, reqHeight, maxDimenSize, addToCache)); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable final BitmapResult result) { - if (result == null) { - callback.onLoad(null, -1, -1); - return; - } - callback.onLoad(result.bitmap, result.width, result.height); - } - - @Override - public void onFailure(@NonNull final Throwable t) { - callback.onFailure(t); - } - }, callbackHandlers); - } - - @Nullable - public static BitmapResult getBitmapResult(final ContentResolver contentResolver, - final Uri uri, - final float reqWidth, - final float reqHeight, - final float maxDimenSize, - final boolean addToCache) { - BitmapFactory.Options bitmapOptions; - float actualReqWidth = reqWidth; - float actualReqHeight = reqHeight; - try (InputStream input = contentResolver.openInputStream(uri)) { - BitmapFactory.Options outBounds = new BitmapFactory.Options(); - outBounds.inJustDecodeBounds = true; - outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888; - BitmapFactory.decodeStream(input, null, outBounds); - if ((outBounds.outWidth == -1) || (outBounds.outHeight == -1)) return null; - bitmapOptions = new BitmapFactory.Options(); - if (maxDimenSize > 0) { - // Raw height and width of image - final int height = outBounds.outHeight; - final int width = outBounds.outWidth; - final float ratio = (float) width / height; - if (height > width) { - actualReqHeight = maxDimenSize; - actualReqWidth = actualReqHeight * ratio; - } else { - actualReqWidth = maxDimenSize; - actualReqHeight = actualReqWidth / ratio; - } - } - bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight); - } catch (Exception e) { - Log.e(TAG, "loadBitmap: ", e); - return null; - } - try (InputStream input = contentResolver.openInputStream(uri)) { - bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; - Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions); - if (addToCache) { - addBitmapToMemoryCache(uri.toString(), bitmap, true); - } - return new BitmapResult(bitmap, (int) actualReqWidth, (int) actualReqHeight); - } catch (Exception e) { - Log.e(TAG, "loadBitmap: ", e); - } - return null; - } - - public static class BitmapResult { - public Bitmap bitmap; - int width; - int height; - - public BitmapResult(final Bitmap bitmap, final int width, final int height) { - this.width = width; - this.height = height; - this.bitmap = bitmap; - } - } - - private static int calculateInSampleSize(final BitmapFactory.Options options, final float reqWidth, final float reqHeight) { - // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - if (height > reqHeight || width > reqWidth) { - final float halfHeight = height / 2f; - final float halfWidth = width / 2f; - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((halfHeight / inSampleSize) >= reqHeight - && (halfWidth / inSampleSize) >= reqWidth) { - inSampleSize *= 2; - } - } - return inSampleSize; - } - - public interface ThumbnailLoadCallback { - /** - * @param bitmap Resulting bitmap - * @param width width of the bitmap (Only correct if loadBitmap was called or -1) - * @param height height of the bitmap (Only correct if loadBitmap was called or -1) - */ - void onLoad(@Nullable Bitmap bitmap, int width, int height); - - void onFailure(@NonNull Throwable t); - } - - /** - * Decodes the bounds of an image from its Uri and returns a pair of the dimensions - * - * @param uri the Uri of the image - * @return dimensions of the image - */ - public static Pair decodeDimensions(@NonNull final ContentResolver contentResolver, - @NonNull final Uri uri) throws IOException { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - try (final InputStream stream = contentResolver.openInputStream(uri)) { - BitmapFactory.decodeStream(stream, null, options); - return (options.outWidth == -1 || options.outHeight == -1) - ? null - : new Pair<>(options.outWidth, options.outHeight); - } - } - - public static File convertToJpegAndSaveToFile(@NonNull final Bitmap bitmap, @Nullable final File file) throws IOException { - File tempFile = file; - if (file == null) { - tempFile = DownloadUtils.getTempFile(); - } - try (OutputStream output = new FileOutputStream(tempFile)) { - final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); - if (!compressResult) { - throw new RuntimeException("Compression failed!"); - } - } - return tempFile; - } - - public static void convertToJpegAndSaveToUri(@NonNull Context context, - @NonNull final Bitmap bitmap, - @NonNull final Uri uri) throws Exception { - try (OutputStream output = context.getContentResolver().openOutputStream(uri)) { - final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); - if (!compressResult) { - throw new RuntimeException("Compression failed!"); - } - } - } -} diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt new file mode 100644 index 00000000..379ea8db --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt @@ -0,0 +1,238 @@ +package awais.instagrabber.utils + +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import android.util.LruCache +import androidx.core.util.Pair +import awais.instagrabber.utils.extensions.TAG +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +object BitmapUtils { + private val bitmapMemoryCache: LruCache + const val THUMBNAIL_SIZE = 200f + + @JvmStatic + fun addBitmapToMemoryCache(key: String, bitmap: Bitmap, force: Boolean) { + if (force || getBitmapFromMemCache(key) == null) { + bitmapMemoryCache.put(key, bitmap) + } + } + + @JvmStatic + fun getBitmapFromMemCache(key: String): Bitmap? { + return bitmapMemoryCache[key] + } + + @JvmStatic + suspend fun getThumbnail(context: Context, uri: Uri): BitmapResult? { + val key = uri.toString() + val cachedBitmap = getBitmapFromMemCache(key) + if (cachedBitmap != null) { + return BitmapResult(cachedBitmap, -1, -1) + } + return loadBitmap(context.contentResolver, uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true) + } + + /** + * Loads bitmap from given Uri + * + * @param contentResolver [ContentResolver] to resolve the uri + * @param uri Uri from where Bitmap will be loaded + * @param reqWidth Required width + * @param reqHeight Required height + * @param addToCache true if the loaded bitmap should be added to the mem cache + */ + suspend fun loadBitmap( + contentResolver: ContentResolver?, + uri: Uri?, + reqWidth: Float, + reqHeight: Float, + addToCache: Boolean, + ): BitmapResult? = loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1f, addToCache) + + /** + * Loads bitmap from given Uri + * + * @param contentResolver [ContentResolver] to resolve the uri + * @param uri Uri from where Bitmap will be loaded + * @param maxDimenSize Max size of the largest side of the image + * @param addToCache true if the loaded bitmap should be added to the mem cache + */ + suspend fun loadBitmap( + contentResolver: ContentResolver?, + uri: Uri?, + maxDimenSize: Float, + addToCache: Boolean, + ): BitmapResult? = loadBitmap(contentResolver, uri, -1f, -1f, maxDimenSize, addToCache) + + /** + * Loads bitmap from given Uri + * + * @param contentResolver [ContentResolver] to resolve the uri + * @param uri Uri from where [Bitmap] will be loaded + * @param reqWidth Required width (set to -1 if maxDimenSize provided) + * @param reqHeight Required height (set to -1 if maxDimenSize provided) + * @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight) + * @param addToCache true if the loaded bitmap should be added to the mem cache + */ + private suspend fun loadBitmap( + contentResolver: ContentResolver?, + uri: Uri?, + reqWidth: Float, + reqHeight: Float, + maxDimenSize: Float, + addToCache: Boolean, + ): BitmapResult? = + if (contentResolver == null || uri == null) null else withContext(Dispatchers.IO) { + getBitmapResult(contentResolver, + uri, + reqWidth, + reqHeight, + maxDimenSize, + addToCache) + } + + fun getBitmapResult( + contentResolver: ContentResolver, + uri: Uri, + reqWidth: Float, + reqHeight: Float, + maxDimenSize: Float, + addToCache: Boolean, + ): BitmapResult? { + var bitmapOptions: BitmapFactory.Options + var actualReqWidth = reqWidth + var actualReqHeight = reqHeight + try { + contentResolver.openInputStream(uri).use { input -> + val outBounds = BitmapFactory.Options() + outBounds.inJustDecodeBounds = true + outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888 + BitmapFactory.decodeStream(input, null, outBounds) + if (outBounds.outWidth == -1 || outBounds.outHeight == -1) return null + bitmapOptions = BitmapFactory.Options() + if (maxDimenSize > 0) { + // Raw height and width of image + val height = outBounds.outHeight + val width = outBounds.outWidth + val ratio = width.toFloat() / height + if (height > width) { + actualReqHeight = maxDimenSize + actualReqWidth = actualReqHeight * ratio + } else { + actualReqWidth = maxDimenSize + actualReqHeight = actualReqWidth / ratio + } + } + bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight) + } + } catch (e: Exception) { + Log.e(TAG, "loadBitmap: ", e) + return null + } + try { + contentResolver.openInputStream(uri).use { input -> + bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888 + val bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions) + if (addToCache && bitmap != null) { + addBitmapToMemoryCache(uri.toString(), bitmap, true) + } + return BitmapResult(bitmap, actualReqWidth.toInt(), actualReqHeight.toInt()) + } + } catch (e: Exception) { + Log.e(TAG, "loadBitmap: ", e) + } + return null + } + + private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Float, reqHeight: Float): Int { + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + if (height > reqHeight || width > reqWidth) { + val halfHeight = height / 2f + val halfWidth = width / 2f + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= reqHeight + && halfWidth / inSampleSize >= reqWidth + ) { + inSampleSize *= 2 + } + } + return inSampleSize + } + + /** + * Decodes the bounds of an image from its Uri and returns a pair of the dimensions + * + * @param uri the Uri of the image + * @return dimensions of the image + */ + @Throws(IOException::class) + fun decodeDimensions( + contentResolver: ContentResolver, + uri: Uri, + ): Pair? { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + contentResolver.openInputStream(uri).use { stream -> + BitmapFactory.decodeStream(stream, null, options) + return if (options.outWidth == -1 || options.outHeight == -1) null else Pair(options.outWidth, options.outHeight) + } + } + + @Throws(IOException::class) + fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File { + val tempFile = file ?: DownloadUtils.getTempFile() + FileOutputStream(tempFile).use { output -> + val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) + if (!compressResult) { + throw RuntimeException("Compression failed!") + } + } + return tempFile + } + + @JvmStatic + @Throws(Exception::class) + fun convertToJpegAndSaveToUri( + context: Context, + bitmap: Bitmap, + uri: Uri, + ) { + context.contentResolver.openOutputStream(uri).use { output -> + val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) + if (!compressResult) { + throw RuntimeException("Compression failed!") + } + } + } + + class BitmapResult(var bitmap: Bitmap?, var width: Int, var height: Int) + + init { + // Get max available VM memory, exceeding this amount will throw an + // OutOfMemory exception. Stored in kilobytes as LruCache takes an + // int in its constructor. + val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() + // Use 1/8th of the available memory for this memory cache. + val cacheSize: Int = maxMemory / 8 + bitmapMemoryCache = object : LruCache(cacheSize) { + override fun sizeOf(key: String, bitmap: Bitmap): Int { + // The cache size will be measured in kilobytes rather than + // number of items. + return bitmap.byteCount / 1024 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/CookieUtils.kt b/app/src/main/java/awais/instagrabber/utils/CookieUtils.kt index 78a32516..83738faf 100644 --- a/app/src/main/java/awais/instagrabber/utils/CookieUtils.kt +++ b/app/src/main/java/awais/instagrabber/utils/CookieUtils.kt @@ -7,7 +7,6 @@ import android.util.Log import android.webkit.CookieManager import awais.instagrabber.db.datasources.AccountDataSource import awais.instagrabber.db.repositories.AccountRepository -import awais.instagrabber.db.repositories.RepositoryCallback import java.net.CookiePolicy import java.net.HttpCookie import java.net.URI @@ -48,14 +47,9 @@ fun setupCookies(cookieRaw: String) { } } -fun removeAllAccounts(context: Context, callback: RepositoryCallback?) { +suspend fun removeAllAccounts(context: Context) { NET_COOKIE_MANAGER.cookieStore.removeAll() - try { - AccountRepository.getInstance(AccountDataSource.getInstance(context)) - .deleteAllAccounts(callback) - } catch (e: Exception) { - Log.e(TAG, "setupCookies", e) - } + AccountRepository.getInstance(AccountDataSource.getInstance(context)).deleteAllAccounts() } fun getUserIdFromCookie(cookies: String?): Long { diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java index 16807994..b34c7260 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -38,10 +38,10 @@ import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.db.repositories.FavoriteRepository; -import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -160,17 +160,20 @@ public final class ExportImportUtils { ); // Log.d(TAG, "importJson: favoriteModel: " + favoriteModel); final FavoriteRepository favRepo = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); - favRepo.getFavorite(query, favoriteType, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - // local has priority since it's more frequently updated - } - - @Override - public void onDataNotAvailable() { - favRepo.insertOrUpdateFavorite(favorite, null); - } - }); + favRepo.getFavorite( + query, + favoriteType, + CoroutineUtilsKt.getContinuation((favorite1, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "importFavorites: ", throwable); + return; + } + if (favorite1 == null) { + favRepo.insertOrUpdateFavorite(favorite, CoroutineUtilsKt.getContinuation((unit, throwable1) -> {}, Dispatchers.getIO())); + } + // local has priority since it's more frequently updated + }), Dispatchers.getIO()) + ); } } @@ -197,7 +200,7 @@ public final class ExportImportUtils { return; } AccountRepository.getInstance(AccountDataSource.getInstance(context)) - .insertOrUpdateAccounts(accounts, null); + .insertOrUpdateAccounts(accounts, CoroutineUtilsKt.getContinuation((unit, throwable) -> {}, Dispatchers.getIO())); } private static void importSettings(final JSONObject jsonObject) { @@ -363,66 +366,64 @@ public final class ExportImportUtils { private static ListenableFuture getFavorites(final Context context) { final SettableFuture future = SettableFuture.create(); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); - favoriteRepository.getAllFavorites(new RepositoryCallback>() { - @Override - public void onSuccess(final List favorites) { - final JSONArray jsonArray = new JSONArray(); - try { - for (final Favorite favorite : favorites) { - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("q", favorite.getQuery()); - jsonObject.put("type", favorite.getType().toString()); - jsonObject.put("s", favorite.getDisplayName()); - jsonObject.put("pic_url", favorite.getPicUrl()); - jsonObject.put("d", favorite.getDateAdded().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); - jsonArray.put(jsonObject); + favoriteRepository.getAllFavorites( + CoroutineUtilsKt.getContinuation((favorites, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + future.set(new JSONArray()); + Log.e(TAG, "getFavorites: ", throwable); + return; } - } catch (Exception e) { - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error exporting favorites", e); + final JSONArray jsonArray = new JSONArray(); + try { + for (final Favorite favorite : favorites) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("q", favorite.getQuery()); + jsonObject.put("type", favorite.getType().toString()); + jsonObject.put("s", favorite.getDisplayName()); + jsonObject.put("pic_url", favorite.getPicUrl()); + jsonObject.put("d", favorite.getDateAdded().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + jsonArray.put(jsonObject); + } + } catch (Exception e) { + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting favorites", e); + } } - } - future.set(jsonArray); - } - - @Override - public void onDataNotAvailable() { - future.set(new JSONArray()); - } - }); + future.set(jsonArray); + }), Dispatchers.getIO()) + ); return future; } private static ListenableFuture getCookies(final Context context) { final SettableFuture future = SettableFuture.create(); final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context)); - accountRepository.getAllAccounts(new RepositoryCallback>() { - @Override - public void onSuccess(final List accounts) { - final JSONArray jsonArray = new JSONArray(); - try { - for (final Account cookie : accounts) { - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("i", cookie.getUid()); - jsonObject.put("u", cookie.getUsername()); - jsonObject.put("c", cookie.getCookie()); - jsonObject.put("full_name", cookie.getFullName()); - jsonObject.put("profile_pic", cookie.getProfilePic()); - jsonArray.put(jsonObject); + accountRepository.getAllAccounts( + CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "getCookies: ", throwable); + future.set(new JSONArray()); + return; } - } catch (Exception e) { - if (BuildConfig.DEBUG) { - Log.e(TAG, "Error exporting accounts", e); + final JSONArray jsonArray = new JSONArray(); + try { + for (final Account cookie : accounts) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("i", cookie.getUid()); + jsonObject.put("u", cookie.getUsername()); + jsonObject.put("c", cookie.getCookie()); + jsonObject.put("full_name", cookie.getFullName()); + jsonObject.put("profile_pic", cookie.getProfilePic()); + jsonArray.put(jsonObject); + } + } catch (Exception e) { + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting accounts", e); + } } - } - future.set(jsonArray); - } - - @Override - public void onDataNotAvailable() { - future.set(new JSONArray()); - } - }); + future.set(jsonArray); + }), Dispatchers.getIO()) + ); return future; } diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt index 25c1d43a..911b30c8 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -4,12 +4,14 @@ import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri import awais.instagrabber.models.UploadVideoOptions -import awais.instagrabber.utils.BitmapUtils.ThumbnailLoadCallback import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.* import okio.BufferedSink import okio.Okio import org.json.JSONObject +import ru.gildor.coroutines.okhttp.await import java.io.File import java.io.FileInputStream import java.io.IOException @@ -17,89 +19,59 @@ import java.io.InputStream object MediaUploader { private const val HOST = "https://i.instagram.com" - private val appExecutors = AppExecutors - - fun uploadPhoto( - uri: Uri, - contentResolver: ContentResolver, - listener: OnMediaUploadCompleteListener, - ) { - BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false, object : ThumbnailLoadCallback { - override fun onLoad(bitmap: Bitmap?, width: Int, height: Int) { - if (bitmap == null) { - listener.onFailure(RuntimeException("Bitmap result was null")) - return - } - uploadPhoto(bitmap, listener) - } - - override fun onFailure(t: Throwable) { - listener.onFailure(t) - } - }) + private val octetStreamMediaType: MediaType = requireNotNull(MediaType.parse("application/octet-stream")) { + "No media type found for application/octet-stream" } - private fun uploadPhoto( + suspend fun uploadPhoto( + uri: Uri, + contentResolver: ContentResolver, + ): 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") + private suspend fun uploadPhoto( bitmap: Bitmap, - listener: OnMediaUploadCompleteListener, - ) { - appExecutors.tasksThread.submit { - val file: File - val byteLength: Long - try { - file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) - byteLength = file.length() - } catch (e: Exception) { - listener.onFailure(e) - return@submit - } - val options = createUploadPhotoOptions(byteLength) - val headers = getUploadPhotoHeaders(options) - val url = HOST + "/rupload_igphoto/" + options.name + "/" - appExecutors.networkIO.execute { - try { - FileInputStream(file).use { input -> upload(input, url, headers, listener) } - } catch (e: IOException) { - listener.onFailure(e) - } finally { - file.delete() - } - } + ): MediaUploadResponse = withContext(Dispatchers.IO) { + val file: File = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) + val byteLength: Long = file.length() + val options = createUploadPhotoOptions(byteLength) + val headers = getUploadPhotoHeaders(options) + val url = HOST + "/rupload_igphoto/" + options.name + "/" + try { + FileInputStream(file).use { input -> upload(input, url, headers) } + } finally { + file.delete() } } @JvmStatic - fun uploadVideo( + @Suppress("BlockingMethodInNonBlockingContext") // See https://youtrack.jetbrains.com/issue/KTIJ-838 + suspend fun uploadVideo( uri: Uri, contentResolver: ContentResolver, options: UploadVideoOptions, - listener: OnMediaUploadCompleteListener, - ) { - appExecutors.tasksThread.submit { - val headers = getUploadVideoHeaders(options) - val url = HOST + "/rupload_igvideo/" + options.name + "/" - appExecutors.networkIO.execute { - try { - contentResolver.openInputStream(uri).use { input -> - if (input == null) { - listener.onFailure(RuntimeException("InputStream was null")) - return@execute - } - upload(input, url, headers, listener) - } - } catch (e: IOException) { - listener.onFailure(e) - } + ): MediaUploadResponse = withContext(Dispatchers.IO) { + val headers = getUploadVideoHeaders(options) + val url = HOST + "/rupload_igvideo/" + options.name + "/" + contentResolver.openInputStream(uri).use { input -> + if (input == null) { + // listener.onFailure(RuntimeException("InputStream was null")) + throw IllegalStateException("InputStream was null") } + upload(input, url, headers) } } - private fun upload( + @Throws(IOException::class) + private suspend fun upload( input: InputStream, url: String, headers: Map, - listener: OnMediaUploadCompleteListener, - ) { + ): MediaUploadResponse { try { val client = OkHttpClient.Builder() // .addInterceptor(new LoggingInterceptor()) @@ -110,46 +82,38 @@ object MediaUploader { val request = Request.Builder() .headers(Headers.of(headers)) .url(url) - .post(create(MediaType.parse("application/octet-stream"), input)) + .post(create(octetStreamMediaType, input)) .build() - val call = client.newCall(request) - val response = call.execute() - val body = response.body() - if (!response.isSuccessful) { - listener.onFailure(IOException("Unexpected code " + response + if (body != null) ": " + body.string() else "")) - return + return withContext(Dispatchers.IO) { + val response = client.newCall(request).await() + val body = response.body() + @Suppress("BlockingMethodInNonBlockingContext") // Blocked by https://github.com/square/okio/issues/501 + MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null) } - listener.onUploadComplete(MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null)) } catch (e: Exception) { - listener.onFailure(e) + // rethrow for proper stacktrace. See https://github.com/gildor/kotlin-coroutines-okhttp/tree/master#wrap-exception-manually + throw IOException(e) } } - 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 - } - } - - @Throws(IOException::class) - @Suppress("DEPRECATION_ERROR") - override fun writeTo(sink: BufferedSink) { - Okio.source(inputStream).use { sink.writeAll(it) } + override fun contentLength(): Long { + return try { + inputStream.available().toLong() + } catch (e: IOException) { + 0 } } - } - interface OnMediaUploadCompleteListener { - fun onUploadComplete(response: MediaUploadResponse) - fun onFailure(t: Throwable) + @Throws(IOException::class) + @Suppress("DEPRECATION_ERROR") + override fun writeTo(sink: BufferedSink) { + Okio.source(inputStream).use { sink.writeAll(it) } + } } data class MediaUploadResponse(val responseCode: Int, val response: JSONObject?) diff --git a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java index 5b5bb54e..f7e4c1e9 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java @@ -12,9 +12,10 @@ import androidx.lifecycle.MutableLiveData; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.UserService; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -32,7 +33,7 @@ public class AppStateViewModel extends AndroidViewModel { cookie = settingsHelper.getString(Constants.COOKIE); final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; if (!isLoggedIn) return; - userService = UserService.getInstance(); + userService = UserService.INSTANCE; // final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); fetchProfileDetails(); } @@ -49,16 +50,12 @@ public class AppStateViewModel extends AndroidViewModel { private void fetchProfileDetails() { final long uid = CookieUtils.getUserIdFromCookie(cookie); if (userService == null) return; - userService.getUserInfo(uid, new ServiceCallback() { - @Override - public void onSuccess(final User user) { - currentUser.postValue(user); + userService.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> { + if (throwable != null) { + Log.e(TAG, "onFailure: ", throwable); + return; } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + currentUser.postValue(user); + }, Dispatchers.getIO())); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java index 431497f0..d8e0481e 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java @@ -30,13 +30,13 @@ import awais.instagrabber.repositories.responses.CommentsFetchResponse; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.CommentService; import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ServiceCallback; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -113,7 +113,7 @@ public class CommentsViewerViewModel extends ViewModel { }; public CommentsViewerViewModel() { - graphQLService = GraphQLService.getInstance(); + graphQLService = GraphQLService.INSTANCE; final String cookie = settingsHelper.getString(Constants.COOKIE); final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); @@ -165,8 +165,12 @@ public class CommentsViewerViewModel extends ViewModel { commentService.fetchComments(postId, rootCursor, ccb); return; } - final Call request = graphQLService.fetchComments(shortCode, true, rootCursor); - enqueueRequest(request, true, shortCode, ccb); + graphQLService.fetchComments( + shortCode, + true, + rootCursor, + enqueueRequest(true, shortCode, ccb) + ); } public void fetchReplies() { @@ -190,54 +194,49 @@ public class CommentsViewerViewModel extends ViewModel { commentService.fetchChildComments(postId, commentId, repliesCursor, rcb); return; } - final Call request = graphQLService.fetchComments(commentId, false, repliesCursor); - enqueueRequest(request, false, commentId, rcb); + graphQLService.fetchComments(commentId, false, repliesCursor, enqueueRequest(false, commentId, rcb)); } - private void enqueueRequest(@NonNull final Call request, - final boolean root, - final String shortCodeOrCommentId, - final ServiceCallback callback) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = root ? new JSONObject(rawBody).getJSONObject("data") - .getJSONObject("shortcode_media") - .getJSONObject("edge_media_to_parent_comment") - : new JSONObject(rawBody).getJSONObject("data") - .getJSONObject("comment") - .getJSONObject("edge_threaded_comments"); - final int count = body.optInt("count"); - final JSONObject pageInfo = body.getJSONObject("page_info"); - final boolean hasNextPage = pageInfo.getBoolean("has_next_page"); - final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor"); - final JSONArray commentsJsonArray = body.getJSONArray("edges"); - final ImmutableList.Builder builder = ImmutableList.builder(); - for (int i = 0; i < commentsJsonArray.length(); i++) { - final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); - builder.add(commentModel); - } - callback.onSuccess(root ? - new CommentsFetchResponse(count, endCursor, builder.build()) : - new ChildCommentsFetchResponse(count, endCursor, builder.build())); - } catch (Exception e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } + private Continuation enqueueRequest(final boolean root, + final String shortCodeOrCommentId, + @SuppressWarnings("rawtypes") final ServiceCallback callback) { + return CoroutineUtilsKt.getContinuation((response, throwable) -> { + if (throwable != null) { + callback.onFailure(throwable); + return; } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); + if (response == null) { + Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); + //noinspection unchecked + callback.onSuccess(null); + return; } - }); + try { + final JSONObject body = root ? new JSONObject(response).getJSONObject("data") + .getJSONObject("shortcode_media") + .getJSONObject("edge_media_to_parent_comment") + : new JSONObject(response).getJSONObject("data") + .getJSONObject("comment") + .getJSONObject("edge_threaded_comments"); + final int count = body.optInt("count"); + final JSONObject pageInfo = body.getJSONObject("page_info"); + final boolean hasNextPage = pageInfo.getBoolean("has_next_page"); + final String endCursor = pageInfo.isNull("end_cursor") || !hasNextPage ? null : pageInfo.optString("end_cursor"); + final JSONArray commentsJsonArray = body.getJSONArray("edges"); + final ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < commentsJsonArray.length(); i++) { + final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); + builder.add(commentModel); + } + final Object result = root ? new CommentsFetchResponse(count, endCursor, builder.build()) + : new ChildCommentsFetchResponse(count, endCursor, builder.build()); + //noinspection unchecked + callback.onSuccess(result); + } catch (Exception e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + }, Dispatchers.getIO()); } @NonNull diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt index 70b3c361..eea45149 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.kt @@ -135,7 +135,7 @@ class DirectSettingsViewModel( if (isAdmin) ACTION_REMOVE_ADMIN else ACTION_MAKE_ADMIN )) } - val blocking: Boolean = user.friendshipStatus.blocking + val blocking: Boolean = user.friendshipStatus?.blocking ?: false options.add(Option( if (blocking) getString(R.string.unblock) else getString(R.string.block), if (blocking) ACTION_UNBLOCK else ACTION_BLOCK @@ -144,7 +144,7 @@ class DirectSettingsViewModel( // options.add(new Option<>(getString(R.string.report), ACTION_REPORT)); val isGroup: Boolean? = threadManager.isGroup.value if (isGroup != null && isGroup) { - val restricted: Boolean = user.friendshipStatus.isRestricted + val restricted: Boolean = user.friendshipStatus?.isRestricted ?: false options.add(Option( if (restricted) getString(R.string.unrestrict) else getString(R.string.restrict), if (restricted) ACTION_UNRESTRICT else ACTION_RESTRICT diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.kt index 8a2feb3a..bdf2381f 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.kt @@ -1,13 +1,18 @@ package awais.instagrabber.viewmodels import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import awais.instagrabber.db.datasources.FavoriteDataSource import awais.instagrabber.db.entities.Favorite import awais.instagrabber.db.repositories.FavoriteRepository -import awais.instagrabber.db.repositories.RepositoryCallback +import awais.instagrabber.utils.extensions.TAG +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class FavoritesViewModel(application: Application) : AndroidViewModel(application) { private val _list = MutableLiveData>() @@ -20,29 +25,24 @@ class FavoritesViewModel(application: Application) : AndroidViewModel(applicatio } fun fetch() { - favoriteRepository.getAllFavorites(object : RepositoryCallback> { - override fun onSuccess(favorites: List?) { - _list.postValue(favorites ?: emptyList()) + viewModelScope.launch(Dispatchers.IO) { + try { + _list.postValue(favoriteRepository.getAllFavorites()) + } catch (e: Exception) { + Log.e(TAG, "fetch: ", e) } - - override fun onDataNotAvailable() {} - }) + } } fun delete(favorite: Favorite, onSuccess: () -> Unit) { - favoriteRepository.deleteFavorite(favorite.query, favorite.type, object : RepositoryCallback { - override fun onSuccess(result: Void?) { - onSuccess() - favoriteRepository.getAllFavorites(object : RepositoryCallback> { - override fun onSuccess(result: List?) { - _list.postValue(result ?: emptyList()) - } - - override fun onDataNotAvailable() {} - }) + viewModelScope.launch(Dispatchers.IO) { + try { + favoriteRepository.deleteFavorite(favorite.query, favorite.type) + withContext(Dispatchers.Main) { onSuccess() } + _list.postValue(favoriteRepository.getAllFavorites()) + } catch (e: Exception) { + Log.e(TAG, "delete: ", e) } - - override fun onDataNotAvailable() {} - }) + } } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt index f70aac34..4261a64f 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt @@ -23,11 +23,9 @@ import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.getCsrfTokenFromCookie import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.webservices.MediaService -import awais.instagrabber.webservices.ServiceCallback import com.google.common.collect.ImmutableList -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.* class PostViewV2ViewModel : ViewModel() { @@ -42,12 +40,15 @@ class PostViewV2ViewModel : ViewModel() { private val liked = MutableLiveData(false) private val saved = MutableLiveData(false) private val options = MutableLiveData>(ArrayList()) - private val viewerId: Long - val isLoggedIn: Boolean + private var messageManager: DirectMessagesManager? = null + private val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + private val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) + private val csrfToken = getCsrfTokenFromCookie(cookie) + private val viewerId = getUserIdFromCookie(cookie) + lateinit var media: Media private set - private var mediaService: MediaService? = null - private var messageManager: DirectMessagesManager? = null + val isLoggedIn = cookie.isNotBlank() && !csrfToken.isNullOrBlank() && viewerId != 0L fun setMedia(media: Media) { this.media = media @@ -127,44 +128,59 @@ class PostViewV2ViewModel : ViewModel() { fun like(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.like(media.pk, getLikeUnlikeCallback(data)) + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val liked = MediaService.like(csrfToken!!, viewerId, deviceUuid, mediaId) + updateMediaLikeUnlike(data, liked) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } fun unlike(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.unlike(media.pk, getLikeUnlikeCallback(data)) + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val unliked = MediaService.unlike(csrfToken!!, viewerId, deviceUuid, mediaId) + updateMediaLikeUnlike(data, unliked) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } - private fun getLikeUnlikeCallback(data: MutableLiveData>): ServiceCallback { - return object : ServiceCallback { - override fun onSuccess(result: Boolean?) { - if (result != null && !result) { - data.postValue(error("", null)) - return - } - data.postValue(success(true)) - val currentLikesCount = media.likeCount - val updatedCount: Long - if (!media.hasLiked) { - updatedCount = currentLikesCount + 1 - media.hasLiked = true - } else { - updatedCount = currentLikesCount - 1 - media.hasLiked = false - } - media.likeCount = updatedCount - likeCount.postValue(updatedCount) - liked.postValue(media.hasLiked) - } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, null)) - Log.e(TAG, "Error during like/unlike", t) - } + private fun updateMediaLikeUnlike(data: MutableLiveData>, result: Boolean) { + if (!result) { + data.postValue(error("", null)) + return } + data.postValue(success(true)) + val currentLikesCount = media.likeCount + val updatedCount: Long + if (!media.hasLiked) { + updatedCount = currentLikesCount + 1 + media.hasLiked = true + } else { + updatedCount = currentLikesCount - 1 + media.hasLiked = false + } + media.likeCount = updatedCount + likeCount.postValue(updatedCount) + liked.postValue(media.hasLiked) } fun toggleSave(): LiveData> { @@ -180,79 +196,99 @@ class PostViewV2ViewModel : ViewModel() { fun save(collection: String?, ignoreSaveState: Boolean): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.save(media.pk, collection, getSaveUnsaveCallback(data, ignoreSaveState)) + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + try { + val mediaId = media.pk ?: return@launch + val saved = MediaService.save(csrfToken!!, viewerId, deviceUuid, mediaId, collection) + getSaveUnsaveCallback(data, saved, ignoreSaveState) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } return data } fun unsave(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.unsave(media.pk, getSaveUnsaveCallback(data, false)) + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + val mediaId = media.pk ?: return@launch + val unsaved = MediaService.unsave(csrfToken!!, viewerId, deviceUuid, mediaId) + getSaveUnsaveCallback(data, unsaved, false) + } return data } private fun getSaveUnsaveCallback( data: MutableLiveData>, + result: Boolean, ignoreSaveState: Boolean, - ): ServiceCallback { - return object : ServiceCallback { - override fun onSuccess(result: Boolean?) { - if (result != null && !result) { - data.postValue(error("", null)) - return - } - data.postValue(success(true)) - if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved - saved.postValue(media.hasViewerSaved) - } - - override fun onFailure(t: Throwable) { - data.postValue(error(t.message, null)) - Log.e(TAG, "Error during save/unsave", t) - } + ) { + if (!result) { + data.postValue(error("", null)) + return } + data.postValue(success(true)) + if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved + saved.postValue(media.hasViewerSaved) } fun updateCaption(caption: String): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - mediaService?.editCaption(media.pk, caption, object : ServiceCallback { - override fun onSuccess(result: Boolean?) { - if (result != null && result) { + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } + viewModelScope.launch(Dispatchers.IO) { + try { + val postId = media.pk ?: return@launch + val result = MediaService.editCaption(csrfToken!!, viewerId, deviceUuid, postId, caption) + if (result) { data.postValue(success("")) media.setPostCaption(caption) this@PostViewV2ViewModel.caption.postValue(media.caption) - return + return@launch } data.postValue(error("", null)) + } catch (e: Exception) { + Log.e(TAG, "Error editing caption", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(t: Throwable) { - Log.e(TAG, "Error editing caption", t) - data.postValue(error(t.message, null)) - } - }) + } return data } fun translateCaption(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) - val value = caption.value ?: return data - mediaService?.translate(value.pk, "1", object : ServiceCallback { - override fun onSuccess(result: String?) { - if (result.isNullOrBlank()) { + 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.isBlank()) { 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 } @@ -267,36 +303,29 @@ class PostViewV2ViewModel : ViewModel() { fun delete(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + if (!isLoggedIn) { + data.postValue(error("Not logged in!", null)) + return data + } val mediaId = media.id val mediaType = media.mediaType if (mediaId == null || mediaType == null) { data.postValue(error("media id or type is null", null)) return data } - val request = mediaService?.delete(mediaId, mediaType) - if (request == null) { - data.postValue(success(Any())) - return data - } - request.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - data.postValue(error(R.string.generic_null_response, null)) - return - } - val body = response.body() - if (body == null) { - data.postValue(error(R.string.generic_null_response, null)) - return + viewModelScope.launch(Dispatchers.IO) { + try { + val response = MediaService.delete(csrfToken!!, viewerId, deviceUuid, mediaId, mediaType) + if (response == null) { + data.postValue(success(Any())) + return@launch } data.postValue(success(Any())) + } catch (e: Exception) { + Log.e(TAG, "delete: ", e) + data.postValue(error(e.message, null)) } - - override fun onFailure(call: Call, t: Throwable) { - Log.e(TAG, "onFailure: ", t) - data.postValue(error(t.message, null)) - } - }) + } return data } @@ -315,15 +344,4 @@ class PostViewV2ViewModel : ViewModel() { val mediaId = media.id ?: return messageManager?.sendMedia(recipients, mediaId, viewModelScope) } - - init { - val cookie = Utils.settingsHelper.getString(Constants.COOKIE) - val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) - val csrfToken: String? = getCsrfTokenFromCookie(cookie) - viewerId = getUserIdFromCookie(cookie) - isLoggedIn = cookie.isNotBlank() && viewerId != 0L - if (!csrfToken.isNullOrBlank()) { - mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId) - } - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt new file mode 100644 index 00000000..47f33264 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt @@ -0,0 +1,21 @@ +package awais.instagrabber.viewmodels + +import androidx.lifecycle.* +import awais.instagrabber.repositories.responses.User + +class ProfileFragmentViewModel( + state: SavedStateHandle, +) : ViewModel() { + private val _profile = MutableLiveData() + val profile: LiveData = _profile + val username: LiveData = Transformations.map(profile) { return@map it?.username ?: "" } + + var currentUser: User? = null + var isLoggedIn = false + get() = currentUser != null + private set + + init { + // Log.d(TAG, state.keys().toString()) + } +} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java index 14c7964f..18811a7d 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java @@ -33,9 +33,11 @@ import awais.instagrabber.repositories.responses.search.SearchResponse; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.Debouncer; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.webservices.SearchService; +import kotlinx.coroutines.Dispatchers; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -191,17 +193,17 @@ public class SearchFragmentViewModel extends AppStateViewModel { recentResultsFuture.set(Collections.emptyList()); } }); - favoriteRepository.getAllFavorites(new RepositoryCallback>() { - @Override - public void onSuccess(final List result) { - favoritesFuture.set(result); - } - - @Override - public void onDataNotAvailable() { - favoritesFuture.set(Collections.emptyList()); - } - }); + favoriteRepository.getAllFavorites( + CoroutineUtilsKt.getContinuation((favorites, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + favoritesFuture.set(Collections.emptyList()); + Log.e(TAG, "showRecentSearchesAndFavorites: ", throwable); + return; + } + //noinspection unchecked + favoritesFuture.set((List) favorites); + }), Dispatchers.getIO()) + ); //noinspection UnstableApiUsage final ListenableFuture>> listenableFuture = Futures.allAsList(recentResultsFuture, favoritesFuture); Futures.addCallback(listenableFuture, new FutureCallback>>() { diff --git a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java index 51ab0e9b..ea012d27 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java @@ -24,7 +24,6 @@ import awais.instagrabber.R; import awais.instagrabber.fragments.UserSearchFragment; import awais.instagrabber.models.Resource; import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.UserSearchResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -37,7 +36,6 @@ import awais.instagrabber.webservices.UserService; import kotlinx.coroutines.Dispatchers; import okhttp3.ResponseBody; import retrofit2.Call; -import retrofit2.Callback; import retrofit2.Response; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -72,8 +70,8 @@ public class UserSearchViewModel extends ViewModel { if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { throw new IllegalArgumentException("User is not logged in!"); } - userService = UserService.getInstance(); - directMessagesService = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); + userService = UserService.INSTANCE; + directMessagesService = DirectMessagesService.INSTANCE; rankedRecipientsCache = RankedRecipientsCache.INSTANCE; if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) { updateRankedRecipientCache(); @@ -170,9 +168,26 @@ public class UserSearchViewModel extends ViewModel { } private void defaultUserSearch() { - searchRequest = userService.search(currentQuery); - //noinspection unchecked - handleRequest((Call) searchRequest); + userService.search(currentQuery, CoroutineUtilsKt.getContinuation((userSearchResponse, throwable) -> { + if (throwable != null) { + Log.e(TAG, "onFailure: ", throwable); + recipients.postValue(Resource.error(throwable.getMessage(), getCachedRecipients())); + searchRequest = null; + return; + } + if (userSearchResponse == null) { + recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients())); + searchRequest = null; + return; + } + final List list = userSearchResponse + .getUsers() + .stream() + .map(RankedRecipient::of) + .collect(Collectors.toList()); + recipients.postValue(Resource.success(mergeResponseWithCache(list))); + searchRequest = null; + })); } private void rankedRecipientSearch() { @@ -194,39 +209,6 @@ public class UserSearchViewModel extends ViewModel { ); } - private void handleRequest(@NonNull final Call request) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorResponse(response, true); - searchRequest = null; - return; - } - final UserSearchResponse userSearchResponse = response.body(); - if (userSearchResponse == null) { - recipients.postValue(Resource.error(R.string.generic_null_response, getCachedRecipients())); - searchRequest = null; - return; - } - final List list = userSearchResponse - .getUsers() - .stream() - .map(RankedRecipient::of) - .collect(Collectors.toList()); - recipients.postValue(Resource.success(mergeResponseWithCache(list))); - searchRequest = null; - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients())); - searchRequest = null; - } - }); - } - private List mergeResponseWithCache(@NonNull final List list) { final Iterator iterator = list.stream() .filter(Objects::nonNull) diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt index 8005ad10..ae45ca61 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.kt @@ -5,16 +5,11 @@ import awais.instagrabber.repositories.requests.directmessages.* import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.utils.TextUtils.extractUrls -import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.Utils import org.json.JSONArray import java.util.* -class DirectMessagesService private constructor( - val csrfToken: String, - val userId: Long, - val deviceUuid: String, -) : BaseService() { +object DirectMessagesService : BaseService() { private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java) suspend fun fetchInbox( @@ -55,6 +50,9 @@ class DirectMessagesService private constructor( suspend fun fetchUnseenCount(): DirectBadgeCount = repository.fetchUnseenCount() suspend fun broadcastText( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, text: String, @@ -63,17 +61,20 @@ class DirectMessagesService private constructor( ): DirectThreadBroadcastResponse { val urls = extractUrls(text) if (urls.isNotEmpty()) { - return broadcastLink(clientContext, threadIdOrUserIds, text, urls, repliedToItemId, repliedToClientContext) + return broadcastLink(csrfToken, userId, deviceUuid, clientContext, threadIdOrUserIds, text, urls, repliedToItemId, repliedToClientContext) } val broadcastOptions = TextBroadcastOptions(clientContext, threadIdOrUserIds, text) if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { broadcastOptions.repliedToItemId = repliedToItemId broadcastOptions.repliedToClientContext = repliedToClientContext } - return broadcast(broadcastOptions) + return broadcast(csrfToken, userId, deviceUuid, broadcastOptions) } private suspend fun broadcastLink( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, linkText: String, @@ -86,75 +87,100 @@ class DirectMessagesService private constructor( broadcastOptions.repliedToItemId = repliedToItemId broadcastOptions.repliedToClientContext = repliedToClientContext } - return broadcast(broadcastOptions) + return broadcast(csrfToken, userId, deviceUuid, broadcastOptions) } suspend fun broadcastPhoto( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, uploadId: String, - ): DirectThreadBroadcastResponse { - return broadcast(PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, PhotoBroadcastOptions(clientContext, threadIdOrUserIds, true, uploadId)) suspend fun broadcastVideo( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, uploadId: String, videoResult: String, sampled: Boolean, - ): DirectThreadBroadcastResponse { - return broadcast(VideoBroadcastOptions(clientContext, threadIdOrUserIds, videoResult, uploadId, sampled)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, VideoBroadcastOptions(clientContext, threadIdOrUserIds, videoResult, uploadId, sampled)) suspend fun broadcastVoice( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, uploadId: String, waveform: List, samplingFreq: Int, - ): DirectThreadBroadcastResponse { - return broadcast(VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq)) suspend fun broadcastStoryReply( + csrfToken: String, + userId: Long, + deviceUuid: String, threadIdOrUserIds: ThreadIdOrUserIds, text: String, mediaId: String, reelId: String, - ): DirectThreadBroadcastResponse { - return broadcast(StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)) suspend fun broadcastReaction( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, itemId: String, emoji: String?, delete: Boolean, - ): DirectThreadBroadcastResponse { - return broadcast(ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)) suspend fun broadcastAnimatedMedia( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, giphyGif: GiphyGif, - ): DirectThreadBroadcastResponse { - return broadcast(AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)) suspend fun broadcastMediaShare( + csrfToken: String, + userId: Long, + deviceUuid: String, clientContext: String, threadIdOrUserIds: ThreadIdOrUserIds, mediaId: String, - ): DirectThreadBroadcastResponse { - return broadcast(MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId)) - } + ): DirectThreadBroadcastResponse = + broadcast(csrfToken, userId, deviceUuid, MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId)) - private suspend fun broadcast(broadcastOptions: BroadcastOptions): DirectThreadBroadcastResponse { - require(!isEmpty(broadcastOptions.clientContext)) { "Broadcast requires a valid client context value" } - val form = mutableMapOf() + private suspend fun broadcast( + csrfToken: String, + userId: Long, + deviceUuid: String, + broadcastOptions: BroadcastOptions, + ): DirectThreadBroadcastResponse { + require(broadcastOptions.clientContext.isNotBlank()) { "Broadcast requires a valid client context value" } + val form = mutableMapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "__uuid" to deviceUuid, + "client_context" to broadcastOptions.clientContext, + "mutation_token" to broadcastOptions.clientContext, + ) val threadId = broadcastOptions.threadId if (!threadId.isNullOrBlank()) { form["thread_id"] = threadId @@ -165,11 +191,6 @@ class DirectMessagesService private constructor( } form["recipient_users"] = JSONArray(userIds).toString() } - form["_csrftoken"] = csrfToken - form["_uid"] = userId - form["__uuid"] = deviceUuid - form["client_context"] = broadcastOptions.clientContext - form["mutation_token"] = broadcastOptions.clientContext val repliedToItemId = broadcastOptions.repliedToItemId val repliedToClientContext = broadcastOptions.repliedToClientContext if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { @@ -183,6 +204,8 @@ class DirectMessagesService private constructor( } suspend fun addUsers( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: Collection, ): DirectThreadDetailsChangeResponse { @@ -195,6 +218,8 @@ class DirectMessagesService private constructor( } suspend fun removeUsers( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: Collection, ): String { @@ -207,6 +232,8 @@ class DirectMessagesService private constructor( } suspend fun updateTitle( + csrfToken: String, + deviceUuid: String, threadId: String, title: String, ): DirectThreadDetailsChangeResponse { @@ -219,6 +246,8 @@ class DirectMessagesService private constructor( } suspend fun addAdmins( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: Collection, ): String { @@ -231,6 +260,8 @@ class DirectMessagesService private constructor( } suspend fun removeAdmins( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: Collection, ): String { @@ -243,6 +274,8 @@ class DirectMessagesService private constructor( } suspend fun deleteItem( + csrfToken: String, + deviceUuid: String, threadId: String, itemId: String, ): String { @@ -292,6 +325,9 @@ class DirectMessagesService private constructor( } suspend fun createThread( + csrfToken: String, + userId: Long, + deviceUuid: String, userIds: List, threadTitle: String?, ): DirectThread { @@ -309,7 +345,11 @@ class DirectMessagesService private constructor( return repository.createThread(signedForm) } - suspend fun mute(threadId: String): String { + suspend fun mute( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid @@ -317,7 +357,11 @@ class DirectMessagesService private constructor( return repository.mute(threadId, form) } - suspend fun unmute(threadId: String): String { + suspend fun unmute( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -325,7 +369,11 @@ class DirectMessagesService private constructor( return repository.unmute(threadId, form) } - suspend fun muteMentions(threadId: String): String { + suspend fun muteMentions( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -333,7 +381,11 @@ class DirectMessagesService private constructor( return repository.muteMentions(threadId, form) } - suspend fun unmuteMentions(threadId: String): String { + suspend fun unmuteMentions( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -350,6 +402,8 @@ class DirectMessagesService private constructor( } suspend fun approveParticipantRequests( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: List, ): DirectThreadDetailsChangeResponse { @@ -363,6 +417,8 @@ class DirectMessagesService private constructor( } suspend fun declineParticipantRequests( + csrfToken: String, + deviceUuid: String, threadId: String, userIds: List, ): DirectThreadDetailsChangeResponse { @@ -374,7 +430,11 @@ class DirectMessagesService private constructor( return repository.declineParticipantRequests(threadId, form) } - suspend fun approvalRequired(threadId: String): DirectThreadDetailsChangeResponse { + suspend fun approvalRequired( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): DirectThreadDetailsChangeResponse { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -382,7 +442,11 @@ class DirectMessagesService private constructor( return repository.approvalRequired(threadId, form) } - suspend fun approvalNotRequired(threadId: String): DirectThreadDetailsChangeResponse { + suspend fun approvalNotRequired( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): DirectThreadDetailsChangeResponse { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -390,7 +454,11 @@ class DirectMessagesService private constructor( return repository.approvalNotRequired(threadId, form) } - suspend fun leave(threadId: String): DirectThreadDetailsChangeResponse { + suspend fun leave( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): DirectThreadDetailsChangeResponse { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -398,7 +466,11 @@ class DirectMessagesService private constructor( return repository.leave(threadId, form) } - suspend fun end(threadId: String): DirectThreadDetailsChangeResponse { + suspend fun end( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): DirectThreadDetailsChangeResponse { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -423,7 +495,11 @@ class DirectMessagesService private constructor( return repository.fetchPendingInbox(queryMap) } - suspend fun approveRequest(threadId: String): String { + suspend fun approveRequest( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -431,7 +507,11 @@ class DirectMessagesService private constructor( return repository.approveRequest(threadId, form) } - suspend fun declineRequest(threadId: String): String { + suspend fun declineRequest( + csrfToken: String, + deviceUuid: String, + threadId: String, + ): String { val form = mapOf( "_csrftoken" to csrfToken, "_uuid" to deviceUuid, @@ -440,6 +520,8 @@ class DirectMessagesService private constructor( } suspend fun markAsSeen( + csrfToken: String, + deviceUuid: String, threadId: String, directItem: DirectItem, ): DirectItemSeenResponse? { @@ -454,25 +536,4 @@ class DirectMessagesService private constructor( ) return repository.markItemSeen(threadId, itemId, form) } - - companion object { - private lateinit var instance: DirectMessagesService - - @JvmStatic - fun getInstance( - csrfToken: String, - userId: Long, - deviceUuid: String, - ): DirectMessagesService { - if (!this::instance.isInitialized - || instance.csrfToken != csrfToken - || instance.userId != userId - || instance.deviceUuid != deviceUuid - ) { - instance = DirectMessagesService(csrfToken, userId, deviceUuid) - } - return instance - } - } - } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java b/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java deleted file mode 100644 index ce20dd29..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java +++ /dev/null @@ -1,264 +0,0 @@ -package awais.instagrabber.webservices; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import awais.instagrabber.models.FollowModel; -import awais.instagrabber.repositories.FriendshipRepository; -import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.FriendshipListFetchResponse; -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class FriendshipService extends BaseService { - private static final String TAG = "FriendshipService"; - - private final FriendshipRepository repository; - private final String deviceUuid, csrfToken; - private final long userId; - - private static FriendshipService instance; - - private FriendshipService(final String deviceUuid, - final String csrfToken, - final long userId) { - this.deviceUuid = deviceUuid; - this.csrfToken = csrfToken; - this.userId = userId; - repository = RetrofitFactory.INSTANCE - .getRetrofit() - .create(FriendshipRepository.class); - } - - public String getCsrfToken() { - return csrfToken; - } - - public String getDeviceUuid() { - return deviceUuid; - } - - public long getUserId() { - return userId; - } - - public static FriendshipService 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 FriendshipService(deviceUuid, csrfToken, userId); - } - return instance; - } - - public void follow(final long targetUserId, - final ServiceCallback callback) { - change("create", targetUserId, callback); - } - - public void unfollow(final long targetUserId, - final ServiceCallback callback) { - change("destroy", targetUserId, callback); - } - - public void changeBlock(final boolean unblock, - final long targetUserId, - final ServiceCallback callback) { - change(unblock ? "unblock" : "block", targetUserId, callback); - } - - public void toggleRestrict(final long targetUserId, - final boolean restrict, - final ServiceCallback callback) { - final Map form = new HashMap<>(3); - form.put("_csrftoken", csrfToken); - form.put("_uuid", deviceUuid); - form.put("target_user_id", String.valueOf(targetUserId)); - final String action = restrict ? "restrict" : "unrestrict"; - final Call request = repository.toggleRestrict(action, form); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback != null) { - callback.onSuccess(response.body()); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void approve(final long targetUserId, - final ServiceCallback callback) { - change("approve", targetUserId, callback); - } - - public void ignore(final long targetUserId, - final ServiceCallback callback) { - change("ignore", targetUserId, callback); - } - - public void removeFollower(final long targetUserId, - final ServiceCallback callback) { - change("remove_follower", targetUserId, callback); - } - - private void change(final String action, - final long targetUserId, - final ServiceCallback callback) { - final Map form = new HashMap<>(5); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("radio_type", "wifi-none"); - form.put("user_id", targetUserId); - final Map signedForm = Utils.sign(form); - final Call request = repository.change(action, targetUserId, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback != null) { - callback.onSuccess(response.body()); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void changeMute(final boolean unmute, - final long targetUserId, - final boolean story, // true for story, false for posts - final ServiceCallback callback) { - final Map form = new HashMap<>(4); - form.put("_csrftoken", csrfToken); - form.put("_uid", String.valueOf(userId)); - form.put("_uuid", deviceUuid); - form.put(story ? "target_reel_author_id" : "target_posts_author_id", String.valueOf(targetUserId)); - final Call request = repository.changeMute(unmute ? - "unmute_posts_or_story_from_follow" : - "mute_posts_or_story_from_follow", - form); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback != null) { - callback.onSuccess(response.body()); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void getList(final boolean follower, - final long targetUserId, - final String maxId, - final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - if (maxId != null) queryMap.put("max_id", maxId); - final Call request = repository.getList( - targetUserId, - follower ? "followers" : "following", - queryMap); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - if (callback == null) { - return; - } - final String body = response.body(); - if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); - return; - } - final FriendshipListFetchResponse friendshipListFetchResponse = parseListResponse(body); - callback.onSuccess(friendshipListFetchResponse); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - private FriendshipListFetchResponse parseListResponse(@NonNull final String body) throws JSONException { - final JSONObject root = new JSONObject(body); - final String nextMaxId = root.optString("next_max_id"); - final String status = root.optString("status"); - final JSONArray itemsJson = root.optJSONArray("users"); - final List items = parseItems(itemsJson); - return new FriendshipListFetchResponse( - nextMaxId, - status, - items - ); - } - - private List parseItems(final JSONArray items) throws JSONException { - if (items == null) { - return Collections.emptyList(); - } - final List followModels = new ArrayList<>(); - for (int i = 0; i < items.length(); i++) { - final JSONObject itemJson = items.optJSONObject(i); - if (itemJson == null) { - continue; - } - final FollowModel followModel = new FollowModel(itemJson.getString("pk"), - itemJson.getString("username"), - itemJson.optString("full_name"), - itemJson.getString("profile_pic_url")); - if (followModel != null) { - followModels.add(followModel); - } - } - return followModels; - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt b/app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt new file mode 100644 index 00000000..4982cd15 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/FriendshipService.kt @@ -0,0 +1,155 @@ +package awais.instagrabber.webservices + +import awais.instagrabber.models.FollowModel +import awais.instagrabber.repositories.FriendshipRepository +import awais.instagrabber.repositories.responses.FriendshipChangeResponse +import awais.instagrabber.repositories.responses.FriendshipListFetchResponse +import awais.instagrabber.repositories.responses.FriendshipRestrictResponse +import awais.instagrabber.utils.Utils +import awais.instagrabber.webservices.RetrofitFactory.retrofit +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +object FriendshipService : BaseService() { + private val repository: FriendshipRepository = retrofit.create(FriendshipRepository::class.java) + + suspend fun follow( + csrfToken: String, + userId: Long, + deviceUuid: String, + targetUserId: Long, + ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "create", targetUserId) + + suspend fun unfollow( + csrfToken: String, + userId: Long, + deviceUuid: String, + targetUserId: Long, + ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "destroy", targetUserId) + + suspend fun changeBlock( + csrfToken: String, + userId: Long, + deviceUuid: String, + unblock: Boolean, + targetUserId: Long, + ): FriendshipChangeResponse { + return change(csrfToken, userId, deviceUuid, if (unblock) "unblock" else "block", targetUserId) + } + + suspend fun toggleRestrict( + csrfToken: String, + deviceUuid: String, + targetUserId: Long, + restrict: Boolean, + ): FriendshipRestrictResponse { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uuid" to deviceUuid, + "target_user_id" to targetUserId.toString(), + ) + val action = if (restrict) "restrict" else "unrestrict" + return repository.toggleRestrict(action, form) + } + + suspend fun approve( + csrfToken: String, + userId: Long, + deviceUuid: String, + targetUserId: Long, + ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "approve", targetUserId) + + suspend fun ignore( + csrfToken: String, + userId: Long, + deviceUuid: String, + targetUserId: Long, + ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "ignore", targetUserId) + + suspend fun removeFollower( + csrfToken: String, + userId: Long, + deviceUuid: String, + targetUserId: Long, + ): FriendshipChangeResponse = change(csrfToken, userId, deviceUuid, "remove_follower", targetUserId) + + private suspend fun change( + csrfToken: String, + userId: Long, + deviceUuid: String, + action: String, + targetUserId: Long, + ): FriendshipChangeResponse { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "radio_type" to "wifi-none", + "user_id" to targetUserId, + ) + val signedForm = Utils.sign(form) + return repository.change(action, targetUserId, signedForm) + } + + suspend fun changeMute( + csrfToken: String, + userId: Long, + deviceUuid: String, + unmute: Boolean, + targetUserId: Long, + story: Boolean, // true for story, false for posts + ): FriendshipChangeResponse { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId.toString(), + "_uuid" to deviceUuid, + (if (story) "target_reel_author_id" else "target_posts_author_id") to targetUserId.toString(), + ) + return repository.changeMute( + if (unmute) "unmute_posts_or_story_from_follow" else "mute_posts_or_story_from_follow", + form + ) + } + + suspend fun getList( + follower: Boolean, + targetUserId: Long, + maxId: String?, + ): FriendshipListFetchResponse { + val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap() + val response = repository.getList(targetUserId, if (follower) "followers" else "following", queryMap) + return parseListResponse(response) + } + + @Throws(JSONException::class) + private fun parseListResponse(body: String): FriendshipListFetchResponse { + val root = JSONObject(body) + val nextMaxId = root.optString("next_max_id") + val status = root.optString("status") + val itemsJson = root.optJSONArray("users") + val items = parseItems(itemsJson) + return FriendshipListFetchResponse( + nextMaxId, + status, + items + ) + } + + @Throws(JSONException::class) + private fun parseItems(items: JSONArray?): List { + if (items == null) { + return emptyList() + } + val followModels = mutableListOf() + for (i in 0 until items.length()) { + val itemJson = items.optJSONObject(i) ?: continue + val followModel = FollowModel(itemJson.getString("pk"), + itemJson.getString("username"), + itemJson.optString("full_name"), + itemJson.getString("profile_pic_url")) + followModels.add(followModel) + } + return followModels + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java deleted file mode 100644 index 1c501e7b..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ /dev/null @@ -1,483 +0,0 @@ -package awais.instagrabber.webservices; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.google.common.collect.ImmutableMap; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import awais.instagrabber.models.enums.FollowingType; -import awais.instagrabber.repositories.GraphQLRepository; -import awais.instagrabber.repositories.responses.FriendshipStatus; -import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; -import awais.instagrabber.repositories.responses.Hashtag; -import awais.instagrabber.repositories.responses.Location; -import awais.instagrabber.repositories.responses.Media; -import awais.instagrabber.repositories.responses.PostsFetchResponse; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.ResponseBodyUtils; -import awais.instagrabber.utils.TextUtils; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class GraphQLService extends BaseService { - private static final String TAG = "GraphQLService"; - - private final GraphQLRepository repository; - - private static GraphQLService instance; - - private GraphQLService() { - repository = RetrofitFactory.INSTANCE - .getRetrofitWeb() - .create(GraphQLRepository.class); - } - - public static GraphQLService getInstance() { - if (instance == null) { - instance = new GraphQLService(); - } - return instance; - } - - // TODO convert string response to a response class - private void fetch(final String queryHash, - final String variables, - final String arg1, - final String arg2, - final User backup, - final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", queryHash); - queryMap.put("variables", variables); - final Call request = repository.fetch(queryMap); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - // Log.d(TAG, "onResponse: body: " + response.body()); - final PostsFetchResponse postsFetchResponse = parsePostResponse(response, arg1, arg2, backup); - if (callback != null) { - callback.onSuccess(postsFetchResponse); - } - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void fetchLocationPosts(final long locationId, - final String maxId, - final ServiceCallback callback) { - fetch("36bd0f2bf5911908de389b8ceaa3be6d", - "{\"id\":\"" + locationId + "\"," + - "\"first\":25," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", - Constants.EXTRAS_LOCATION, - "edge_location_to_media", - null, - callback); - } - - public void fetchHashtagPosts(@NonNull final String tag, - final String maxId, - final ServiceCallback callback) { - fetch("9b498c08113f1e09617a1703c22b2f32", - "{\"tag_name\":\"" + tag + "\"," + - "\"first\":25," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", - Constants.EXTRAS_HASHTAG, - "edge_hashtag_to_media", - null, - callback); - } - - public void fetchProfilePosts(final long profileId, - final int postsPerPage, - final String maxId, - final User backup, - final ServiceCallback callback) { - fetch("02e14f6a7812a876f7d133c9555b1151", - "{\"id\":\"" + profileId + "\"," + - "\"first\":" + postsPerPage + "," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", - Constants.EXTRAS_USER, - "edge_owner_to_timeline_media", - backup, - callback); - } - - public void fetchTaggedPosts(final long profileId, - final int postsPerPage, - final String maxId, - final ServiceCallback callback) { - fetch("31fe64d9463cbbe58319dced405c6206", - "{\"id\":\"" + profileId + "\"," + - "\"first\":" + postsPerPage + "," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", - Constants.EXTRAS_USER, - "edge_user_to_photos_of_you", - null, - callback); - } - - @NonNull - private PostsFetchResponse parsePostResponse(@NonNull final Response response, - @NonNull final String arg1, - @NonNull final String arg2, - final User backup) - throws JSONException { - if (TextUtils.isEmpty(response.body())) { - Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); - return new PostsFetchResponse(Collections.emptyList(), false, null); - } - return parseResponseBody(response.body(), arg1, arg2, backup); - } - - @NonNull - private PostsFetchResponse parseResponseBody(@NonNull final String body, - @NonNull final String arg1, - @NonNull final String arg2, - final User backup) - throws JSONException { - final List items = new ArrayList<>(); - final JSONObject timelineFeed = new JSONObject(body) - .getJSONObject("data") - .getJSONObject(arg1) - .getJSONObject(arg2); - final String endCursor; - final boolean hasNextPage; - - final JSONObject pageInfo = timelineFeed.getJSONObject("page_info"); - if (pageInfo.has("has_next_page")) { - hasNextPage = pageInfo.getBoolean("has_next_page"); - endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null; - } else { - hasNextPage = false; - endCursor = null; - } - - final JSONArray feedItems = timelineFeed.getJSONArray("edges"); - - for (int i = 0; i < feedItems.length(); ++i) { - final JSONObject itemJson = feedItems.optJSONObject(i); - if (itemJson == null) { - continue; - } - final Media media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup); - if (media != null) { - items.add(media); - } - } - return new PostsFetchResponse(items, hasNextPage, endCursor); - } - - // TODO convert string response to a response class - public void fetchCommentLikers(final String commentId, - final String endCursor, - final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae"); - queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," + - "\"first\":30," + - "\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}"); - final Call request = repository.fetch(queryMap); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql comment likes of " + commentId); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = new JSONObject(rawBody); - final String status = body.getString("status"); - final JSONObject data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by"); - final JSONObject pageInfo = data.getJSONObject("page_info"); - final String endCursor = pageInfo.getBoolean("has_next_page") ? pageInfo.getString("end_cursor") : null; - final JSONArray users = data.getJSONArray("edges"); - final int usersLen = users.length(); - final List userModels = new ArrayList<>(); - for (int j = 0; j < usersLen; ++j) { - final JSONObject userObject = users.getJSONObject(j).getJSONObject("node"); - userModels.add(new User( - userObject.getLong("id"), - userObject.getString("username"), - userObject.optString("full_name"), - userObject.optBoolean("is_private"), - userObject.getString("profile_pic_url"), - userObject.optBoolean("is_verified") - )); - // userModels.add(new ProfileModel(userObject.optBoolean("is_private"), - // false, - // userObject.optBoolean("is_verified"), - // userObject.getString("id"), - // userObject.getString("username"), - // userObject.optString("full_name"), - // null, null, - // userObject.getString("profile_pic_url"), - // null, 0, 0, 0, false, false, false, false, false)); - } - callback.onSuccess(new GraphQLUserListFetchResponse(endCursor, status, userModels)); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public Call fetchComments(final String shortCodeOrCommentId, - final boolean root, - final String cursor) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62"); - final Map variables = ImmutableMap.of( - root ? "shortcode" : "comment_id", shortCodeOrCommentId, - "first", 50, - "after", cursor == null ? "" : cursor - ); - queryMap.put("variables", new JSONObject(variables).toString()); - return repository.fetch(queryMap); - } - - // TODO convert string response to a response class - public void fetchUser(final String username, - final ServiceCallback callback) { - final Call request = repository.getUser(username); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql user of " + username); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = new JSONObject(rawBody); - final JSONObject userJson = body.getJSONObject("graphql") - .getJSONObject(Constants.EXTRAS_USER); - - boolean isPrivate = userJson.getBoolean("is_private"); - final long id = userJson.optLong(Constants.EXTRAS_ID, 0); - final JSONObject timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media"); - // if (timelineMedia.has("edges")) { - // final JSONArray edges = timelineMedia.getJSONArray("edges"); - // } - - String url = userJson.optString("external_url"); - if (TextUtils.isEmpty(url)) url = null; - - callback.onSuccess(new User( - id, - username, - userJson.getString("full_name"), - isPrivate, - userJson.getString("profile_pic_url_hd"), - null, - new FriendshipStatus( - userJson.optBoolean("followed_by_viewer"), - userJson.optBoolean("follows_viewer"), - userJson.optBoolean("blocked_by_viewer"), - false, - isPrivate, - userJson.optBoolean("has_requested_viewer"), - userJson.optBoolean("requested_by_viewer"), - false, - userJson.optBoolean("restricted_by_viewer"), - false - ), - userJson.getBoolean("is_verified"), - false, - false, - false, - false, - false, - null, - null, - timelineMedia.getLong("count"), - userJson.getJSONObject("edge_followed_by").getLong("count"), - userJson.getJSONObject("edge_follow").getLong("count"), - 0, - userJson.getString("biography"), - url, - 0, - null, - null, - null, - null, - null, - null)); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - // TODO convert string response to a response class - public void fetchPost(final String shortcode, - final ServiceCallback callback) { - final Call request = repository.getPost(shortcode); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql post of " + shortcode); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = new JSONObject(rawBody); - final JSONObject media = body.getJSONObject("graphql") - .getJSONObject("shortcode_media"); - callback.onSuccess(ResponseBodyUtils.parseGraphQLItem(media, null)); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - // TODO convert string response to a response class - public void fetchTag(final String tag, - final ServiceCallback callback) { - final Call request = repository.getTag(tag); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql tag of " + tag); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = new JSONObject(rawBody) - .getJSONObject("graphql") - .getJSONObject(Constants.EXTRAS_HASHTAG); - final JSONObject timelineMedia = body.getJSONObject("edge_hashtag_to_media"); - callback.onSuccess(new Hashtag( - body.getString(Constants.EXTRAS_ID), - body.getString("name"), - timelineMedia.getLong("count"), - body.optBoolean("is_following") ? FollowingType.FOLLOWING : FollowingType.NOT_FOLLOWING, - null)); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - // TODO convert string response to a response class - public void fetchLocation(final long locationId, - final ServiceCallback callback) { - final Call request = repository.getLocation(locationId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String rawBody = response.body(); - if (rawBody == null) { - Log.e(TAG, "Error occurred while fetching gql location of " + locationId); - callback.onSuccess(null); - return; - } - try { - final JSONObject body = new JSONObject(rawBody) - .getJSONObject("graphql") - .getJSONObject(Constants.EXTRAS_LOCATION); - final JSONObject timelineMedia = body.getJSONObject("edge_location_to_media"); - final JSONObject address = new JSONObject(body.getString("address_json")); - callback.onSuccess(new Location( - body.getLong(Constants.EXTRAS_ID), - body.getString("slug"), - body.getString("name"), - address.optString("street_address"), - address.optString("city_name"), - body.optDouble("lng", 0d), - body.optDouble("lat", 0d) - )); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt new file mode 100644 index 00000000..ac6e881e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.kt @@ -0,0 +1,266 @@ +package awais.instagrabber.webservices + +import android.util.Log +import awais.instagrabber.models.enums.FollowingType +import awais.instagrabber.repositories.GraphQLRepository +import awais.instagrabber.repositories.responses.* +import awais.instagrabber.utils.Constants +import awais.instagrabber.utils.ResponseBodyUtils +import awais.instagrabber.utils.extensions.TAG +import awais.instagrabber.webservices.RetrofitFactory.retrofitWeb +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +object GraphQLService : BaseService() { + private val repository: GraphQLRepository = retrofitWeb.create(GraphQLRepository::class.java) + + // TODO convert string response to a response class + private suspend fun fetch( + queryHash: String, + variables: String, + arg1: String, + arg2: String, + backup: User?, + ): PostsFetchResponse { + val queryMap = mapOf( + "query_hash" to queryHash, + "variables" to variables, + ) + val response = repository.fetch(queryMap) + return parsePostResponse(response, arg1, arg2, backup) + } + + suspend fun fetchLocationPosts( + locationId: Long, + maxId: String?, + ): PostsFetchResponse = fetch( + "36bd0f2bf5911908de389b8ceaa3be6d", + "{\"id\":\"" + locationId + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", + Constants.EXTRAS_LOCATION, + "edge_location_to_media", + null + ) + + suspend fun fetchHashtagPosts( + tag: String, + maxId: String?, + ): PostsFetchResponse = fetch( + "9b498c08113f1e09617a1703c22b2f32", + "{\"tag_name\":\"" + tag + "\"," + "\"first\":25," + "\"after\":\"" + (maxId ?: "") + "\"}", + Constants.EXTRAS_HASHTAG, + "edge_hashtag_to_media", + null, + ) + + suspend fun fetchProfilePosts( + profileId: Long, + postsPerPage: Int, + maxId: String?, + backup: User?, + ): PostsFetchResponse = fetch( + "02e14f6a7812a876f7d133c9555b1151", + "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", + Constants.EXTRAS_USER, + "edge_owner_to_timeline_media", + backup, + ) + + suspend fun fetchTaggedPosts( + profileId: Long, + postsPerPage: Int, + maxId: String?, + ): PostsFetchResponse = fetch( + "31fe64d9463cbbe58319dced405c6206", + "{\"id\":\"" + profileId + "\"," + "\"first\":" + postsPerPage + "," + "\"after\":\"" + (maxId ?: "") + "\"}", + Constants.EXTRAS_USER, + "edge_user_to_photos_of_you", + null, + ) + + @Throws(JSONException::class) + private fun parsePostResponse( + response: String, + arg1: String, + arg2: String, + backup: User?, + ): PostsFetchResponse { + if (response.isBlank()) { + Log.e(TAG, "parseResponse: feed response body is empty") + return PostsFetchResponse(emptyList(), false, null) + } + return parseResponseBody(response, arg1, arg2, backup) + } + + @Throws(JSONException::class) + private fun parseResponseBody( + body: String, + arg1: String, + arg2: String, + backup: User?, + ): PostsFetchResponse { + val items: MutableList = ArrayList() + val timelineFeed = JSONObject(body) + .getJSONObject("data") + .getJSONObject(arg1) + .getJSONObject(arg2) + val endCursor: String? + val hasNextPage: Boolean + val pageInfo = timelineFeed.getJSONObject("page_info") + if (pageInfo.has("has_next_page")) { + hasNextPage = pageInfo.getBoolean("has_next_page") + endCursor = if (hasNextPage) pageInfo.getString("end_cursor") else null + } else { + hasNextPage = false + endCursor = null + } + val feedItems = timelineFeed.getJSONArray("edges") + for (i in 0 until feedItems.length()) { + val itemJson = feedItems.optJSONObject(i) ?: continue + val media = ResponseBodyUtils.parseGraphQLItem(itemJson, backup) + if (media != null) { + items.add(media) + } + } + return PostsFetchResponse(items, hasNextPage, endCursor) + } + + // TODO convert string response to a response class + suspend fun fetchCommentLikers( + commentId: String, + endCursor: String?, + ): GraphQLUserListFetchResponse { + val queryMap = mapOf( + "query_hash" to "5f0b1f6281e72053cbc07909c8d154ae", + "variables" to "{\"comment_id\":\"" + commentId + "\"," + "\"first\":30," + "\"after\":\"" + (endCursor ?: "") + "\"}" + ) + val response = repository.fetch(queryMap) + val body = JSONObject(response) + val status = body.getString("status") + val data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by") + val pageInfo = data.getJSONObject("page_info") + val newEndCursor = if (pageInfo.getBoolean("has_next_page")) pageInfo.getString("end_cursor") else null + val users = data.getJSONArray("edges") + val usersLen = users.length() + val userModels: MutableList = ArrayList() + for (j in 0 until usersLen) { + val userObject = users.getJSONObject(j).getJSONObject("node") + userModels.add(User( + userObject.getLong("id"), + userObject.getString("username"), + userObject.optString("full_name"), + userObject.optBoolean("is_private"), + userObject.getString("profile_pic_url"), + userObject.optBoolean("is_verified") + )) + } + return GraphQLUserListFetchResponse(newEndCursor, status, userModels) + } + + suspend fun fetchComments( + shortCodeOrCommentId: String?, + root: Boolean, + cursor: String?, + ): String { + val variables = mapOf( + (if (root) "shortcode" else "comment_id") to shortCodeOrCommentId, + "first" to 50, + "after" to (cursor ?: "") + ) + val queryMap = mapOf( + "query_hash" to if (root) "bc3296d1ce80a24b1b6e40b1e72903f5" else "51fdd02b67508306ad4484ff574a0b62", + "variables" to JSONObject(variables).toString() + ) + return repository.fetch(queryMap) + } + + // TODO convert string response to a response class + suspend fun fetchUser( + username: String, + ): User { + val response = repository.getUser(username) + val body = JSONObject(response) + val userJson = body.getJSONObject("graphql").getJSONObject(Constants.EXTRAS_USER) + val isPrivate = userJson.getBoolean("is_private") + val id = userJson.optLong(Constants.EXTRAS_ID, 0) + val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media") + // if (timelineMedia.has("edges")) { + // final JSONArray edges = timelineMedia.getJSONArray("edges"); + // } + var url: String? = userJson.optString("external_url") + if (url.isNullOrBlank()) url = null + return User( + id, + username, + userJson.getString("full_name"), + isPrivate, + userJson.getString("profile_pic_url_hd"), + userJson.getBoolean("is_verified"), + friendshipStatus = FriendshipStatus( + userJson.optBoolean("followed_by_viewer"), + userJson.optBoolean("follows_viewer"), + userJson.optBoolean("blocked_by_viewer"), + false, + isPrivate, + userJson.optBoolean("has_requested_viewer"), + userJson.optBoolean("requested_by_viewer"), + false, + userJson.optBoolean("restricted_by_viewer"), + false + ), + mediaCount = timelineMedia.getLong("count"), + followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"), + followingCount = userJson.getJSONObject("edge_follow").getLong("count"), + biography = userJson.getString("biography"), + externalUrl = url, + ) + } + + // TODO convert string response to a response class + suspend fun fetchPost( + shortcode: String, + ): Media { + val response = repository.getPost(shortcode) + val body = JSONObject(response) + val media = body.getJSONObject("graphql").getJSONObject("shortcode_media") + return ResponseBodyUtils.parseGraphQLItem(media, null) + } + + // TODO convert string response to a response class + suspend fun fetchTag( + tag: String, + ): Hashtag { + val response = repository.getTag(tag) + val body = JSONObject(response) + .getJSONObject("graphql") + .getJSONObject(Constants.EXTRAS_HASHTAG) + val timelineMedia = body.getJSONObject("edge_hashtag_to_media") + return Hashtag( + body.getString(Constants.EXTRAS_ID), + body.getString("name"), + timelineMedia.getLong("count"), + if (body.optBoolean("is_following")) FollowingType.FOLLOWING else FollowingType.NOT_FOLLOWING, + null) + } + + // TODO convert string response to a response class + suspend fun fetchLocation( + locationId: Long, + ): Location { + val response = repository.getLocation(locationId) + val body = JSONObject(response) + .getJSONObject("graphql") + .getJSONObject(Constants.EXTRAS_LOCATION) + // val timelineMedia = body.getJSONObject("edge_location_to_media") + val address = JSONObject(body.getString("address_json")) + return Location( + body.getLong(Constants.EXTRAS_ID), + body.getString("slug"), + body.getString("name"), + address.optString("street_address"), + address.optString("city_name"), + body.optDouble("lng", 0.0), + body.optDouble("lat", 0.0) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java deleted file mode 100644 index 333a526d..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ /dev/null @@ -1,320 +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 com.google.gson.Gson; - -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 java.util.UUID; - -import awais.instagrabber.models.Comment; -import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.repositories.MediaRepository; -import awais.instagrabber.repositories.requests.Clip; -import awais.instagrabber.repositories.requests.UploadFinishOptions; -import awais.instagrabber.repositories.requests.VideoOptions; -import awais.instagrabber.repositories.responses.LikersResponse; -import awais.instagrabber.repositories.responses.Media; -import awais.instagrabber.repositories.responses.MediaInfoResponse; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.DateUtils; -import awais.instagrabber.utils.MediaUploadHelper; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class MediaService extends BaseService { - private static final String TAG = "MediaService"; - private static final List DELETABLE_ITEMS_TYPES = ImmutableList.of(MediaItemType.MEDIA_TYPE_IMAGE, - MediaItemType.MEDIA_TYPE_VIDEO, - MediaItemType.MEDIA_TYPE_SLIDER); - - private final MediaRepository repository; - private final String deviceUuid, csrfToken; - private final long userId; - - private static MediaService instance; - - private MediaService(final String deviceUuid, - final String csrfToken, - final long userId) { - this.deviceUuid = deviceUuid; - this.csrfToken = csrfToken; - this.userId = userId; - repository = RetrofitFactory.INSTANCE - .getRetrofit() - .create(MediaRepository.class); - } - - public String getCsrfToken() { - return csrfToken; - } - - public String getDeviceUuid() { - return deviceUuid; - } - - public long getUserId() { - return userId; - } - - public static MediaService getInstance(final String deviceUuid, final String csrfToken, final long userId) { - if (instance == null - || !Objects.equals(instance.getCsrfToken(), csrfToken) - || !Objects.equals(instance.getDeviceUuid(), deviceUuid) - || !Objects.equals(instance.getUserId(), userId)) { - instance = new MediaService(deviceUuid, csrfToken, userId); - } - return instance; - } - - public void fetch(final long mediaId, - final ServiceCallback callback) { - final Call request = repository.fetch(mediaId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback == null) return; - final MediaInfoResponse mediaInfoResponse = response.body(); - if (mediaInfoResponse == null || mediaInfoResponse.getItems() == null || mediaInfoResponse.getItems().isEmpty()) { - callback.onSuccess(null); - return; - } - callback.onSuccess(mediaInfoResponse.getItems().get(0)); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void like(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "like", null, callback); - } - - public void unlike(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "unlike", null, callback); - } - - public void save(final String mediaId, - final String collection, - final ServiceCallback callback) { - action(mediaId, "save", collection, callback); - } - - public void unsave(final String mediaId, - final ServiceCallback callback) { - action(mediaId, "unsave", null, callback); - } - - private void action(final String mediaId, - final String action, - final String collection, - final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("media_id", mediaId); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - // form.put("radio_type", "wifi-none"); - if (action.equals("save") && !TextUtils.isEmpty(collection)) form.put("added_collection_ids", "[" + collection + "]"); - // there also exists "removed_collection_ids" which can be used with "save" and "unsave" - final Map signedForm = Utils.sign(form); - final Call request = repository.action(action, mediaId, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback == null) return; - final String body = response.body(); - if (body == null) { - callback.onFailure(new RuntimeException("Returned body is null")); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); - } catch (JSONException e) { - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void editCaption(final String postId, - final String newCaption, - @NonNull final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("igtv_feed_preview", "false"); - form.put("media_id", postId); - form.put("caption_text", newCaption); - final Map signedForm = Utils.sign(form); - final Call request = repository.editCaption(postId, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "Error occurred while editing caption"); - callback.onSuccess(false); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); - } catch (JSONException e) { - // Log.e(TAG, "Error parsing body", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error editing caption", t); - callback.onFailure(t); - } - }); - } - - public void fetchLikes(final String mediaId, - final boolean isComment, - @NonNull final ServiceCallback> callback) { - final Call likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers"); - likesRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final LikersResponse likersResponse = response.body(); - if (likersResponse == null) { - Log.e(TAG, "Error occurred while fetching likes of " + mediaId); - callback.onSuccess(null); - return; - } - callback.onSuccess(likersResponse.getUsers()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error getting likes", t); - callback.onFailure(t); - } - }); - } - - public void translate(final String id, - final String type, // 1 caption 2 comment 3 bio - @NonNull final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("id", String.valueOf(id)); - form.put("type", type); - final Call request = repository.translate(form); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "Error occurred while translating"); - callback.onSuccess(null); - return; - } - try { - final JSONObject jsonObject = new JSONObject(body); - final String translation = jsonObject.optString("translation"); - callback.onSuccess(translation); - } catch (JSONException e) { - // Log.e(TAG, "Error parsing body", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Error translating", t); - callback.onFailure(t); - } - }); - } - - public Call uploadFinish(@NonNull final UploadFinishOptions options) { - if (options.getVideoOptions() != null) { - final VideoOptions videoOptions = options.getVideoOptions(); - if (videoOptions.getClips().isEmpty()) { - videoOptions.setClips(Collections.singletonList(new Clip(videoOptions.getLength(), options.getSourceType()))); - } - } - final String timezoneOffset = String.valueOf(DateUtils.getTimezoneOffset()); - final ImmutableMap.Builder formBuilder = ImmutableMap.builder() - .put("timezone_offset", timezoneOffset) - .put("_csrftoken", csrfToken) - .put("source_type", options.getSourceType()) - .put("_uid", String.valueOf(userId)) - .put("_uuid", deviceUuid) - .put("upload_id", options.getUploadId()); - if (options.getVideoOptions() != null) { - formBuilder.putAll(options.getVideoOptions().getMap()); - } - final Map queryMap = options.getVideoOptions() != null ? ImmutableMap.of("video", "1") : Collections.emptyMap(); - final Map signedForm = Utils.sign(formBuilder.build()); - return repository.uploadFinish(MediaUploadHelper.getRetryContextString(), queryMap, signedForm); - } - - public Call delete(@NonNull final String postId, - @NonNull final MediaItemType type) { - if (!DELETABLE_ITEMS_TYPES.contains(type)) return null; - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("igtv_feed_preview", "false"); - form.put("media_id", postId); - final Map signedForm = Utils.sign(form); - final String mediaType; - switch (type) { - case MEDIA_TYPE_IMAGE: - mediaType = "PHOTO"; - break; - case MEDIA_TYPE_VIDEO: - mediaType = "VIDEO"; - break; - case MEDIA_TYPE_SLIDER: - mediaType = "CAROUSEL"; - break; - default: - return null; - } - return repository.delete(postId, mediaType, signedForm); - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.kt b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt new file mode 100644 index 00000000..a08747af --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt @@ -0,0 +1,182 @@ +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 + +object MediaService : BaseService() { + private val DELETABLE_ITEMS_TYPES = listOf( + MediaItemType.MEDIA_TYPE_IMAGE, + MediaItemType.MEDIA_TYPE_VIDEO, + MediaItemType.MEDIA_TYPE_SLIDER + ) + 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( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, + ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "like", null) + + suspend fun unlike( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, + ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unlike", null) + + suspend fun save( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, collection: String?, + ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "save", collection) + + suspend fun unsave( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, + ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unsave", null) + + private suspend fun action( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, + action: String, + collection: String?, + ): Boolean { + val form: MutableMap = mutableMapOf( + "media_id" to mediaId, + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + ) + // form.put("radio_type", "wifi-none"); + if (action == "save" && !collection.isNullOrBlank()) { + form["added_collection_ids"] = "[$collection]" + } + // there also exists "removed_collection_ids" which can be used with "save" and "unsave" + val signedForm = Utils.sign(form) + val response = repository.action(action, mediaId, signedForm) + val jsonObject = JSONObject(response) + val status = jsonObject.optString("status") + return status == "ok" + } + + suspend fun editCaption( + csrfToken: String, + userId: Long, + deviceUuid: String, + postId: String, + newCaption: String, + ): Boolean { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "igtv_feed_preview" to "false", + "media_id" to postId, + "caption_text" to newCaption, + ) + val signedForm = Utils.sign(form) + val response = repository.editCaption(postId, signedForm) + val jsonObject = JSONObject(response) + val status = jsonObject.optString("status") + return status == "ok" + } + + suspend fun fetchLikes( + mediaId: String, + isComment: Boolean, + ): List { + val response = repository.fetchLikes(mediaId, if (isComment) "comment_likers" else "likers") + return response.users + } + + suspend fun translate( + id: String, + type: String, // 1 caption 2 comment 3 bio + ): String { + val form = mapOf( + "id" to id, + "type" to type, + ) + val response = repository.translate(form) + val jsonObject = JSONObject(response) + return jsonObject.optString("translation") + } + + suspend fun uploadFinish( + csrfToken: String, + userId: Long, + deviceUuid: String, + options: UploadFinishOptions, + ): String { + if (options.videoOptions != null) { + val videoOptions = options.videoOptions + if (videoOptions.clips.isEmpty()) { + videoOptions.clips = listOf(Clip(videoOptions.length, options.sourceType)) + } + } + val timezoneOffset = DateUtils.getTimezoneOffset().toString() + val form = mutableMapOf( + "timezone_offset" to timezoneOffset, + "_csrftoken" to csrfToken, + "source_type" to options.sourceType, + "_uid" to userId.toString(), + "_uuid" to deviceUuid, + "upload_id" to options.uploadId, + ) + if (options.videoOptions != null) { + form.putAll(options.videoOptions.map) + } + val queryMap = if (options.videoOptions != null) mapOf("video" to "1") else emptyMap() + val signedForm = Utils.sign(form) + return repository.uploadFinish(retryContextString, queryMap, signedForm) + } + + suspend fun delete( + csrfToken: String, + userId: Long, + deviceUuid: String, + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java deleted file mode 100644 index f72d8344..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ /dev/null @@ -1,548 +0,0 @@ -package awais.instagrabber.webservices; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import awais.instagrabber.fragments.settings.PreferenceKeys; -import awais.instagrabber.models.FeedStoryModel; -import awais.instagrabber.models.HighlightModel; -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.repositories.StoriesRepository; -import awais.instagrabber.repositories.requests.StoryViewerOptions; -import awais.instagrabber.repositories.responses.StoryStickerResponse; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.ResponseBodyUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class StoriesService extends BaseService { - private static final String TAG = "StoriesService"; - - private static StoriesService instance; - - private final StoriesRepository repository; - private final String csrfToken; - private final long userId; - private final String deviceUuid; - - private StoriesService(@NonNull final String csrfToken, - final long userId, - @NonNull final String deviceUuid) { - this.csrfToken = csrfToken; - this.userId = userId; - this.deviceUuid = deviceUuid; - repository = RetrofitFactory.INSTANCE - .getRetrofit() - .create(StoriesRepository.class); - } - - public String getCsrfToken() { - return csrfToken; - } - - public long getUserId() { - return userId; - } - - public String getDeviceUuid() { - return deviceUuid; - } - - public static StoriesService getInstance(final String csrfToken, - final long userId, - final String deviceUuid) { - if (instance == null - || !Objects.equals(instance.getCsrfToken(), csrfToken) - || !Objects.equals(instance.getUserId(), userId) - || !Objects.equals(instance.getDeviceUuid(), deviceUuid)) { - instance = new StoriesService(csrfToken, userId, deviceUuid); - } - return instance; - } - - public void fetch(final long mediaId, - final ServiceCallback callback) { - final Call request = repository.fetch(mediaId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback == null) return; - final String body = response.body(); - if (body == null) { - callback.onSuccess(null); - return; - } - try { - final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); - callback.onSuccess(ResponseBodyUtils.parseStoryItem(itemJson, false, null)); - } catch (JSONException e) { - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void getFeedStories(final ServiceCallback> callback) { - final Call response = repository.getFeedStories(); - response.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "getFeedStories: body is empty"); - return; - } - parseStoriesBody(body, callback); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); - } - }); - } - - private void parseStoriesBody(final String body, final ServiceCallback> callback) { - try { - final List feedStoryModels = new ArrayList<>(); - final JSONArray feedStoriesReel = new JSONObject(body).getJSONArray("tray"); - for (int i = 0; i < feedStoriesReel.length(); ++i) { - final JSONObject node = feedStoriesReel.getJSONObject(i); - if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue; - final JSONObject userJson = node.getJSONObject(node.has("user") ? "user" : "owner"); - try { - final User user = new User(userJson.getLong("pk"), - userJson.getString("username"), - userJson.optString("full_name"), - userJson.optBoolean("is_private"), - userJson.getString("profile_pic_url"), - userJson.optBoolean("is_verified") - ); - final long timestamp = node.getLong("latest_reel_media"); - final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; - final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").optJSONObject(0) : null; - StoryModel firstStoryModel = null; - if (itemJson != null) { - firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null); - } - feedStoryModels.add(new FeedStoryModel( - node.getString("id"), - user, - fullyRead, - timestamp, - firstStoryModel, - node.getInt("media_count"), - false, - node.optBoolean("has_besties_media"))); - } catch (Exception e) { - Log.e(TAG, "parseStoriesBody: ", e); - } // to cover promotional reels with non-long user pk's - } - final JSONArray broadcasts = new JSONObject(body).getJSONArray("broadcasts"); - for (int i = 0; i < broadcasts.length(); ++i) { - final JSONObject node = broadcasts.getJSONObject(i); - final JSONObject userJson = node.getJSONObject("broadcast_owner"); - // final ProfileModel profileModel = new ProfileModel(false, false, false, - // userJson.getString("pk"), - // userJson.getString("username"), - // null, null, null, - // userJson.getString("profile_pic_url"), - // null, 0, 0, 0, false, false, false, false, false); - final User user = new User(userJson.getLong("pk"), - userJson.getString("username"), - userJson.optString("full_name"), - userJson.optBoolean("is_private"), - userJson.getString("profile_pic_url"), - userJson.optBoolean("is_verified") - ); - feedStoryModels.add(new FeedStoryModel( - node.getString("id"), - user, - false, - node.getLong("published_time"), - ResponseBodyUtils.parseBroadcastItem(node), - 1, - true, - false - )); - } - callback.onSuccess(sort(feedStoryModels)); - } catch (JSONException e) { - Log.e(TAG, "Error parsing json", e); - } - } - - public void fetchHighlights(final long profileId, - final ServiceCallback> callback) { - final Call request = repository.fetchHighlights(profileId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - if (callback == null) { - return; - } - final String body = response.body(); - if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); - return; - } - final JSONArray highlightsReel = new JSONObject(body).getJSONArray("tray"); - - final int length = highlightsReel.length(); - final List highlightModels = new ArrayList<>(); - - for (int i = 0; i < length; ++i) { - final JSONObject highlightNode = highlightsReel.getJSONObject(i); - highlightModels.add(new HighlightModel( - highlightNode.getString("title"), - highlightNode.getString(Constants.EXTRAS_ID), - highlightNode.getJSONObject("cover_media") - .getJSONObject("cropped_image_version") - .getString("url"), - highlightNode.getLong("latest_reel_media"), - highlightNode.getInt("media_count") - )); - } - callback.onSuccess(highlightModels); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void fetchArchive(final String maxId, - final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("include_suggested_highlights", "false"); - form.put("is_in_archive_home", "true"); - form.put("include_cover", "1"); - if (!TextUtils.isEmpty(maxId)) { - form.put("max_id", maxId); // NOT TESTED - } - final Call request = repository.fetchArchive(form); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - if (callback == null) { - return; - } - final String body = response.body(); - if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); - return; - } - final JSONObject data = new JSONObject(body); - final JSONArray highlightsReel = data.getJSONArray("items"); - - final int length = highlightsReel.length(); - final List highlightModels = new ArrayList<>(); - - for (int i = 0; i < length; ++i) { - final JSONObject highlightNode = highlightsReel.getJSONObject(i); - highlightModels.add(new HighlightModel( - null, - highlightNode.getString(Constants.EXTRAS_ID), - highlightNode.getJSONObject("cover_image_version").getString("url"), - highlightNode.getLong("latest_reel_media"), - highlightNode.getInt("media_count") - )); - } - callback.onSuccess(new ArchiveFetchResponse(highlightModels, - data.getBoolean("more_available"), - data.getString("max_id"))); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - public void getUserStory(final StoryViewerOptions options, - final ServiceCallback> callback) { - final String url = buildUrl(options); - final Call userStoryCall = repository.getUserStory(url); - final boolean isLocOrHashtag = options.getType() == StoryViewerOptions.Type.LOCATION || options.getType() == StoryViewerOptions.Type.HASHTAG; - final boolean isHighlight = options.getType() == StoryViewerOptions.Type.HIGHLIGHT || options - .getType() == StoryViewerOptions.Type.STORY_ARCHIVE; - userStoryCall.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - JSONObject data; - try { - final String body = response.body(); - if (body == null) { - Log.e(TAG, "body is null"); - return; - } - data = new JSONObject(body); - - if (!isHighlight) { - data = data.optJSONObject((isLocOrHashtag) ? "story" : "reel"); - } else { - data = data.getJSONObject("reels").optJSONObject(options.getName()); - } - - String username = null; - if (data != null - // && localUsername == null - && !isLocOrHashtag) { - username = data.getJSONObject("user").getString("username"); - } - - JSONArray media; - if (data != null - && (media = data.optJSONArray("items")) != null - && media.length() > 0 && media.optJSONObject(0) != null) { - final int mediaLen = media.length(); - final List models = new ArrayList<>(); - for (int i = 0; i < mediaLen; ++i) { - data = media.getJSONObject(i); - models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)); - } - callback.onSuccess(models); - } else { - callback.onSuccess(null); - } - } catch (JSONException e) { - Log.e(TAG, "Error parsing string", e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); - } - }); - } - - private void respondToSticker(final String storyId, - final String stickerId, - final String action, - final String arg1, - final String arg2, - final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("mutation_token", UUID.randomUUID().toString()); - form.put("client_context", UUID.randomUUID().toString()); - form.put("radio_type", "wifi-none"); - form.put(arg1, arg2); - final Map signedForm = Utils.sign(form); - final Call request = - repository.respondToSticker(storyId, stickerId, action, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback != null) { - callback.onSuccess(response.body()); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - // RespondAction.java - public void respondToQuestion(final String storyId, - final String stickerId, - final String answer, - final ServiceCallback callback) { - respondToSticker(storyId, stickerId, "story_question_response", "response", answer, callback); - } - - // QuizAction.java - public void respondToQuiz(final String storyId, - final String stickerId, - final int answer, - final ServiceCallback callback) { - respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), callback); - } - - // VoteAction.java - public void respondToPoll(final String storyId, - final String stickerId, - final int answer, - final ServiceCallback callback) { - respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), callback); - } - - public void respondToSlider(final String storyId, - final String stickerId, - final double answer, - final ServiceCallback callback) { - respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), callback); - } - - public void seen(final String storyMediaId, - final long takenAt, - final long seenAt, - final ServiceCallback callback) { - final Map form = new HashMap<>(); - form.put("_csrftoken", csrfToken); - form.put("_uid", userId); - form.put("_uuid", deviceUuid); - form.put("container_module", "feed_timeline"); - final Map reelsForm = new HashMap<>(); - reelsForm.put(storyMediaId, Collections.singletonList(takenAt + "_" + seenAt)); - form.put("reels", reelsForm); - final Map signedForm = Utils.sign(form); - final Map queryMap = new HashMap<>(); - queryMap.put("reel", "1"); - queryMap.put("live_vod", "0"); - final Call request = repository.seen(queryMap, signedForm); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (callback != null) { - callback.onSuccess(response.body()); - } - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - @Nullable - private String buildUrl(@NonNull final StoryViewerOptions options) { - final StringBuilder builder = new StringBuilder(); - builder.append("https://i.instagram.com/api/v1/"); - final StoryViewerOptions.Type type = options.getType(); - String id = null; - switch (type) { - case HASHTAG: - builder.append("tags/"); - id = options.getName(); - break; - case LOCATION: - builder.append("locations/"); - id = String.valueOf(options.getId()); - break; - case USER: - builder.append("feed/user/"); - id = String.valueOf(options.getId()); - break; - case HIGHLIGHT: - case STORY_ARCHIVE: - builder.append("feed/reels_media/?user_ids="); - id = options.getName(); - break; - case STORY: - break; - // case FEED_STORY_POSITION: - // break; - } - if (id == null) { - return null; - } - builder.append(id); - if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { - builder.append("/story/"); - } - return builder.toString(); - } - - private List sort(final List list) { - final List listCopy = new ArrayList<>(list); - Collections.sort(listCopy, (o1, o2) -> { - int result; - switch (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { - case "1": - result = Long.compare(o2.getTimestamp(), o1.getTimestamp()); - break; - case "2": - result = Long.compare(o1.getTimestamp(), o2.getTimestamp()); - break; - default: - result = 0; - } - return result; - }); - return listCopy; - } - - public static class ArchiveFetchResponse { - private final List archives; - private final boolean hasNextPage; - private final String nextCursor; - - public ArchiveFetchResponse(final List archives, final boolean hasNextPage, final String nextCursor) { - this.archives = archives; - this.hasNextPage = hasNextPage; - this.nextCursor = nextCursor; - } - - public List getResult() { - return archives; - } - - public boolean hasNextPage() { - return hasNextPage; - } - - public String getNextCursor() { - return nextCursor; - } - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.kt b/app/src/main/java/awais/instagrabber/webservices/StoriesService.kt new file mode 100644 index 00000000..b64adf42 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.kt @@ -0,0 +1,309 @@ +package awais.instagrabber.webservices + +import android.util.Log +import awais.instagrabber.fragments.settings.PreferenceKeys +import awais.instagrabber.models.FeedStoryModel +import awais.instagrabber.models.HighlightModel +import awais.instagrabber.models.StoryModel +import awais.instagrabber.repositories.StoriesRepository +import awais.instagrabber.repositories.requests.StoryViewerOptions +import awais.instagrabber.repositories.responses.StoryStickerResponse +import awais.instagrabber.repositories.responses.User +import awais.instagrabber.utils.Constants +import awais.instagrabber.utils.ResponseBodyUtils +import awais.instagrabber.utils.TextUtils.isEmpty +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.extensions.TAG +import awais.instagrabber.webservices.RetrofitFactory.retrofit +import org.json.JSONArray +import org.json.JSONObject +import java.util.* + +object StoriesService : BaseService() { + private val repository: StoriesRepository = retrofit.create(StoriesRepository::class.java) + + suspend fun fetch(mediaId: Long): StoryModel { + val response = repository.fetch(mediaId) + val itemJson = JSONObject(response).getJSONArray("items").getJSONObject(0) + return ResponseBodyUtils.parseStoryItem(itemJson, false, null) + } + + suspend fun getFeedStories(): List { + val response = repository.getFeedStories() + return parseStoriesBody(response) + } + + private fun parseStoriesBody(body: String): List { + val feedStoryModels: MutableList = ArrayList() + val feedStoriesReel = JSONObject(body).getJSONArray("tray") + for (i in 0 until feedStoriesReel.length()) { + val node = feedStoriesReel.getJSONObject(i) + if (node.optBoolean("hide_from_feed_unit") && Utils.settingsHelper.getBoolean(PreferenceKeys.HIDE_MUTED_REELS)) continue + val userJson = node.getJSONObject(if (node.has("user")) "user" else "owner") + try { + val user = User(userJson.getLong("pk"), + userJson.getString("username"), + userJson.optString("full_name"), + userJson.optBoolean("is_private"), + userJson.getString("profile_pic_url"), + userJson.optBoolean("is_verified") + ) + val timestamp = node.getLong("latest_reel_media") + val fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp + val itemJson = if (node.has("items")) node.getJSONArray("items").optJSONObject(0) else null + var firstStoryModel: StoryModel? = null + if (itemJson != null) { + firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, null) + } + feedStoryModels.add(FeedStoryModel( + node.getString("id"), + user, + fullyRead, + timestamp, + firstStoryModel, + node.getInt("media_count"), + false, + node.optBoolean("has_besties_media"))) + } catch (e: Exception) { + Log.e(TAG, "parseStoriesBody: ", e) + } // to cover promotional reels with non-long user pk's + } + val broadcasts = JSONObject(body).getJSONArray("broadcasts") + for (i in 0 until broadcasts.length()) { + val node = broadcasts.getJSONObject(i) + val userJson = node.getJSONObject("broadcast_owner") + val user = User(userJson.getLong("pk"), + userJson.getString("username"), + userJson.optString("full_name"), + userJson.optBoolean("is_private"), + userJson.getString("profile_pic_url"), + userJson.optBoolean("is_verified") + ) + feedStoryModels.add(FeedStoryModel( + node.getString("id"), + user, + false, + node.getLong("published_time"), + ResponseBodyUtils.parseBroadcastItem(node), + 1, + isLive = true, + isBestie = false + )) + } + return sort(feedStoryModels) + } + + suspend fun fetchHighlights(profileId: Long): List { + val response = repository.fetchHighlights(profileId) + val highlightsReel = JSONObject(response).getJSONArray("tray") + val length = highlightsReel.length() + val highlightModels: MutableList = ArrayList() + for (i in 0 until length) { + val highlightNode = highlightsReel.getJSONObject(i) + highlightModels.add(HighlightModel( + highlightNode.getString("title"), + highlightNode.getString(Constants.EXTRAS_ID), + highlightNode.getJSONObject("cover_media") + .getJSONObject("cropped_image_version") + .getString("url"), + highlightNode.getLong("latest_reel_media"), + highlightNode.getInt("media_count") + )) + } + return highlightModels + } + + suspend fun fetchArchive(maxId: String): ArchiveFetchResponse { + val form = mutableMapOf( + "include_suggested_highlights" to "false", + "is_in_archive_home" to "true", + "include_cover" to "1", + ) + if (!isEmpty(maxId)) { + form["max_id"] = maxId // NOT TESTED + } + val response = repository.fetchArchive(form) + val data = JSONObject(response) + val highlightsReel = data.getJSONArray("items") + val length = highlightsReel.length() + val highlightModels: MutableList = ArrayList() + for (i in 0 until length) { + val highlightNode = highlightsReel.getJSONObject(i) + highlightModels.add(HighlightModel( + null, + highlightNode.getString(Constants.EXTRAS_ID), + highlightNode.getJSONObject("cover_image_version").getString("url"), + highlightNode.getLong("latest_reel_media"), + highlightNode.getInt("media_count") + )) + } + return ArchiveFetchResponse(highlightModels, data.getBoolean("more_available"), data.getString("max_id")) + } + + suspend fun getUserStory(options: StoryViewerOptions): List { + val url = buildUrl(options) ?: return emptyList() + val response = repository.getUserStory(url) + val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG + val isHighlight = options.type == StoryViewerOptions.Type.HIGHLIGHT || options.type == StoryViewerOptions.Type.STORY_ARCHIVE + var data: JSONObject? = JSONObject(response) + data = if (!isHighlight) { + data?.optJSONObject(if (isLocOrHashtag) "story" else "reel") + } else { + data?.getJSONObject("reels")?.optJSONObject(options.name) + } + var username: String? = null + if (data != null && !isLocOrHashtag) { + username = data.getJSONObject("user").getString("username") + } + val media: JSONArray? = data?.optJSONArray("items") + return if (media?.length() ?: 0 > 0 && media?.optJSONObject(0) != null) { + val mediaLen = media.length() + val models: MutableList = ArrayList() + for (i in 0 until mediaLen) { + data = media.getJSONObject(i) + models.add(ResponseBodyUtils.parseStoryItem(data, isLocOrHashtag, username)) + } + models + } else emptyList() + } + + private suspend fun respondToSticker( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyId: String, + stickerId: String, + action: String, + arg1: String, + arg2: String, + ): StoryStickerResponse { + val form = mapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "mutation_token" to UUID.randomUUID().toString(), + "client_context" to UUID.randomUUID().toString(), + "radio_type" to "wifi-none", + arg1 to arg2, + ) + val signedForm = Utils.sign(form) + return repository.respondToSticker(storyId, stickerId, action, signedForm) + } + + suspend fun respondToQuestion( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyId: String, + stickerId: String, + answer: String, + ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_question_response", "response", answer) + + suspend fun respondToQuiz( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyId: String, + stickerId: String, + answer: Int, + ): StoryStickerResponse { + return respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_quiz_answer", "answer", answer.toString()) + } + + suspend fun respondToPoll( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyId: String, + stickerId: String, + answer: Int, + ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_poll_vote", "vote", answer.toString()) + + suspend fun respondToSlider( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyId: String, + stickerId: String, + answer: Double, + ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_slider_vote", "vote", answer.toString()) + + suspend fun seen( + csrfToken: String, + userId: Long, + deviceUuid: String, + storyMediaId: String, + takenAt: Long, + seenAt: Long, + ): String { + val reelsForm = mapOf(storyMediaId to listOf(takenAt.toString() + "_" + seenAt)) + val form = mutableMapOf( + "_csrftoken" to csrfToken, + "_uid" to userId, + "_uuid" to deviceUuid, + "container_module" to "feed_timeline", + "reels" to reelsForm, + ) + val signedForm = Utils.sign(form) + val queryMap = mapOf( + "reel" to "1", + "live_vod" to "0", + ) + return repository.seen(queryMap, signedForm) + } + + private fun buildUrl(options: StoryViewerOptions): String? { + val builder = StringBuilder() + builder.append("https://i.instagram.com/api/v1/") + val type = options.type + var id: String? = null + when (type) { + StoryViewerOptions.Type.HASHTAG -> { + builder.append("tags/") + id = options.name + } + StoryViewerOptions.Type.LOCATION -> { + builder.append("locations/") + id = options.id.toString() + } + StoryViewerOptions.Type.USER -> { + builder.append("feed/user/") + id = options.id.toString() + } + StoryViewerOptions.Type.HIGHLIGHT, StoryViewerOptions.Type.STORY_ARCHIVE -> { + builder.append("feed/reels_media/?user_ids=") + id = options.name + } + StoryViewerOptions.Type.STORY -> { + } + else -> { + } + } + if (id == null) { + return null + } + builder.append(id) + if (type != StoryViewerOptions.Type.HIGHLIGHT && type != StoryViewerOptions.Type.STORY_ARCHIVE) { + builder.append("/story/") + } + return builder.toString() + } + + private fun sort(list: List): List { + val listCopy = ArrayList(list) + listCopy.sortWith { o1, o2 -> + when (Utils.settingsHelper.getString(PreferenceKeys.STORY_SORT)) { + "1" -> return@sortWith o2.timestamp.compareTo(o1.timestamp) + "2" -> return@sortWith o1.timestamp.compareTo(o2.timestamp) + else -> return@sortWith 0 + } + } + return listCopy + } + + class ArchiveFetchResponse(val result: List, val hasNextPage: Boolean, val nextCursor: String) { + fun hasNextPage(): Boolean { + return hasNextPage + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/UserService.java b/app/src/main/java/awais/instagrabber/webservices/UserService.java deleted file mode 100644 index 2537e6ee..00000000 --- a/app/src/main/java/awais/instagrabber/webservices/UserService.java +++ /dev/null @@ -1,101 +0,0 @@ -package awais.instagrabber.webservices; - -import androidx.annotation.NonNull; - -import java.util.TimeZone; - -import awais.instagrabber.repositories.UserRepository; -import awais.instagrabber.repositories.responses.FriendshipStatus; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.UserSearchResponse; -import awais.instagrabber.repositories.responses.WrappedUser; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class UserService extends BaseService { - private static final String TAG = UserService.class.getSimpleName(); - - private final UserRepository repository; - - private static UserService instance; - - private UserService() { - repository = RetrofitFactory.INSTANCE - .getRetrofit() - .create(UserRepository.class); - } - - public static UserService getInstance() { - if (instance == null) { - instance = new UserService(); - } - return instance; - } - - public void getUserInfo(final long uid, final ServiceCallback callback) { - final Call request = repository.getUserInfo(uid); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final WrappedUser user = response.body(); - if (user == null) { - callback.onSuccess(null); - return; - } - callback.onSuccess(user.getUser()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); - } - }); - } - - public void getUsernameInfo(final String username, final ServiceCallback callback) { - final Call request = repository.getUsernameInfo(username); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final WrappedUser user = response.body(); - if (user == null) { - callback.onFailure(null); - return; - } - callback.onSuccess(user.getUser()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); - } - }); - } - - public void getUserFriendship(final long uid, final ServiceCallback callback) { - final Call request = repository.getUserFriendship(uid); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final FriendshipStatus status = response.body(); - if (status == null) { - callback.onSuccess(null); - return; - } - callback.onSuccess(status); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - callback.onFailure(t); - } - }); - } - - - public Call search(final String query) { - final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000; - return repository.search(timezoneOffset, query); - } -} diff --git a/app/src/main/java/awais/instagrabber/webservices/UserService.kt b/app/src/main/java/awais/instagrabber/webservices/UserService.kt new file mode 100644 index 00000000..708f1d8b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/UserService.kt @@ -0,0 +1,29 @@ +package awais.instagrabber.webservices + +import awais.instagrabber.repositories.UserRepository +import awais.instagrabber.repositories.responses.FriendshipStatus +import awais.instagrabber.repositories.responses.User +import awais.instagrabber.repositories.responses.UserSearchResponse +import awais.instagrabber.webservices.RetrofitFactory.retrofit +import java.util.* + +object UserService : BaseService() { + private val repository: UserRepository = retrofit.create(UserRepository::class.java) + + suspend fun getUserInfo(uid: Long): User { + val response = repository.getUserInfo(uid) + return response.user + } + + suspend fun getUsernameInfo(username: String): User { + val response = repository.getUsernameInfo(username) + return response.user + } + + suspend fun getUserFriendship(uid: Long): FriendshipStatus = repository.getUserFriendship(uid) + + suspend fun search(query: String): UserSearchResponse { + val timezoneOffset = TimeZone.getDefault().rawOffset.toFloat() / 1000 + return repository.search(timezoneOffset, query) + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java deleted file mode 100644 index 09d61c25..00000000 --- a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java +++ /dev/null @@ -1,399 +0,0 @@ -package awais.instagrabber.workers; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.FileProvider; -import androidx.work.Data; -import androidx.work.ForegroundInfo; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Scanner; -import java.util.Set; -import java.util.concurrent.ExecutionException; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.R; -import awais.instagrabber.services.DeleteImageIntentService; -import awais.instagrabber.utils.BitmapUtils; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.DownloadUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; - -import static awais.instagrabber.utils.BitmapUtils.THUMBNAIL_SIZE; -import static awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID; -import static awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME; - -public class DownloadWorker extends Worker { - private static final String TAG = "DownloadWorker"; - private static final String DOWNLOAD_GROUP = "DOWNLOAD_GROUP"; - - public static final String PROGRESS = "PROGRESS"; - public static final String URL = "URL"; - public static final String KEY_DOWNLOAD_REQUEST_JSON = "download_request_json"; - public static final int DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE = 2020; - public static final int DELETE_IMAGE_REQUEST_CODE = 2030; - - private final NotificationManagerCompat notificationManager; - - public DownloadWorker(@NonNull final Context context, @NonNull final WorkerParameters workerParams) { - super(context, workerParams); - notificationManager = NotificationManagerCompat.from(context); - } - - @NonNull - @Override - public Result doWork() { - final String downloadRequestFilePath = getInputData().getString(KEY_DOWNLOAD_REQUEST_JSON); - if (TextUtils.isEmpty(downloadRequestFilePath)) { - return Result.failure(new Data.Builder() - .putString("error", "downloadRequest is empty or null") - .build()); - } - final String downloadRequestString; - final File requestFile = new File(downloadRequestFilePath); - try (Scanner scanner = new Scanner(requestFile)) { - downloadRequestString = scanner.useDelimiter("\\A").next(); - } catch (Exception e) { - Log.e(TAG, "doWork: ", e); - return Result.failure(new Data.Builder() - .putString("error", e.getLocalizedMessage()) - .build()); - } - if (TextUtils.isEmpty(downloadRequestString)) { - return Result.failure(new Data.Builder() - .putString("error", "downloadRequest is empty or null") - .build()); - } - final DownloadRequest downloadRequest; - try { - downloadRequest = new Gson().fromJson(downloadRequestString, DownloadRequest.class); - } catch (JsonSyntaxException e) { - Log.e(TAG, "doWork", e); - return Result.failure(new Data.Builder() - .putString("error", e.getLocalizedMessage()) - .build()); - } - if (downloadRequest == null) { - return Result.failure(new Data.Builder() - .putString("error", "downloadRequest is null") - .build()); - } - final Map urlToFilePathMap = downloadRequest.getUrlToFilePathMap(); - download(urlToFilePathMap); - new Handler(Looper.getMainLooper()).postDelayed(() -> showSummary(urlToFilePathMap), 500); - final boolean deleted = requestFile.delete(); - if (!deleted) { - Log.w(TAG, "doWork: requestFile not deleted!"); - } - return Result.success(); - } - - private void download(final Map urlToFilePathMap) { - final int notificationId = getNotificationId(); - final Set> entries = urlToFilePathMap.entrySet(); - int count = 1; - final int total = urlToFilePathMap.size(); - for (final Map.Entry urlAndFilePath : entries) { - final String url = urlAndFilePath.getKey(); - updateDownloadProgress(notificationId, count, total, 0); - download(notificationId, count, total, url, urlAndFilePath.getValue()); - count++; - } - } - - private int getNotificationId() { - return Math.abs(getId().hashCode()); - } - - private void download(final int notificationId, - final int position, - final int total, - final String url, - final String filePath) { - final boolean isJpg = filePath.endsWith("jpg"); - // using temp file approach to remove IPTC so that download progress can be reported - final File outFile = isJpg ? DownloadUtils.getTempFile() : new File(filePath); - try { - final URLConnection urlConnection = new URL(url).openConnection(); - final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() : - urlConnection.getContentLength(); - float totalRead = 0; - try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream()); - final FileOutputStream fos = new FileOutputStream(outFile)) { - final byte[] buffer = new byte[0x2000]; - int count; - while ((count = bis.read(buffer, 0, 0x2000)) != -1) { - totalRead = totalRead + count; - fos.write(buffer, 0, count); - setProgressAsync(new Data.Builder().putString(URL, url) - .putFloat(PROGRESS, totalRead * 100f / fileSize) - .build()); - updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize); - } - fos.flush(); - } catch (final Exception e) { - Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.getAbsolutePath(), e); - } - if (isJpg) { - final File finalFile = new File(filePath); - try (FileInputStream fis = new FileInputStream(outFile); - FileOutputStream fos = new FileOutputStream(finalFile)) { - final JpegIptcRewriter jpegIptcRewriter = new JpegIptcRewriter(); - jpegIptcRewriter.removeIPTC(fis, fos); - } catch (Exception e) { - Log.e(TAG, "Error while removing iptc: url: " + url - + ", tempFile: " + outFile.getAbsolutePath() - + ", finalFile: " + finalFile.getAbsolutePath(), e); - } - final boolean deleted = outFile.delete(); - if (!deleted) { - Log.w(TAG, "download: tempFile not deleted!"); - } - } - } catch (final Exception e) { - Log.e(TAG, "Error while downloading: " + url, e); - } - setProgressAsync(new Data.Builder().putString(URL, url) - .putFloat(PROGRESS, 100) - .build()); - updateDownloadProgress(notificationId, position, total, 100); - } - - private void updateDownloadProgress(final int notificationId, - final int position, - final int total, - final float percent) { - final Notification notification = createProgressNotification(position, total, percent); - try { - if (notification == null) { - notificationManager.cancel(notificationId); - return; - } - setForegroundAsync(new ForegroundInfo(notificationId, notification)).get(); - } catch (ExecutionException | InterruptedException e) { - Log.e(TAG, "updateDownloadProgress", e); - } - } - - private Notification createProgressNotification(final int position, final int total, final float percent) { - final Context context = getApplicationContext(); - boolean ongoing = true; - int totalPercent; - if (position == total && percent == 100) { - ongoing = false; - totalPercent = 100; - } else { - totalPercent = (int) ((100f * (position - 1) / total) + (1f / total) * (percent)); - } - if (totalPercent == 100) { - return null; - } - // Log.d(TAG, "createProgressNotification: position: " + position - // + ", total: " + total - // + ", percent: " + percent - // + ", totalPercent: " + totalPercent); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setSmallIcon(R.drawable.ic_download) - .setOngoing(ongoing) - .setProgress(100, totalPercent, totalPercent < 0) - .setAutoCancel(false) - .setOnlyAlertOnce(true) - .setContentTitle(context.getString(R.string.downloader_downloading_post)); - if (total > 1) { - builder.setContentText(context.getString(R.string.downloader_downloading_child, position, total)); - } - return builder.build(); - } - - private void showSummary(final Map urlToFilePathMap) { - final Context context = getApplicationContext(); - final Collection filePaths = urlToFilePathMap.values(); - final List notifications = new LinkedList<>(); - final List notificationIds = new LinkedList<>(); - int count = 1; - for (final String filePath : filePaths) { - final File file = new File(filePath); - context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); - MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null); - final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file); - final ContentResolver contentResolver = context.getContentResolver(); - final Bitmap bitmap = getThumbnail(context, file, uri, contentResolver); - final String downloadComplete = context.getString(R.string.downloader_complete); - final Intent intent = new Intent(Intent.ACTION_VIEW, uri) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_FROM_BACKGROUND - | Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, uri); - final PendingIntent pendingIntent = PendingIntent.getActivity( - context, - DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, - intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT - ); - final int notificationId = getNotificationId() + count; - notificationIds.add(notificationId); - count++; - final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_download) - .setContentText(null) - .setContentTitle(downloadComplete) - .setWhen(System.currentTimeMillis()) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setGroup(NOTIF_GROUP_NAME + "_" + getId()) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setContentIntent(pendingIntent) - .addAction(R.drawable.ic_delete, - context.getString(R.string.delete), - DeleteImageIntentService.pendingIntent(context, filePath, notificationId)); - if (bitmap != null) { - builder.setLargeIcon(bitmap) - .setStyle(new NotificationCompat.BigPictureStyle() - .bigPicture(bitmap) - .bigLargeIcon(null)) - .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); - } - notifications.add(builder); - } - Notification summaryNotification = null; - if (urlToFilePathMap.size() != 1) { - final String text = "Downloaded " + urlToFilePathMap.size() + " items"; - summaryNotification = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) - .setContentTitle("Downloaded") - .setContentText(text) - .setSmallIcon(R.drawable.ic_download) - .setStyle(new NotificationCompat.InboxStyle().setSummaryText(text)) - .setGroup(NOTIF_GROUP_NAME + "_" + getId()) - .setGroupSummary(true) - .build(); - } - for (int i = 0; i < notifications.size(); i++) { - final NotificationCompat.Builder builder = notifications.get(i); - // only make sound and vibrate for the last notification - if (i != notifications.size() - 1) { - builder.setSound(null) - .setVibrate(null); - } - notificationManager.notify(notificationIds.get(i), builder.build()); - } - if (summaryNotification != null) { - notificationManager.notify(getNotificationId() + count, summaryNotification); - } - } - - @Nullable - private Bitmap getThumbnail(final Context context, - final File file, - final Uri uri, - final ContentResolver contentResolver) { - final String mimeType = Utils.getMimeType(uri, contentResolver); - if (TextUtils.isEmpty(mimeType)) return null; - Bitmap bitmap = null; - if (mimeType.startsWith("image")) { - try { - final BitmapUtils.BitmapResult bitmapResult = BitmapUtils - .getBitmapResult(context.getContentResolver(), uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, -1, true); - if (bitmapResult == null) return null; - bitmap = bitmapResult.bitmap; - } catch (final Exception e) { - Log.e(TAG, "", e); - } - return bitmap; - } - if (mimeType.startsWith("video")) { - try { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - try { - try { - retriever.setDataSource(context, uri); - } catch (final Exception e) { - retriever.setDataSource(file.getAbsolutePath()); - } - bitmap = retriever.getFrameAtTime(); - } finally { - try { - retriever.release(); - } catch (Exception e) { - Log.e(TAG, "getThumbnail: ", e); - } - } - } catch (final Exception e) { - Log.e(TAG, "", e); - } - } - return bitmap; - } - - public static class DownloadRequest { - private final Map urlToFilePathMap; - - public static class Builder { - private Map urlToFilePathMap; - - public Builder setUrlToFilePathMap(final Map urlToFilePathMap) { - this.urlToFilePathMap = urlToFilePathMap; - return this; - } - - public Builder addUrl(@NonNull final String url, @NonNull final String filePath) { - if (urlToFilePathMap == null) { - urlToFilePathMap = new HashMap<>(); - } - urlToFilePathMap.put(url, filePath); - return this; - } - - public DownloadRequest build() { - return new DownloadRequest(urlToFilePathMap); - } - } - - public static Builder builder() { - return new Builder(); - } - - private DownloadRequest(final Map urlToFilePathMap) { - this.urlToFilePathMap = urlToFilePathMap; - } - - public Map getUrlToFilePathMap() { - return urlToFilePathMap; - } - } -} diff --git a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt new file mode 100644 index 00000000..a749cc07 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt @@ -0,0 +1,383 @@ +package awais.instagrabber.workers + +import android.app.Notification +import android.app.PendingIntent +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.FileProvider +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import awais.instagrabber.BuildConfig +import awais.instagrabber.R +import awais.instagrabber.services.DeleteImageIntentService +import awais.instagrabber.utils.BitmapUtils +import awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID +import awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME +import awais.instagrabber.utils.DownloadUtils +import awais.instagrabber.utils.TextUtils.isEmpty +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.extensions.TAG +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.net.URL +import java.util.* +import java.util.concurrent.ExecutionException +import kotlin.math.abs + +class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { + private val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(context) + + override suspend fun doWork(): Result { + val downloadRequestFilePath = inputData.getString(KEY_DOWNLOAD_REQUEST_JSON) + if (downloadRequestFilePath.isNullOrBlank()) { + return Result.failure(Data.Builder() + .putString("error", "downloadRequest is empty or null") + .build()) + } + val downloadRequestString: String + val requestFile = File(downloadRequestFilePath) + try { + downloadRequestString = requestFile.bufferedReader().use { it.readText() } + } catch (e: Exception) { + Log.e(TAG, "doWork: ", e) + return Result.failure(Data.Builder() + .putString("error", e.localizedMessage) + .build()) + } + if (downloadRequestString.isBlank()) { + return Result.failure(Data.Builder() + .putString("error", "downloadRequest is empty") + .build()) + } + val downloadRequest: DownloadRequest = try { + Gson().fromJson(downloadRequestString, DownloadRequest::class.java) + } catch (e: JsonSyntaxException) { + Log.e(TAG, "doWork", e) + return Result.failure(Data.Builder() + .putString("error", e.localizedMessage) + .build()) + } ?: return Result.failure(Data.Builder() + .putString("error", "downloadRequest is null") + .build()) + val urlToFilePathMap = downloadRequest.urlToFilePathMap + download(urlToFilePathMap) + Handler(Looper.getMainLooper()).postDelayed({ showSummary(urlToFilePathMap) }, 500) + val deleted = requestFile.delete() + if (!deleted) { + Log.w(TAG, "doWork: requestFile not deleted!") + } + return Result.success() + } + + private suspend fun download(urlToFilePathMap: Map) { + val notificationId = notificationId + val entries = urlToFilePathMap.entries + var count = 1 + val total = urlToFilePathMap.size + for ((url, value) in entries) { + updateDownloadProgress(notificationId, count, total, 0f) + withContext(Dispatchers.IO) { + download(notificationId, count, total, url, value) + } + count++ + } + } + + private val notificationId: Int + get() = abs(id.hashCode()) + + private fun download( + notificationId: Int, + position: Int, + total: Int, + url: String, + filePath: String, + ) { + val isJpg = filePath.endsWith("jpg") + // using temp file approach to remove IPTC so that download progress can be reported + val outFile = if (isJpg) DownloadUtils.getTempFile() else File(filePath) + try { + val urlConnection = URL(url).openConnection() + val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong() + var totalRead = 0f + try { + BufferedInputStream(urlConnection.getInputStream()).use { bis -> + FileOutputStream(outFile).use { fos -> + val buffer = ByteArray(0x2000) + var count: Int + while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) { + totalRead += count + fos.write(buffer, 0, count) + setProgressAsync(Data.Builder().putString(URL, url) + .putFloat(PROGRESS, totalRead * 100f / fileSize) + .build()) + updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize) + } + fos.flush() + } + } + } catch (e: Exception) { + Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.absolutePath, e) + } + if (isJpg) { + val finalFile = File(filePath) + try { + FileInputStream(outFile).use { fis -> + FileOutputStream(finalFile).use { fos -> + val jpegIptcRewriter = JpegIptcRewriter() + jpegIptcRewriter.removeIPTC(fis, fos) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error while removing iptc: url: " + url + + ", tempFile: " + outFile.absolutePath + + ", finalFile: " + finalFile.absolutePath, e) + } + val deleted = outFile.delete() + if (!deleted) { + Log.w(TAG, "download: tempFile not deleted!") + } + } + } catch (e: Exception) { + Log.e(TAG, "Error while downloading: $url", e) + } + setProgressAsync(Data.Builder().putString(URL, url) + .putFloat(PROGRESS, 100f) + .build()) + updateDownloadProgress(notificationId, position, total, 100f) + } + + private fun updateDownloadProgress( + notificationId: Int, + position: Int, + total: Int, + percent: Float, + ) { + val notification = createProgressNotification(position, total, percent) + try { + if (notification == null) { + notificationManager.cancel(notificationId) + return + } + setForegroundAsync(ForegroundInfo(notificationId, notification)).get() + } catch (e: ExecutionException) { + Log.e(TAG, "updateDownloadProgress", e) + } catch (e: InterruptedException) { + Log.e(TAG, "updateDownloadProgress", e) + } + } + + private fun createProgressNotification(position: Int, total: Int, percent: Float): Notification? { + val context = applicationContext + var ongoing = true + val totalPercent: Int + if (position == total && percent == 100f) { + ongoing = false + totalPercent = 100 + } else { + totalPercent = (100f * (position - 1) / total + 1f / total * percent).toInt() + } + if (totalPercent == 100) { + return null + } + // Log.d(TAG, "createProgressNotification: position: " + position + // + ", total: " + total + // + ", percent: " + percent + // + ", totalPercent: " + totalPercent); + val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(ongoing) + .setProgress(100, totalPercent, totalPercent < 0) + .setAutoCancel(false) + .setOnlyAlertOnce(true) + .setContentTitle(context.getString(R.string.downloader_downloading_post)) + if (total > 1) { + builder.setContentText(context.getString(R.string.downloader_downloading_child, position, total)) + } + return builder.build() + } + + private fun showSummary(urlToFilePathMap: Map?) { + val context = applicationContext + val filePaths = urlToFilePathMap!!.values + val notifications: MutableList = LinkedList() + val notificationIds: MutableList = LinkedList() + var count = 1 + for (filePath in filePaths) { + val file = File(filePath) + context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))) + MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null, null) + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + val contentResolver = context.contentResolver + val bitmap = getThumbnail(context, file, uri, contentResolver) + val downloadComplete = context.getString(R.string.downloader_complete) + val intent = Intent(Intent.ACTION_VIEW, uri) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_FROM_BACKGROUND + or Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, uri) + val pendingIntent = PendingIntent.getActivity( + context, + DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT + ) + val notificationId = notificationId + count + notificationIds.add(notificationId) + count++ + val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_download) + .setContentText(null) + .setContentTitle(downloadComplete) + .setWhen(System.currentTimeMillis()) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setGroup(NOTIF_GROUP_NAME + "_" + id) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent(pendingIntent) + .addAction(R.drawable.ic_delete, + context.getString(R.string.delete), + DeleteImageIntentService.pendingIntent(context, filePath, notificationId)) + if (bitmap != null) { + builder.setLargeIcon(bitmap) + .setStyle(NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null)) + .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) + } + notifications.add(builder) + } + var summaryNotification: Notification? = null + if (urlToFilePathMap.size != 1) { + val text = "Downloaded " + urlToFilePathMap.size + " items" + summaryNotification = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setContentTitle("Downloaded") + .setContentText(text) + .setSmallIcon(R.drawable.ic_download) + .setStyle(NotificationCompat.InboxStyle().setSummaryText(text)) + .setGroup(NOTIF_GROUP_NAME + "_" + id) + .setGroupSummary(true) + .build() + } + for (i in notifications.indices) { + val builder = notifications[i] + // only make sound and vibrate for the last notification + if (i != notifications.size - 1) { + builder.setSound(null) + .setVibrate(null) + } + notificationManager.notify(notificationIds[i], builder.build()) + } + if (summaryNotification != null) { + notificationManager.notify(notificationId + count, summaryNotification) + } + } + + private fun getThumbnail( + context: Context, + file: File, + uri: Uri, + contentResolver: ContentResolver, + ): Bitmap? { + val mimeType = Utils.getMimeType(uri, contentResolver) + if (isEmpty(mimeType)) return null + var bitmap: Bitmap? = null + if (mimeType.startsWith("image")) { + try { + val bitmapResult = BitmapUtils.getBitmapResult( + context.contentResolver, + uri, + BitmapUtils.THUMBNAIL_SIZE, + BitmapUtils.THUMBNAIL_SIZE, + -1f, + true + ) ?: return null + bitmap = bitmapResult.bitmap + } catch (e: Exception) { + Log.e(TAG, "", e) + } + return bitmap + } + if (mimeType.startsWith("video")) { + try { + val retriever = MediaMetadataRetriever() + bitmap = try { + try { + retriever.setDataSource(context, uri) + } catch (e: Exception) { + retriever.setDataSource(file.absolutePath) + } + retriever.frameAtTime + } finally { + try { + retriever.release() + } catch (e: Exception) { + Log.e(TAG, "getThumbnail: ", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "", e) + } + } + return bitmap + } + + class DownloadRequest private constructor(val urlToFilePathMap: Map) { + + class Builder { + private var urlToFilePathMap: MutableMap = mutableMapOf() + fun setUrlToFilePathMap(urlToFilePathMap: MutableMap): Builder { + this.urlToFilePathMap = urlToFilePathMap + return this + } + + fun addUrl(url: String, filePath: String): Builder { + urlToFilePathMap[url] = filePath + return this + } + + fun build(): DownloadRequest { + return DownloadRequest(urlToFilePathMap) + } + } + + companion object { + @JvmStatic + fun builder(): Builder { + return Builder() + } + } + } + + companion object { + const val PROGRESS = "PROGRESS" + const val URL = "URL" + const val KEY_DOWNLOAD_REQUEST_JSON = "download_request_json" + private const val DOWNLOAD_GROUP = "DOWNLOAD_GROUP" + private const val DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE = 2020 + private const val DELETE_IMAGE_REQUEST_CODE = 2030 + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorites.xml b/app/src/main/res/layout/fragment_favorites.xml index d437e6e7..fa45a0ce 100644 --- a/app/src/main/res/layout/fragment_favorites.xml +++ b/app/src/main/res/layout/fragment_favorites.xml @@ -4,4 +4,5 @@ android:id="@+id/favorite_list" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="?attr/actionBarSize" tools:listitem="@layout/item_search_result" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_search.xml b/app/src/main/res/layout/fragment_user_search.xml index 2fecdb54..7309f05e 100644 --- a/app/src/main/res/layout/fragment_user_search.xml +++ b/app/src/main/res/layout/fragment_user_search.xml @@ -37,15 +37,16 @@ app:layout_constraintBottom_toTopOf="@id/done" app:layout_constraintTop_toBottomOf="@id/group" /> - \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index bd0eefcd..dce30b6a 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -211,6 +211,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/discover_nav_graph.xml b/app/src/main/res/navigation/discover_nav_graph.xml index 49a07187..d63d7a7c 100644 --- a/app/src/main/res/navigation/discover_nav_graph.xml +++ b/app/src/main/res/navigation/discover_nav_graph.xml @@ -150,6 +150,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/feed_nav_graph.xml b/app/src/main/res/navigation/feed_nav_graph.xml index 2914b6d1..f770216f 100644 --- a/app/src/main/res/navigation/feed_nav_graph.xml +++ b/app/src/main/res/navigation/feed_nav_graph.xml @@ -154,6 +154,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/hashtag_nav_graph.xml b/app/src/main/res/navigation/hashtag_nav_graph.xml index 414de4be..55937cbd 100644 --- a/app/src/main/res/navigation/hashtag_nav_graph.xml +++ b/app/src/main/res/navigation/hashtag_nav_graph.xml @@ -125,6 +125,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/location_nav_graph.xml b/app/src/main/res/navigation/location_nav_graph.xml index f359306c..3828e5c4 100644 --- a/app/src/main/res/navigation/location_nav_graph.xml +++ b/app/src/main/res/navigation/location_nav_graph.xml @@ -124,6 +124,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/more_nav_graph.xml b/app/src/main/res/navigation/more_nav_graph.xml index 55877561..6e29723c 100644 --- a/app/src/main/res/navigation/more_nav_graph.xml +++ b/app/src/main/res/navigation/more_nav_graph.xml @@ -172,5 +172,5 @@ + android:label="@string/post" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/notification_viewer_nav_graph.xml b/app/src/main/res/navigation/notification_viewer_nav_graph.xml index 67b63937..83833a83 100644 --- a/app/src/main/res/navigation/notification_viewer_nav_graph.xml +++ b/app/src/main/res/navigation/notification_viewer_nav_graph.xml @@ -121,6 +121,6 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml index 1f6c0c8e..e5a8e87e 100644 --- a/app/src/main/res/navigation/profile_nav_graph.xml +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -177,7 +177,7 @@ \ No newline at end of file diff --git a/app/src/main/res/navigation/saved_nav_graph.xml b/app/src/main/res/navigation/saved_nav_graph.xml index 088889c7..dba1cb54 100644 --- a/app/src/main/res/navigation/saved_nav_graph.xml +++ b/app/src/main/res/navigation/saved_nav_graph.xml @@ -114,6 +114,6 @@ \ No newline at end of file diff --git a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt new file mode 100644 index 00000000..32013e9d --- /dev/null +++ b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt @@ -0,0 +1,18 @@ +package awais.instagrabber.viewmodels + +import androidx.lifecycle.SavedStateHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class ProfileFragmentViewModelTest { + @Test + fun testNoUsernameNoCurrentUser() { + val state = SavedStateHandle(mutableMapOf( + "username" to "" + )) + val viewModel = ProfileFragmentViewModel(state) + + } +} \ No newline at end of file