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:
commit
ae23dd74ba
@ -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'
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
}
|
25
app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
Normal file
25
app/src/main/java/awais/instagrabber/db/dao/AccountDao.kt
Normal 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()
|
||||
}
|
@ -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();
|
||||
}
|
26
app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.kt
Normal file
26
app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.kt
Normal 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()
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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) {}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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?> ->
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 ?: ""
|
||||
}
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
238
app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt
Normal file
238
app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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?)
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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() {}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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<?>>>() {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
182
app/src/main/java/awais/instagrabber/webservices/MediaService.kt
Normal file
182
app/src/main/java/awais/instagrabber/webservices/MediaService.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
383
app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt
Normal file
383
app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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" />
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -172,5 +172,5 @@
|
||||
<fragment
|
||||
android:id="@+id/postViewFragment"
|
||||
android:name="awais.instagrabber.fragments.PostViewV2Fragment"
|
||||
android:label="Post" />
|
||||
android:label="@string/post" />
|
||||
</navigation>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user