diff --git a/app/build.gradle b/app/build.gradle index 815ff708..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 { @@ -190,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 @@ -231,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 f7490120..10ac6278 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt @@ -49,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.* @@ -61,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 @@ -71,6 +69,7 @@ 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 @@ -92,8 +91,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL var currentTabs: List = emptyList() private set private var showBottomViewDestinations: List = emptyList() - private var graphQLService: GraphQLService? = null - private var mediaService: MediaService? = null private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { @@ -637,42 +634,32 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL .setCancelable(false) .setView(R.layout.dialog_opening_post) .create() - if (graphQLService == null) graphQLService = GraphQLService.getInstance() - if (mediaService == null) { - mediaService = deviceUuid?.let { csrfToken?.let { it1 -> MediaService.getInstance(it, it1, userId) } } - } - val postCb: ServiceCallback = object : ServiceCallback { - override fun onSuccess(feedModel: Media?) { - if (feedModel != null) { - 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() - } - } - alertDialog.show() - if (isLoggedIn) { - lifecycleScope.launch(Dispatchers.IO) { - try { - val media = mediaService?.fetch(shortcodeToId(shortCode)) - postCb.onSuccess(media) - } catch (e: Exception) { - postCb.onFailure(e) + } + } catch (e: Exception) { + Log.e(TAG, "showPostView: ", e) + } finally { + withContext(Dispatchers.Main) { + alertDialog.dismiss() } } - } else { - graphQLService?.fetchPost(shortCode, postCb) } } 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..4a9e356d --- /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): List + + @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/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/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/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 969a1e87..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); } @@ -235,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); } }; @@ -245,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; } }); @@ -253,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); @@ -269,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(); } @@ -337,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; @@ -354,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..1d141437 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; @@ -55,7 +54,6 @@ 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 +61,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 +72,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 +219,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); - } - - @Override - public void onFailure(final Throwable t) { - opening = false; - Log.e(TAG, "Error", t); + graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> { + opening = false; + if (throwable != null) { + Log.e(TAG, "Error", throwable); + return; } - }); + if (media == null) return; + AppExecutors.INSTANCE.getMainThread().execute(() -> openPostDialog(media, profilePicView, mainPostImage, position)); + }, Dispatchers.getIO())); return; } opening = true; @@ -303,8 +299,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 +381,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() { @@ -578,24 +580,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 a58016f3..57ca0761 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -109,11 +109,11 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme final String cookie = settingsHelper.getString(Constants.COOKIE); final long userId = CookieUtils.getUserIdFromCookie(cookie); isLoggedIn = !TextUtils.isEmpty(cookie) && userId != 0; - final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); + // final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); if (csrfToken == null) return; - mediaService = isLoggedIn ? MediaService.getInstance(deviceUuid, csrfToken, userId) : null; - graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + mediaService = isLoggedIn ? MediaService.INSTANCE : null; + graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE; // setHasOptionsMenu(true); } @@ -135,7 +135,17 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme public void onRefresh() { if (isComment && !isLoggedIn) { lazyLoader.resetState(); - graphQLService.fetchCommentLikers(postId, null, anonCb); + 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, @@ -164,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..df0ff4eb 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; @@ -53,14 +52,15 @@ 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 +68,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 +209,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 +292,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 +401,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() { @@ -577,22 +585,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 8271f9af..52570afe 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -34,7 +34,6 @@ 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.notification.Notification; import awais.instagrabber.repositories.responses.notification.NotificationArgs; import awais.instagrabber.repositories.responses.notification.NotificationImage; @@ -68,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 @@ -168,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; } }; @@ -219,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(); } - 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); - mediaService = MediaService.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 4d5488f4..9a6ab1f0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -87,7 +87,6 @@ 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.StoryStickerResponse; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -113,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; @@ -148,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(deviceId, csrfToken, userIdFromCookie); - directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); + storiesService = StoriesService.INSTANCE; + mediaService = MediaService.INSTANCE; + directMessagesService = DirectMessagesService.INSTANCE; setHasOptionsMenu(true); } @@ -220,6 +222,9 @@ public class StoryViewerFragment extends Fragment { .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) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { @@ -229,6 +234,9 @@ public class StoryViewerFragment extends Fragment { return; } directMessagesService.broadcastStoryReply( + csrfToken, + userId, + deviceId, ThreadIdOrUserIds.of(thread.getThreadId()), input.getText().toString(), currentStory.getStoryMediaId(), @@ -514,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(); @@ -550,27 +561,30 @@ public class StoryViewerFragment extends Fragment { .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(); @@ -605,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) @@ -673,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(); @@ -786,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; @@ -840,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) { @@ -944,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.kt b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt index 069ebe09..74b56d7a 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.kt @@ -296,8 +296,8 @@ class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback usersAdapter = DirectUsersAdapter( inviter?.pk ?: -1, { _: Int, user: User, _: Boolean -> - if (isEmpty(user.username) && !isEmpty(user.fbId)) { - Utils.openURL(context, "https://facebook.com/" + user.fbId) + if (user.username.isBlank() && !user.interopMessagingUserFbid.isNullOrBlank()) { + Utils.openURL(context, "https://facebook.com/" + user.interopMessagingUserFbid) return@DirectUsersAdapter } if (isEmpty(user.username)) return@DirectUsersAdapter 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 6e115d70..16e12d5d 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java @@ -31,8 +31,6 @@ import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.discover.TopicCluster; import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse; import awais.instagrabber.utils.AppExecutors; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.TopicClusterViewModel; @@ -57,11 +55,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); discoverService = DiscoverService.getInstance(); - final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); - final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - final long userId = CookieUtils.getUserIdFromCookie(cookie); - mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); + // 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 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 10365ef1..05676d67 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -62,7 +62,6 @@ 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; @@ -74,12 +73,10 @@ 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; @@ -93,6 +90,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; @@ -139,6 +137,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 @@ -156,7 +162,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)) { @@ -318,11 +323,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) { @@ -330,20 +330,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(deviceUuid, csrfToken, myId) : 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); } @@ -373,6 +374,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; @@ -430,7 +432,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) { @@ -448,25 +450,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(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error while performing " + action, t); + CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + if (throwable != null) { + Log.e(TAG, "Error while performing " + action, throwable); + return; } - }); + // 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) { @@ -481,25 +496,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); @@ -583,65 +630,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() { @@ -856,7 +889,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) @@ -977,7 +1010,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()); @@ -993,69 +1026,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 -> { @@ -1078,6 +1142,9 @@ 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) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { @@ -1120,7 +1187,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) { + 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; + })); + } + 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 Context context1 = getContext(); - final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1); - if (preference == null) return; - accountCategory.addPreference(preference); + 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); } - // 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(); - 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; - })); - } - } - }); + }), 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 9630151d..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) @@ -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 c23707f4..52bff758 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt @@ -19,8 +19,6 @@ 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 @@ -31,10 +29,10 @@ 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 @@ -45,17 +43,16 @@ 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 @@ -64,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? -> @@ -135,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 @@ -163,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) @@ -355,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, @@ -410,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 @@ -455,8 +452,11 @@ class ThreadManager private constructor( "4", null ) - mediaService.uploadFinish(uploadFinishOptions) - val broadcastResponse = service.broadcastVoice( + MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) + val broadcastResponse = DirectMessagesService.broadcastVoice( + csrfToken, + viewerId, + deviceUuid, clientContext, threadIdOrUserIds, uploadDmVoiceOptions.uploadId, @@ -497,7 +497,10 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.broadcastReaction( + DirectMessagesService.broadcastReaction( + csrfToken, + userId, + deviceUuid, clientContext, threadIdOrUserIds, itemId, @@ -534,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) @@ -553,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)) @@ -629,7 +641,7 @@ class ThreadManager private constructor( } scope.launch(Dispatchers.IO) { try { - service.forward( + DirectMessagesService.forward( thread.threadId, itemTypeName, threadId, @@ -648,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) @@ -662,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) @@ -707,9 +719,8 @@ 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)) @@ -719,7 +730,7 @@ class ThreadManager private constructor( if (handleInvalidResponse(data, response)) return@launch val response1 = response.response ?: return@launch val uploadId = response1.optString("upload_id") - val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId) + val response2 = DirectMessagesService.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdOrUserIds, uploadId) parseResponse(response2, data, directItem) } catch (e: Exception) { data.postValue(error(e.message, null)) @@ -779,8 +790,11 @@ class ThreadManager private constructor( "2", VideoOptions(duration / 1000f, emptyList(), 0, false) ) - mediaService.uploadFinish(uploadFinishOptions) - val broadcastResponse = service.broadcastVideo( + MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) + val broadcastResponse = DirectMessagesService.broadcastVideo( + csrfToken, + viewerId, + deviceUuid, clientContext, threadIdOrUserIds, uploadDmVideoOptions.uploadId, @@ -907,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) { } @@ -919,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 } ) @@ -936,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 @@ -971,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()) @@ -999,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 @@ -1029,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 { @@ -1057,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 { @@ -1085,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 { @@ -1113,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 { @@ -1133,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 } @@ -1196,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 } ) @@ -1215,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 } ) @@ -1255,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 { @@ -1283,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 { @@ -1306,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) @@ -1321,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 { @@ -1358,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 @@ -1381,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/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/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/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..3d1a5431 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -42,6 +42,7 @@ 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; @@ -396,33 +397,32 @@ public final class ExportImportUtils { 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/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/PostViewV2ViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt index 543c9db0..4261a64f 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.kt @@ -40,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 @@ -125,11 +128,15 @@ class PostViewV2ViewModel : ViewModel() { fun like(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + 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(mediaId) - updateMediaLikeUnlike(data, liked ?: false) + val liked = MediaService.like(csrfToken!!, viewerId, deviceUuid, mediaId) + updateMediaLikeUnlike(data, liked) } catch (e: Exception) { data.postValue(error(e.message, null)) } @@ -140,11 +147,15 @@ class PostViewV2ViewModel : ViewModel() { fun unlike(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + 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(mediaId) - updateMediaLikeUnlike(data, unliked ?: false) + val unliked = MediaService.unlike(csrfToken!!, viewerId, deviceUuid, mediaId) + updateMediaLikeUnlike(data, unliked) } catch (e: Exception) { data.postValue(error(e.message, null)) } @@ -185,11 +196,15 @@ class PostViewV2ViewModel : ViewModel() { fun save(collection: String?, ignoreSaveState: Boolean): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + 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(mediaId, collection) - getSaveUnsaveCallback(data, saved ?: false, ignoreSaveState) + val saved = MediaService.save(csrfToken!!, viewerId, deviceUuid, mediaId, collection) + getSaveUnsaveCallback(data, saved, ignoreSaveState) } catch (e: Exception) { data.postValue(error(e.message, null)) } @@ -200,10 +215,14 @@ class PostViewV2ViewModel : ViewModel() { fun unsave(): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + 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(mediaId) - getSaveUnsaveCallback(data, unsaved ?: false, false) + val unsaved = MediaService.unsave(csrfToken!!, viewerId, deviceUuid, mediaId) + getSaveUnsaveCallback(data, unsaved, false) } return data } @@ -225,11 +244,15 @@ class PostViewV2ViewModel : ViewModel() { fun updateCaption(caption: String): LiveData> { val data = MutableLiveData>() data.postValue(loading(null)) + 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(postId, caption) - if (result != null && result) { + val result = MediaService.editCaption(csrfToken!!, viewerId, deviceUuid, postId, caption) + if (result) { data.postValue(success("")) media.setPostCaption(caption) this@PostViewV2ViewModel.caption.postValue(media.caption) @@ -255,8 +278,8 @@ class PostViewV2ViewModel : ViewModel() { } viewModelScope.launch(Dispatchers.IO) { try { - val result = mediaService?.translate(pk, "1") - if (result.isNullOrBlank()) { + val result = MediaService.translate(pk, "1") + if (result.isBlank()) { data.postValue(error("", null)) return@launch } @@ -280,6 +303,10 @@ 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) { @@ -288,7 +315,7 @@ class PostViewV2ViewModel : ViewModel() { } viewModelScope.launch(Dispatchers.IO) { try { - val response = mediaService?.delete(mediaId, mediaType) + val response = MediaService.delete(csrfToken!!, viewerId, deviceUuid, mediaId, mediaType) if (response == null) { data.postValue(success(Any())) return@launch @@ -317,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/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 = + broadcast(csrfToken, userId, deviceUuid, MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId)) + + private suspend fun broadcast( + csrfToken: String, + userId: Long, + deviceUuid: String, + broadcastOptions: BroadcastOptions, ): DirectThreadBroadcastResponse { - return broadcast(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() + 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.kt b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt index ee128d73..a08747af 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.kt +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.kt @@ -12,11 +12,12 @@ import awais.instagrabber.utils.retryContextString import awais.instagrabber.webservices.RetrofitFactory.retrofit import org.json.JSONObject -class MediaService private constructor( - val deviceUuid: String, - val csrfToken: String, - val userId: Long, -) : BaseService() { +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( @@ -28,15 +29,38 @@ class MediaService private constructor( } else response.items[0] } - suspend fun like(mediaId: String): Boolean = action(mediaId, "like", null) - - suspend fun unlike(mediaId: String): Boolean = action(mediaId, "unlike", null) - - suspend fun save(mediaId: String, collection: String?): Boolean = action(mediaId, "save", collection) + suspend fun like( + csrfToken: String, + userId: Long, + deviceUuid: String, + mediaId: String, + ): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "like", null) - suspend fun unsave(mediaId: String): Boolean = action(mediaId, "unsave", 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?, @@ -60,6 +84,9 @@ class MediaService private constructor( } suspend fun editCaption( + csrfToken: String, + userId: Long, + deviceUuid: String, postId: String, newCaption: String, ): Boolean { @@ -99,7 +126,12 @@ class MediaService private constructor( return jsonObject.optString("translation") } - suspend fun uploadFinish(options: UploadFinishOptions): String { + suspend fun uploadFinish( + csrfToken: String, + userId: Long, + deviceUuid: String, + options: UploadFinishOptions, + ): String { if (options.videoOptions != null) { val videoOptions = options.videoOptions if (videoOptions.clips.isEmpty()) { @@ -124,6 +156,9 @@ class MediaService private constructor( } suspend fun delete( + csrfToken: String, + userId: Long, + deviceUuid: String, postId: String, type: MediaItemType, ): String? { @@ -144,26 +179,4 @@ class MediaService private constructor( } return repository.delete(postId, mediaType, signedForm) } - - companion object { - private val DELETABLE_ITEMS_TYPES = listOf( - MediaItemType.MEDIA_TYPE_IMAGE, - MediaItemType.MEDIA_TYPE_VIDEO, - MediaItemType.MEDIA_TYPE_SLIDER - ) - private lateinit var instance: MediaService - - @JvmStatic - fun getInstance(deviceUuid: String, csrfToken: String, userId: Long): MediaService { - if (!this::instance.isInitialized - || instance.csrfToken != csrfToken - || instance.deviceUuid != deviceUuid - || instance.userId != userId - ) { - instance = MediaService(deviceUuid, csrfToken, userId) - } - return instance - } - } - } \ No newline at end of file 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/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