1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-12-23 05:16:58 +00:00

Merge branch 'austinhuang0131:master' into restore_scroll_favorites

This commit is contained in:
Vonter 2021-06-07 23:02:11 +05:30
commit ae23dd74ba
90 changed files with 4498 additions and 5520 deletions

View File

@ -147,6 +147,11 @@ android {
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
}
testOptions.unitTests {
includeAndroidResources = true
}
}
configurations.all {
@ -172,7 +177,6 @@ dependencies {
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.work:work-runtime:2.5.0"
implementation 'androidx.palette:palette:1.0.0'
implementation 'com.google.guava:guava:27.0.1-android'
@ -191,6 +195,7 @@ dependencies {
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX
@ -204,6 +209,13 @@ dependencies {
implementation "androidx.emoji:emoji:$emoji_compat_version"
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
// Work
def work_version = '2.5.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
implementation 'com.facebook.fresco:fresco:2.3.0'
implementation 'com.facebook.fresco:animated-webp:2.3.0'
implementation 'com.facebook.fresco:webpsupport:2.3.0'
@ -225,6 +237,9 @@ dependencies {
githubImplementation 'io.sentry:sentry-android:4.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
testImplementation "androidx.test.ext:junit-ktx:1.1.2"
testImplementation "androidx.test:core-ktx:1.3.0"
testImplementation "org.robolectric:robolectric:4.5.1"
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
androidTestImplementation 'androidx.test:core:1.3.0'

View File

@ -31,6 +31,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination
@ -48,7 +49,6 @@ import awais.instagrabber.models.IntentModel
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Tab
import awais.instagrabber.models.enums.IntentModelType
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.*
@ -60,7 +60,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectInboxViewModel
import awais.instagrabber.webservices.GraphQLService
import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.google.android.material.appbar.CollapsingToolbarLayout
@ -68,6 +67,9 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.textfield.TextInputLayout
import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterators
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import java.util.stream.Collectors
@ -81,13 +83,14 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private var isActivityCheckerServiceBound = false
private var isBackStackEmpty = false
private var isLoggedIn = false
private var deviceUuid: String? = null
private var csrfToken: String? = null
private var userId: Long = 0
// private var behavior: HideBottomViewOnScrollBehavior<BottomNavigationView>? = null
var currentTabs: List<Tab> = emptyList()
private set
private var showBottomViewDestinations: List<Int> = emptyList<Int>()
private var graphQLService: GraphQLService? = null
private var mediaService: MediaService? = null
private var showBottomViewDestinations: List<Int> = emptyList()
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
@ -157,17 +160,17 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun setupCookie() {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
var userId: Long = 0
var csrfToken: String? = null
if (!isEmpty(cookie)) {
userId = 0
csrfToken = null
if (cookie.isNotBlank()) {
userId = getUserIdFromCookie(cookie)
csrfToken = getCsrfTokenFromCookie(cookie)
}
if (isEmpty(cookie) || userId == 0L || isEmpty(csrfToken)) {
if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
isLoggedIn = false
return
}
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
if (isEmpty(deviceUuid)) {
Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
@ -175,6 +178,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
isLoggedIn = true
}
@Suppress("unused")
private fun initDmService() {
if (!isLoggedIn) return
val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH)
@ -369,7 +373,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
.collect(Collectors.toList())
showBottomViewDestinations = currentTabs.asSequence().map {
it.startDestinationFragmentId
}.toMutableList().apply { add(R.id.postViewFragment) }
}.toMutableList().apply {
add(R.id.postViewFragment)
add(R.id.favoritesFragment)
}
if (setDefaultTabFromSettings) {
setSelectedTab(currentTabs)
} else {
@ -627,30 +634,33 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create()
if (graphQLService == null) graphQLService = GraphQLService.getInstance()
if (mediaService == null) mediaService = MediaService.getInstance(null, null, 0L)
val postCb: ServiceCallback<Media> = object : ServiceCallback<Media> {
override fun onSuccess(feedModel: Media?) {
if (feedModel != null) {
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
alertDialog.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
val media = if (isLoggedIn) MediaService.fetch(shortcodeToId(shortCode)) else GraphQLService.fetchPost(shortCode)
withContext(Dispatchers.Main) {
if (media == null) {
Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
return@withContext
}
val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel)
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media)
try {
navController?.navigate(R.id.action_global_post_view, bundle)
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
}
} else Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
alertDialog.dismiss()
}
override fun onFailure(t: Throwable) {
alertDialog.dismiss()
}
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
} finally {
withContext(Dispatchers.Main) {
alertDialog.dismiss()
}
}
}
alertDialog.show()
if (isLoggedIn) mediaService?.fetch(shortcodeToId(shortCode), postCb) else graphQLService?.fetchPost(shortCode, postCb)
}
private fun showLocationView(intentModel: IntentModel) {

View File

@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableList;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@ -164,7 +165,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
binding.ivProfilePic.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
binding.tvUsername.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
if (messageDirection == MessageDirection.INCOMING && thread.isGroup()) {
final User user = getUser(item.getUserId(), thread.getUsers());
final List<User> allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
final User user = getUser(item.getUserId(), allUsers);
if (user != null) {
binding.tvUsername.setText(user.getUsername());
binding.ivProfilePic.setImageURI(user.getProfilePicUrl());
@ -220,7 +223,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
private void setupReply(final DirectItem item, final MessageDirection messageDirection) {
if (item.getRepliedToMessage() != null) {
setReply(item, messageDirection, thread.getUsers());
final List<User> allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
setReply(item, messageDirection, allUsers);
} else {
binding.quoteLine.setVisibility(View.GONE);
binding.replyContainer.setVisibility(View.GONE);

View File

@ -37,7 +37,7 @@ public class RecipientThreadViewHolder extends RecyclerView.ViewHolder {
final DirectThread thread,
final boolean showSelection,
final boolean isSelected) {
if (thread == null) return;
if (thread == null || thread.getUsers().size() == 0) return;
binding.getRoot().setOnClickListener(v -> {
if (onThreadClickListener == null) return;
onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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<Account> getAllAccounts();
@Query("SELECT * FROM accounts WHERE uid = :uid")
Account findAccountByUid(String uid);
@Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> insertAccounts(Account... accounts);
@Update
void updateAccounts(Account... accounts);
@Delete
void deleteAccounts(Account... accounts);
@Query("DELETE from accounts")
void deleteAllAccounts();
}

View File

@ -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<Account>
@Query("SELECT * FROM accounts WHERE uid = :uid")
suspend fun findAccountByUid(uid: String): Account?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAccounts(vararg accounts: Account)
@Update
suspend fun updateAccounts(vararg accounts: Account)
@Delete
suspend fun deleteAccounts(vararg accounts: Account)
@Query("DELETE from accounts")
suspend fun deleteAllAccounts()
}

View File

@ -1,35 +0,0 @@
package awais.instagrabber.db.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
@Dao
public interface FavoriteDao {
@Query("SELECT * FROM favorites")
List<Favorite> getAllFavorites();
@Query("SELECT * FROM favorites WHERE query_text = :query and type = :type")
Favorite findFavoriteByQueryAndType(String query, FavoriteType type);
@Insert
List<Long> insertFavorites(Favorite... favorites);
@Update
void updateFavorites(Favorite... favorites);
@Delete
void deleteFavorites(Favorite... favorites);
@Query("DELETE from favorites")
void deleteAllFavorites();
}

View File

@ -0,0 +1,26 @@
package awais.instagrabber.db.dao
import androidx.room.*
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.models.enums.FavoriteType
@Dao
interface FavoriteDao {
@Query("SELECT * FROM favorites")
suspend fun getAllFavorites(): List<Favorite>
@Query("SELECT * FROM favorites WHERE query_text = :query and type = :type")
suspend fun findFavoriteByQueryAndType(query: String, type: FavoriteType): Favorite?
@Insert
suspend fun insertFavorites(vararg favorites: Favorite)
@Update
suspend fun updateFavorites(vararg favorites: Favorite)
@Delete
suspend fun deleteFavorites(vararg favorites: Favorite)
@Query("DELETE from favorites")
suspend fun deleteAllFavorites()
}

View File

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

View File

@ -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<Account> = 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
}
}
}

View File

@ -1,62 +0,0 @@
package awais.instagrabber.db.datasources;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import awais.instagrabber.db.AppDatabase;
import awais.instagrabber.db.dao.FavoriteDao;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
public class FavoriteDataSource {
private static final String TAG = FavoriteDataSource.class.getSimpleName();
private static FavoriteDataSource INSTANCE;
private final FavoriteDao favoriteDao;
private FavoriteDataSource(final FavoriteDao favoriteDao) {
this.favoriteDao = favoriteDao;
}
public static synchronized FavoriteDataSource getInstance(@NonNull Context context) {
if (INSTANCE == null) {
synchronized (FavoriteDataSource.class) {
if (INSTANCE == null) {
final AppDatabase database = AppDatabase.getDatabase(context);
INSTANCE = new FavoriteDataSource(database.favoriteDao());
}
}
}
return INSTANCE;
}
@Nullable
public final Favorite getFavorite(@NonNull final String query, @NonNull final FavoriteType type) {
return favoriteDao.findFavoriteByQueryAndType(query, type);
}
@NonNull
public final List<Favorite> getAllFavorites() {
return favoriteDao.getAllFavorites();
}
public final void insertOrUpdateFavorite(@NonNull final Favorite favorite) {
if (favorite.getId() != 0) {
favoriteDao.updateFavorites(favorite);
return;
}
favoriteDao.insertFavorites(favorite);
}
public final void deleteFavorite(@NonNull final String query, @NonNull final FavoriteType type) {
final Favorite favorite = getFavorite(query, type);
if (favorite == null) return;
favoriteDao.deleteFavorites(favorite);
}
}

View File

@ -0,0 +1,44 @@
package awais.instagrabber.db.datasources
import android.content.Context
import awais.instagrabber.db.AppDatabase
import awais.instagrabber.db.dao.FavoriteDao
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.models.enums.FavoriteType
class FavoriteDataSource private constructor(private val favoriteDao: FavoriteDao) {
suspend fun getFavorite(query: String, type: FavoriteType): Favorite? = favoriteDao.findFavoriteByQueryAndType(query, type)
suspend fun getAllFavorites(): List<Favorite> = favoriteDao.getAllFavorites()
suspend fun insertOrUpdateFavorite(favorite: Favorite) {
if (favorite.id != 0) {
favoriteDao.updateFavorites(favorite)
return
}
favoriteDao.insertFavorites(favorite)
}
suspend fun deleteFavorite(query: String, type: FavoriteType) {
val favorite = getFavorite(query, type) ?: return
favoriteDao.deleteFavorites(favorite)
}
companion object {
private lateinit var INSTANCE: FavoriteDataSource
@JvmStatic
@Synchronized
fun getInstance(context: Context): FavoriteDataSource {
if (!this::INSTANCE.isInitialized) {
synchronized(FavoriteDataSource::class.java) {
if (!this::INSTANCE.isInitialized) {
val database = AppDatabase.getDatabase(context)
INSTANCE = FavoriteDataSource(database.favoriteDao())
}
}
}
return INSTANCE
}
}
}

View File

@ -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<Account> 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<Account> 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<List<Account>> callback) {
// request on the I/O thread
appExecutors.getDiskIO().execute(() -> {
final List<Account> 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<Account> accounts,
final RepositoryCallback<Void> 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<Account> 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<Void> 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<Void> 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);
});
});
}
}

View File

@ -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<Account> = accountDataSource.getAllAccounts()
suspend fun insertOrUpdateAccounts(accounts: List<Account>) {
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
}
}
}

View File

@ -1,88 +0,0 @@
package awais.instagrabber.db.repositories;
import java.util.List;
import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.AppExecutors;
public class FavoriteRepository {
private static final String TAG = FavoriteRepository.class.getSimpleName();
private static FavoriteRepository instance;
private final AppExecutors appExecutors;
private final FavoriteDataSource favoriteDataSource;
private FavoriteRepository(final AppExecutors appExecutors, final FavoriteDataSource favoriteDataSource) {
this.appExecutors = appExecutors;
this.favoriteDataSource = favoriteDataSource;
}
public static FavoriteRepository getInstance(final FavoriteDataSource favoriteDataSource) {
if (instance == null) {
instance = new FavoriteRepository(AppExecutors.INSTANCE, favoriteDataSource);
}
return instance;
}
public void getFavorite(final String query, final FavoriteType type, final RepositoryCallback<Favorite> callback) {
// request on the I/O thread
appExecutors.getDiskIO().execute(() -> {
final Favorite favorite = favoriteDataSource.getFavorite(query, type);
// notify on the main thread
appExecutors.getMainThread().execute(() -> {
if (callback == null) return;
if (favorite == null) {
callback.onDataNotAvailable();
return;
}
callback.onSuccess(favorite);
});
});
}
public void getAllFavorites(final RepositoryCallback<List<Favorite>> callback) {
// request on the I/O thread
appExecutors.getDiskIO().execute(() -> {
final List<Favorite> favorites = favoriteDataSource.getAllFavorites();
// notify on the main thread
appExecutors.getMainThread().execute(() -> {
if (callback == null) return;
if (favorites == null) {
callback.onDataNotAvailable();
return;
}
callback.onSuccess(favorites);
});
});
}
public void insertOrUpdateFavorite(final Favorite favorite,
final RepositoryCallback<Void> callback) {
// request on the I/O thread
appExecutors.getDiskIO().execute(() -> {
favoriteDataSource.insertOrUpdateFavorite(favorite);
// notify on the main thread
appExecutors.getMainThread().execute(() -> {
if (callback == null) return;
callback.onSuccess(null);
});
});
}
public void deleteFavorite(final String query,
final FavoriteType type,
final RepositoryCallback<Void> callback) {
// request on the I/O thread
appExecutors.getDiskIO().execute(() -> {
favoriteDataSource.deleteFavorite(query, type);
// notify on the main thread
appExecutors.getMainThread().execute(() -> {
if (callback == null) return;
callback.onSuccess(null);
});
});
}
}

View File

@ -0,0 +1,28 @@
package awais.instagrabber.db.repositories
import awais.instagrabber.db.datasources.FavoriteDataSource
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.models.enums.FavoriteType
class FavoriteRepository private constructor(private val favoriteDataSource: FavoriteDataSource) {
suspend fun getFavorite(query: String, type: FavoriteType): Favorite? = favoriteDataSource.getFavorite(query, type)
suspend fun getAllFavorites(): List<Favorite> = favoriteDataSource.getAllFavorites()
suspend fun insertOrUpdateFavorite(favorite: Favorite) = favoriteDataSource.insertOrUpdateFavorite(favorite)
suspend fun deleteFavorite(query: String, type: FavoriteType) = favoriteDataSource.deleteFavorite(query, type)
companion object {
private lateinit var instance: FavoriteRepository
@JvmStatic
fun getInstance(favoriteDataSource: FavoriteDataSource): FavoriteRepository {
if (!this::instance.isInitialized) {
instance = FavoriteRepository(favoriteDataSource)
}
return instance
}
}
}

View File

@ -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<Void>() {
@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<List<Account>>() {
@Override
public void onSuccess(final List<Account> 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<Account> copy = new ArrayList<>(accounts);
sortUserList(cookie, copy);
adapter.submitList(copy);
}), Dispatchers.getIO())
);
binding.addAccountBtn.setOnClickListener(v -> {
if (onAddAccountClickListener == null) return;
onAddAccountClickListener.onAddAccountClick(this);

View File

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

View File

@ -30,9 +30,12 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentFollowersViewerBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import thoughtbot.expandableadapter.ExpandableGroup;
public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
@ -68,10 +71,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
if (!isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipService.getList(false, profileId, endCursor, this);
friendshipService.getList(
false,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0) {
if (!isFollowersList) moreAvailable = false;
friendshipService.getList(true, profileId, null, followingFetchCb);
friendshipService.getList(
true,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (!isFollowersList) moreAvailable = false;
showCompare();
@ -84,8 +109,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, following)", t);
}
};
@ -97,10 +121,32 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
if (isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipService.getList(true, profileId, endCursor, this);
friendshipService.getList(
true,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followingModels.size() == 0) {
if (isFollowersList) moreAvailable = false;
friendshipService.getList(false, profileId, null, followingFetchCb);
friendshipService.getList(
false,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (isFollowersList) moreAvailable = false;
showCompare();
@ -113,8 +159,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, follower)", t);
}
};
@ -122,7 +167,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
friendshipService = FriendshipService.getInstance(null, null, 0);
friendshipService = FriendshipService.INSTANCE;
fragmentActivity = (AppCompatActivity) getActivity();
setHasOptionsMenu(true);
}
@ -174,6 +219,13 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
onRefresh();
}
@Override
public void onResume() {
super.onResume();
setTitle(username);
setSubtitle(type);
}
private void setTitle(final String title) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
@ -228,8 +280,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (single)", t);
}
};
@ -238,7 +289,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
if (!TextUtils.isEmpty(endCursor) && !searching) {
binding.swipeRefreshLayout.setRefreshing(true);
layoutManager.setStackFromEnd(true);
friendshipService.getList(isFollowersList, profileId, endCursor, cb);
friendshipService.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
endCursor = null;
}
});
@ -246,7 +308,18 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
binding.rvFollow.setLayoutManager(layoutManager);
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
friendshipService.getList(isFollowersList, profileId, endCursor, cb);
friendshipService.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(0);
@ -262,17 +335,34 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipService.getList(isFollowersList,
profileId,
endCursor,
isFollowersList ? followersFetchCb : followingFetchCb);
friendshipService.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> 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<FriendshipListFetchResponse> callback = isFollowersList ? followingFetchCb : followersFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO()));
} else showCompare();
}
@ -330,10 +420,10 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
final Context context = getContext();
if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show();
else if (isCompare) {
isCompare = !isCompare;
isCompare = false;
listFollows();
} else {
isCompare = !isCompare;
isCompare = true;
listCompare();
}
return true;
@ -347,16 +437,15 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
final ArrayList<ExpandableGroup> 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);

View File

@ -39,7 +39,6 @@ import com.google.android.material.snackbar.Snackbar;
import com.google.common.collect.ImmutableList;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import awais.instagrabber.R;
@ -52,10 +51,8 @@ import awais.instagrabber.databinding.LayoutHashtagDetailsBinding;
import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.FollowingType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
@ -63,8 +60,10 @@ import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
@ -72,6 +71,7 @@ import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService;
import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
@ -218,20 +218,15 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (TextUtils.isEmpty(user.getUsername())) {
// this only happens for anons
opening = true;
graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media newFeedModel) {
opening = false;
if (newFeedModel == null) return;
openPostDialog(newFeedModel, profilePicView, mainPostImage, position);
graphQLService.fetchPost(feedModel.getCode(), CoroutineUtilsKt.getContinuation((media, throwable) -> {
opening = false;
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
}
@Override
public void onFailure(final Throwable t) {
opening = false;
Log.e(TAG, "Error", t);
}
});
if (media == null) return;
AppExecutors.INSTANCE.getMainThread().execute(() -> openPostDialog(media, profilePicView, mainPostImage, position));
}, Dispatchers.getIO()));
return;
}
opening = true;
@ -303,8 +298,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
tagsService = isLoggedIn ? TagsService.getInstance() : null;
storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
storiesService = isLoggedIn ? StoriesService.INSTANCE : null;
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
setHasOptionsMenu(true);
}
@ -385,7 +380,13 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
private void fetchHashtagModel() {
binding.swipeRefreshLayout.setRefreshing(true);
if (isLoggedIn) tagsService.fetch(hashtag, cb);
else graphQLService.fetchTag(hashtag, cb);
else graphQLService.fetchTag(hashtag, CoroutineUtilsKt.getContinuation((hashtag1, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> cb.onSuccess(hashtag1));
}, Dispatchers.getIO()));
}
private void setupPosts() {
@ -478,73 +479,81 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
final Context context = getContext();
if (context == null) return;
final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
favoriteRepository.getFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
favoriteRepository.insertOrUpdateFavorite(new Favorite(
result.getId(),
hashtag,
FavoriteType.HASHTAG,
hashtagModel.getName(),
"res:/" + R.drawable.ic_hashtag,
result.getDateAdded()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
hashtagDetailsBinding.favChip.setText(R.string.favorite_short);
favoriteRepository.getFavorite(
hashtag,
FavoriteType.HASHTAG,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null || favorite == null) {
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites);
return;
}
@Override
public void onDataNotAvailable() {}
});
}
@Override
public void onDataNotAvailable() {
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites);
}
});
hashtagDetailsBinding.favChip.setOnClickListener(
v -> favoriteRepository.getFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
favoriteRepository.deleteFavorite(hashtag, FavoriteType.HASHTAG, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
favorite.getId(),
hashtag,
FavoriteType.HASHTAG,
hashtagModel.getName(),
"res:/" + R.drawable.ic_hashtag,
favorite.getDateAdded()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
return;
}
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
hashtagDetailsBinding.favChip.setText(R.string.favorite_short);
}), Dispatchers.getIO())
);
}), Dispatchers.getIO())
);
hashtagDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite(
hashtag,
FavoriteType.HASHTAG,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "setHashtagDetails: ", throwable);
return;
}
if (favorite == null) {
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
0,
hashtag,
FavoriteType.HASHTAG,
hashtagModel.getName(),
"res:/" + R.drawable.ic_hashtag,
LocalDateTime.now()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onDataNotAvailable: ", throwable1);
return;
}
hashtagDetailsBinding.favChip.setText(R.string.favorite_short);
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}), Dispatchers.getIO())
);
return;
}
favoriteRepository.deleteFavorite(
hashtag,
FavoriteType.HASHTAG,
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
return;
}
hashtagDetailsBinding.favChip.setText(R.string.add_to_favorites);
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
showSnackbar(getString(R.string.removed_from_favs));
}
@Override
public void onDataNotAvailable() {}
});
}
@Override
public void onDataNotAvailable() {
favoriteRepository.insertOrUpdateFavorite(new Favorite(
0,
hashtag,
FavoriteType.HASHTAG,
hashtagModel.getName(),
"res:/" + R.drawable.ic_hashtag,
LocalDateTime.now()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
hashtagDetailsBinding.favChip.setText(R.string.favorite_short);
hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}
@Override
public void onDataNotAvailable() {}
});
}
}));
}), Dispatchers.getIO())
);
}), Dispatchers.getIO())
)
);
hashtagDetailsBinding.mainHashtagImage.setImageURI("res:/" + R.drawable.ic_hashtag);
final String postCount = String.valueOf(hashtagModel.getMediaCount());
final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline,
@ -578,24 +587,21 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
storiesFetching = true;
storiesService.getUserStory(
StoryViewerOptions.forHashtag(hashtagModel.getName()),
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> 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() {

View File

@ -25,12 +25,15 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentLikesBinding;
import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -104,10 +107,13 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
// final AppCompatActivity fragmentActivity = (AppCompatActivity) getActivity();
mediaService = isLoggedIn ? MediaService.getInstance(null, null, 0) : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
final long userId = CookieUtils.getUserIdFromCookie(cookie);
isLoggedIn = !TextUtils.isEmpty(cookie) && userId != 0;
// final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (csrfToken == null) return;
mediaService = isLoggedIn ? MediaService.INSTANCE : null;
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
// setHasOptionsMenu(true);
}
@ -129,8 +135,31 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
public void onRefresh() {
if (isComment && !isLoggedIn) {
lazyLoader.resetState();
graphQLService.fetchCommentLikers(postId, null, anonCb);
} else mediaService.fetchLikes(postId, isComment, cb);
graphQLService.fetchCommentLikers(
postId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
anonCb.onFailure(throwable);
return;
}
anonCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
mediaService.fetchLikes(
postId,
isComment,
CoroutineUtilsKt.getContinuation((users, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
//noinspection unchecked
cb.onSuccess((List<User>) users);
}), Dispatchers.getIO())
);
}
}
private void init() {
@ -145,8 +174,19 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
binding.rvLikes.setLayoutManager(layoutManager);
binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL));
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!TextUtils.isEmpty(endCursor))
graphQLService.fetchCommentLikers(postId, endCursor, anonCb);
if (!TextUtils.isEmpty(endCursor)) {
graphQLService.fetchCommentLikers(
postId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
anonCb.onFailure(throwable);
return;
}
anonCb.onSuccess(response);
}), Dispatchers.getIO())
);
}
endCursor = null;
});
binding.rvLikes.addOnScrollListener(lazyLoader);

View File

@ -37,7 +37,6 @@ import com.google.android.material.snackbar.Snackbar;
import com.google.common.collect.ImmutableList;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import awais.instagrabber.R;
@ -50,17 +49,17 @@ import awais.instagrabber.databinding.LayoutLocationDetailsBinding;
import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
@ -68,6 +67,7 @@ import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService;
import kotlinx.coroutines.Dispatchers;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
@ -208,20 +208,18 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
if (user == null) return;
if (TextUtils.isEmpty(user.getUsername())) {
opening = true;
graphQLService.fetchPost(feedModel.getCode(), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media newFeedModel) {
opening = false;
if (newFeedModel == null) return;
openPostDialog(newFeedModel, profilePicView, mainPostImage, position);
}
@Override
public void onFailure(final Throwable t) {
opening = false;
Log.e(TAG, "Error", t);
}
});
graphQLService.fetchPost(
feedModel.getCode(),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
opening = false;
if (throwable != null) {
Log.e(TAG, "Error", throwable);
return;
}
if (media == null) return;
openPostDialog(media, profilePicView, mainPostImage, position);
}))
);
return;
}
opening = true;
@ -293,8 +291,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
locationService = isLoggedIn ? LocationService.getInstance() : null;
storiesService = StoriesService.getInstance(null, 0L, null);
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
storiesService = StoriesService.INSTANCE;
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
setHasOptionsMenu(true);
}
@ -402,7 +400,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
private void fetchLocationModel() {
binding.swipeRefreshLayout.setRefreshing(true);
if (isLoggedIn) locationService.fetch(locationId, cb);
else graphQLService.fetchLocation(locationId, cb);
else graphQLService.fetchLocation(
locationId,
CoroutineUtilsKt.getContinuation((location, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(location);
}))
);
}
private void setupLocationDetails() {
@ -485,75 +492,82 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final FavoriteDataSource dataSource = FavoriteDataSource.getInstance(context);
final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(dataSource);
locationDetailsBinding.favChip.setVisibility(View.VISIBLE);
favoriteRepository.getFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
locationDetailsBinding.favChip.setText(R.string.favorite_short);
favoriteRepository.insertOrUpdateFavorite(new Favorite(
result.getId(),
String.valueOf(locationId),
FavoriteType.LOCATION,
locationModel.getName(),
"res:/" + R.drawable.ic_location,
result.getDateAdded()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {}
@Override
public void onDataNotAvailable() {}
});
}
@Override
public void onDataNotAvailable() {
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
locationDetailsBinding.favChip.setText(R.string.add_to_favorites);
}
});
locationDetailsBinding.favChip.setOnClickListener(v -> {
favoriteRepository.getFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
favoriteRepository.deleteFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
locationDetailsBinding.favChip.setText(R.string.add_to_favorites);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
showSnackbar(getString(R.string.removed_from_favs));
}
@Override
public void onDataNotAvailable() {}
});
}
@Override
public void onDataNotAvailable() {
favoriteRepository.insertOrUpdateFavorite(new Favorite(
0,
favoriteRepository.getFavorite(
String.valueOf(locationId),
FavoriteType.LOCATION,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null || favorite == null) {
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
locationDetailsBinding.favChip.setText(R.string.add_to_favorites);
Log.e(TAG, "setupLocationDetails: ", throwable);
return;
}
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
locationDetailsBinding.favChip.setText(R.string.favorite_short);
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
favorite.getId(),
String.valueOf(locationId),
FavoriteType.LOCATION,
locationModel.getName(),
"res:/" + R.drawable.ic_location,
favorite.getDateAdded()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
}
}), Dispatchers.getIO())
);
}), Dispatchers.getIO())
);
locationDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite(
String.valueOf(locationId),
FavoriteType.LOCATION,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "setupLocationDetails: ", throwable);
return;
}
if (favorite == null) {
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
0,
String.valueOf(locationId),
FavoriteType.LOCATION,
locationModel.getName(),
"res:/" + R.drawable.ic_location,
LocalDateTime.now()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onDataNotAvailable: ", throwable1);
return;
}
locationDetailsBinding.favChip.setText(R.string.favorite_short);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}), Dispatchers.getIO())
);
return;
}
favoriteRepository.deleteFavorite(
String.valueOf(locationId),
FavoriteType.LOCATION,
locationModel.getName(),
"res:/" + R.drawable.ic_location,
LocalDateTime.now()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
locationDetailsBinding.favChip.setText(R.string.favorite_short);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}
@Override
public void onDataNotAvailable() {}
});
}
});
});
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
return;
}
locationDetailsBinding.favChip.setText(R.string.add_to_favorites);
locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
showSnackbar(getString(R.string.removed_from_favs));
}), Dispatchers.getIO())
);
}), Dispatchers.getIO())
));
locationDetailsBinding.mainLocationImage.setOnClickListener(v -> {
if (hasStories) {
// show stories
@ -577,22 +591,19 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
storiesFetching = true;
storiesService.getUserStory(
StoryViewerOptions.forLocation(locationId, locationModel.getName()),
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> 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())
);
}
}

View File

@ -34,13 +34,13 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe
import awais.instagrabber.databinding.FragmentNotificationsViewerBinding;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.FriendshipChangeResponse;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.notification.Notification;
import awais.instagrabber.repositories.responses.notification.NotificationArgs;
import awais.instagrabber.repositories.responses.notification.NotificationImage;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.NotificationViewModel;
@ -48,6 +48,7 @@ import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -66,6 +67,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
private String type;
private long targetId;
private Context context;
private long userId;
private final ServiceCallback<List<Notification>> cb = new ServiceCallback<List<Notification>>() {
@Override
@ -106,26 +108,25 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
mediaService.fetch(mediaId, new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media feedModel) {
final NavController navController = NavHostFragment.findNavController(NotificationsViewerFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "onSuccess: ", e);
}
}
@Override
public void onFailure(final Throwable t) {
alertDialog.dismiss();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
});
mediaService.fetch(
mediaId,
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
alertDialog.dismiss();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
final NavController navController = NavHostFragment.findNavController(NotificationsViewerFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "onSuccess: ", e);
}
}), Dispatchers.getIO())
);
}
}
@ -167,34 +168,40 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
break;
case 1:
if (model.getType() == NotificationType.REQUEST) {
friendshipService.approve(args.getUserId(), new ServiceCallback<FriendshipChangeResponse>() {
@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<FriendshipChangeResponse>() {
@Override
public void onSuccess(final FriendshipChangeResponse result) {
onRefresh();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "ignore: onFailure: ", t);
}
});
friendshipService.ignore(
csrfToken,
userId,
deviceUuid,
args.getUserId(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "approve: onFailure: ", throwable);
return;
}
onRefresh();
}), Dispatchers.getIO())
);
break;
}
};
@ -218,11 +225,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
if (TextUtils.isEmpty(cookie)) {
Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show();
}
mediaService = MediaService.getInstance(null, null, 0);
final long userId = CookieUtils.getUserIdFromCookie(cookie);
userId = CookieUtils.getUserIdFromCookie(cookie);
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId);
friendshipService = FriendshipService.INSTANCE;
mediaService = MediaService.INSTANCE;
newsService = NewsService.getInstance();
}

View File

@ -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<List<FeedStoryModel>>() {
@Override
public void onSuccess(final List<FeedStoryModel> 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<FeedStoryModel>) feedStoryModels);
//noinspection unchecked
adapter.submitList((List<FeedStoryModel>) 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())
);
}
}

View File

@ -7,6 +7,8 @@ import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
@ -85,8 +87,7 @@ import awais.instagrabber.models.stickers.SwipeUpModel;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.requests.StoryViewerOptions.Type;
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.StoryStickerResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
@ -111,6 +112,8 @@ import static awais.instagrabber.utils.Utils.settingsHelper;
public class StoryViewerFragment extends Fragment {
private static final String TAG = "StoryViewerFragment";
private final String cookie = settingsHelper.getString(Constants.COOKIE);
private AppCompatActivity fragmentActivity;
private View root;
private FragmentStoryViewerBinding binding;
@ -146,21 +149,22 @@ public class StoryViewerFragment extends Fragment {
// private boolean isArchive;
// private boolean isNotification;
private DirectMessagesService directMessagesService;
private final String cookie = settingsHelper.getString(Constants.COOKIE);
private StoryViewerOptions options;
private String csrfToken;
private String deviceId;
private long userId;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (csrfToken == null) return;
final long userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie);
final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID);
userId = CookieUtils.getUserIdFromCookie(cookie);
deviceId = settingsHelper.getString(Constants.DEVICE_UUID);
fragmentActivity = (AppCompatActivity) requireActivity();
storiesService = StoriesService.getInstance(csrfToken, userIdFromCookie, deviceId);
mediaService = MediaService.getInstance(null, null, 0);
directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId);
storiesService = StoriesService.INSTANCE;
mediaService = MediaService.INSTANCE;
directMessagesService = DirectMessagesService.INSTANCE;
setHasOptionsMenu(true);
}
@ -214,37 +218,58 @@ public class StoryViewerFragment extends Fragment {
if (itemId == R.id.action_dms) {
final EditText input = new EditText(context);
input.setHint(R.string.reply_hint);
new AlertDialog.Builder(context)
final AlertDialog ad = new AlertDialog.Builder(context)
.setTitle(R.string.reply_story)
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.createThread(
csrfToken,
userId,
deviceId,
Collections.singletonList(currentStory.getUserId()),
null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> {
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "onOptionsItemSelected: ", throwable);
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
directMessagesService.broadcastStoryReply(
csrfToken,
userId,
deviceId,
ThreadIdOrUserIds.of(thread.getThreadId()),
input.getText().toString(),
currentStory.getStoryMediaId(),
String.valueOf(currentStory.getUserId()),
CoroutineUtilsKt.getContinuation((directThreadBroadcastResponse, throwable1) -> {
if (throwable1 != null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
Log.e(TAG, "onFailure: ", throwable1);
return;
}
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
}, Dispatchers.getIO())
CoroutineUtilsKt.getContinuation(
(directThreadBroadcastResponse, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
Log.e(TAG, "onFailure: ", throwable1);
return;
}
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
}), Dispatchers.getIO()
)
);
}, Dispatchers.getIO())
}), Dispatchers.getIO())
))
.setNegativeButton(R.string.cancel, null)
.show();
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s));
}
@Override
public void afterTextChanged(final Editable s) {}
});
return true;
}
if (itemId == R.id.action_profile) {
@ -451,26 +476,25 @@ public class StoryViewerFragment extends Fragment {
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
mediaService.fetch(Long.parseLong(mediaId), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media feedModel) {
final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "openPostDialog: ", e);
}
}
@Override
public void onFailure(final Throwable t) {
alertDialog.dismiss();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
});
mediaService.fetch(
Long.parseLong(mediaId),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
alertDialog.dismiss();
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "openPostDialog: ", e);
}
}), Dispatchers.getIO())
);
});
final View.OnClickListener storyActionListener = v -> {
final Object tag = v.getTag();
@ -498,28 +522,31 @@ public class StoryViewerFragment extends Fragment {
}), (d, w) -> {
sticking = true;
storiesService.respondToPoll(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
poll.getId(),
w,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
})
.setPositiveButton(R.string.cancel, null)
.show();
@ -528,36 +555,52 @@ public class StoryViewerFragment extends Fragment {
question = (QuestionModel) tag;
final EditText input = new EditText(context);
input.setHint(R.string.answer_hint);
new AlertDialog.Builder(context)
final AlertDialog ad = new AlertDialog.Builder(context)
.setTitle(question.getQuestion())
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true;
storiesService.respondToQuestion(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
question.getId(),
input.getText().toString(),
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s));
}
@Override
public void afterTextChanged(final Editable s) {}
});
} else if (tag instanceof String[]) {
mentions = (String[]) tag;
new AlertDialog.Builder(context)
@ -576,28 +619,31 @@ public class StoryViewerFragment extends Fragment {
if (quiz.getMyChoice() == -1) {
sticking = true;
storiesService.respondToQuiz(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
quiz.getId(),
w,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}),
Dispatchers.getIO()
)
);
}
})
.setPositiveButton(R.string.cancel, null)
@ -644,28 +690,30 @@ public class StoryViewerFragment extends Fragment {
.setPositiveButton(R.string.confirm, (d, w) -> {
sticking = true;
storiesService.respondToSlider(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId().split("_")[0],
slider.getId(),
sliderValue,
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
@Override
public void onFailure(final Throwable t) {
sticking = false;
Log.e(TAG, "Error responding", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}
});
CoroutineUtilsKt.getContinuation(
(storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
sticking = false;
Log.e(TAG, "Error responding", throwable);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
return;
}
sticking = false;
try {
slider.setMyChoice(sliderValue);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} catch (Exception ignored) {}
}), Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
@ -757,27 +805,26 @@ public class StoryViewerFragment extends Fragment {
setTitle(type);
storiesViewModel.getList().setValue(Collections.emptyList());
if (type == Type.STORY) {
storiesService.fetch(options.getId(), new ServiceCallback<StoryModel>() {
@Override
public void onSuccess(final StoryModel storyModel) {
fetching = false;
binding.storiesList.setVisibility(View.GONE);
if (storyModel == null) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
return;
}
storiesViewModel.getList().setValue(Collections.singletonList(storyModel));
currentStory = storyModel;
refreshStory();
}
@Override
public void onFailure(final Throwable t) {
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", t);
}
});
storiesService.fetch(
options.getId(),
CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error", throwable);
return;
}
fetching = false;
binding.storiesList.setVisibility(View.GONE);
if (storyModel == null) {
storiesViewModel.getList().setValue(Collections.emptyList());
currentStory = null;
return;
}
storiesViewModel.getList().setValue(Collections.singletonList(storyModel));
currentStory = storyModel;
refreshStory();
}), Dispatchers.getIO())
);
return;
}
if (currentStoryMediaId == null) return;
@ -811,7 +858,17 @@ public class StoryViewerFragment extends Fragment {
storyCallback.onSuccess(Collections.singletonList(live));
return;
}
storiesService.getUserStory(fetchOptions, storyCallback);
storiesService.getUserStory(
fetchOptions,
CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
storyCallback.onFailure(throwable);
return;
}
//noinspection unchecked
storyCallback.onSuccess((List<StoryModel>) storyModels);
}), Dispatchers.getIO())
);
}
private void setTitle(final Type type) {
@ -915,10 +972,15 @@ public class StoryViewerFragment extends Fragment {
}
if (settingsHelper.getBoolean(MARK_AS_SEEN))
storiesService.seen(currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
null);
storiesService.seen(
csrfToken,
userId,
deviceId,
currentStory.getStoryMediaId(),
currentStory.getTimestamp(),
System.currentTimeMillis() / 1000,
CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO())
);
}
private void downloadStory() {

View File

@ -1,531 +0,0 @@
package awais.instagrabber.fragments.directmessages;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import awais.instagrabber.ProfileNavGraphDirections;
import awais.instagrabber.R;
import awais.instagrabber.UserSearchNavGraphDirections;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.DirectPendingUsersAdapter;
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser;
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback;
import awais.instagrabber.adapters.DirectUsersAdapter;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback;
import awais.instagrabber.dialogs.MultiOptionDialogFragment;
import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option;
import awais.instagrabber.fragments.UserSearchFragment;
import awais.instagrabber.fragments.UserSearchFragmentDirections;
import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.DirectSettingsViewModel;
import awais.instagrabber.viewmodels.factories.DirectSettingsViewModelFactory;
public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback {
private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName();
private static final int APPROVAL_REQUIRED_REQUEST_CODE = 200;
private static final int LEAVE_THREAD_REQUEST_CODE = 201;
private static final int END_THREAD_REQUEST_CODE = 202;
private FragmentDirectMessagesSettingsBinding binding;
private DirectSettingsViewModel viewModel;
private DirectUsersAdapter usersAdapter;
private boolean isPendingRequestsSetupDone = false;
private DirectPendingUsersAdapter pendingUsersAdapter;
private Set<User> approvalRequiredUsers;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
if (arguments == null) return;
final DirectMessageSettingsFragmentArgs args = DirectMessageSettingsFragmentArgs.fromBundle(arguments);
final MainActivity fragmentActivity = (MainActivity) requireActivity();
final AppStateViewModel appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class);
final DirectSettingsViewModelFactory viewModelFactory = new DirectSettingsViewModelFactory(
fragmentActivity.getApplication(),
args.getThreadId(),
args.getPending(),
appStateViewModel.getCurrentUser()
);
viewModel = new ViewModelProvider(this, viewModelFactory).get(DirectSettingsViewModel.class);
}
@NonNull
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false);
// currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute();
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
final Bundle arguments = getArguments();
if (arguments == null) return;
init();
setupObservers();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
isPendingRequestsSetupDone = false;
}
private void setupObservers() {
viewModel.getInputMode().observe(getViewLifecycleOwner(), inputMode -> {
if (inputMode == null || inputMode == 0) return;
if (inputMode == 1) {
binding.groupSettings.setVisibility(View.GONE);
binding.pendingMembersGroup.setVisibility(View.GONE);
binding.approvalRequired.setVisibility(View.GONE);
binding.approvalRequiredLabel.setVisibility(View.GONE);
binding.muteMessagesLabel.setVisibility(View.GONE);
binding.muteMessages.setVisibility(View.GONE);
}
});
// Need to observe, so that getValue is correct
viewModel.getUsers().observe(getViewLifecycleOwner(), users -> {});
viewModel.getLeftUsers().observe(getViewLifecycleOwner(), users -> {});
viewModel.getUsersAndLeftUsers().observe(getViewLifecycleOwner(), usersPair -> {
if (usersAdapter == null) return;
usersAdapter.submitUsers(usersPair.first, usersPair.second);
});
viewModel.getTitle().observe(getViewLifecycleOwner(), title -> binding.titleEdit.setText(title));
viewModel.getAdminUserIds().observe(getViewLifecycleOwner(), adminUserIds -> {
if (usersAdapter == null) return;
usersAdapter.setAdminUserIds(adminUserIds);
});
viewModel.isMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted));
viewModel.isPending().observe(getViewLifecycleOwner(), pending -> binding.muteMessages.setVisibility(pending ? View.GONE : View.VISIBLE));
viewModel.isViewerAdmin().observe(getViewLifecycleOwner(), this::setApprovalRelatedUI);
viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required));
viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests);
viewModel.isGroup().observe(getViewLifecycleOwner(), this::setupSettings);
final NavController navController = NavHostFragment.findNavController(this);
final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry();
if (backStackEntry != null) {
final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result");
resultLiveData.observe(getViewLifecycleOwner(), result -> {
if ((result instanceof RankedRecipient)) {
final RankedRecipient recipient = (RankedRecipient) result;
final User user = getUser(recipient);
// Log.d(TAG, "result: " + user);
if (user != null) {
addMembers(Collections.singleton(recipient.getUser()));
}
} else if ((result instanceof Set)) {
try {
//noinspection unchecked
final Set<RankedRecipient> recipients = (Set<RankedRecipient>) result;
final Set<User> users = recipients.stream()
.filter(Objects::nonNull)
.map(this::getUser)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// Log.d(TAG, "result: " + users);
addMembers(users);
} catch (Exception e) {
Log.e(TAG, "search users result: ", e);
Snackbar.make(binding.getRoot(), e.getMessage() != null ? e.getMessage() : "", Snackbar.LENGTH_LONG).show();
}
}
});
}
}
private void addMembers(final Set<User> users) {
final Boolean approvalRequired = viewModel.getApprovalRequiredToJoin().getValue();
Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue();
if (isViewerAdmin == null) {
isViewerAdmin = false;
}
if (!isViewerAdmin && approvalRequired != null && approvalRequired) {
approvalRequiredUsers = users;
final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance(
APPROVAL_REQUIRED_REQUEST_CODE,
R.string.admin_approval_required,
R.string.admin_approval_required_description,
R.string.ok,
R.string.cancel,
0
);
confirmDialogFragment.show(getChildFragmentManager(), "approval_required_dialog");
return;
}
final LiveData<Resource<Object>> detailsChangeResourceLiveData = viewModel.addMembers(users);
observeDetailsChange(detailsChangeResourceLiveData);
}
@Nullable
private User getUser(@NonNull final RankedRecipient recipient) {
User user = null;
if (recipient.getUser() != null) {
user = recipient.getUser();
} else if (recipient.getThread() != null && !recipient.getThread().isGroup()) {
user = recipient.getThread().getUsers().get(0);
}
return user;
}
private void init() {
// setupSettings();
setupMembers();
}
private void setupSettings(final boolean isGroup) {
binding.groupSettings.setVisibility(isGroup ? View.VISIBLE : View.GONE);
binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle());
binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> {
final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute();
handleSwitchChangeResource(resourceLiveData, buttonView);
});
if (!isGroup) return;
binding.titleEdit.addTextChangedListener(new TextWatcherAdapter() {
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
if (s.toString().trim().equals(viewModel.getTitle().getValue())) {
binding.titleEditInputLayout.setSuffixText(null);
return;
}
binding.titleEditInputLayout.setSuffixText(getString(R.string.save));
}
});
binding.titleEditInputLayout.getSuffixTextView().setOnClickListener(v -> {
final Editable text = binding.titleEdit.getText();
if (text == null) return;
final String newTitle = text.toString().trim();
if (newTitle.equals(viewModel.getTitle().getValue())) return;
observeDetailsChange(viewModel.updateTitle(newTitle));
});
binding.addMembers.setOnClickListener(v -> {
if (!isAdded()) return;
final NavController navController = NavHostFragment.findNavController(this);
final NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination == null) return;
if (currentDestination.getId() != R.id.directMessagesSettingsFragment) return;
final List<User> users = viewModel.getUsers().getValue();
final long[] currentUserIds;
if (users != null) {
currentUserIds = users.stream()
.mapToLong(User::getPk)
.sorted()
.toArray();
} else {
currentUserIds = new long[0];
}
final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections
.actionGlobalUserSearch()
.setTitle(getString(R.string.add_members))
.setActionLabel(getString(R.string.add))
.setHideUserIds(currentUserIds)
.setSearchMode(UserSearchFragment.SearchMode.RAVEN)
.setMultiple(true);
navController.navigate(actionGlobalUserSearch);
});
binding.muteMentionsLabel.setOnClickListener(v -> binding.muteMentions.toggle());
binding.muteMentions.setOnCheckedChangeListener((buttonView, isChecked) -> {
final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.muteMentions() : viewModel.unmuteMentions();
handleSwitchChangeResource(resourceLiveData, buttonView);
});
binding.leave.setOnClickListener(v -> {
final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance(
LEAVE_THREAD_REQUEST_CODE,
R.string.dms_action_leave_question,
0,
R.string.yes,
R.string.no,
0
);
confirmDialogFragment.show(getChildFragmentManager(), "leave_thread_confirmation_dialog");
});
Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue();
if (isViewerAdmin == null) isViewerAdmin = false;
if (isViewerAdmin) {
binding.end.setVisibility(View.VISIBLE);
binding.end.setOnClickListener(v -> {
final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance(
END_THREAD_REQUEST_CODE,
R.string.dms_action_end_question,
R.string.dms_action_end_description,
R.string.yes,
R.string.no,
0
);
confirmDialogFragment.show(getChildFragmentManager(), "end_thread_confirmation_dialog");
});
} else {
binding.end.setVisibility(View.GONE);
}
}
private void setApprovalRelatedUI(final boolean isViewerAdmin) {
if (!isViewerAdmin) {
binding.pendingMembersGroup.setVisibility(View.GONE);
binding.approvalRequired.setVisibility(View.GONE);
binding.approvalRequiredLabel.setVisibility(View.GONE);
return;
}
binding.approvalRequired.setVisibility(View.VISIBLE);
binding.approvalRequiredLabel.setVisibility(View.VISIBLE);
binding.approvalRequiredLabel.setOnClickListener(v -> binding.approvalRequired.toggle());
binding.approvalRequired.setOnCheckedChangeListener((buttonView, isChecked) -> {
final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.approvalRequired() : viewModel.approvalNotRequired();
handleSwitchChangeResource(resourceLiveData, buttonView);
});
}
private void handleSwitchChangeResource(final LiveData<Resource<Object>> resourceLiveData, final CompoundButton buttonView) {
resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
buttonView.setEnabled(true);
break;
case ERROR:
buttonView.setEnabled(true);
buttonView.setChecked(!buttonView.isChecked());
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
if (resource.resId != 0) {
Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
}
break;
case LOADING:
buttonView.setEnabled(false);
break;
}
});
}
private void setupMembers() {
final Context context = getContext();
if (context == null) return;
binding.users.setLayoutManager(new LinearLayoutManager(context));
final User inviter = viewModel.getInviter().getValue();
usersAdapter = new DirectUsersAdapter(
inviter != null ? inviter.getPk() : -1,
(position, user, selected) -> {
if (TextUtils.isEmpty(user.getUsername()) && !TextUtils.isEmpty(user.getFbId())) {
Utils.openURL(context, "https://facebook.com/" + user.getFbId());
return;
}
if (TextUtils.isEmpty(user.getUsername())) return;
final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections
.actionGlobalProfileFragment("@" + user.getUsername());
NavHostFragment.findNavController(this).navigate(directions);
},
(position, user) -> {
final ArrayList<Option<String>> options = viewModel.createUserOptions(user);
if (options == null || options.isEmpty()) return true;
final MultiOptionDialogFragment<String> fragment = MultiOptionDialogFragment.newInstance(-1, options);
fragment.setSingleCallback(new MultiOptionDialogFragment.MultiOptionDialogSingleCallback<String>() {
@Override
public void onSelect(final String action) {
if (action == null) return;
observeDetailsChange(viewModel.doAction(user, action));
}
@Override
public void onCancel() {}
});
final FragmentManager fragmentManager = getChildFragmentManager();
fragment.show(fragmentManager, "actions");
return true;
}
);
binding.users.setAdapter(usersAdapter);
}
private void setPendingRequests(final DirectThreadParticipantRequestsResponse requests) {
if (requests == null || requests.getUsers() == null || requests.getUsers().isEmpty()) {
binding.pendingMembersGroup.setVisibility(View.GONE);
return;
}
if (!isPendingRequestsSetupDone) {
final Context context = getContext();
if (context == null) return;
binding.pendingMembers.setLayoutManager(new LinearLayoutManager(context));
pendingUsersAdapter = new DirectPendingUsersAdapter(new PendingUserCallback() {
@Override
public void onClick(final int position, final PendingUser pendingUser) {
final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections
.actionGlobalProfileFragment("@" + pendingUser.getUser().getUsername());
NavHostFragment.findNavController(DirectMessageSettingsFragment.this).navigate(directions);
}
@Override
public void onApprove(final int position, final PendingUser pendingUser) {
final LiveData<Resource<Object>> resourceLiveData = viewModel.approveUsers(Collections.singletonList(pendingUser.getUser()));
observeApprovalChange(resourceLiveData, position, pendingUser);
}
@Override
public void onDeny(final int position, final PendingUser pendingUser) {
final LiveData<Resource<Object>> resourceLiveData = viewModel.denyUsers(Collections.singletonList(pendingUser.getUser()));
observeApprovalChange(resourceLiveData, position, pendingUser);
}
});
binding.pendingMembers.setAdapter(pendingUsersAdapter);
binding.pendingMembersGroup.setVisibility(View.VISIBLE);
isPendingRequestsSetupDone = true;
}
if (pendingUsersAdapter != null) {
pendingUsersAdapter.submitPendingRequests(requests);
}
}
private void observeDetailsChange(@NonNull final LiveData<Resource<Object>> resourceLiveData) {
resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
case LOADING:
break;
case ERROR:
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
if (resource.resId != 0) {
Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
}
break;
}
});
}
private void observeApprovalChange(@NonNull final LiveData<Resource<Object>> detailsChangeResourceLiveData,
final int position,
@NonNull final PendingUser pendingUser) {
detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
// pending user will be removed from the list, so no need to set the progress to false
// pendingUser.setInProgress(false);
break;
case LOADING:
pendingUser.setInProgress(true);
break;
case ERROR:
pendingUser.setInProgress(false);
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
if (resource.resId != 0) {
Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
}
break;
}
pendingUsersAdapter.notifyItemChanged(position);
});
}
@Override
public void onPositiveButtonClicked(final int requestCode) {
if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE && approvalRequiredUsers != null) {
final LiveData<Resource<Object>> detailsChangeResourceLiveData = viewModel.addMembers(approvalRequiredUsers);
observeDetailsChange(detailsChangeResourceLiveData);
return;
}
if (requestCode == LEAVE_THREAD_REQUEST_CODE) {
final LiveData<Resource<Object>> resourceLiveData = viewModel.leave();
resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
final NavDirections directions = DirectMessageSettingsFragmentDirections.actionSettingsToInbox();
NavHostFragment.findNavController(this).navigate(directions);
break;
case ERROR:
binding.leave.setEnabled(true);
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
if (resource.resId != 0) {
Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
}
break;
case LOADING:
binding.leave.setEnabled(false);
break;
}
});
return;
}
if (requestCode == END_THREAD_REQUEST_CODE) {
final LiveData<Resource<Object>> resourceLiveData = viewModel.end();
resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
break;
case ERROR:
binding.end.setEnabled(true);
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
if (resource.resId != 0) {
Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
}
break;
case LOADING:
binding.end.setEnabled(false);
break;
}
});
}
}
@Override
public void onNegativeButtonClicked(final int requestCode) {
if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) {
approvalRequiredUsers = null;
}
}
@Override
public void onNeutralButtonClicked(final int requestCode) {}
}

View File

@ -0,0 +1,475 @@
package awais.instagrabber.fragments.directmessages
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import awais.instagrabber.ProfileNavGraphDirections
import awais.instagrabber.R
import awais.instagrabber.activities.MainActivity
import awais.instagrabber.adapters.DirectPendingUsersAdapter
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback
import awais.instagrabber.adapters.DirectUsersAdapter
import awais.instagrabber.customviews.helpers.TextWatcherAdapter
import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding
import awais.instagrabber.dialogs.ConfirmDialogFragment
import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback
import awais.instagrabber.dialogs.MultiOptionDialogFragment
import awais.instagrabber.dialogs.MultiOptionDialogFragment.MultiOptionDialogSingleCallback
import awais.instagrabber.fragments.UserSearchFragment
import awais.instagrabber.fragments.UserSearchFragmentDirections
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectSettingsViewModel
import awais.instagrabber.viewmodels.factories.DirectSettingsViewModelFactory
import com.google.android.material.snackbar.Snackbar
import java.util.*
class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback {
private lateinit var viewModel: DirectSettingsViewModel
private lateinit var binding: FragmentDirectMessagesSettingsBinding
private var usersAdapter: DirectUsersAdapter? = null
private var isPendingRequestsSetupDone = false
private var pendingUsersAdapter: DirectPendingUsersAdapter? = null
private var approvalRequiredUsers: Set<User>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val arguments = arguments ?: return
val args = DirectMessageSettingsFragmentArgs.fromBundle(arguments)
val fragmentActivity = requireActivity() as MainActivity
val appStateViewModel: AppStateViewModel by activityViewModels()
val currentUser = appStateViewModel.currentUser ?: return
val viewModelFactory = DirectSettingsViewModelFactory(
fragmentActivity.application,
args.threadId,
args.pending,
currentUser
)
viewModel = ViewModelProvider(this, viewModelFactory).get(DirectSettingsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false)
// currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute();
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
init()
setupObservers()
}
override fun onDestroyView() {
super.onDestroyView()
isPendingRequestsSetupDone = false
}
private fun setupObservers() {
viewModel.inputMode.observe(viewLifecycleOwner, { inputMode: Int? ->
if (inputMode == null || inputMode == 0) return@observe
if (inputMode == 1) {
binding.groupSettings.visibility = View.GONE
binding.pendingMembersGroup.visibility = View.GONE
binding.approvalRequired.visibility = View.GONE
binding.approvalRequiredLabel.visibility = View.GONE
binding.muteMessagesLabel.visibility = View.GONE
binding.muteMessages.visibility = View.GONE
}
})
// Need to observe, so that getValue is correct
viewModel.getUsers().observe(viewLifecycleOwner, { })
viewModel.getLeftUsers().observe(viewLifecycleOwner, { })
viewModel.getUsersAndLeftUsers().observe(viewLifecycleOwner, { usersAdapter?.submitUsers(it.first, it.second) })
viewModel.getTitle().observe(viewLifecycleOwner, { binding.titleEdit.setText(it) })
viewModel.getAdminUserIds().observe(viewLifecycleOwner, { usersAdapter?.setAdminUserIds(it) })
viewModel.isMuted().observe(viewLifecycleOwner, { binding.muteMessages.isChecked = it })
viewModel.isPending().observe(viewLifecycleOwner, { binding.muteMessages.visibility = if (it) View.GONE else View.VISIBLE })
viewModel.isViewerAdmin().observe(viewLifecycleOwner, { setApprovalRelatedUI(it) })
viewModel.getApprovalRequiredToJoin().observe(viewLifecycleOwner, { binding.approvalRequired.isChecked = it })
viewModel.getPendingRequests().observe(viewLifecycleOwner, { setPendingRequests(it) })
viewModel.isGroup().observe(viewLifecycleOwner, { isGroup: Boolean -> setupSettings(isGroup) })
val navController = NavHostFragment.findNavController(this)
val backStackEntry = navController.currentBackStackEntry
if (backStackEntry != null) {
val resultLiveData = backStackEntry.savedStateHandle.getLiveData<Any>("result")
resultLiveData.observe(viewLifecycleOwner, { result: Any? ->
if (result == null) return@observe
if (result is RankedRecipient) {
val user = getUser(result)
// Log.d(TAG, "result: " + user);
if (user != null) {
addMembers(setOf(user))
}
} else if (result is Set<*>) {
try {
@Suppress("UNCHECKED_CAST") val recipients = result as Set<RankedRecipient>
val users: Set<User> = recipients.asSequence()
.filterNotNull()
.map { getUser(it) }
.filterNotNull()
.toSet()
// Log.d(TAG, "result: " + users);
addMembers(users)
} catch (e: Exception) {
Log.e(TAG, "search users result: ", e)
Snackbar.make(binding.root, e.message ?: "", Snackbar.LENGTH_LONG).show()
}
}
})
}
}
private fun addMembers(users: Set<User>) {
val approvalRequired = viewModel.getApprovalRequiredToJoin().value
var isViewerAdmin = viewModel.isViewerAdmin().value
if (isViewerAdmin == null) {
isViewerAdmin = false
}
if (!isViewerAdmin && approvalRequired != null && approvalRequired) {
approvalRequiredUsers = users
val confirmDialogFragment = ConfirmDialogFragment.newInstance(
APPROVAL_REQUIRED_REQUEST_CODE,
R.string.admin_approval_required,
R.string.admin_approval_required_description,
R.string.ok,
R.string.cancel,
0
)
confirmDialogFragment.show(childFragmentManager, "approval_required_dialog")
return
}
val detailsChangeResourceLiveData = viewModel.addMembers(users)
observeDetailsChange(detailsChangeResourceLiveData)
}
private fun getUser(recipient: RankedRecipient): User? {
var user: User? = null
if (recipient.user != null) {
user = recipient.user
} else if (recipient.thread != null && !recipient.thread.isGroup) {
user = recipient.thread.users?.get(0)
}
return user
}
private fun init() {
// setupSettings();
setupMembers()
}
private fun setupSettings(isGroup: Boolean) {
binding.groupSettings.visibility = if (isGroup) View.VISIBLE else View.GONE
binding.muteMessagesLabel.setOnClickListener { binding.muteMessages.toggle() }
binding.muteMessages.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->
val resourceLiveData = if (isChecked) viewModel.mute() else viewModel.unmute()
handleSwitchChangeResource(resourceLiveData, buttonView)
}
if (!isGroup) return
binding.titleEdit.addTextChangedListener(object : TextWatcherAdapter() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.toString().trim { it <= ' ' } == viewModel.getTitle().value) {
binding.titleEditInputLayout.suffixText = null
return
}
binding.titleEditInputLayout.suffixText = getString(R.string.save)
}
})
binding.titleEditInputLayout.suffixTextView.setOnClickListener {
val text = binding.titleEdit.text ?: return@setOnClickListener
val newTitle = text.toString().trim { it <= ' ' }
if (newTitle == viewModel.getTitle().value) return@setOnClickListener
observeDetailsChange(viewModel.updateTitle(newTitle))
}
binding.addMembers.setOnClickListener {
if (!isAdded) return@setOnClickListener
val navController = NavHostFragment.findNavController(this)
val currentDestination = navController.currentDestination ?: return@setOnClickListener
if (currentDestination.id != R.id.directMessagesSettingsFragment) return@setOnClickListener
val users = viewModel.getUsers().value
val currentUserIds: LongArray = users?.asSequence()?.map { obj: User -> obj.pk }?.sorted()?.toList()?.toLongArray() ?: LongArray(0)
val actionGlobalUserSearch = UserSearchFragmentDirections
.actionGlobalUserSearch()
.setTitle(getString(R.string.add_members))
.setActionLabel(getString(R.string.add))
.setHideUserIds(currentUserIds)
.setSearchMode(UserSearchFragment.SearchMode.RAVEN)
.setMultiple(true)
navController.navigate(actionGlobalUserSearch)
}
binding.muteMentionsLabel.setOnClickListener { binding.muteMentions.toggle() }
binding.muteMentions.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->
val resourceLiveData = if (isChecked) viewModel.muteMentions() else viewModel.unmuteMentions()
handleSwitchChangeResource(resourceLiveData, buttonView)
}
binding.leave.setOnClickListener {
val confirmDialogFragment = ConfirmDialogFragment.newInstance(
LEAVE_THREAD_REQUEST_CODE,
R.string.dms_action_leave_question,
0,
R.string.yes,
R.string.no,
0
)
confirmDialogFragment.show(childFragmentManager, "leave_thread_confirmation_dialog")
}
var isViewerAdmin = viewModel.isViewerAdmin().value
if (isViewerAdmin == null) isViewerAdmin = false
if (isViewerAdmin) {
binding.end.visibility = View.VISIBLE
binding.end.setOnClickListener {
val confirmDialogFragment = ConfirmDialogFragment.newInstance(
END_THREAD_REQUEST_CODE,
R.string.dms_action_end_question,
R.string.dms_action_end_description,
R.string.yes,
R.string.no,
0
)
confirmDialogFragment.show(childFragmentManager, "end_thread_confirmation_dialog")
}
} else {
binding.end.visibility = View.GONE
}
}
private fun setApprovalRelatedUI(isViewerAdmin: Boolean) {
if (!isViewerAdmin) {
binding.pendingMembersGroup.visibility = View.GONE
binding.approvalRequired.visibility = View.GONE
binding.approvalRequiredLabel.visibility = View.GONE
return
}
binding.approvalRequired.visibility = View.VISIBLE
binding.approvalRequiredLabel.visibility = View.VISIBLE
binding.approvalRequiredLabel.setOnClickListener { binding.approvalRequired.toggle() }
binding.approvalRequired.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->
val resourceLiveData = if (isChecked) viewModel.approvalRequired() else viewModel.approvalNotRequired()
handleSwitchChangeResource(resourceLiveData, buttonView)
}
}
private fun handleSwitchChangeResource(resourceLiveData: LiveData<Resource<Any?>>, buttonView: CompoundButton) {
resourceLiveData.observe(viewLifecycleOwner, { resource: Resource<Any?>? ->
if (resource == null) return@observe
when (resource.status) {
Resource.Status.SUCCESS -> buttonView.isEnabled = true
Resource.Status.ERROR -> {
buttonView.isEnabled = true
buttonView.isChecked = !buttonView.isChecked
if (resource.message != null) {
Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show()
}
if (resource.resId != 0) {
Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show()
}
}
Resource.Status.LOADING -> buttonView.isEnabled = false
}
})
}
private fun setupMembers() {
val context = context ?: return
binding.users.layoutManager = LinearLayoutManager(context)
val inviter = viewModel.getInviter().value
usersAdapter = DirectUsersAdapter(
inviter?.pk ?: -1,
{ _: Int, user: User, _: Boolean ->
if (user.username.isBlank() && !user.interopMessagingUserFbid.isNullOrBlank()) {
Utils.openURL(context, "https://facebook.com/" + user.interopMessagingUserFbid)
return@DirectUsersAdapter
}
if (isEmpty(user.username)) return@DirectUsersAdapter
val directions = ProfileNavGraphDirections
.actionGlobalProfileFragment("@" + user.username)
NavHostFragment.findNavController(this).navigate(directions)
},
{ _: Int, user: User? ->
val options = viewModel.createUserOptions(user)
if (options.isEmpty()) return@DirectUsersAdapter true
val fragment = MultiOptionDialogFragment.newInstance(-1, options)
fragment.setSingleCallback(object : MultiOptionDialogSingleCallback<String?> {
override fun onSelect(action: String?) {
if (action == null) return
val resourceLiveData = viewModel.doAction(user, action)
if (resourceLiveData != null) {
observeDetailsChange(resourceLiveData)
}
}
override fun onCancel() {}
})
val fragmentManager = childFragmentManager
fragment.show(fragmentManager, "actions")
true
}
)
binding.users.adapter = usersAdapter
}
private fun setPendingRequests(requests: DirectThreadParticipantRequestsResponse?) {
val nullOrEmpty: Boolean = requests?.users?.isNullOrEmpty() ?: true
if (nullOrEmpty) {
binding.pendingMembersGroup.visibility = View.GONE
return
}
if (!isPendingRequestsSetupDone) {
val context = context ?: return
binding.pendingMembers.layoutManager = LinearLayoutManager(context)
pendingUsersAdapter = DirectPendingUsersAdapter(object : PendingUserCallback {
override fun onClick(position: Int, pendingUser: PendingUser) {
val directions = ProfileNavGraphDirections
.actionGlobalProfileFragment("@" + pendingUser.user.username)
NavHostFragment.findNavController(this@DirectMessageSettingsFragment).navigate(directions)
}
override fun onApprove(position: Int, pendingUser: PendingUser) {
val resourceLiveData = viewModel.approveUsers(listOf(pendingUser.user))
observeApprovalChange(resourceLiveData, position, pendingUser)
}
override fun onDeny(position: Int, pendingUser: PendingUser) {
val resourceLiveData = viewModel.denyUsers(listOf(pendingUser.user))
observeApprovalChange(resourceLiveData, position, pendingUser)
}
})
binding.pendingMembers.adapter = pendingUsersAdapter
binding.pendingMembersGroup.visibility = View.VISIBLE
isPendingRequestsSetupDone = true
}
pendingUsersAdapter?.submitPendingRequests(requests)
}
private fun observeDetailsChange(resourceLiveData: LiveData<Resource<Any?>>) {
resourceLiveData.observe(viewLifecycleOwner, { resource: Resource<Any?>? ->
if (resource == null) return@observe
when (resource.status) {
Resource.Status.SUCCESS,
Resource.Status.LOADING,
-> {
}
Resource.Status.ERROR -> {
if (resource.message != null) {
Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show()
}
if (resource.resId != 0) {
Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show()
}
}
}
})
}
private fun observeApprovalChange(
detailsChangeResourceLiveData: LiveData<Resource<Any?>>,
position: Int,
pendingUser: PendingUser,
) {
detailsChangeResourceLiveData.observe(viewLifecycleOwner, { resource: Resource<Any?>? ->
if (resource == null) return@observe
when (resource.status) {
Resource.Status.SUCCESS -> {
}
Resource.Status.LOADING -> pendingUser.isInProgress = true
Resource.Status.ERROR -> {
pendingUser.isInProgress = false
if (resource.message != null) {
Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show()
}
if (resource.resId != 0) {
Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show()
}
}
}
pendingUsersAdapter?.notifyItemChanged(position)
})
}
override fun onPositiveButtonClicked(requestCode: Int) {
if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) {
approvalRequiredUsers?.let {
val detailsChangeResourceLiveData = viewModel.addMembers(it)
observeDetailsChange(detailsChangeResourceLiveData)
}
return
}
if (requestCode == LEAVE_THREAD_REQUEST_CODE) {
val resourceLiveData = viewModel.leave()
resourceLiveData.observe(viewLifecycleOwner, { resource: Resource<Any?>? ->
if (resource == null) return@observe
when (resource.status) {
Resource.Status.SUCCESS -> {
val directions = DirectMessageSettingsFragmentDirections.actionSettingsToInbox()
NavHostFragment.findNavController(this).navigate(directions)
}
Resource.Status.ERROR -> {
binding.leave.isEnabled = true
if (resource.message != null) {
Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show()
}
if (resource.resId != 0) {
Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show()
}
}
Resource.Status.LOADING -> binding.leave.isEnabled = false
}
})
return
}
if (requestCode == END_THREAD_REQUEST_CODE) {
val resourceLiveData = viewModel.end()
resourceLiveData.observe(viewLifecycleOwner, { resource: Resource<Any?>? ->
if (resource == null) return@observe
when (resource.status) {
Resource.Status.SUCCESS -> {
}
Resource.Status.ERROR -> {
binding.end.isEnabled = true
if (resource.message != null) {
Snackbar.make(binding.root, resource.message, Snackbar.LENGTH_LONG).show()
}
if (resource.resId != 0) {
Snackbar.make(binding.root, resource.resId, Snackbar.LENGTH_LONG).show()
}
}
Resource.Status.LOADING -> binding.end.isEnabled = false
}
})
}
}
override fun onNegativeButtonClicked(requestCode: Int) {
if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) {
approvalRequiredUsers = null
}
}
override fun onNeutralButtonClicked(requestCode: Int) {}
companion object {
private const val APPROVAL_REQUIRED_REQUEST_CODE = 200
private const val LEAVE_THREAD_REQUEST_CODE = 201
private const val END_THREAD_REQUEST_CODE = 202
}
}

View File

@ -47,6 +47,7 @@ import awais.instagrabber.fragments.imageedit.filters.properties.FloatProperty;
import awais.instagrabber.fragments.imageedit.filters.properties.Property;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.BitmapUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.SerializablePair;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.FiltersFragmentViewModel;
@ -54,6 +55,7 @@ import awais.instagrabber.viewmodels.ImageEditViewModel;
import jp.co.cyberagent.android.gpuimage.GPUImage;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup;
import kotlinx.coroutines.Dispatchers;
public class FiltersFragment extends Fragment {
private static final String TAG = FiltersFragment.class.getSimpleName();
@ -460,32 +462,33 @@ public class FiltersFragment extends Fragment {
filtersAdapter.setSelected(position);
appliedFilter = filter;
};
BitmapUtils.getThumbnail(context, sourceUri, new BitmapUtils.ThumbnailLoadCallback() {
@Override
public void onLoad(@Nullable final Bitmap bitmap, final int width, final int height) {
filtersAdapter = new FiltersAdapter(
tuningFilters.values()
.stream()
.map(Filter::getInstance)
.collect(Collectors.toList()),
sourceUri.toString(),
bitmap,
onFilterClickListener
);
appExecutors.getMainThread().execute(() -> {
BitmapUtils.getThumbnail(
context,
sourceUri,
CoroutineUtilsKt.getContinuation((bitmapResult, throwable) -> appExecutors.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "setupFilters: ", throwable);
return;
}
if (bitmapResult == null || bitmapResult.getBitmap() == null) {
return;
}
filtersAdapter = new FiltersAdapter(
tuningFilters.values()
.stream()
.map(Filter::getInstance)
.collect(Collectors.toList()),
sourceUri.toString(),
bitmapResult.getBitmap(),
onFilterClickListener
);
binding.filters.setAdapter(filtersAdapter);
filtersAdapter.submitList(FiltersHelper.getFilters(), () -> {
if (appliedFilter == null) return;
filtersAdapter.setSelectedFilter(appliedFilter.getInstance());
});
});
}
@Override
public void onFailure(@NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
}
});
}), Dispatchers.getIO())
);
addInitialFilter();
binding.preview.setFilter(filterGroup);
}

View File

@ -30,11 +30,14 @@ import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.TopicClusterViewModel;
import awais.instagrabber.webservices.DiscoverService;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "DiscoverFragment";
@ -52,7 +55,11 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR
super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity();
discoverService = DiscoverService.getInstance();
mediaService = MediaService.getInstance(null, null, 0);
// final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
// final String cookie = Utils.settingsHelper.getString(Constants.COOKIE);
// final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
// final long userId = CookieUtils.getUserIdFromCookie(cookie);
mediaService = MediaService.INSTANCE;
}
@Override
@ -104,29 +111,29 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
mediaService.fetch(Long.valueOf(coverMedia.getPk()), new ServiceCallback<Media>() {
@Override
public void onSuccess(final Media feedModel) {
final NavController navController = NavHostFragment.findNavController(DiscoverFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, feedModel);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "onSuccess: ", e);
}
}
@Override
public void onFailure(final Throwable t) {
alertDialog.dismiss();
try {
Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
catch (Throwable e) {}
}
});
final String pk = coverMedia.getPk();
if (pk == null) return;
mediaService.fetch(
Long.parseLong(pk),
CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
alertDialog.dismiss();
try {
Toast.makeText(requireContext(), R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
return;
}
final NavController navController = NavHostFragment.findNavController(DiscoverFragment.this);
final Bundle bundle = new Bundle();
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media);
try {
navController.navigate(R.id.action_global_post_view, bundle);
alertDialog.dismiss();
} catch (Exception e) {
Log.e(TAG, "onTopicLongClick: ", e);
}
}), Dispatchers.getIO())
);
}
};
final DiscoverTopicsAdapter adapter = new DiscoverTopicsAdapter(otcl);

View File

@ -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<List<FeedStoryModel>>() {
@Override
public void onSuccess(final List<FeedStoryModel> 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<FeedStoryModel>) feedStoryModels);
//noinspection unchecked
feedStoriesAdapter.submitList((List<FeedStoryModel>) feedStoryModels);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
}), Dispatchers.getIO())
);
}
private void showPostsLayoutPreferences() {

View File

@ -62,11 +62,9 @@ import awais.instagrabber.databinding.FragmentProfileBinding;
import awais.instagrabber.databinding.LayoutProfileDetailsBinding;
import awais.instagrabber.db.datasources.AccountDataSource;
import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.dialogs.ProfilePicDialogFragment;
import awais.instagrabber.fragments.PostViewV2Fragment;
@ -74,16 +72,15 @@ import awais.instagrabber.managers.DirectMessagesManager;
import awais.instagrabber.managers.InboxManager;
import awais.instagrabber.models.HighlightModel;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.FriendshipChangeResponse;
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.UserProfileContextLink;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
@ -92,6 +89,7 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.HighlightsViewModel;
import awais.instagrabber.viewmodels.ProfileFragmentViewModel;
import awais.instagrabber.webservices.DirectMessagesService;
import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.GraphQLService;
@ -138,6 +136,14 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
private int downloadChildPosition = -1;
private long myId;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_PROFILE_POSTS_LAYOUT);
private LayoutProfileDetailsBinding profileDetailsBinding;
private AccountRepository accountRepository;
private FavoriteRepository favoriteRepository;
private AppStateViewModel appStateViewModel;
private boolean disableDm = false;
private ProfileFragmentViewModel viewModel;
private String csrfToken;
private String deviceUuid;
private final ServiceCallback<FriendshipChangeResponse> changeCb = new ServiceCallback<FriendshipChangeResponse>() {
@Override
@ -155,7 +161,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
Log.e(TAG, "Error editing relationship", t);
}
};
private final Runnable usernameSettingRunnable = () -> {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null && !TextUtils.isEmpty(username)) {
@ -317,11 +322,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
}
};
private LayoutProfileDetailsBinding profileDetailsBinding;
private AccountRepository accountRepository;
private FavoriteRepository favoriteRepository;
private AppStateViewModel appStateViewModel;
private boolean disableDm = false;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
@ -329,20 +329,21 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
cookie = Utils.settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
myId = CookieUtils.getUserIdFromCookie(cookie);
final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
fragmentActivity = (MainActivity) requireActivity();
friendshipService = isLoggedIn ? FriendshipService.getInstance(deviceUuid, csrfToken, myId) : null;
directMessagesService = isLoggedIn ? DirectMessagesService.getInstance(csrfToken, myId, deviceUuid) : null;
storiesService = isLoggedIn ? StoriesService.getInstance(null, 0L, null) : null;
mediaService = isLoggedIn ? MediaService.getInstance(null, null, 0) : null;
userService = isLoggedIn ? UserService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
friendshipService = isLoggedIn ? FriendshipService.INSTANCE : null;
directMessagesService = isLoggedIn ? DirectMessagesService.INSTANCE : null;
storiesService = isLoggedIn ? StoriesService.INSTANCE : null;
mediaService = isLoggedIn ? MediaService.INSTANCE : null;
userService = isLoggedIn ? UserService.INSTANCE : null;
graphQLService = isLoggedIn ? null : GraphQLService.INSTANCE;
final Context context = getContext();
if (context == null) return;
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context));
favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class);
viewModel = new ViewModelProvider(this).get(ProfileFragmentViewModel.class);
setHasOptionsMenu(true);
}
@ -372,6 +373,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
shouldRefresh = false;
return root;
}
// appStateViewModel.getCurrentUserLiveData().observe(getViewLifecycleOwner(), user -> viewModel.setCurrentUser(user));
binding = FragmentProfileBinding.inflate(inflater, container, false);
root = binding.getRoot();
profileDetailsBinding = binding.header;
@ -429,7 +431,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
chainingMenuItem = menu.findItem(R.id.chaining);
if (chainingMenuItem != null) {
chainingMenuItem.setVisible(isNotMe && profileModel.hasChaining());
chainingMenuItem.setVisible(isNotMe && profileModel.getHasChaining());
}
removeFollowerMenuItem = menu.findItem(R.id.remove_follower);
if (removeFollowerMenuItem != null) {
@ -447,25 +449,38 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().isRestricted() ? "Unrestrict" : "Restrict";
friendshipService.toggleRestrict(
csrfToken,
deviceUuid,
profileModel.getPk(),
!profileModel.getFriendshipStatus().isRestricted(),
new ServiceCallback<FriendshipRestrictResponse>() {
@Override
public void onSuccess(final FriendshipRestrictResponse result) {
Log.d(TAG, action + " success: " + result);
fetchProfileDetails();
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error while performing " + action, throwable);
return;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error while performing " + action, t);
}
});
// Log.d(TAG, action + " success: " + response);
fetchProfileDetails();
}), Dispatchers.getIO())
);
return true;
}
if (item.getItemId() == R.id.block) {
if (!isLoggedIn) return false;
friendshipService.changeBlock(profileModel.getFriendshipStatus().getBlocking(), profileModel.getPk(), changeCb);
// changeCb
friendshipService.changeBlock(
csrfToken,
myId,
deviceUuid,
profileModel.getFriendshipStatus().getBlocking(),
profileModel.getPk(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true;
}
if (item.getItemId() == R.id.chaining) {
@ -480,25 +495,57 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().isMutingReel() ? "Unmute stories" : "Mute stories";
friendshipService.changeMute(
csrfToken,
myId,
deviceUuid,
profileModel.getFriendshipStatus().isMutingReel(),
profileModel.getPk(),
true,
changeCb);
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true;
}
if (item.getItemId() == R.id.mute_posts) {
if (!isLoggedIn) return false;
final String action = profileModel.getFriendshipStatus().getMuting() ? "Unmute stories" : "Mute stories";
friendshipService.changeMute(
csrfToken,
myId,
deviceUuid,
profileModel.getFriendshipStatus().getMuting(),
profileModel.getPk(),
false,
changeCb);
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true;
}
if (item.getItemId() == R.id.remove_follower) {
if (!isLoggedIn) return false;
friendshipService.removeFollower(profileModel.getPk(), changeCb);
friendshipService.removeFollower(
csrfToken,
myId,
deviceUuid,
profileModel.getPk(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
return true;
}
return super.onOptionsItemSelected(item);
@ -582,65 +629,51 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
return;
}
if (isLoggedIn) {
userService.getUsernameInfo(usernameTemp, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
userService.getUserFriendship(user.getPk(), new ServiceCallback<FriendshipStatus>() {
@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<User>() {
@Override
public void onSuccess(final User user) {
profileModel = user;
setProfileDetails();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error fetching profile", t);
final Context context = getContext();
try {
if (t == null)
Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_LONG).show();
else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (final Throwable ignored) {
}
}
});
graphQLService.fetchUser(
usernameTemp,
CoroutineUtilsKt.getContinuation((user, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error fetching profile", throwable);
final Context context = getContext();
if (context == null) return;
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
profileModel = user;
setProfileDetails();
}))
);
}
private void setProfileDetails() {
@ -668,76 +701,80 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
setupButtons(profileId);
final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext()));
favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
profileDetailsBinding.favChip.setText(R.string.favorite_short);
favoriteRepository.insertOrUpdateFavorite(new Favorite(
result.getId(),
profileModel.getUsername(),
FavoriteType.USER,
profileModel.getFullName(),
profileModel.getProfilePicUrl(),
result.getDateAdded()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
favoriteRepository.getFavorite(
profileModel.getUsername(),
FavoriteType.USER,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null || favorite == null) {
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
profileDetailsBinding.favChip.setText(R.string.add_to_favorites);
Log.e(TAG, "setProfileDetails: ", throwable);
return;
}
@Override
public void onDataNotAvailable() {
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
profileDetailsBinding.favChip.setText(R.string.favorite_short);
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
favorite.getId(),
profileModel.getUsername(),
FavoriteType.USER,
profileModel.getFullName(),
profileModel.getProfilePicUrl(),
favorite.getDateAdded()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
}
}), Dispatchers.getIO())
);
}))
);
profileDetailsBinding.favChip.setOnClickListener(v -> favoriteRepository.getFavorite(
profileModel.getUsername(),
FavoriteType.USER,
CoroutineUtilsKt.getContinuation((favorite, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "setProfileDetails: ", throwable);
return;
}
});
}
@Override
public void onDataNotAvailable() {
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
profileDetailsBinding.favChip.setText(R.string.add_to_favorites);
}
});
profileDetailsBinding.favChip.setOnClickListener(
v -> favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
favoriteRepository.deleteFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
if (favorite == null) {
favoriteRepository.insertOrUpdateFavorite(
new Favorite(
0,
profileModel.getUsername(),
FavoriteType.USER,
profileModel.getFullName(),
profileModel.getProfilePicUrl(),
LocalDateTime.now()
),
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onDataNotAvailable: ", throwable1);
return;
}
profileDetailsBinding.favChip.setText(R.string.favorite_short);
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}), Dispatchers.getIO())
);
return;
}
favoriteRepository.deleteFavorite(
profileModel.getUsername(),
FavoriteType.USER,
CoroutineUtilsKt.getContinuation((unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "onSuccess: ", throwable1);
return;
}
profileDetailsBinding.favChip.setText(R.string.add_to_favorites);
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24);
showSnackbar(getString(R.string.removed_from_favs));
}
@Override
public void onDataNotAvailable() {
}
});
}
@Override
public void onDataNotAvailable() {
favoriteRepository.insertOrUpdateFavorite(new Favorite(
0,
profileModel.getUsername(),
FavoriteType.USER,
profileModel.getFullName(),
profileModel.getProfilePicUrl(),
LocalDateTime.now()
), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
profileDetailsBinding.favChip.setText(R.string.favorite_short);
profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24);
showSnackbar(getString(R.string.added_to_favs));
}
@Override
public void onDataNotAvailable() {
}
});
}
}));
}), Dispatchers.getIO())
);
}), Dispatchers.getIO())
));
profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getProfilePicUrl());
profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE);
@ -821,26 +858,26 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
Utils.copyText(context, biography);
break;
case 1:
mediaService.translate(String.valueOf(profileModel.getPk()), "3", new ServiceCallback<String>() {
@Override
public void onSuccess(final String result) {
if (TextUtils.isEmpty(result)) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
return;
}
new AlertDialog.Builder(context)
.setTitle(profileModel.getUsername())
.setMessage(result)
.setPositiveButton(R.string.ok, null)
.show();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error translating bio", t);
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
mediaService.translate(String.valueOf(profileModel.getPk()), "3", CoroutineUtilsKt.getContinuation(
(result, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "Error translating bio", throwable);
Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(result)) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT)
.show();
return;
}
new AlertDialog.Builder(context)
.setTitle(profileModel.getUsername())
.setMessage(result)
.setPositiveButton(R.string.ok, null)
.show();
}),
Dispatchers.getIO()
));
break;
}
})
@ -855,7 +892,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
profileDetailsBinding.profileContext.setVisibility(View.GONE);
} else {
profileDetailsBinding.profileContext.setVisibility(View.VISIBLE);
final List<UserProfileContextLink> userProfileContextLinks = profileModel.getProfileContextLinks();
final List<UserProfileContextLink> userProfileContextLinks = profileModel.getProfileContextLinksWithUserIds();
for (int i = 0; i < userProfileContextLinks.size(); i++) {
final UserProfileContextLink link = userProfileContextLinks.get(i);
if (link.getUsername() != null)
@ -976,7 +1013,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().getMuting() ? R.string.unmute_posts : R.string.mute_posts);
}
if (chainingMenuItem != null) {
chainingMenuItem.setVisible(profileModel.hasChaining());
chainingMenuItem.setVisible(profileModel.getHasChaining());
}
if (removeFollowerMenuItem != null) {
removeFollowerMenuItem.setVisible(profileModel.getFriendshipStatus().getFollowedBy());
@ -992,69 +1029,100 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
cookie,
profileModel.getFullName(),
profileModel.getProfilePicUrl(),
new RepositoryCallback<Account>() {
@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<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> 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<List<HighlightModel>>() {
@Override
public void onSuccess(final List<HighlightModel> 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<HighlightModel>) highlightModels);
} else {
profileDetailsBinding.highlightsList.setVisibility(View.GONE);
}
}), Dispatchers.getIO())
);
}
private void setupCommonListeners() {
final Context context = getContext();
if (context == null) return;
profileDetailsBinding.btnFollow.setOnClickListener(v -> {
if (profileModel.getFriendshipStatus().getFollowing() && profileModel.isPrivate()) {
new AlertDialog.Builder(context)
.setTitle(R.string.priv_acc)
.setMessage(R.string.priv_acc_confirm)
.setPositiveButton(R.string.confirm, (d, w) ->
friendshipService.unfollow(profileModel.getPk(), changeCb))
.setPositiveButton(R.string.confirm, (d, w) -> friendshipService.unfollow(
csrfToken,
myId,
deviceUuid,
profileModel.getPk(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
))
.setNegativeButton(R.string.cancel, null)
.show();
} else if (profileModel.getFriendshipStatus().getFollowing() || profileModel.getFriendshipStatus().getOutgoingRequest()) {
friendshipService.unfollow(profileModel.getPk(), changeCb);
friendshipService.unfollow(
csrfToken,
myId,
deviceUuid,
profileModel.getPk(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
friendshipService.follow(profileModel.getPk(), changeCb);
friendshipService.follow(
csrfToken,
myId,
deviceUuid,
profileModel.getPk(),
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
changeCb.onFailure(throwable);
return;
}
changeCb.onSuccess(response);
}), Dispatchers.getIO())
);
}
});
profileDetailsBinding.btnSaved.setOnClickListener(v -> {
@ -1077,9 +1145,12 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
profileDetailsBinding.btnDM.setOnClickListener(v -> {
profileDetailsBinding.btnDM.setEnabled(false);
directMessagesService.createThread(
csrfToken,
myId,
deviceUuid,
Collections.singletonList(profileModel.getPk()),
null,
CoroutineUtilsKt.getContinuation((thread, throwable) -> {
CoroutineUtilsKt.getContinuation((thread, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "setupCommonListeners: ", throwable);
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
@ -1092,7 +1163,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
inboxManager.addThread(thread, 0);
}
fragmentActivity.navigateToThread(thread.getThreadId(), profileModel.getUsername());
}, Dispatchers.getIO())
}), Dispatchers.getIO())
);
});
}
@ -1119,7 +1190,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
showProfilePicDialog();
};
if (context == null) return;
new AlertDialog.Builder(context)
.setItems(options, profileDialogListener)
.setNegativeButton(R.string.cancel, null)

View File

@ -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<List<Account>>() {
@Override
public void onSuccess(@NonNull final List<Account> accounts) {
if (!isLoggedIn) {
if (accounts.size() > 0) {
final Context context1 = getContext();
final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1);
if (preference == null) return;
accountCategory.addPreference(preference);
}
// Need to show something to trigger login activity
final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
final Context context1 = getContext();
if (context1 == null) return false;
startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE);
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
if (accounts.size() > 0) {
final Preference preference1 = getPreference(
R.string.remove_all_acc,
null,
R.drawable.ic_account_multiple_remove_24,
preference -> {
if (getContext() == null) return false;
new AlertDialog.Builder(getContext())
.setTitle(R.string.logout)
.setMessage(R.string.remove_all_acc_warning)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final Context context1 = getContext();
if (context1 == null) return;
CookieUtils.removeAllAccounts(context1, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
// shouldRecreate();
final Context context1 = getContext();
if (context1 == null) return;
Toast.makeText(context1, R.string.logout_success, Toast.LENGTH_SHORT).show();
settingsHelper.putString(Constants.COOKIE, "");
AppExecutors.INSTANCE.getMainThread().execute(() -> ProcessPhoenix.triggerRebirth(context1), 200);
}
@Override
public void onDataNotAvailable() {}
});
})
.setNegativeButton(R.string.cancel, null)
.show();
accountRepository.getAllAccounts(
CoroutineUtilsKt.getContinuation((accounts, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.d(TAG, "getAllAccounts", throwable);
if (!isLoggedIn) {
// Need to show something to trigger login activity
accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE);
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
}
@Override
public void onDataNotAvailable() {
Log.d(TAG, "onDataNotAvailable");
if (!isLoggedIn) {
// Need to show something to trigger login activity
accountCategory.addPreference(getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE);
return true;
}));
}
}
});
}));
}
return;
}
if (!isLoggedIn) {
if (accounts.size() > 0) {
final Context context1 = getContext();
final AccountSwitcherPreference preference = getAccountSwitcherPreference(null, context1);
if (preference == null) return;
accountCategory.addPreference(preference);
}
// Need to show something to trigger login activity
final Preference preference1 = getPreference(R.string.add_account, R.drawable.ic_add, preference -> {
final Context context1 = getContext();
if (context1 == null) return false;
startActivityForResult(new Intent(context1, Login.class), Constants.LOGIN_RESULT_CODE);
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
if (accounts.size() > 0) {
final Preference preference1 = getPreference(
R.string.remove_all_acc,
null,
R.drawable.ic_account_multiple_remove_24,
preference -> {
if (getContext() == null) return false;
new AlertDialog.Builder(getContext())
.setTitle(R.string.logout)
.setMessage(R.string.remove_all_acc_warning)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final Context context1 = getContext();
if (context1 == null) return;
CookieUtils.removeAllAccounts(
context1,
CoroutineUtilsKt.getContinuation(
(unit, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
return;
}
final Context context2 = getContext();
if (context2 == null) return;
Toast.makeText(context2, R.string.logout_success, Toast.LENGTH_SHORT).show();
settingsHelper.putString(Constants.COOKIE, "");
AppExecutors.INSTANCE
.getMainThread()
.execute(() -> ProcessPhoenix.triggerRebirth(context1), 200);
}),
Dispatchers.getIO()
)
);
})
.setNegativeButton(R.string.cancel, null)
.show();
return true;
});
if (preference1 == null) return;
accountCategory.addPreference(preference1);
}
}), Dispatchers.getIO())
);
// final PreferenceCategory generalCategory = new PreferenceCategory(context);
// generalCategory.setTitle(R.string.pref_category_general);
@ -288,44 +285,33 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
// adds cookies to database for quick access
final long uid = CookieUtils.getUserIdFromCookie(cookie);
final UserService userService = UserService.getInstance();
userService.getUserInfo(uid, new ServiceCallback<User>() {
@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<Account>() {
@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<Account>() {
@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())
);
}
}
}

View File

@ -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<RankedRecipient>, 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)
}
}

View File

@ -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<Resource<DirectInbox>> = ControlledRunner()
// private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null))
private val unseenCount = MutableLiveData<Resource<Int?>>()
private val pendingRequestsTotal = MutableLiveData(0)
val threads: LiveData<List<DirectThread>>
private val service: DirectMessagesService
private var inboxRequest: Call<DirectInboxResponse?>? = null
private var unseenCountRequest: Call<DirectBadgeCount?>? = null
private var seqId: Long = 0
@ -58,7 +57,11 @@ class InboxManager private constructor(private val pending: Boolean) {
inbox.postValue(loading(currentDirectInbox))
scope.launch(Dispatchers.IO) {
try {
val inboxValue = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId)
val inboxValue = if (pending) {
DirectMessagesService.fetchPendingInbox(cursor, seqId)
} else {
DirectMessagesService.fetchInbox(cursor, seqId)
}
parseInboxResponse(inboxValue)
} catch (e: Exception) {
inbox.postValue(error(e.message, currentDirectInbox))
@ -74,7 +77,7 @@ class InboxManager private constructor(private val pending: Boolean) {
unseenCount.postValue(loading(currentUnseenCount))
scope.launch(Dispatchers.IO) {
try {
val directBadgeCount = service.fetchUnseenCount()
val directBadgeCount = DirectMessagesService.fetchUnseenCount()
unseenCount.postValue(success(directBadgeCount.badgeCount))
} catch (e: Exception) {
Log.e(TAG, "Failed fetching unseen count", e)
@ -117,7 +120,7 @@ class InboxManager private constructor(private val pending: Boolean) {
val threads = it.threads
val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads)
threadsCopy.addAll(inbox.threads ?: emptyList())
inbox.threads = threads
inbox.threads = threadsCopy
}
}
this.inbox.postValue(success(inbox))
@ -286,7 +289,6 @@ class InboxManager private constructor(private val pending: Boolean) {
}
companion object {
private val TAG = InboxManager::class.java.simpleName
private val THREAD_LOCKS = CacheBuilder
.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected
@ -299,10 +301,6 @@ class InboxManager private constructor(private val pending: Boolean) {
if (t2FirstDirectItem == null) return@Comparator -1
t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp())
}
fun getInstance(pending: Boolean): InboxManager {
return InboxManager(pending)
}
}
init {
@ -311,7 +309,6 @@ class InboxManager private constructor(private val pending: Boolean) {
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
val csrfToken = getCsrfTokenFromCookie(cookie)
require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" }
service = getInstance(csrfToken, viewerId, deviceUuid)
// Transformations
threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?> ->

View File

@ -19,46 +19,40 @@ import awais.instagrabber.repositories.requests.UploadFinishOptions
import awais.instagrabber.repositories.requests.VideoOptions
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of
import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif
import awais.instagrabber.utils.*
import awais.instagrabber.utils.MediaUploader.MediaUploadResponse
import awais.instagrabber.utils.MediaUploader.OnMediaUploadCompleteListener
import awais.instagrabber.utils.MediaUploader.uploadPhoto
import awais.instagrabber.utils.MediaUploader.uploadVideo
import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.FriendshipService
import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterables
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Collectors
class ThreadManager private constructor(
class ThreadManager(
private val threadId: String,
pending: Boolean,
currentUser: User,
contentResolver: ContentResolver,
viewerId: Long,
csrfToken: String,
deviceUuid: String,
private val currentUser: User?,
private val contentResolver: ContentResolver,
private val viewerId: Long,
private val csrfToken: String,
private val deviceUuid: String,
) {
private val _fetching = MutableLiveData<Resource<Any?>>()
val fetching: LiveData<Resource<Any?>> = _fetching
@ -67,13 +61,7 @@ class ThreadManager private constructor(
private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null)
val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _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<DirectThread?> by lazy {
distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? ->
@ -138,7 +126,7 @@ class ThreadManager private constructor(
_fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val threadFeedResponse = service.fetchThread(threadId, cursor)
val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch
@ -166,7 +154,7 @@ class ThreadManager private constructor(
if (isGroup == null || !isGroup) return
scope.launch(Dispatchers.IO) {
try {
val response = service.participantRequests(threadId, 1)
val response = DirectMessagesService.participantRequests(threadId, 1)
_pendingRequests.postValue(response)
} catch (e: Exception) {
Log.e(TAG, "fetchPendingRequests: ", e)
@ -358,7 +346,10 @@ class ThreadManager private constructor(
val repliedToClientContext = replyToItemValue?.clientContext
scope.launch(Dispatchers.IO) {
try {
val response = service.broadcastText(
val response = DirectMessagesService.broadcastText(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdOrUserIds,
text,
@ -413,7 +404,10 @@ class ThreadManager private constructor(
data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) {
try {
val request = service.broadcastAnimatedMedia(
val request = DirectMessagesService.broadcastAnimatedMedia(
csrfToken,
userId,
deviceUuid,
clientContext,
threadIdOrUserIds,
giphyGif
@ -448,56 +442,33 @@ class ThreadManager private constructor(
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
val uploadDmVoiceOptions = createUploadDmVoiceOptions(byteLength, duration)
uploadVideo(uri, contentResolver, uploadDmVoiceOptions, object : OnMediaUploadCompleteListener {
override fun onUploadComplete(response: MediaUploadResponse) {
scope.launch(Dispatchers.IO) {
try {
val response = uploadVideo(uri, contentResolver, uploadDmVoiceOptions)
// Log.d(TAG, "onUploadComplete: " + response);
if (handleInvalidResponse(data, response)) return
if (handleInvalidResponse(data, response)) return@launch
val uploadFinishOptions = UploadFinishOptions(
uploadDmVoiceOptions.uploadId,
"4",
null
)
val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions)
uploadFinishRequest.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
if (response.isSuccessful) {
scope.launch(Dispatchers.IO) {
try {
val request = service.broadcastVoice(
clientContext,
threadIdOrUserIds,
uploadDmVoiceOptions.uploadId,
waveform,
samplingFreq
)
parseResponse(request, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVoice: ", e)
}
}
return
}
if (response.errorBody() != null) {
handleErrorBody(call, response, data)
return
}
data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem))
Log.e(TAG, "uploadFinishRequest was not successful and response error body was null")
}
override fun onFailure(call: Call<String?>, t: Throwable) {
data.postValue(error(t.message, directItem))
Log.e(TAG, "onFailure: ", t)
}
})
MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVoice(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdOrUserIds,
uploadDmVoiceOptions.uploadId,
waveform,
samplingFreq
)
parseResponse(broadcastResponse, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVoice: ", e)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, directItem))
Log.e(TAG, "onFailure: ", t)
}
})
}
}
fun sendReaction(
@ -526,7 +497,10 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.broadcastReaction(
DirectMessagesService.broadcastReaction(
csrfToken,
userId,
deviceUuid,
clientContext,
threadIdOrUserIds,
itemId,
@ -563,7 +537,16 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.broadcastReaction(clientContext, threadIdOrUserIds, itemId1, null, true)
DirectMessagesService.broadcastReaction(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdOrUserIds,
itemId1,
null,
true
)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendDeleteReaction: ", e)
@ -582,7 +565,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.deleteItem(threadId, itemId)
DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId)
} catch (e: Exception) {
// add the item back if unsuccessful
addItems(index, listOf(item))
@ -658,7 +641,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.forward(
DirectMessagesService.forward(
thread.threadId,
itemTypeName,
threadId,
@ -677,7 +660,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
service.approveRequest(threadId)
DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "acceptRequest: ", e)
@ -691,7 +674,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
service.declineRequest(threadId)
DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "declineRequest: ", e)
@ -736,33 +719,24 @@ class ThreadManager private constructor(
height: Int,
scope: CoroutineScope,
) {
val userId = getCurrentUserId(data) ?: return
val clientContext = UUID.randomUUID().toString()
val directItem = createImageOrVideo(userId, clientContext, uri, width, height, false)
val directItem = createImageOrVideo(viewerId, clientContext, uri, width, height, false)
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
uploadPhoto(uri, contentResolver, object : OnMediaUploadCompleteListener {
override fun onUploadComplete(response: MediaUploadResponse) {
if (handleInvalidResponse(data, response)) return
val response1 = response.response ?: return
scope.launch(Dispatchers.IO) {
try {
val response = uploadPhoto(uri, contentResolver)
if (handleInvalidResponse(data, response)) return@launch
val response1 = response.response ?: return@launch
val uploadId = response1.optString("upload_id")
scope.launch(Dispatchers.IO) {
try {
val response2 = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId)
parseResponse(response2, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendPhoto: ", e)
}
}
val response2 = DirectMessagesService.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdOrUserIds, uploadId)
parseResponse(response2, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendPhoto: ", e)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, directItem))
Log.e(TAG, "onFailure: ", t)
}
})
}
}
private fun sendVideo(
@ -806,56 +780,33 @@ class ThreadManager private constructor(
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
val uploadDmVideoOptions = createUploadDmVideoOptions(byteLength, duration, width, height)
uploadVideo(uri, contentResolver, uploadDmVideoOptions, object : OnMediaUploadCompleteListener {
override fun onUploadComplete(response: MediaUploadResponse) {
scope.launch(Dispatchers.IO) {
try {
val response = uploadVideo(uri, contentResolver, uploadDmVideoOptions)
// Log.d(TAG, "onUploadComplete: " + response);
if (handleInvalidResponse(data, response)) return
if (handleInvalidResponse(data, response)) return@launch
val uploadFinishOptions = UploadFinishOptions(
uploadDmVideoOptions.uploadId,
"2",
VideoOptions(duration / 1000f, emptyList(), 0, false)
)
val uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions)
uploadFinishRequest.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
if (response.isSuccessful) {
scope.launch(Dispatchers.IO) {
try {
val response1 = service.broadcastVideo(
clientContext,
threadIdOrUserIds,
uploadDmVideoOptions.uploadId,
"",
true
)
parseResponse(response1, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendVideo: ", e)
}
}
return
}
if (response.errorBody() != null) {
handleErrorBody(call, response, data)
return
}
data.postValue(error("uploadFinishRequest was not successful and response error body was null", directItem))
Log.e(TAG, "uploadFinishRequest was not successful and response error body was null")
}
override fun onFailure(call: Call<String?>, t: Throwable) {
data.postValue(error(t.message, directItem))
Log.e(TAG, "onFailure: ", t)
}
})
MediaService.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVideo(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdOrUserIds,
uploadDmVideoOptions.uploadId,
"",
true
)
parseResponse(broadcastResponse, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVideo: ", e)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, directItem))
Log.e(TAG, "onFailure: ", t)
}
})
}
}
private fun parseResponse(
@ -912,26 +863,6 @@ class ThreadManager private constructor(
}
}
private fun handleErrorBody(
call: Call<*>,
response: Response<*>,
data: MutableLiveData<Resource<Any?>>?,
) {
try {
val string = response.errorBody()?.string() ?: ""
val msg = String.format(Locale.US,
"onResponse: url: %s, responseCode: %d, errorBody: %s",
call.request().url().toString(),
response.code(),
string)
data?.postValue(error(msg, null))
Log.e(TAG, msg)
} catch (e: IOException) {
data?.postValue(error(e.message, null))
Log.e(TAG, "onResponse: ", e)
}
}
private fun handleInvalidResponse(
data: MutableLiveData<Resource<Any?>>,
response: MediaUploadResponse,
@ -990,7 +921,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = service.updateTitle(threadId, newTitle.trim())
val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
handleDetailsChangeResponse(data, response)
} catch (e: Exception) {
}
@ -1002,7 +933,9 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = service.addUsers(
val response = DirectMessagesService.addUsers(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
@ -1019,7 +952,7 @@ class ThreadManager private constructor(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
service.removeUsers(threadId, setOf(user.pk))
DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
data.postValue(success(Any()))
var activeUsers = users.value
var leftUsersValue = leftUsers.value
@ -1054,7 +987,7 @@ class ThreadManager private constructor(
if (isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
service.addAdmins(threadId, setOf(user.pk))
DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdminIds = adminUserIds.value
val updatedAdminIds = ImmutableList.builder<Long>()
.addAll(currentAdminIds ?: emptyList())
@ -1082,7 +1015,7 @@ class ThreadManager private constructor(
if (!isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
service.removeAdmins(threadId, setOf(user.pk))
DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdmins = adminUserIds.value ?: return@launch
val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return@launch
@ -1112,7 +1045,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.mute(threadId)
DirectMessagesService.mute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1140,7 +1073,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.unmute(threadId)
DirectMessagesService.unmute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1168,7 +1101,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.muteMentions(threadId)
DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1196,7 +1129,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
service.unmuteMentions(threadId)
DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1216,61 +1149,57 @@ class ThreadManager private constructor(
fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.changeBlock(false, user.pk, object : ServiceCallback<FriendshipChangeResponse?> {
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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.changeBlock(true, user.pk, object : ServiceCallback<FriendshipChangeResponse?> {
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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.toggleRestrict(user.pk, true, object : ServiceCallback<FriendshipRestrictResponse?> {
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<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
friendshipService.toggleRestrict(user.pk, false, object : ServiceCallback<FriendshipRestrictResponse?> {
override fun onSuccess(result: FriendshipRestrictResponse?) {
scope.launch(Dispatchers.IO) {
try {
FriendshipService.toggleRestrict(csrfToken, deviceUuid, user.pk, false)
refreshChats(scope)
} catch (e: Exception) {
Log.e(TAG, "onFailure: ", e)
data.postValue(error(e.message, null))
}
override fun onFailure(t: Throwable) {
Log.e(TAG, "onFailure: ", t)
data.postValue(error(t.message, null))
}
})
}
return data
}
@ -1279,7 +1208,9 @@ class ThreadManager private constructor(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = service.approveParticipantRequests(
val response = DirectMessagesService.approveParticipantRequests(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
@ -1298,7 +1229,9 @@ class ThreadManager private constructor(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = service.declineParticipantRequests(
val response = DirectMessagesService.declineParticipantRequests(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
@ -1338,7 +1271,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
val response = service.approvalRequired(threadId)
val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, response)
val currentThread = thread.value ?: return@launch
try {
@ -1366,7 +1299,7 @@ class ThreadManager private constructor(
}
scope.launch(Dispatchers.IO) {
try {
val request = service.approvalNotRequired(threadId)
val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
@ -1389,7 +1322,7 @@ class ThreadManager private constructor(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = service.leave(threadId)
val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
} catch (e: Exception) {
Log.e(TAG, "leave: ", e)
@ -1404,7 +1337,7 @@ class ThreadManager private constructor(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = service.end(threadId)
val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
@ -1441,7 +1374,7 @@ class ThreadManager private constructor(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = service.markAsSeen(threadId, directItem)
val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
if (response == null) {
data.postValue(error(R.string.generic_null_response, null))
return@launch
@ -1464,43 +1397,4 @@ class ThreadManager private constructor(
}
return data
}
companion object {
private val TAG = ThreadManager::class.java.simpleName
private val LOCK = Any()
private val INSTANCE_MAP: MutableMap<String, ThreadManager> = 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();
}
}

View File

@ -4,7 +4,7 @@ import awais.instagrabber.utils.TextUtils
import java.util.*
data class HighlightModel(
val title: String,
val title: String?,
val id: String,
val thumbnailUrl: String,
val timestamp: Long,

View File

@ -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<FriendshipChangeResponse> change(@Path("action") String action,
@Path("id") long id,
@FieldMap Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/restrict_action/{action}/")
Call<FriendshipRestrictResponse> toggleRestrict(@Path("action") String action,
@FieldMap Map<String, String> form);
@GET("/api/v1/friendships/{userId}/{type}/")
Call<String> getList(@Path("userId") long userId,
@Path("type") String type, // following or followers
@QueryMap(encoded = true) Map<String, String> queryParams);
@FormUrlEncoded
@POST("/api/v1/friendships/{action}/")
Call<FriendshipChangeResponse> changeMute(@Path("action") String action,
@FieldMap Map<String, String> form);
}

View File

@ -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<String, String>,
): FriendshipChangeResponse
@FormUrlEncoded
@POST("/api/v1/restrict_action/{action}/")
suspend fun toggleRestrict(
@Path("action") action: String,
@FieldMap form: Map<String, String>,
): 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, String>,
): String
@FormUrlEncoded
@POST("/api/v1/friendships/{action}/")
suspend fun changeMute(
@Path("action") action: String,
@FieldMap form: Map<String, String>,
): FriendshipChangeResponse
}

View File

@ -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<String> fetch(@QueryMap(encoded = true) Map<String, String> queryParams);
@GET("/{username}/?__a=1")
Call<String> getUser(@Path("username") String username);
@GET("/p/{shortcode}/?__a=1")
Call<String> getPost(@Path("shortcode") String shortcode);
@GET("/explore/tags/{tag}/?__a=1")
Call<String> getTag(@Path("tag") String tag);
@GET("/explore/locations/{locationId}/?__a=1")
Call<String> getLocation(@Path("locationId") long locationId);
}

View File

@ -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, String>): 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
}

View File

@ -1,55 +0,0 @@
package awais.instagrabber.repositories;
import java.util.Map;
import awais.instagrabber.repositories.responses.LikersResponse;
import awais.instagrabber.repositories.responses.MediaInfoResponse;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
public interface MediaRepository {
@GET("/api/v1/media/{mediaId}/info/")
Call<MediaInfoResponse> fetch(@Path("mediaId") final long mediaId);
@GET("/api/v1/media/{mediaId}/{action}/")
Call<LikersResponse> fetchLikes(@Path("mediaId") final String mediaId,
@Path("action") final String action); // one of "likers" or "comment_likers"
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/{action}/")
Call<String> action(@Path("action") final String action,
@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/edit_media/")
Call<String> editCaption(@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
@GET("/api/v1/language/translate/")
Call<String> translate(@QueryMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/media/upload_finish/")
Call<String> uploadFinish(@Header("retry_context") final String retryContext,
@QueryMap Map<String, String> queryParams,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/delete/")
Call<String> delete(@Path("mediaId") final String mediaId,
@Query("media_type") final String mediaType,
@FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/archive/")
Call<String> archive(@Path("mediaId") final String mediaId,
@FieldMap final Map<String, String> signedForm);
}

View File

@ -0,0 +1,57 @@
package awais.instagrabber.repositories
import awais.instagrabber.repositories.responses.LikersResponse
import awais.instagrabber.repositories.responses.MediaInfoResponse
import retrofit2.http.*
interface MediaRepository {
@GET("/api/v1/media/{mediaId}/info/")
suspend fun fetch(@Path("mediaId") mediaId: Long): MediaInfoResponse
@GET("/api/v1/media/{mediaId}/{action}/")
suspend fun fetchLikes(
@Path("mediaId") mediaId: String, // one of "likers" or "comment_likers"
@Path("action") action: String,
): LikersResponse
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/{action}/")
suspend fun action(
@Path("action") action: String,
@Path("mediaId") mediaId: String,
@FieldMap signedForm: Map<String, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/edit_media/")
suspend fun editCaption(
@Path("mediaId") mediaId: String,
@FieldMap signedForm: Map<String, String>,
): String
@GET("/api/v1/language/translate/")
suspend fun translate(@QueryMap form: Map<String, String>): String
@FormUrlEncoded
@POST("/api/v1/media/upload_finish/")
suspend fun uploadFinish(
@Header("retry_context") retryContext: String,
@QueryMap queryParams: Map<String, String>,
@FieldMap signedForm: Map<String, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/delete/")
suspend fun delete(
@Path("mediaId") mediaId: String,
@Query("media_type") mediaType: String,
@FieldMap signedForm: Map<String, String>,
): String
@FormUrlEncoded
@POST("/api/v1/media/{mediaId}/archive/")
suspend fun archive(
@Path("mediaId") mediaId: String,
@FieldMap signedForm: Map<String, String>,
): String
}

View File

@ -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<String> 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<String> getFeedStories();
@GET("/api/v1/highlights/{uid}/highlights_tray/")
Call<String> fetchHighlights(@Path("uid") final long uid);
@GET("/api/v1/archive/reel/day_shells/")
Call<String> fetchArchive(@QueryMap Map<String, String> queryParams);
@GET
Call<String> getUserStory(@Url String url);
@FormUrlEncoded
@POST("/api/v1/media/{storyId}/{stickerId}/{action}/")
Call<StoryStickerResponse> 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<String, String> form);
@FormUrlEncoded
@POST("/api/v2/media/seen/")
Call<String> seen(@QueryMap Map<String, String> queryParams, @FieldMap Map<String, String> form);
}

View File

@ -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, String>): 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<String, String>,
): StoryStickerResponse
@FormUrlEncoded
@POST("/api/v2/media/seen/")
suspend fun seen(
@QueryMap queryParams: Map<String, String>,
@FieldMap form: Map<String, String>,
): String
}

View File

@ -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<WrappedUser> getUserInfo(@Path("uid") final long uid);
@GET("/api/v1/users/{username}/usernameinfo/")
Call<WrappedUser> getUsernameInfo(@Path("username") final String username);
@GET("/api/v1/friendships/show/{uid}/")
Call<FriendshipStatus> getUserFriendship(@Path("uid") final long uid);
@GET("/api/v1/users/search/")
Call<UserSearchResponse> search(@Query("timezone_offset") float timezoneOffset,
@Query("q") String query);
}

View File

@ -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
}

View File

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

View File

@ -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<UserProfileContextLink>? = null, // ^
val socialContext: String? = null, // AYML
val interopMessagingUserFbid: String? = null, // in DMs only: Facebook user ID
) : Serializable {
val hDProfilePicUrl: String
get() = hdProfilePicUrlInfo?.url ?: profilePicUrl ?: ""
}

View File

@ -1,280 +0,0 @@
package awais.instagrabber.utils;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class BitmapUtils {
private static final String TAG = BitmapUtils.class.getSimpleName();
private static final LruCache<String, Bitmap> bitmapMemoryCache;
private static final AppExecutors appExecutors = AppExecutors.INSTANCE;
private static final ExecutorService callbackHandlers = Executors
.newCachedThreadPool(r -> new Thread(r, "bm-load-callback-handler#" + NumberUtils.random(0, 100)));
public static final float THUMBNAIL_SIZE = 200f;
static {
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
bitmapMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
}
public static void addBitmapToMemoryCache(final String key, final Bitmap bitmap, final boolean force) {
if (force || getBitmapFromMemCache(key) == null) {
bitmapMemoryCache.put(key, bitmap);
}
}
public static Bitmap getBitmapFromMemCache(final String key) {
return bitmapMemoryCache.get(key);
}
public static void getThumbnail(final Context context, final Uri uri, final ThumbnailLoadCallback callback) {
if (context == null || uri == null || callback == null) return;
final String key = uri.toString();
final Bitmap cachedBitmap = getBitmapFromMemCache(key);
if (cachedBitmap != null) {
callback.onLoad(cachedBitmap, -1, -1);
return;
}
loadBitmap(context.getContentResolver(), uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true, callback);
}
/**
* Loads bitmap from given Uri
*
* @param contentResolver {@link ContentResolver} to resolve the uri
* @param uri Uri from where Bitmap will be loaded
* @param reqWidth Required width
* @param reqHeight Required height
* @param addToCache true if the loaded bitmap should be added to the mem cache
* @param callback Bitmap load callback
*/
public static void loadBitmap(final ContentResolver contentResolver,
final Uri uri,
final float reqWidth,
final float reqHeight,
final boolean addToCache,
final ThumbnailLoadCallback callback) {
loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1, addToCache, callback);
}
/**
* Loads bitmap from given Uri
*
* @param contentResolver {@link ContentResolver} to resolve the uri
* @param uri Uri from where Bitmap will be loaded
* @param maxDimenSize Max size of the largest side of the image
* @param addToCache true if the loaded bitmap should be added to the mem cache
* @param callback Bitmap load callback
*/
public static void loadBitmap(final ContentResolver contentResolver,
final Uri uri,
final float maxDimenSize,
final boolean addToCache,
final ThumbnailLoadCallback callback) {
loadBitmap(contentResolver, uri, -1, -1, maxDimenSize, addToCache, callback);
}
/**
* Loads bitmap from given Uri
*
* @param contentResolver {@link ContentResolver} to resolve the uri
* @param uri Uri from where {@link Bitmap} will be loaded
* @param reqWidth Required width (set to -1 if maxDimenSize provided)
* @param reqHeight Required height (set to -1 if maxDimenSize provided)
* @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight)
* @param addToCache true if the loaded bitmap should be added to the mem cache
* @param callback Bitmap load callback
*/
private static void loadBitmap(final ContentResolver contentResolver,
final Uri uri,
final float reqWidth,
final float reqHeight,
final float maxDimenSize,
final boolean addToCache,
final ThumbnailLoadCallback callback) {
if (contentResolver == null || uri == null || callback == null) return;
final ListenableFuture<BitmapResult> future = appExecutors
.getTasksThread()
.submit(() -> getBitmapResult(contentResolver, uri, reqWidth, reqHeight, maxDimenSize, addToCache));
Futures.addCallback(future, new FutureCallback<BitmapResult>() {
@Override
public void onSuccess(@Nullable final BitmapResult result) {
if (result == null) {
callback.onLoad(null, -1, -1);
return;
}
callback.onLoad(result.bitmap, result.width, result.height);
}
@Override
public void onFailure(@NonNull final Throwable t) {
callback.onFailure(t);
}
}, callbackHandlers);
}
@Nullable
public static BitmapResult getBitmapResult(final ContentResolver contentResolver,
final Uri uri,
final float reqWidth,
final float reqHeight,
final float maxDimenSize,
final boolean addToCache) {
BitmapFactory.Options bitmapOptions;
float actualReqWidth = reqWidth;
float actualReqHeight = reqHeight;
try (InputStream input = contentResolver.openInputStream(uri)) {
BitmapFactory.Options outBounds = new BitmapFactory.Options();
outBounds.inJustDecodeBounds = true;
outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888;
BitmapFactory.decodeStream(input, null, outBounds);
if ((outBounds.outWidth == -1) || (outBounds.outHeight == -1)) return null;
bitmapOptions = new BitmapFactory.Options();
if (maxDimenSize > 0) {
// Raw height and width of image
final int height = outBounds.outHeight;
final int width = outBounds.outWidth;
final float ratio = (float) width / height;
if (height > width) {
actualReqHeight = maxDimenSize;
actualReqWidth = actualReqHeight * ratio;
} else {
actualReqWidth = maxDimenSize;
actualReqHeight = actualReqWidth / ratio;
}
}
bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight);
} catch (Exception e) {
Log.e(TAG, "loadBitmap: ", e);
return null;
}
try (InputStream input = contentResolver.openInputStream(uri)) {
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
if (addToCache) {
addBitmapToMemoryCache(uri.toString(), bitmap, true);
}
return new BitmapResult(bitmap, (int) actualReqWidth, (int) actualReqHeight);
} catch (Exception e) {
Log.e(TAG, "loadBitmap: ", e);
}
return null;
}
public static class BitmapResult {
public Bitmap bitmap;
int width;
int height;
public BitmapResult(final Bitmap bitmap, final int width, final int height) {
this.width = width;
this.height = height;
this.bitmap = bitmap;
}
}
private static int calculateInSampleSize(final BitmapFactory.Options options, final float reqWidth, final float reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final float halfHeight = height / 2f;
final float halfWidth = width / 2f;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public interface ThumbnailLoadCallback {
/**
* @param bitmap Resulting bitmap
* @param width width of the bitmap (Only correct if loadBitmap was called or -1)
* @param height height of the bitmap (Only correct if loadBitmap was called or -1)
*/
void onLoad(@Nullable Bitmap bitmap, int width, int height);
void onFailure(@NonNull Throwable t);
}
/**
* Decodes the bounds of an image from its Uri and returns a pair of the dimensions
*
* @param uri the Uri of the image
* @return dimensions of the image
*/
public static Pair<Integer, Integer> decodeDimensions(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri) throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try (final InputStream stream = contentResolver.openInputStream(uri)) {
BitmapFactory.decodeStream(stream, null, options);
return (options.outWidth == -1 || options.outHeight == -1)
? null
: new Pair<>(options.outWidth, options.outHeight);
}
}
public static File convertToJpegAndSaveToFile(@NonNull final Bitmap bitmap, @Nullable final File file) throws IOException {
File tempFile = file;
if (file == null) {
tempFile = DownloadUtils.getTempFile();
}
try (OutputStream output = new FileOutputStream(tempFile)) {
final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output);
if (!compressResult) {
throw new RuntimeException("Compression failed!");
}
}
return tempFile;
}
public static void convertToJpegAndSaveToUri(@NonNull Context context,
@NonNull final Bitmap bitmap,
@NonNull final Uri uri) throws Exception {
try (OutputStream output = context.getContentResolver().openOutputStream(uri)) {
final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output);
if (!compressResult) {
throw new RuntimeException("Compression failed!");
}
}
}
}

View File

@ -0,0 +1,238 @@
package awais.instagrabber.utils
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import android.util.LruCache
import androidx.core.util.Pair
import awais.instagrabber.utils.extensions.TAG
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object BitmapUtils {
private val bitmapMemoryCache: LruCache<String, Bitmap>
const val THUMBNAIL_SIZE = 200f
@JvmStatic
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap, force: Boolean) {
if (force || getBitmapFromMemCache(key) == null) {
bitmapMemoryCache.put(key, bitmap)
}
}
@JvmStatic
fun getBitmapFromMemCache(key: String): Bitmap? {
return bitmapMemoryCache[key]
}
@JvmStatic
suspend fun getThumbnail(context: Context, uri: Uri): BitmapResult? {
val key = uri.toString()
val cachedBitmap = getBitmapFromMemCache(key)
if (cachedBitmap != null) {
return BitmapResult(cachedBitmap, -1, -1)
}
return loadBitmap(context.contentResolver, uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, true)
}
/**
* Loads bitmap from given Uri
*
* @param contentResolver [ContentResolver] to resolve the uri
* @param uri Uri from where Bitmap will be loaded
* @param reqWidth Required width
* @param reqHeight Required height
* @param addToCache true if the loaded bitmap should be added to the mem cache
*/
suspend fun loadBitmap(
contentResolver: ContentResolver?,
uri: Uri?,
reqWidth: Float,
reqHeight: Float,
addToCache: Boolean,
): BitmapResult? = loadBitmap(contentResolver, uri, reqWidth, reqHeight, -1f, addToCache)
/**
* Loads bitmap from given Uri
*
* @param contentResolver [ContentResolver] to resolve the uri
* @param uri Uri from where Bitmap will be loaded
* @param maxDimenSize Max size of the largest side of the image
* @param addToCache true if the loaded bitmap should be added to the mem cache
*/
suspend fun loadBitmap(
contentResolver: ContentResolver?,
uri: Uri?,
maxDimenSize: Float,
addToCache: Boolean,
): BitmapResult? = loadBitmap(contentResolver, uri, -1f, -1f, maxDimenSize, addToCache)
/**
* Loads bitmap from given Uri
*
* @param contentResolver [ContentResolver] to resolve the uri
* @param uri Uri from where [Bitmap] will be loaded
* @param reqWidth Required width (set to -1 if maxDimenSize provided)
* @param reqHeight Required height (set to -1 if maxDimenSize provided)
* @param maxDimenSize Max size of the largest side of the image (set to -1 if setting reqWidth and reqHeight)
* @param addToCache true if the loaded bitmap should be added to the mem cache
*/
private suspend fun loadBitmap(
contentResolver: ContentResolver?,
uri: Uri?,
reqWidth: Float,
reqHeight: Float,
maxDimenSize: Float,
addToCache: Boolean,
): BitmapResult? =
if (contentResolver == null || uri == null) null else withContext(Dispatchers.IO) {
getBitmapResult(contentResolver,
uri,
reqWidth,
reqHeight,
maxDimenSize,
addToCache)
}
fun getBitmapResult(
contentResolver: ContentResolver,
uri: Uri,
reqWidth: Float,
reqHeight: Float,
maxDimenSize: Float,
addToCache: Boolean,
): BitmapResult? {
var bitmapOptions: BitmapFactory.Options
var actualReqWidth = reqWidth
var actualReqHeight = reqHeight
try {
contentResolver.openInputStream(uri).use { input ->
val outBounds = BitmapFactory.Options()
outBounds.inJustDecodeBounds = true
outBounds.inPreferredConfig = Bitmap.Config.ARGB_8888
BitmapFactory.decodeStream(input, null, outBounds)
if (outBounds.outWidth == -1 || outBounds.outHeight == -1) return null
bitmapOptions = BitmapFactory.Options()
if (maxDimenSize > 0) {
// Raw height and width of image
val height = outBounds.outHeight
val width = outBounds.outWidth
val ratio = width.toFloat() / height
if (height > width) {
actualReqHeight = maxDimenSize
actualReqWidth = actualReqHeight * ratio
} else {
actualReqWidth = maxDimenSize
actualReqHeight = actualReqWidth / ratio
}
}
bitmapOptions.inSampleSize = calculateInSampleSize(outBounds, actualReqWidth, actualReqHeight)
}
} catch (e: Exception) {
Log.e(TAG, "loadBitmap: ", e)
return null
}
try {
contentResolver.openInputStream(uri).use { input ->
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888
val bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions)
if (addToCache && bitmap != null) {
addBitmapToMemoryCache(uri.toString(), bitmap, true)
}
return BitmapResult(bitmap, actualReqWidth.toInt(), actualReqHeight.toInt())
}
} catch (e: Exception) {
Log.e(TAG, "loadBitmap: ", e)
}
return null
}
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Float, reqHeight: Float): Int {
// Raw height and width of image
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2f
val halfWidth = width / 2f
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight
&& halfWidth / inSampleSize >= reqWidth
) {
inSampleSize *= 2
}
}
return inSampleSize
}
/**
* Decodes the bounds of an image from its Uri and returns a pair of the dimensions
*
* @param uri the Uri of the image
* @return dimensions of the image
*/
@Throws(IOException::class)
fun decodeDimensions(
contentResolver: ContentResolver,
uri: Uri,
): Pair<Int, Int>? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
contentResolver.openInputStream(uri).use { stream ->
BitmapFactory.decodeStream(stream, null, options)
return if (options.outWidth == -1 || options.outHeight == -1) null else Pair(options.outWidth, options.outHeight)
}
}
@Throws(IOException::class)
fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File {
val tempFile = file ?: DownloadUtils.getTempFile()
FileOutputStream(tempFile).use { output ->
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
if (!compressResult) {
throw RuntimeException("Compression failed!")
}
}
return tempFile
}
@JvmStatic
@Throws(Exception::class)
fun convertToJpegAndSaveToUri(
context: Context,
bitmap: Bitmap,
uri: Uri,
) {
context.contentResolver.openOutputStream(uri).use { output ->
val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
if (!compressResult) {
throw RuntimeException("Compression failed!")
}
}
}
class BitmapResult(var bitmap: Bitmap?, var width: Int, var height: Int)
init {
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
// Use 1/8th of the available memory for this memory cache.
val cacheSize: Int = maxMemory / 8
bitmapMemoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.byteCount / 1024
}
}
}
}

View File

@ -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<Void?>?) {
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 {

View File

@ -38,10 +38,10 @@ import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -160,17 +160,20 @@ public final class ExportImportUtils {
);
// Log.d(TAG, "importJson: favoriteModel: " + favoriteModel);
final FavoriteRepository favRepo = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
favRepo.getFavorite(query, favoriteType, new RepositoryCallback<Favorite>() {
@Override
public void onSuccess(final Favorite result) {
// local has priority since it's more frequently updated
}
@Override
public void onDataNotAvailable() {
favRepo.insertOrUpdateFavorite(favorite, null);
}
});
favRepo.getFavorite(
query,
favoriteType,
CoroutineUtilsKt.getContinuation((favorite1, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
Log.e(TAG, "importFavorites: ", throwable);
return;
}
if (favorite1 == null) {
favRepo.insertOrUpdateFavorite(favorite, CoroutineUtilsKt.getContinuation((unit, throwable1) -> {}, Dispatchers.getIO()));
}
// local has priority since it's more frequently updated
}), Dispatchers.getIO())
);
}
}
@ -197,7 +200,7 @@ public final class ExportImportUtils {
return;
}
AccountRepository.getInstance(AccountDataSource.getInstance(context))
.insertOrUpdateAccounts(accounts, null);
.insertOrUpdateAccounts(accounts, CoroutineUtilsKt.getContinuation((unit, throwable) -> {}, Dispatchers.getIO()));
}
private static void importSettings(final JSONObject jsonObject) {
@ -363,66 +366,64 @@ public final class ExportImportUtils {
private static ListenableFuture<JSONArray> getFavorites(final Context context) {
final SettableFuture<JSONArray> future = SettableFuture.create();
final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
favoriteRepository.getAllFavorites(new RepositoryCallback<List<Favorite>>() {
@Override
public void onSuccess(final List<Favorite> favorites) {
final JSONArray jsonArray = new JSONArray();
try {
for (final Favorite favorite : favorites) {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("q", favorite.getQuery());
jsonObject.put("type", favorite.getType().toString());
jsonObject.put("s", favorite.getDisplayName());
jsonObject.put("pic_url", favorite.getPicUrl());
jsonObject.put("d", favorite.getDateAdded().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
jsonArray.put(jsonObject);
favoriteRepository.getAllFavorites(
CoroutineUtilsKt.getContinuation((favorites, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
future.set(new JSONArray());
Log.e(TAG, "getFavorites: ", throwable);
return;
}
} catch (Exception e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error exporting favorites", e);
final JSONArray jsonArray = new JSONArray();
try {
for (final Favorite favorite : favorites) {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("q", favorite.getQuery());
jsonObject.put("type", favorite.getType().toString());
jsonObject.put("s", favorite.getDisplayName());
jsonObject.put("pic_url", favorite.getPicUrl());
jsonObject.put("d", favorite.getDateAdded().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
jsonArray.put(jsonObject);
}
} catch (Exception e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error exporting favorites", e);
}
}
}
future.set(jsonArray);
}
@Override
public void onDataNotAvailable() {
future.set(new JSONArray());
}
});
future.set(jsonArray);
}), Dispatchers.getIO())
);
return future;
}
private static ListenableFuture<JSONArray> getCookies(final Context context) {
final SettableFuture<JSONArray> future = SettableFuture.create();
final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(context));
accountRepository.getAllAccounts(new RepositoryCallback<List<Account>>() {
@Override
public void onSuccess(final List<Account> 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;
}

View File

@ -4,12 +4,14 @@ import android.content.ContentResolver
import android.graphics.Bitmap
import android.net.Uri
import awais.instagrabber.models.UploadVideoOptions
import awais.instagrabber.utils.BitmapUtils.ThumbnailLoadCallback
import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.*
import okio.BufferedSink
import okio.Okio
import org.json.JSONObject
import ru.gildor.coroutines.okhttp.await
import java.io.File
import java.io.FileInputStream
import java.io.IOException
@ -17,89 +19,59 @@ import java.io.InputStream
object MediaUploader {
private const val HOST = "https://i.instagram.com"
private val appExecutors = AppExecutors
fun uploadPhoto(
uri: Uri,
contentResolver: ContentResolver,
listener: OnMediaUploadCompleteListener,
) {
BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false, object : ThumbnailLoadCallback {
override fun onLoad(bitmap: Bitmap?, width: Int, height: Int) {
if (bitmap == null) {
listener.onFailure(RuntimeException("Bitmap result was null"))
return
}
uploadPhoto(bitmap, listener)
}
override fun onFailure(t: Throwable) {
listener.onFailure(t)
}
})
private val octetStreamMediaType: MediaType = requireNotNull(MediaType.parse("application/octet-stream")) {
"No media type found for application/octet-stream"
}
private fun uploadPhoto(
suspend fun uploadPhoto(
uri: Uri,
contentResolver: ContentResolver,
): MediaUploadResponse = withContext(Dispatchers.IO) {
val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false)
val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null")
uploadPhoto(bitmap)
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun uploadPhoto(
bitmap: Bitmap,
listener: OnMediaUploadCompleteListener,
) {
appExecutors.tasksThread.submit {
val file: File
val byteLength: Long
try {
file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null)
byteLength = file.length()
} catch (e: Exception) {
listener.onFailure(e)
return@submit
}
val options = createUploadPhotoOptions(byteLength)
val headers = getUploadPhotoHeaders(options)
val url = HOST + "/rupload_igphoto/" + options.name + "/"
appExecutors.networkIO.execute {
try {
FileInputStream(file).use { input -> upload(input, url, headers, listener) }
} catch (e: IOException) {
listener.onFailure(e)
} finally {
file.delete()
}
}
): MediaUploadResponse = withContext(Dispatchers.IO) {
val file: File = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null)
val byteLength: Long = file.length()
val options = createUploadPhotoOptions(byteLength)
val headers = getUploadPhotoHeaders(options)
val url = HOST + "/rupload_igphoto/" + options.name + "/"
try {
FileInputStream(file).use { input -> upload(input, url, headers) }
} finally {
file.delete()
}
}
@JvmStatic
fun uploadVideo(
@Suppress("BlockingMethodInNonBlockingContext") // See https://youtrack.jetbrains.com/issue/KTIJ-838
suspend fun uploadVideo(
uri: Uri,
contentResolver: ContentResolver,
options: UploadVideoOptions,
listener: OnMediaUploadCompleteListener,
) {
appExecutors.tasksThread.submit {
val headers = getUploadVideoHeaders(options)
val url = HOST + "/rupload_igvideo/" + options.name + "/"
appExecutors.networkIO.execute {
try {
contentResolver.openInputStream(uri).use { input ->
if (input == null) {
listener.onFailure(RuntimeException("InputStream was null"))
return@execute
}
upload(input, url, headers, listener)
}
} catch (e: IOException) {
listener.onFailure(e)
}
): MediaUploadResponse = withContext(Dispatchers.IO) {
val headers = getUploadVideoHeaders(options)
val url = HOST + "/rupload_igvideo/" + options.name + "/"
contentResolver.openInputStream(uri).use { input ->
if (input == null) {
// listener.onFailure(RuntimeException("InputStream was null"))
throw IllegalStateException("InputStream was null")
}
upload(input, url, headers)
}
}
private fun upload(
@Throws(IOException::class)
private suspend fun upload(
input: InputStream,
url: String,
headers: Map<String, String>,
listener: OnMediaUploadCompleteListener,
) {
): MediaUploadResponse {
try {
val client = OkHttpClient.Builder()
// .addInterceptor(new LoggingInterceptor())
@ -110,46 +82,38 @@ object MediaUploader {
val request = Request.Builder()
.headers(Headers.of(headers))
.url(url)
.post(create(MediaType.parse("application/octet-stream"), input))
.post(create(octetStreamMediaType, input))
.build()
val call = client.newCall(request)
val response = call.execute()
val body = response.body()
if (!response.isSuccessful) {
listener.onFailure(IOException("Unexpected code " + response + if (body != null) ": " + body.string() else ""))
return
return withContext(Dispatchers.IO) {
val response = client.newCall(request).await()
val body = response.body()
@Suppress("BlockingMethodInNonBlockingContext") // Blocked by https://github.com/square/okio/issues/501
MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null)
}
listener.onUploadComplete(MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null))
} catch (e: Exception) {
listener.onFailure(e)
// rethrow for proper stacktrace. See https://github.com/gildor/kotlin-coroutines-okhttp/tree/master#wrap-exception-manually
throw IOException(e)
}
}
private fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody {
return object : RequestBody() {
override fun contentType(): MediaType? {
return mediaType
}
private fun create(mediaType: MediaType, inputStream: InputStream): RequestBody = object : RequestBody() {
override fun contentType(): MediaType {
return mediaType
}
override fun contentLength(): Long {
return try {
inputStream.available().toLong()
} catch (e: IOException) {
0
}
}
@Throws(IOException::class)
@Suppress("DEPRECATION_ERROR")
override fun writeTo(sink: BufferedSink) {
Okio.source(inputStream).use { sink.writeAll(it) }
override fun contentLength(): Long {
return try {
inputStream.available().toLong()
} catch (e: IOException) {
0
}
}
}
interface OnMediaUploadCompleteListener {
fun onUploadComplete(response: MediaUploadResponse)
fun onFailure(t: Throwable)
@Throws(IOException::class)
@Suppress("DEPRECATION_ERROR")
override fun writeTo(sink: BufferedSink) {
Okio.source(inputStream).use { sink.writeAll(it) }
}
}
data class MediaUploadResponse(val responseCode: Int, val response: JSONObject?)

View File

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

View File

@ -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<String> 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<String> 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<String> request,
final boolean root,
final String shortCodeOrCommentId,
final ServiceCallback callback) {
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<Comment> 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<String> 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<String> 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<Comment> 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

View File

@ -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

View File

@ -1,13 +1,18 @@
package awais.instagrabber.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import awais.instagrabber.db.datasources.FavoriteDataSource
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.db.repositories.RepositoryCallback
import awais.instagrabber.utils.extensions.TAG
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FavoritesViewModel(application: Application) : AndroidViewModel(application) {
private val _list = MutableLiveData<List<Favorite>>()
@ -20,29 +25,24 @@ class FavoritesViewModel(application: Application) : AndroidViewModel(applicatio
}
fun fetch() {
favoriteRepository.getAllFavorites(object : RepositoryCallback<List<Favorite>> {
override fun onSuccess(favorites: List<Favorite>?) {
_list.postValue(favorites ?: emptyList())
viewModelScope.launch(Dispatchers.IO) {
try {
_list.postValue(favoriteRepository.getAllFavorites())
} catch (e: Exception) {
Log.e(TAG, "fetch: ", e)
}
override fun onDataNotAvailable() {}
})
}
}
fun delete(favorite: Favorite, onSuccess: () -> Unit) {
favoriteRepository.deleteFavorite(favorite.query, favorite.type, object : RepositoryCallback<Void> {
override fun onSuccess(result: Void?) {
onSuccess()
favoriteRepository.getAllFavorites(object : RepositoryCallback<List<Favorite>> {
override fun onSuccess(result: List<Favorite>?) {
_list.postValue(result ?: emptyList())
}
override fun onDataNotAvailable() {}
})
viewModelScope.launch(Dispatchers.IO) {
try {
favoriteRepository.deleteFavorite(favorite.query, favorite.type)
withContext(Dispatchers.Main) { onSuccess() }
_list.postValue(favoriteRepository.getAllFavorites())
} catch (e: Exception) {
Log.e(TAG, "delete: ", e)
}
override fun onDataNotAvailable() {}
})
}
}
}

View File

@ -23,11 +23,9 @@ import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.MediaService
import awais.instagrabber.webservices.ServiceCallback
import com.google.common.collect.ImmutableList
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
class PostViewV2ViewModel : ViewModel() {
@ -42,12 +40,15 @@ class PostViewV2ViewModel : ViewModel() {
private val liked = MutableLiveData(false)
private val saved = MutableLiveData(false)
private val options = MutableLiveData<List<Int>>(ArrayList())
private val viewerId: Long
val isLoggedIn: Boolean
private var messageManager: DirectMessagesManager? = null
private val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
private val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
private val csrfToken = getCsrfTokenFromCookie(cookie)
private val viewerId = getUserIdFromCookie(cookie)
lateinit var media: Media
private set
private var mediaService: MediaService? = null
private var messageManager: DirectMessagesManager? = null
val isLoggedIn = cookie.isNotBlank() && !csrfToken.isNullOrBlank() && viewerId != 0L
fun setMedia(media: Media) {
this.media = media
@ -127,44 +128,59 @@ class PostViewV2ViewModel : ViewModel() {
fun like(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.like(media.pk, getLikeUnlikeCallback(data))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
try {
val mediaId = media.pk ?: return@launch
val liked = MediaService.like(csrfToken!!, viewerId, deviceUuid, mediaId)
updateMediaLikeUnlike(data, liked)
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun unlike(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.unlike(media.pk, getLikeUnlikeCallback(data))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
try {
val mediaId = media.pk ?: return@launch
val unliked = MediaService.unlike(csrfToken!!, viewerId, deviceUuid, mediaId)
updateMediaLikeUnlike(data, unliked)
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
private fun getLikeUnlikeCallback(data: MutableLiveData<Resource<Any?>>): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && !result) {
data.postValue(error("", null))
return
}
data.postValue(success(true))
val currentLikesCount = media.likeCount
val updatedCount: Long
if (!media.hasLiked) {
updatedCount = currentLikesCount + 1
media.hasLiked = true
} else {
updatedCount = currentLikesCount - 1
media.hasLiked = false
}
media.likeCount = updatedCount
likeCount.postValue(updatedCount)
liked.postValue(media.hasLiked)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, null))
Log.e(TAG, "Error during like/unlike", t)
}
private fun updateMediaLikeUnlike(data: MutableLiveData<Resource<Any?>>, result: Boolean) {
if (!result) {
data.postValue(error("", null))
return
}
data.postValue(success(true))
val currentLikesCount = media.likeCount
val updatedCount: Long
if (!media.hasLiked) {
updatedCount = currentLikesCount + 1
media.hasLiked = true
} else {
updatedCount = currentLikesCount - 1
media.hasLiked = false
}
media.likeCount = updatedCount
likeCount.postValue(updatedCount)
liked.postValue(media.hasLiked)
}
fun toggleSave(): LiveData<Resource<Any?>> {
@ -180,79 +196,99 @@ class PostViewV2ViewModel : ViewModel() {
fun save(collection: String?, ignoreSaveState: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.save(media.pk, collection, getSaveUnsaveCallback(data, ignoreSaveState))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
try {
val mediaId = media.pk ?: return@launch
val saved = MediaService.save(csrfToken!!, viewerId, deviceUuid, mediaId, collection)
getSaveUnsaveCallback(data, saved, ignoreSaveState)
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun unsave(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.unsave(media.pk, getSaveUnsaveCallback(data, false))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
val mediaId = media.pk ?: return@launch
val unsaved = MediaService.unsave(csrfToken!!, viewerId, deviceUuid, mediaId)
getSaveUnsaveCallback(data, unsaved, false)
}
return data
}
private fun getSaveUnsaveCallback(
data: MutableLiveData<Resource<Any?>>,
result: Boolean,
ignoreSaveState: Boolean,
): ServiceCallback<Boolean?> {
return object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && !result) {
data.postValue(error("", null))
return
}
data.postValue(success(true))
if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved
saved.postValue(media.hasViewerSaved)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, null))
Log.e(TAG, "Error during save/unsave", t)
}
) {
if (!result) {
data.postValue(error("", null))
return
}
data.postValue(success(true))
if (!ignoreSaveState) media.hasViewerSaved = !media.hasViewerSaved
saved.postValue(media.hasViewerSaved)
}
fun updateCaption(caption: String): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
mediaService?.editCaption(media.pk, caption, object : ServiceCallback<Boolean?> {
override fun onSuccess(result: Boolean?) {
if (result != null && result) {
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
try {
val postId = media.pk ?: return@launch
val result = MediaService.editCaption(csrfToken!!, viewerId, deviceUuid, postId, caption)
if (result) {
data.postValue(success(""))
media.setPostCaption(caption)
this@PostViewV2ViewModel.caption.postValue(media.caption)
return
return@launch
}
data.postValue(error("", null))
} catch (e: Exception) {
Log.e(TAG, "Error editing caption", e)
data.postValue(error(e.message, null))
}
override fun onFailure(t: Throwable) {
Log.e(TAG, "Error editing caption", t)
data.postValue(error(t.message, null))
}
})
}
return data
}
fun translateCaption(): LiveData<Resource<String?>> {
val data = MutableLiveData<Resource<String?>>()
data.postValue(loading(null))
val value = caption.value ?: return data
mediaService?.translate(value.pk, "1", object : ServiceCallback<String?> {
override fun onSuccess(result: String?) {
if (result.isNullOrBlank()) {
val value = caption.value
val pk = value?.pk
if (pk == null) {
data.postValue(error("caption is null", null))
return data
}
viewModelScope.launch(Dispatchers.IO) {
try {
val result = MediaService.translate(pk, "1")
if (result.isBlank()) {
data.postValue(error("", null))
return
return@launch
}
data.postValue(success(result))
} catch (e: Exception) {
Log.e(TAG, "Error translating comment", e)
data.postValue(error(e.message, null))
}
override fun onFailure(t: Throwable) {
Log.e(TAG, "Error translating comment", t)
data.postValue(error(t.message, null))
}
})
}
return data
}
@ -267,36 +303,29 @@ class PostViewV2ViewModel : ViewModel() {
fun delete(): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
if (!isLoggedIn) {
data.postValue(error("Not logged in!", null))
return data
}
val mediaId = media.id
val mediaType = media.mediaType
if (mediaId == null || mediaType == null) {
data.postValue(error("media id or type is null", null))
return data
}
val request = mediaService?.delete(mediaId, mediaType)
if (request == null) {
data.postValue(success(Any()))
return data
}
request.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
if (!response.isSuccessful) {
data.postValue(error(R.string.generic_null_response, null))
return
}
val body = response.body()
if (body == null) {
data.postValue(error(R.string.generic_null_response, null))
return
viewModelScope.launch(Dispatchers.IO) {
try {
val response = MediaService.delete(csrfToken!!, viewerId, deviceUuid, mediaId, mediaType)
if (response == null) {
data.postValue(success(Any()))
return@launch
}
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "delete: ", e)
data.postValue(error(e.message, null))
}
override fun onFailure(call: Call<String?>, t: Throwable) {
Log.e(TAG, "onFailure: ", t)
data.postValue(error(t.message, null))
}
})
}
return data
}
@ -315,15 +344,4 @@ class PostViewV2ViewModel : ViewModel() {
val mediaId = media.id ?: return
messageManager?.sendMedia(recipients, mediaId, viewModelScope)
}
init {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
val csrfToken: String? = getCsrfTokenFromCookie(cookie)
viewerId = getUserIdFromCookie(cookie)
isLoggedIn = cookie.isNotBlank() && viewerId != 0L
if (!csrfToken.isNullOrBlank()) {
mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId)
}
}
}

View File

@ -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<User?>()
val profile: LiveData<User?> = _profile
val username: LiveData<String> = 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())
}
}

View File

@ -33,9 +33,11 @@ import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.Debouncer;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.SearchService;
import kotlinx.coroutines.Dispatchers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -191,17 +193,17 @@ public class SearchFragmentViewModel extends AppStateViewModel {
recentResultsFuture.set(Collections.emptyList());
}
});
favoriteRepository.getAllFavorites(new RepositoryCallback<List<Favorite>>() {
@Override
public void onSuccess(final List<Favorite> result) {
favoritesFuture.set(result);
}
@Override
public void onDataNotAvailable() {
favoritesFuture.set(Collections.emptyList());
}
});
favoriteRepository.getAllFavorites(
CoroutineUtilsKt.getContinuation((favorites, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
favoritesFuture.set(Collections.emptyList());
Log.e(TAG, "showRecentSearchesAndFavorites: ", throwable);
return;
}
//noinspection unchecked
favoritesFuture.set((List<Favorite>) favorites);
}), Dispatchers.getIO())
);
//noinspection UnstableApiUsage
final ListenableFuture<List<List<?>>> listenableFuture = Futures.allAsList(recentResultsFuture, favoritesFuture);
Futures.addCallback(listenableFuture, new FutureCallback<List<List<?>>>() {

View File

@ -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<UserSearchResponse>) 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<RankedRecipient> 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<UserSearchResponse> request) {
request.enqueue(new Callback<UserSearchResponse>() {
@Override
public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> 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<RankedRecipient> 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<UserSearchResponse> call, @NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients()));
searchRequest = null;
}
});
}
private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) {
final Iterator<RankedRecipient> iterator = list.stream()
.filter(Objects::nonNull)

View File

@ -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<Float>,
samplingFreq: Int,
): DirectThreadBroadcastResponse {
return broadcast(VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq))
suspend fun broadcastStoryReply(
csrfToken: String,
userId: Long,
deviceUuid: String,
threadIdOrUserIds: ThreadIdOrUserIds,
text: String,
mediaId: String,
reelId: String,
): DirectThreadBroadcastResponse {
return broadcast(StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId))
suspend fun broadcastReaction(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds,
itemId: String,
emoji: String?,
delete: Boolean,
): DirectThreadBroadcastResponse {
return broadcast(ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete))
suspend fun broadcastAnimatedMedia(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds,
giphyGif: GiphyGif,
): DirectThreadBroadcastResponse {
return broadcast(AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif))
suspend fun broadcastMediaShare(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String,
threadIdOrUserIds: ThreadIdOrUserIds,
mediaId: String,
): DirectThreadBroadcastResponse {
return broadcast(MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId))
}
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, MediaShareBroadcastOptions(clientContext, threadIdOrUserIds, mediaId))
private suspend fun broadcast(broadcastOptions: BroadcastOptions): DirectThreadBroadcastResponse {
require(!isEmpty(broadcastOptions.clientContext)) { "Broadcast requires a valid client context value" }
val form = mutableMapOf<String, Any>()
private suspend fun broadcast(
csrfToken: String,
userId: Long,
deviceUuid: String,
broadcastOptions: BroadcastOptions,
): DirectThreadBroadcastResponse {
require(broadcastOptions.clientContext.isNotBlank()) { "Broadcast requires a valid client context value" }
val form = mutableMapOf<String, Any>(
"_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<Long>,
): DirectThreadDetailsChangeResponse {
@ -195,6 +218,8 @@ class DirectMessagesService private constructor(
}
suspend fun removeUsers(
csrfToken: String,
deviceUuid: String,
threadId: String,
userIds: Collection<Long>,
): 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<Long>,
): String {
@ -231,6 +260,8 @@ class DirectMessagesService private constructor(
}
suspend fun removeAdmins(
csrfToken: String,
deviceUuid: String,
threadId: String,
userIds: Collection<Long>,
): 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<Long>,
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<Long>,
): DirectThreadDetailsChangeResponse {
@ -363,6 +417,8 @@ class DirectMessagesService private constructor(
}
suspend fun declineParticipantRequests(
csrfToken: String,
deviceUuid: String,
threadId: String,
userIds: List<Long>,
): 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
}
}
}

View File

@ -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<FriendshipChangeResponse> callback) {
change("create", targetUserId, callback);
}
public void unfollow(final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
change("destroy", targetUserId, callback);
}
public void changeBlock(final boolean unblock,
final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
change(unblock ? "unblock" : "block", targetUserId, callback);
}
public void toggleRestrict(final long targetUserId,
final boolean restrict,
final ServiceCallback<FriendshipRestrictResponse> callback) {
final Map<String, String> 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<FriendshipRestrictResponse> request = repository.toggleRestrict(action, form);
request.enqueue(new Callback<FriendshipRestrictResponse>() {
@Override
public void onResponse(@NonNull final Call<FriendshipRestrictResponse> call,
@NonNull final Response<FriendshipRestrictResponse> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<FriendshipRestrictResponse> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void approve(final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
change("approve", targetUserId, callback);
}
public void ignore(final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
change("ignore", targetUserId, callback);
}
public void removeFollower(final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
change("remove_follower", targetUserId, callback);
}
private void change(final String action,
final long targetUserId,
final ServiceCallback<FriendshipChangeResponse> callback) {
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<FriendshipChangeResponse> request = repository.change(action, targetUserId, signedForm);
request.enqueue(new Callback<FriendshipChangeResponse>() {
@Override
public void onResponse(@NonNull final Call<FriendshipChangeResponse> call,
@NonNull final Response<FriendshipChangeResponse> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<FriendshipChangeResponse> 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<FriendshipChangeResponse> callback) {
final Map<String, String> 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<FriendshipChangeResponse> request = repository.changeMute(unmute ?
"unmute_posts_or_story_from_follow" :
"mute_posts_or_story_from_follow",
form);
request.enqueue(new Callback<FriendshipChangeResponse>() {
@Override
public void onResponse(@NonNull final Call<FriendshipChangeResponse> call,
@NonNull final Response<FriendshipChangeResponse> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<FriendshipChangeResponse> 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<FriendshipListFetchResponse> callback) {
final Map<String, String> queryMap = new HashMap<>();
if (maxId != null) queryMap.put("max_id", maxId);
final Call<String> request = repository.getList(
targetUserId,
follower ? "followers" : "following",
queryMap);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> 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<FollowModel> items = parseItems(itemsJson);
return new FriendshipListFetchResponse(
nextMaxId,
status,
items
);
}
private List<FollowModel> parseItems(final JSONArray items) throws JSONException {
if (items == null) {
return Collections.emptyList();
}
final List<FollowModel> 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;
}
}

View File

@ -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<FollowModel> {
if (items == null) {
return emptyList()
}
val followModels = mutableListOf<FollowModel>()
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
}
}

View File

@ -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<PostsFetchResponse> callback) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", queryHash);
queryMap.put("variables", variables);
final Call<String> request = repository.fetch(queryMap);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void fetchLocationPosts(final long locationId,
final String maxId,
final ServiceCallback<PostsFetchResponse> 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<PostsFetchResponse> 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<PostsFetchResponse> 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<PostsFetchResponse> 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<String> 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<Media> 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<GraphQLUserListFetchResponse> callback) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae");
queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," +
"\"first\":30," +
"\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}");
final Call<String> request = repository.fetch(queryMap);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<User> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public Call<String> fetchComments(final String shortCodeOrCommentId,
final boolean root,
final String cursor) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62");
final Map<String, Object> 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<User> callback) {
final Call<String> request = repository.getUser(username);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> 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<Media> callback) {
final Call<String> request = repository.getPost(shortcode);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> 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<Hashtag> callback) {
final Call<String> request = repository.getTag(tag);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> 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<Location> callback) {
final Call<String> request = repository.getLocation(locationId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
}

View File

@ -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<Media> = 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<User> = 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)
)
}
}

View File

@ -1,320 +0,0 @@
package awais.instagrabber.webservices;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import awais.instagrabber.models.Comment;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.MediaRepository;
import awais.instagrabber.repositories.requests.Clip;
import awais.instagrabber.repositories.requests.UploadFinishOptions;
import awais.instagrabber.repositories.requests.VideoOptions;
import awais.instagrabber.repositories.responses.LikersResponse;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.MediaInfoResponse;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.DateUtils;
import awais.instagrabber.utils.MediaUploadHelper;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MediaService extends BaseService {
private static final String TAG = "MediaService";
private static final List<MediaItemType> DELETABLE_ITEMS_TYPES = ImmutableList.of(MediaItemType.MEDIA_TYPE_IMAGE,
MediaItemType.MEDIA_TYPE_VIDEO,
MediaItemType.MEDIA_TYPE_SLIDER);
private final MediaRepository repository;
private final String deviceUuid, csrfToken;
private final long userId;
private static MediaService instance;
private MediaService(final String deviceUuid,
final String csrfToken,
final long userId) {
this.deviceUuid = deviceUuid;
this.csrfToken = csrfToken;
this.userId = userId;
repository = RetrofitFactory.INSTANCE
.getRetrofit()
.create(MediaRepository.class);
}
public String getCsrfToken() {
return csrfToken;
}
public String getDeviceUuid() {
return deviceUuid;
}
public long getUserId() {
return userId;
}
public static MediaService getInstance(final String deviceUuid, final String csrfToken, final long userId) {
if (instance == null
|| !Objects.equals(instance.getCsrfToken(), csrfToken)
|| !Objects.equals(instance.getDeviceUuid(), deviceUuid)
|| !Objects.equals(instance.getUserId(), userId)) {
instance = new MediaService(deviceUuid, csrfToken, userId);
}
return instance;
}
public void fetch(final long mediaId,
final ServiceCallback<Media> callback) {
final Call<MediaInfoResponse> request = repository.fetch(mediaId);
request.enqueue(new Callback<MediaInfoResponse>() {
@Override
public void onResponse(@NonNull final Call<MediaInfoResponse> call,
@NonNull final Response<MediaInfoResponse> response) {
if (callback == null) return;
final MediaInfoResponse mediaInfoResponse = response.body();
if (mediaInfoResponse == null || mediaInfoResponse.getItems() == null || mediaInfoResponse.getItems().isEmpty()) {
callback.onSuccess(null);
return;
}
callback.onSuccess(mediaInfoResponse.getItems().get(0));
}
@Override
public void onFailure(@NonNull final Call<MediaInfoResponse> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void like(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "like", null, callback);
}
public void unlike(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unlike", null, callback);
}
public void save(final String mediaId,
final String collection,
final ServiceCallback<Boolean> callback) {
action(mediaId, "save", collection, callback);
}
public void unsave(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unsave", null, callback);
}
private void action(final String mediaId,
final String action,
final String collection,
final ServiceCallback<Boolean> callback) {
final Map<String, Object> form = new HashMap<>();
form.put("media_id", mediaId);
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);
// form.put("radio_type", "wifi-none");
if (action.equals("save") && !TextUtils.isEmpty(collection)) form.put("added_collection_ids", "[" + collection + "]");
// there also exists "removed_collection_ids" which can be used with "save" and "unsave"
final Map<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.action(action, mediaId, signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (callback == null) return;
final String body = response.body();
if (body == null) {
callback.onFailure(new RuntimeException("Returned body is null"));
return;
}
try {
final JSONObject jsonObject = new JSONObject(body);
final String status = jsonObject.optString("status");
callback.onSuccess(status.equals("ok"));
} catch (JSONException e) {
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void editCaption(final String postId,
final String newCaption,
@NonNull final ServiceCallback<Boolean> callback) {
final Map<String, Object> form = new HashMap<>();
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);
form.put("igtv_feed_preview", "false");
form.put("media_id", postId);
form.put("caption_text", newCaption);
final Map<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.editCaption(postId, signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) {
Log.e(TAG, "Error occurred while editing caption");
callback.onSuccess(false);
return;
}
try {
final JSONObject jsonObject = new JSONObject(body);
final String status = jsonObject.optString("status");
callback.onSuccess(status.equals("ok"));
} catch (JSONException e) {
// Log.e(TAG, "Error parsing body", e);
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
Log.e(TAG, "Error editing caption", t);
callback.onFailure(t);
}
});
}
public void fetchLikes(final String mediaId,
final boolean isComment,
@NonNull final ServiceCallback<List<User>> callback) {
final Call<LikersResponse> likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers");
likesRequest.enqueue(new Callback<LikersResponse>() {
@Override
public void onResponse(@NonNull final Call<LikersResponse> call, @NonNull final Response<LikersResponse> response) {
final LikersResponse likersResponse = response.body();
if (likersResponse == null) {
Log.e(TAG, "Error occurred while fetching likes of " + mediaId);
callback.onSuccess(null);
return;
}
callback.onSuccess(likersResponse.getUsers());
}
@Override
public void onFailure(@NonNull final Call<LikersResponse> call, @NonNull final Throwable t) {
Log.e(TAG, "Error getting likes", t);
callback.onFailure(t);
}
});
}
public void translate(final String id,
final String type, // 1 caption 2 comment 3 bio
@NonNull final ServiceCallback<String> callback) {
final Map<String, String> form = new HashMap<>();
form.put("id", String.valueOf(id));
form.put("type", type);
final Call<String> request = repository.translate(form);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) {
Log.e(TAG, "Error occurred while translating");
callback.onSuccess(null);
return;
}
try {
final JSONObject jsonObject = new JSONObject(body);
final String translation = jsonObject.optString("translation");
callback.onSuccess(translation);
} catch (JSONException e) {
// Log.e(TAG, "Error parsing body", e);
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
Log.e(TAG, "Error translating", t);
callback.onFailure(t);
}
});
}
public Call<String> uploadFinish(@NonNull final UploadFinishOptions options) {
if (options.getVideoOptions() != null) {
final VideoOptions videoOptions = options.getVideoOptions();
if (videoOptions.getClips().isEmpty()) {
videoOptions.setClips(Collections.singletonList(new Clip(videoOptions.getLength(), options.getSourceType())));
}
}
final String timezoneOffset = String.valueOf(DateUtils.getTimezoneOffset());
final ImmutableMap.Builder<String, Object> formBuilder = ImmutableMap.<String, Object>builder()
.put("timezone_offset", timezoneOffset)
.put("_csrftoken", csrfToken)
.put("source_type", options.getSourceType())
.put("_uid", String.valueOf(userId))
.put("_uuid", deviceUuid)
.put("upload_id", options.getUploadId());
if (options.getVideoOptions() != null) {
formBuilder.putAll(options.getVideoOptions().getMap());
}
final Map<String, String> queryMap = options.getVideoOptions() != null ? ImmutableMap.of("video", "1") : Collections.emptyMap();
final Map<String, String> signedForm = Utils.sign(formBuilder.build());
return repository.uploadFinish(MediaUploadHelper.getRetryContextString(), queryMap, signedForm);
}
public Call<String> delete(@NonNull final String postId,
@NonNull final MediaItemType type) {
if (!DELETABLE_ITEMS_TYPES.contains(type)) return null;
final Map<String, Object> form = new HashMap<>();
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);
form.put("igtv_feed_preview", "false");
form.put("media_id", postId);
final Map<String, String> signedForm = Utils.sign(form);
final String mediaType;
switch (type) {
case MEDIA_TYPE_IMAGE:
mediaType = "PHOTO";
break;
case MEDIA_TYPE_VIDEO:
mediaType = "VIDEO";
break;
case MEDIA_TYPE_SLIDER:
mediaType = "CAROUSEL";
break;
default:
return null;
}
return repository.delete(postId, mediaType, signedForm);
}
}

View File

@ -0,0 +1,182 @@
package awais.instagrabber.webservices
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.repositories.MediaRepository
import awais.instagrabber.repositories.requests.Clip
import awais.instagrabber.repositories.requests.UploadFinishOptions
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.utils.DateUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.retryContextString
import awais.instagrabber.webservices.RetrofitFactory.retrofit
import org.json.JSONObject
object MediaService : BaseService() {
private val DELETABLE_ITEMS_TYPES = listOf(
MediaItemType.MEDIA_TYPE_IMAGE,
MediaItemType.MEDIA_TYPE_VIDEO,
MediaItemType.MEDIA_TYPE_SLIDER
)
private val repository: MediaRepository = retrofit.create(MediaRepository::class.java)
suspend fun fetch(
mediaId: Long,
): Media? {
val response = repository.fetch(mediaId)
return if (response.items.isNullOrEmpty()) {
null
} else response.items[0]
}
suspend fun like(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "like", null)
suspend fun unlike(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unlike", null)
suspend fun save(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String, collection: String?,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "save", collection)
suspend fun unsave(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
): Boolean = action(csrfToken, userId, deviceUuid, mediaId, "unsave", null)
private suspend fun action(
csrfToken: String,
userId: Long,
deviceUuid: String,
mediaId: String,
action: String,
collection: String?,
): Boolean {
val form: MutableMap<String, Any> = mutableMapOf(
"media_id" to mediaId,
"_csrftoken" to csrfToken,
"_uid" to userId,
"_uuid" to deviceUuid,
)
// form.put("radio_type", "wifi-none");
if (action == "save" && !collection.isNullOrBlank()) {
form["added_collection_ids"] = "[$collection]"
}
// there also exists "removed_collection_ids" which can be used with "save" and "unsave"
val signedForm = Utils.sign(form)
val response = repository.action(action, mediaId, signedForm)
val jsonObject = JSONObject(response)
val status = jsonObject.optString("status")
return status == "ok"
}
suspend fun editCaption(
csrfToken: String,
userId: Long,
deviceUuid: String,
postId: String,
newCaption: String,
): Boolean {
val form = mapOf(
"_csrftoken" to csrfToken,
"_uid" to userId,
"_uuid" to deviceUuid,
"igtv_feed_preview" to "false",
"media_id" to postId,
"caption_text" to newCaption,
)
val signedForm = Utils.sign(form)
val response = repository.editCaption(postId, signedForm)
val jsonObject = JSONObject(response)
val status = jsonObject.optString("status")
return status == "ok"
}
suspend fun fetchLikes(
mediaId: String,
isComment: Boolean,
): List<User> {
val response = repository.fetchLikes(mediaId, if (isComment) "comment_likers" else "likers")
return response.users
}
suspend fun translate(
id: String,
type: String, // 1 caption 2 comment 3 bio
): String {
val form = mapOf(
"id" to id,
"type" to type,
)
val response = repository.translate(form)
val jsonObject = JSONObject(response)
return jsonObject.optString("translation")
}
suspend fun uploadFinish(
csrfToken: String,
userId: Long,
deviceUuid: String,
options: UploadFinishOptions,
): String {
if (options.videoOptions != null) {
val videoOptions = options.videoOptions
if (videoOptions.clips.isEmpty()) {
videoOptions.clips = listOf(Clip(videoOptions.length, options.sourceType))
}
}
val timezoneOffset = DateUtils.getTimezoneOffset().toString()
val form = mutableMapOf<String, Any>(
"timezone_offset" to timezoneOffset,
"_csrftoken" to csrfToken,
"source_type" to options.sourceType,
"_uid" to userId.toString(),
"_uuid" to deviceUuid,
"upload_id" to options.uploadId,
)
if (options.videoOptions != null) {
form.putAll(options.videoOptions.map)
}
val queryMap = if (options.videoOptions != null) mapOf("video" to "1") else emptyMap()
val signedForm = Utils.sign(form)
return repository.uploadFinish(retryContextString, queryMap, signedForm)
}
suspend fun delete(
csrfToken: String,
userId: Long,
deviceUuid: String,
postId: String,
type: MediaItemType,
): String? {
if (!DELETABLE_ITEMS_TYPES.contains(type)) return null
val form = mapOf(
"_csrftoken" to csrfToken,
"_uid" to userId,
"_uuid" to deviceUuid,
"igtv_feed_preview" to "false",
"media_id" to postId,
)
val signedForm = Utils.sign(form)
val mediaType: String = when (type) {
MediaItemType.MEDIA_TYPE_IMAGE -> "PHOTO"
MediaItemType.MEDIA_TYPE_VIDEO -> "VIDEO"
MediaItemType.MEDIA_TYPE_SLIDER -> "CAROUSEL"
else -> return null
}
return repository.delete(postId, mediaType, signedForm)
}
}

View File

@ -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<StoryModel> callback) {
final Call<String> request = repository.fetch(mediaId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (callback == null) return;
final String body = response.body();
if (body == null) {
callback.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<String> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void getFeedStories(final ServiceCallback<List<FeedStoryModel>> callback) {
final Call<String> response = repository.getFeedStories();
response.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) {
Log.e(TAG, "getFeedStories: body is empty");
return;
}
parseStoriesBody(body, callback);
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
private void parseStoriesBody(final String body, final ServiceCallback<List<FeedStoryModel>> callback) {
try {
final List<FeedStoryModel> 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<List<HighlightModel>> callback) {
final Call<String> request = repository.fetchHighlights(profileId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<HighlightModel> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void fetchArchive(final String maxId,
final ServiceCallback<ArchiveFetchResponse> callback) {
final Map<String, String> 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<String> request = repository.fetchArchive(form);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<HighlightModel> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void getUserStory(final StoryViewerOptions options,
final ServiceCallback<List<StoryModel>> callback) {
final String url = buildUrl(options);
final Call<String> 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<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<StoryModel> 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<String> 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<StoryStickerResponse> callback) {
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<StoryStickerResponse> request =
repository.respondToSticker(storyId, stickerId, action, signedForm);
request.enqueue(new Callback<StoryStickerResponse>() {
@Override
public void onResponse(@NonNull final Call<StoryStickerResponse> call,
@NonNull final Response<StoryStickerResponse> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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<String> callback) {
final Map<String, Object> form = new HashMap<>();
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);
form.put("container_module", "feed_timeline");
final Map<String, Object> reelsForm = new HashMap<>();
reelsForm.put(storyMediaId, Collections.singletonList(takenAt + "_" + seenAt));
form.put("reels", reelsForm);
final Map<String, String> signedForm = Utils.sign(form);
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("reel", "1");
queryMap.put("live_vod", "0");
final Call<String> request = repository.seen(queryMap, signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<String> 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<FeedStoryModel> sort(final List<FeedStoryModel> list) {
final List<FeedStoryModel> 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<HighlightModel> archives;
private final boolean hasNextPage;
private final String nextCursor;
public ArchiveFetchResponse(final List<HighlightModel> archives, final boolean hasNextPage, final String nextCursor) {
this.archives = archives;
this.hasNextPage = hasNextPage;
this.nextCursor = nextCursor;
}
public List<HighlightModel> getResult() {
return archives;
}
public boolean hasNextPage() {
return hasNextPage;
}
public String getNextCursor() {
return nextCursor;
}
}
}

View File

@ -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<FeedStoryModel> {
val response = repository.getFeedStories()
return parseStoriesBody(response)
}
private fun parseStoriesBody(body: String): List<FeedStoryModel> {
val feedStoryModels: MutableList<FeedStoryModel> = 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<HighlightModel> {
val response = repository.fetchHighlights(profileId)
val highlightsReel = JSONObject(response).getJSONArray("tray")
val length = highlightsReel.length()
val highlightModels: MutableList<HighlightModel> = 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<HighlightModel> = 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<StoryModel> {
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<StoryModel> = 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<FeedStoryModel>): List<FeedStoryModel> {
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<HighlightModel>, val hasNextPage: Boolean, val nextCursor: String) {
fun hasNextPage(): Boolean {
return hasNextPage
}
}
}

View File

@ -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<User> callback) {
final Call<WrappedUser> request = repository.getUserInfo(uid);
request.enqueue(new Callback<WrappedUser>() {
@Override
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) {
final WrappedUser user = response.body();
if (user == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(user.getUser());
}
@Override
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public void getUsernameInfo(final String username, final ServiceCallback<User> callback) {
final Call<WrappedUser> request = repository.getUsernameInfo(username);
request.enqueue(new Callback<WrappedUser>() {
@Override
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) {
final WrappedUser user = response.body();
if (user == null) {
callback.onFailure(null);
return;
}
callback.onSuccess(user.getUser());
}
@Override
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public void getUserFriendship(final long uid, final ServiceCallback<FriendshipStatus> callback) {
final Call<FriendshipStatus> request = repository.getUserFriendship(uid);
request.enqueue(new Callback<FriendshipStatus>() {
@Override
public void onResponse(@NonNull final Call<FriendshipStatus> call, @NonNull final Response<FriendshipStatus> response) {
final FriendshipStatus status = response.body();
if (status == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(status);
}
@Override
public void onFailure(@NonNull final Call<FriendshipStatus> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public Call<UserSearchResponse> search(final String query) {
final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000;
return repository.search(timezoneOffset, query);
}
}

View File

@ -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)
}
}

View File

@ -1,399 +0,0 @@
package awais.instagrabber.workers;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.FileProvider;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.services.DeleteImageIntentService;
import awais.instagrabber.utils.BitmapUtils;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.BitmapUtils.THUMBNAIL_SIZE;
import static awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID;
import static awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME;
public class DownloadWorker extends Worker {
private static final String TAG = "DownloadWorker";
private static final String DOWNLOAD_GROUP = "DOWNLOAD_GROUP";
public static final String PROGRESS = "PROGRESS";
public static final String URL = "URL";
public static final String KEY_DOWNLOAD_REQUEST_JSON = "download_request_json";
public static final int DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE = 2020;
public static final int DELETE_IMAGE_REQUEST_CODE = 2030;
private final NotificationManagerCompat notificationManager;
public DownloadWorker(@NonNull final Context context, @NonNull final WorkerParameters workerParams) {
super(context, workerParams);
notificationManager = NotificationManagerCompat.from(context);
}
@NonNull
@Override
public Result doWork() {
final String downloadRequestFilePath = getInputData().getString(KEY_DOWNLOAD_REQUEST_JSON);
if (TextUtils.isEmpty(downloadRequestFilePath)) {
return Result.failure(new Data.Builder()
.putString("error", "downloadRequest is empty or null")
.build());
}
final String downloadRequestString;
final File requestFile = new File(downloadRequestFilePath);
try (Scanner scanner = new Scanner(requestFile)) {
downloadRequestString = scanner.useDelimiter("\\A").next();
} catch (Exception e) {
Log.e(TAG, "doWork: ", e);
return Result.failure(new Data.Builder()
.putString("error", e.getLocalizedMessage())
.build());
}
if (TextUtils.isEmpty(downloadRequestString)) {
return Result.failure(new Data.Builder()
.putString("error", "downloadRequest is empty or null")
.build());
}
final DownloadRequest downloadRequest;
try {
downloadRequest = new Gson().fromJson(downloadRequestString, DownloadRequest.class);
} catch (JsonSyntaxException e) {
Log.e(TAG, "doWork", e);
return Result.failure(new Data.Builder()
.putString("error", e.getLocalizedMessage())
.build());
}
if (downloadRequest == null) {
return Result.failure(new Data.Builder()
.putString("error", "downloadRequest is null")
.build());
}
final Map<String, String> urlToFilePathMap = downloadRequest.getUrlToFilePathMap();
download(urlToFilePathMap);
new Handler(Looper.getMainLooper()).postDelayed(() -> showSummary(urlToFilePathMap), 500);
final boolean deleted = requestFile.delete();
if (!deleted) {
Log.w(TAG, "doWork: requestFile not deleted!");
}
return Result.success();
}
private void download(final Map<String, String> urlToFilePathMap) {
final int notificationId = getNotificationId();
final Set<Map.Entry<String, String>> entries = urlToFilePathMap.entrySet();
int count = 1;
final int total = urlToFilePathMap.size();
for (final Map.Entry<String, String> urlAndFilePath : entries) {
final String url = urlAndFilePath.getKey();
updateDownloadProgress(notificationId, count, total, 0);
download(notificationId, count, total, url, urlAndFilePath.getValue());
count++;
}
}
private int getNotificationId() {
return Math.abs(getId().hashCode());
}
private void download(final int notificationId,
final int position,
final int total,
final String url,
final String filePath) {
final boolean isJpg = filePath.endsWith("jpg");
// using temp file approach to remove IPTC so that download progress can be reported
final File outFile = isJpg ? DownloadUtils.getTempFile() : new File(filePath);
try {
final URLConnection urlConnection = new URL(url).openConnection();
final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() :
urlConnection.getContentLength();
float totalRead = 0;
try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream());
final FileOutputStream fos = new FileOutputStream(outFile)) {
final byte[] buffer = new byte[0x2000];
int count;
while ((count = bis.read(buffer, 0, 0x2000)) != -1) {
totalRead = totalRead + count;
fos.write(buffer, 0, count);
setProgressAsync(new Data.Builder().putString(URL, url)
.putFloat(PROGRESS, totalRead * 100f / fileSize)
.build());
updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize);
}
fos.flush();
} catch (final Exception e) {
Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.getAbsolutePath(), e);
}
if (isJpg) {
final File finalFile = new File(filePath);
try (FileInputStream fis = new FileInputStream(outFile);
FileOutputStream fos = new FileOutputStream(finalFile)) {
final JpegIptcRewriter jpegIptcRewriter = new JpegIptcRewriter();
jpegIptcRewriter.removeIPTC(fis, fos);
} catch (Exception e) {
Log.e(TAG, "Error while removing iptc: url: " + url
+ ", tempFile: " + outFile.getAbsolutePath()
+ ", finalFile: " + finalFile.getAbsolutePath(), e);
}
final boolean deleted = outFile.delete();
if (!deleted) {
Log.w(TAG, "download: tempFile not deleted!");
}
}
} catch (final Exception e) {
Log.e(TAG, "Error while downloading: " + url, e);
}
setProgressAsync(new Data.Builder().putString(URL, url)
.putFloat(PROGRESS, 100)
.build());
updateDownloadProgress(notificationId, position, total, 100);
}
private void updateDownloadProgress(final int notificationId,
final int position,
final int total,
final float percent) {
final Notification notification = createProgressNotification(position, total, percent);
try {
if (notification == null) {
notificationManager.cancel(notificationId);
return;
}
setForegroundAsync(new ForegroundInfo(notificationId, notification)).get();
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "updateDownloadProgress", e);
}
}
private Notification createProgressNotification(final int position, final int total, final float percent) {
final Context context = getApplicationContext();
boolean ongoing = true;
int totalPercent;
if (position == total && percent == 100) {
ongoing = false;
totalPercent = 100;
} else {
totalPercent = (int) ((100f * (position - 1) / total) + (1f / total) * (percent));
}
if (totalPercent == 100) {
return null;
}
// Log.d(TAG, "createProgressNotification: position: " + position
// + ", total: " + total
// + ", percent: " + percent
// + ", totalPercent: " + totalPercent);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, Constants.DOWNLOAD_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.ic_download)
.setOngoing(ongoing)
.setProgress(100, totalPercent, totalPercent < 0)
.setAutoCancel(false)
.setOnlyAlertOnce(true)
.setContentTitle(context.getString(R.string.downloader_downloading_post));
if (total > 1) {
builder.setContentText(context.getString(R.string.downloader_downloading_child, position, total));
}
return builder.build();
}
private void showSummary(final Map<String, String> urlToFilePathMap) {
final Context context = getApplicationContext();
final Collection<String> filePaths = urlToFilePathMap.values();
final List<NotificationCompat.Builder> notifications = new LinkedList<>();
final List<Integer> notificationIds = new LinkedList<>();
int count = 1;
for (final String filePath : filePaths) {
final File file = new File(filePath);
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);
final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);
final ContentResolver contentResolver = context.getContentResolver();
final Bitmap bitmap = getThumbnail(context, file, uri, contentResolver);
final String downloadComplete = context.getString(R.string.downloader_complete);
final Intent intent = new Intent(Intent.ACTION_VIEW, uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_FROM_BACKGROUND
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, uri);
final PendingIntent pendingIntent = PendingIntent.getActivity(
context,
DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT
);
final int notificationId = getNotificationId() + count;
notificationIds.add(notificationId);
count++;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentText(null)
.setContentTitle(downloadComplete)
.setWhen(System.currentTimeMillis())
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setGroup(NOTIF_GROUP_NAME + "_" + getId())
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_delete,
context.getString(R.string.delete),
DeleteImageIntentService.pendingIntent(context, filePath, notificationId));
if (bitmap != null) {
builder.setLargeIcon(bitmap)
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null))
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);
}
notifications.add(builder);
}
Notification summaryNotification = null;
if (urlToFilePathMap.size() != 1) {
final String text = "Downloaded " + urlToFilePathMap.size() + " items";
summaryNotification = new NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setContentTitle("Downloaded")
.setContentText(text)
.setSmallIcon(R.drawable.ic_download)
.setStyle(new NotificationCompat.InboxStyle().setSummaryText(text))
.setGroup(NOTIF_GROUP_NAME + "_" + getId())
.setGroupSummary(true)
.build();
}
for (int i = 0; i < notifications.size(); i++) {
final NotificationCompat.Builder builder = notifications.get(i);
// only make sound and vibrate for the last notification
if (i != notifications.size() - 1) {
builder.setSound(null)
.setVibrate(null);
}
notificationManager.notify(notificationIds.get(i), builder.build());
}
if (summaryNotification != null) {
notificationManager.notify(getNotificationId() + count, summaryNotification);
}
}
@Nullable
private Bitmap getThumbnail(final Context context,
final File file,
final Uri uri,
final ContentResolver contentResolver) {
final String mimeType = Utils.getMimeType(uri, contentResolver);
if (TextUtils.isEmpty(mimeType)) return null;
Bitmap bitmap = null;
if (mimeType.startsWith("image")) {
try {
final BitmapUtils.BitmapResult bitmapResult = BitmapUtils
.getBitmapResult(context.getContentResolver(), uri, THUMBNAIL_SIZE, THUMBNAIL_SIZE, -1, true);
if (bitmapResult == null) return null;
bitmap = bitmapResult.bitmap;
} catch (final Exception e) {
Log.e(TAG, "", e);
}
return bitmap;
}
if (mimeType.startsWith("video")) {
try {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
try {
retriever.setDataSource(context, uri);
} catch (final Exception e) {
retriever.setDataSource(file.getAbsolutePath());
}
bitmap = retriever.getFrameAtTime();
} finally {
try {
retriever.release();
} catch (Exception e) {
Log.e(TAG, "getThumbnail: ", e);
}
}
} catch (final Exception e) {
Log.e(TAG, "", e);
}
}
return bitmap;
}
public static class DownloadRequest {
private final Map<String, String> urlToFilePathMap;
public static class Builder {
private Map<String, String> urlToFilePathMap;
public Builder setUrlToFilePathMap(final Map<String, String> urlToFilePathMap) {
this.urlToFilePathMap = urlToFilePathMap;
return this;
}
public Builder addUrl(@NonNull final String url, @NonNull final String filePath) {
if (urlToFilePathMap == null) {
urlToFilePathMap = new HashMap<>();
}
urlToFilePathMap.put(url, filePath);
return this;
}
public DownloadRequest build() {
return new DownloadRequest(urlToFilePathMap);
}
}
public static Builder builder() {
return new Builder();
}
private DownloadRequest(final Map<String, String> urlToFilePathMap) {
this.urlToFilePathMap = urlToFilePathMap;
}
public Map<String, String> getUrlToFilePathMap() {
return urlToFilePathMap;
}
}
}

View File

@ -0,0 +1,383 @@
package awais.instagrabber.workers
import android.app.Notification
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import awais.instagrabber.BuildConfig
import awais.instagrabber.R
import awais.instagrabber.services.DeleteImageIntentService
import awais.instagrabber.utils.BitmapUtils
import awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID
import awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME
import awais.instagrabber.utils.DownloadUtils
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL
import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.math.abs
class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
private val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(context)
override suspend fun doWork(): Result {
val downloadRequestFilePath = inputData.getString(KEY_DOWNLOAD_REQUEST_JSON)
if (downloadRequestFilePath.isNullOrBlank()) {
return Result.failure(Data.Builder()
.putString("error", "downloadRequest is empty or null")
.build())
}
val downloadRequestString: String
val requestFile = File(downloadRequestFilePath)
try {
downloadRequestString = requestFile.bufferedReader().use { it.readText() }
} catch (e: Exception) {
Log.e(TAG, "doWork: ", e)
return Result.failure(Data.Builder()
.putString("error", e.localizedMessage)
.build())
}
if (downloadRequestString.isBlank()) {
return Result.failure(Data.Builder()
.putString("error", "downloadRequest is empty")
.build())
}
val downloadRequest: DownloadRequest = try {
Gson().fromJson(downloadRequestString, DownloadRequest::class.java)
} catch (e: JsonSyntaxException) {
Log.e(TAG, "doWork", e)
return Result.failure(Data.Builder()
.putString("error", e.localizedMessage)
.build())
} ?: return Result.failure(Data.Builder()
.putString("error", "downloadRequest is null")
.build())
val urlToFilePathMap = downloadRequest.urlToFilePathMap
download(urlToFilePathMap)
Handler(Looper.getMainLooper()).postDelayed({ showSummary(urlToFilePathMap) }, 500)
val deleted = requestFile.delete()
if (!deleted) {
Log.w(TAG, "doWork: requestFile not deleted!")
}
return Result.success()
}
private suspend fun download(urlToFilePathMap: Map<String, String>) {
val notificationId = notificationId
val entries = urlToFilePathMap.entries
var count = 1
val total = urlToFilePathMap.size
for ((url, value) in entries) {
updateDownloadProgress(notificationId, count, total, 0f)
withContext(Dispatchers.IO) {
download(notificationId, count, total, url, value)
}
count++
}
}
private val notificationId: Int
get() = abs(id.hashCode())
private fun download(
notificationId: Int,
position: Int,
total: Int,
url: String,
filePath: String,
) {
val isJpg = filePath.endsWith("jpg")
// using temp file approach to remove IPTC so that download progress can be reported
val outFile = if (isJpg) DownloadUtils.getTempFile() else File(filePath)
try {
val urlConnection = URL(url).openConnection()
val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong()
var totalRead = 0f
try {
BufferedInputStream(urlConnection.getInputStream()).use { bis ->
FileOutputStream(outFile).use { fos ->
val buffer = ByteArray(0x2000)
var count: Int
while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) {
totalRead += count
fos.write(buffer, 0, count)
setProgressAsync(Data.Builder().putString(URL, url)
.putFloat(PROGRESS, totalRead * 100f / fileSize)
.build())
updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize)
}
fos.flush()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.absolutePath, e)
}
if (isJpg) {
val finalFile = File(filePath)
try {
FileInputStream(outFile).use { fis ->
FileOutputStream(finalFile).use { fos ->
val jpegIptcRewriter = JpegIptcRewriter()
jpegIptcRewriter.removeIPTC(fis, fos)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error while removing iptc: url: " + url
+ ", tempFile: " + outFile.absolutePath
+ ", finalFile: " + finalFile.absolutePath, e)
}
val deleted = outFile.delete()
if (!deleted) {
Log.w(TAG, "download: tempFile not deleted!")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error while downloading: $url", e)
}
setProgressAsync(Data.Builder().putString(URL, url)
.putFloat(PROGRESS, 100f)
.build())
updateDownloadProgress(notificationId, position, total, 100f)
}
private fun updateDownloadProgress(
notificationId: Int,
position: Int,
total: Int,
percent: Float,
) {
val notification = createProgressNotification(position, total, percent)
try {
if (notification == null) {
notificationManager.cancel(notificationId)
return
}
setForegroundAsync(ForegroundInfo(notificationId, notification)).get()
} catch (e: ExecutionException) {
Log.e(TAG, "updateDownloadProgress", e)
} catch (e: InterruptedException) {
Log.e(TAG, "updateDownloadProgress", e)
}
}
private fun createProgressNotification(position: Int, total: Int, percent: Float): Notification? {
val context = applicationContext
var ongoing = true
val totalPercent: Int
if (position == total && percent == 100f) {
ongoing = false
totalPercent = 100
} else {
totalPercent = (100f * (position - 1) / total + 1f / total * percent).toInt()
}
if (totalPercent == 100) {
return null
}
// Log.d(TAG, "createProgressNotification: position: " + position
// + ", total: " + total
// + ", percent: " + percent
// + ", totalPercent: " + totalPercent);
val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.ic_download)
.setOngoing(ongoing)
.setProgress(100, totalPercent, totalPercent < 0)
.setAutoCancel(false)
.setOnlyAlertOnce(true)
.setContentTitle(context.getString(R.string.downloader_downloading_post))
if (total > 1) {
builder.setContentText(context.getString(R.string.downloader_downloading_child, position, total))
}
return builder.build()
}
private fun showSummary(urlToFilePathMap: Map<String, String>?) {
val context = applicationContext
val filePaths = urlToFilePathMap!!.values
val notifications: MutableList<NotificationCompat.Builder> = LinkedList()
val notificationIds: MutableList<Int> = LinkedList()
var count = 1
for (filePath in filePaths) {
val file = File(filePath)
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)))
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null, null)
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
val contentResolver = context.contentResolver
val bitmap = getThumbnail(context, file, uri, contentResolver)
val downloadComplete = context.getString(R.string.downloader_complete)
val intent = Intent(Intent.ACTION_VIEW, uri)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_FROM_BACKGROUND
or Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, uri)
val pendingIntent = PendingIntent.getActivity(
context,
DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT
)
val notificationId = notificationId + count
notificationIds.add(notificationId)
count++
val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentText(null)
.setContentTitle(downloadComplete)
.setWhen(System.currentTimeMillis())
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setGroup(NOTIF_GROUP_NAME + "_" + id)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_delete,
context.getString(R.string.delete),
DeleteImageIntentService.pendingIntent(context, filePath, notificationId))
if (bitmap != null) {
builder.setLargeIcon(bitmap)
.setStyle(NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null))
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
}
notifications.add(builder)
}
var summaryNotification: Notification? = null
if (urlToFilePathMap.size != 1) {
val text = "Downloaded " + urlToFilePathMap.size + " items"
summaryNotification = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setContentTitle("Downloaded")
.setContentText(text)
.setSmallIcon(R.drawable.ic_download)
.setStyle(NotificationCompat.InboxStyle().setSummaryText(text))
.setGroup(NOTIF_GROUP_NAME + "_" + id)
.setGroupSummary(true)
.build()
}
for (i in notifications.indices) {
val builder = notifications[i]
// only make sound and vibrate for the last notification
if (i != notifications.size - 1) {
builder.setSound(null)
.setVibrate(null)
}
notificationManager.notify(notificationIds[i], builder.build())
}
if (summaryNotification != null) {
notificationManager.notify(notificationId + count, summaryNotification)
}
}
private fun getThumbnail(
context: Context,
file: File,
uri: Uri,
contentResolver: ContentResolver,
): Bitmap? {
val mimeType = Utils.getMimeType(uri, contentResolver)
if (isEmpty(mimeType)) return null
var bitmap: Bitmap? = null
if (mimeType.startsWith("image")) {
try {
val bitmapResult = BitmapUtils.getBitmapResult(
context.contentResolver,
uri,
BitmapUtils.THUMBNAIL_SIZE,
BitmapUtils.THUMBNAIL_SIZE,
-1f,
true
) ?: return null
bitmap = bitmapResult.bitmap
} catch (e: Exception) {
Log.e(TAG, "", e)
}
return bitmap
}
if (mimeType.startsWith("video")) {
try {
val retriever = MediaMetadataRetriever()
bitmap = try {
try {
retriever.setDataSource(context, uri)
} catch (e: Exception) {
retriever.setDataSource(file.absolutePath)
}
retriever.frameAtTime
} finally {
try {
retriever.release()
} catch (e: Exception) {
Log.e(TAG, "getThumbnail: ", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
return bitmap
}
class DownloadRequest private constructor(val urlToFilePathMap: Map<String, String>) {
class Builder {
private var urlToFilePathMap: MutableMap<String, String> = mutableMapOf()
fun setUrlToFilePathMap(urlToFilePathMap: MutableMap<String, String>): Builder {
this.urlToFilePathMap = urlToFilePathMap
return this
}
fun addUrl(url: String, filePath: String): Builder {
urlToFilePathMap[url] = filePath
return this
}
fun build(): DownloadRequest {
return DownloadRequest(urlToFilePathMap)
}
}
companion object {
@JvmStatic
fun builder(): Builder {
return Builder()
}
}
}
companion object {
const val PROGRESS = "PROGRESS"
const val URL = "URL"
const val KEY_DOWNLOAD_REQUEST_JSON = "download_request_json"
private const val DOWNLOAD_GROUP = "DOWNLOAD_GROUP"
private const val DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE = 2020
private const val DELETE_IMAGE_REQUEST_CODE = 2030
}
}

View File

@ -4,4 +4,5 @@
android:id="@+id/favorite_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="?attr/actionBarSize"
tools:listitem="@layout/item_search_result" />

View File

@ -37,15 +37,16 @@
app:layout_constraintBottom_toTopOf="@id/done"
app:layout_constraintTop_toBottomOf="@id/group" />
<com.google.android.material.button.MaterialButton
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/done"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:backgroundTint="?attr/colorPrimary"
android:text="@string/done"
android:textColor="?attr/colorOnPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/results"
tools:layout_editor_absoluteX="8dp"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -211,6 +211,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -150,6 +150,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -154,6 +154,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -125,6 +125,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -124,6 +124,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -172,5 +172,5 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post" />
android:label="@string/post" />
</navigation>

View File

@ -121,6 +121,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -177,7 +177,7 @@
<fragment
android:id="@+id/followViewerFragment"
android:name="awais.instagrabber.fragments.FollowViewerFragment"
android:label="Followers"
android:label=""
tools:layout="@layout/fragment_followers_viewer">
<argument
android:name="profileId"
@ -218,6 +218,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -114,6 +114,6 @@
<fragment
android:id="@+id/postViewFragment"
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
android:label="Post"
android:label="@string/post"
tools:layout="@layout/dialog_post_view" />
</navigation>

View File

@ -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<String, Any>(
"username" to ""
))
val viewModel = ProfileFragmentViewModel(state)
}
}