Ammar Githam
4 years ago
13 changed files with 2300 additions and 2628 deletions
-
2app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
-
403app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.kt
-
617app/src/main/java/awais/instagrabber/managers/InboxManager.kt
-
1880app/src/main/java/awais/instagrabber/managers/ThreadManager.java
-
1675app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
-
2app/src/main/java/awais/instagrabber/services/DMSyncService.java
-
193app/src/main/java/awais/instagrabber/utils/MediaUploader.java
-
155app/src/main/java/awais/instagrabber/utils/MediaUploader.kt
-
2app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java
-
2app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java
-
2app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java
-
13app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java
-
4app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java
@ -1,275 +1,222 @@ |
|||||
package awais.instagrabber.managers; |
|
||||
|
|
||||
import android.content.ContentResolver; |
|
||||
import android.util.Log; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.annotation.Nullable; |
|
||||
import androidx.lifecycle.LiveData; |
|
||||
import androidx.lifecycle.MutableLiveData; |
|
||||
|
|
||||
import com.google.common.collect.Iterables; |
|
||||
|
|
||||
import java.io.IOException; |
|
||||
import java.util.Collections; |
|
||||
import java.util.List; |
|
||||
import java.util.Locale; |
|
||||
import java.util.Set; |
|
||||
import java.util.UUID; |
|
||||
import java.util.function.Function; |
|
||||
|
|
||||
import awais.instagrabber.models.Resource; |
|
||||
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds; |
|
||||
import awais.instagrabber.repositories.responses.User; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectItem; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectThread; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; |
|
||||
import awais.instagrabber.utils.Constants; |
|
||||
import awais.instagrabber.utils.CookieUtils; |
|
||||
import awais.instagrabber.webservices.DirectMessagesService; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|
||||
|
|
||||
public final class DirectMessagesManager { |
|
||||
private static final String TAG = DirectMessagesManager.class.getSimpleName(); |
|
||||
private static final Object LOCK = new Object(); |
|
||||
|
|
||||
private static DirectMessagesManager instance; |
|
||||
|
|
||||
private final InboxManager inboxManager; |
|
||||
private final InboxManager pendingInboxManager; |
|
||||
|
|
||||
private DirectMessagesService service; |
|
||||
|
|
||||
public static DirectMessagesManager getInstance() { |
|
||||
if (instance == null) { |
|
||||
synchronized (LOCK) { |
|
||||
if (instance == null) { |
|
||||
instance = new DirectMessagesManager(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return instance; |
|
||||
} |
|
||||
|
|
||||
private DirectMessagesManager() { |
|
||||
inboxManager = InboxManager.getInstance(false); |
|
||||
pendingInboxManager = InboxManager.getInstance(true); |
|
||||
final String cookie = settingsHelper.getString(Constants.COOKIE); |
|
||||
final long viewerId = CookieUtils.getUserIdFromCookie(cookie); |
|
||||
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); |
|
||||
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); |
|
||||
if (csrfToken == null) return; |
|
||||
service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); |
|
||||
} |
|
||||
|
|
||||
public void moveThreadFromPending(@NonNull final String threadId) { |
|
||||
final List<DirectThread> pendingThreads = pendingInboxManager.getThreads().getValue(); |
|
||||
if (pendingThreads == null) return; |
|
||||
final int index = Iterables.indexOf(pendingThreads, t -> t != null && t.getThreadId().equals(threadId)); |
|
||||
if (index < 0) return; |
|
||||
final DirectThread thread = pendingThreads.get(index); |
|
||||
final DirectItem threadFirstDirectItem = thread.getFirstDirectItem(); |
|
||||
if (threadFirstDirectItem == null) return; |
|
||||
final List<DirectThread> threads = inboxManager.getThreads().getValue(); |
|
||||
int insertIndex = 0; |
|
||||
|
package awais.instagrabber.managers |
||||
|
|
||||
|
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 |
||||
|
import awais.instagrabber.models.Resource.Companion.success |
||||
|
import awais.instagrabber.repositories.requests.directmessages.ThreadIdOrUserIds.Companion.of |
||||
|
import awais.instagrabber.repositories.responses.User |
||||
|
import awais.instagrabber.repositories.responses.directmessages.DirectThread |
||||
|
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse |
||||
|
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient |
||||
|
import awais.instagrabber.utils.Constants |
||||
|
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 retrofit2.Call |
||||
|
import retrofit2.Callback |
||||
|
import retrofit2.Response |
||||
|
import java.io.IOException |
||||
|
import java.util.* |
||||
|
|
||||
|
object DirectMessagesManager { |
||||
|
val inboxManager: InboxManager by lazy { InboxManager.getInstance(false) } |
||||
|
val pendingInboxManager: InboxManager by lazy { InboxManager.getInstance(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 |
||||
|
val index = pendingThreads.indexOfFirst { it.threadId == threadId } |
||||
|
if (index < 0) return |
||||
|
val thread = pendingThreads[index] |
||||
|
val threadFirstDirectItem = thread.firstDirectItem ?: return |
||||
|
val threads = inboxManager.threads.value |
||||
|
var insertIndex = 0 |
||||
if (threads != null) { |
if (threads != null) { |
||||
for (final DirectThread tempThread : threads) { |
|
||||
final DirectItem firstDirectItem = tempThread.getFirstDirectItem(); |
|
||||
if (firstDirectItem == null) continue; |
|
||||
final long timestamp = firstDirectItem.getTimestamp(); |
|
||||
|
for (tempThread in threads) { |
||||
|
val firstDirectItem = tempThread.firstDirectItem ?: continue |
||||
|
val timestamp = firstDirectItem.getTimestamp() |
||||
if (timestamp < threadFirstDirectItem.getTimestamp()) { |
if (timestamp < threadFirstDirectItem.getTimestamp()) { |
||||
break; |
|
||||
|
break |
||||
} |
} |
||||
insertIndex++; |
|
||||
|
insertIndex++ |
||||
} |
} |
||||
} |
} |
||||
thread.setPending(false); |
|
||||
inboxManager.addThread(thread, insertIndex); |
|
||||
pendingInboxManager.removeThread(threadId); |
|
||||
final Integer currentTotal = inboxManager.getPendingRequestsTotal().getValue(); |
|
||||
if (currentTotal == null) return; |
|
||||
inboxManager.setPendingRequestsTotal(currentTotal - 1); |
|
||||
} |
|
||||
|
|
||||
public InboxManager getInboxManager() { |
|
||||
return inboxManager; |
|
||||
|
thread.pending = false |
||||
|
inboxManager.addThread(thread, insertIndex) |
||||
|
pendingInboxManager.removeThread(threadId) |
||||
|
val currentTotal = inboxManager.getPendingRequestsTotal().value ?: return |
||||
|
inboxManager.setPendingRequestsTotal(currentTotal - 1) |
||||
} |
} |
||||
|
|
||||
public InboxManager getPendingInboxManager() { |
|
||||
return pendingInboxManager; |
|
||||
|
fun getThreadManager( |
||||
|
threadId: String, |
||||
|
pending: Boolean, |
||||
|
currentUser: User, |
||||
|
contentResolver: ContentResolver, |
||||
|
): ThreadManager { |
||||
|
return getInstance(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) |
||||
} |
} |
||||
|
|
||||
public ThreadManager getThreadManager(@NonNull final String threadId, |
|
||||
final boolean pending, |
|
||||
@NonNull final User currentUser, |
|
||||
@NonNull final ContentResolver contentResolver) { |
|
||||
return ThreadManager.getInstance(threadId, pending, currentUser, contentResolver); |
|
||||
} |
|
||||
|
|
||||
public void createThread(final long userPk, |
|
||||
@Nullable final Function<DirectThread, Void> callback) { |
|
||||
if (service == null) return; |
|
||||
final Call<DirectThread> createThreadRequest = service.createThread(Collections.singletonList(userPk), null); |
|
||||
createThreadRequest.enqueue(new Callback<DirectThread>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<DirectThread> call, @NonNull final Response<DirectThread> response) { |
|
||||
if (!response.isSuccessful()) { |
|
||||
if (response.errorBody() != null) { |
|
||||
|
fun createThread( |
||||
|
userPk: Long, |
||||
|
callback: ((DirectThread) -> Unit)?, |
||||
|
) { |
||||
|
val createThreadRequest = service.createThread(listOf(userPk), null) |
||||
|
createThreadRequest.enqueue(object : Callback<DirectThread?> { |
||||
|
override fun onResponse(call: Call<DirectThread?>, response: Response<DirectThread?>) { |
||||
|
if (!response.isSuccessful) { |
||||
|
val errorBody = response.errorBody() |
||||
|
if (errorBody != null) { |
||||
try { |
try { |
||||
final String string = response.errorBody().string(); |
|
||||
final String msg = String.format(Locale.US, |
|
||||
|
val string = errorBody.string() |
||||
|
val msg = String.format(Locale.US, |
||||
"onResponse: url: %s, responseCode: %d, errorBody: %s", |
"onResponse: url: %s, responseCode: %d, errorBody: %s", |
||||
call.request().url().toString(), |
call.request().url().toString(), |
||||
response.code(), |
response.code(), |
||||
string); |
|
||||
Log.e(TAG, msg); |
|
||||
} catch (IOException e) { |
|
||||
Log.e(TAG, "onResponse: ", e); |
|
||||
|
string) |
||||
|
Log.e(TAG, msg) |
||||
|
} catch (e: IOException) { |
||||
|
Log.e(TAG, "onResponse: ", e) |
||||
} |
} |
||||
return; |
|
||||
|
return |
||||
} |
} |
||||
Log.e(TAG, "onResponse: request was not successful and response error body was null"); |
|
||||
return; |
|
||||
|
Log.e(TAG, "onResponse: request was not successful and response error body was null") |
||||
|
return |
||||
} |
} |
||||
final DirectThread thread = response.body(); |
|
||||
|
val thread = response.body() |
||||
if (thread == null) { |
if (thread == null) { |
||||
Log.e(TAG, "onResponse: thread is null"); |
|
||||
return; |
|
||||
} |
|
||||
if (callback != null) { |
|
||||
callback.apply(thread); |
|
||||
|
Log.e(TAG, "onResponse: thread is null") |
||||
|
return |
||||
} |
} |
||||
|
callback?.invoke(thread) |
||||
} |
} |
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<DirectThread> call, @NonNull final Throwable t) { |
|
||||
|
|
||||
} |
|
||||
}); |
|
||||
|
override fun onFailure(call: Call<DirectThread?>, t: Throwable) {} |
||||
|
}) |
||||
} |
} |
||||
|
|
||||
public void sendMedia(@NonNull final Set<RankedRecipient> recipients, final String mediaId) { |
|
||||
final int[] resultsCount = {0}; |
|
||||
final Function<Void, Void> callback = unused -> { |
|
||||
resultsCount[0]++; |
|
||||
if (resultsCount[0] == recipients.size()) { |
|
||||
inboxManager.refresh(); |
|
||||
|
fun sendMedia(recipients: Set<RankedRecipient>, mediaId: String) { |
||||
|
val resultsCount = intArrayOf(0) |
||||
|
val callback: () -> Unit = { |
||||
|
resultsCount[0]++ |
||||
|
if (resultsCount[0] == recipients.size) { |
||||
|
inboxManager.refresh() |
||||
} |
} |
||||
return null; |
|
||||
}; |
|
||||
for (final RankedRecipient recipient : recipients) { |
|
||||
if (recipient == null) continue; |
|
||||
sendMedia(recipient, mediaId, false, callback); |
|
||||
|
} |
||||
|
for (recipient in recipients) { |
||||
|
sendMedia(recipient, mediaId, false, callback) |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public void sendMedia(@NonNull final RankedRecipient recipient, final String mediaId) { |
|
||||
sendMedia(recipient, mediaId, true, null); |
|
||||
|
fun sendMedia(recipient: RankedRecipient, mediaId: String) { |
||||
|
sendMedia(recipient, mediaId, true, null) |
||||
} |
} |
||||
|
|
||||
private void sendMedia(@NonNull final RankedRecipient recipient, |
|
||||
@NonNull final String mediaId, |
|
||||
final boolean refreshInbox, |
|
||||
@Nullable final Function<Void, Void> callback) { |
|
||||
if (recipient.getThread() == null && recipient.getUser() != null) { |
|
||||
|
private fun sendMedia( |
||||
|
recipient: RankedRecipient, |
||||
|
mediaId: String, |
||||
|
refreshInbox: Boolean, |
||||
|
callback: (() -> Unit)?, |
||||
|
) { |
||||
|
if (recipient.thread == null && recipient.user != null) { |
||||
// create thread and forward |
// create thread and forward |
||||
createThread(recipient.getUser().getPk(), directThread -> { |
|
||||
sendMedia(directThread, mediaId, unused -> { |
|
||||
|
createThread(recipient.user.pk) { (threadId) -> |
||||
|
val threadIdTemp = threadId ?: return@createThread |
||||
|
sendMedia(threadIdTemp, mediaId) { |
||||
if (refreshInbox) { |
if (refreshInbox) { |
||||
inboxManager.refresh(); |
|
||||
|
inboxManager.refresh() |
||||
|
} |
||||
|
callback?.invoke() |
||||
} |
} |
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
} |
} |
||||
return null; |
|
||||
}); |
|
||||
return null; |
|
||||
}); |
|
||||
} |
} |
||||
if (recipient.getThread() == null) return; |
|
||||
|
if (recipient.thread == null) return |
||||
// just forward |
// just forward |
||||
final DirectThread thread = recipient.getThread(); |
|
||||
sendMedia(thread, mediaId, unused -> { |
|
||||
|
val thread = recipient.thread |
||||
|
val threadId = thread.threadId ?: return |
||||
|
sendMedia(threadId, mediaId) { |
||||
if (refreshInbox) { |
if (refreshInbox) { |
||||
inboxManager.refresh(); |
|
||||
|
inboxManager.refresh() |
||||
} |
} |
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
|
callback?.invoke() |
||||
} |
} |
||||
return null; |
|
||||
}); |
|
||||
} |
} |
||||
|
|
||||
@NonNull |
|
||||
public LiveData<Resource<Object>> sendMedia(@NonNull final DirectThread thread, |
|
||||
@NonNull final String mediaId, |
|
||||
@Nullable final Function<Void, Void> callback) { |
|
||||
return sendMedia(thread.getThreadId(), mediaId, callback); |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
public LiveData<Resource<Object>> sendMedia(@NonNull final String threadId, |
|
||||
@NonNull final String mediaId, |
|
||||
@Nullable final Function<Void, Void> callback) { |
|
||||
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); |
|
||||
data.postValue(Resource.loading(null)); |
|
||||
final Call<DirectThreadBroadcastResponse> request = service.broadcastMediaShare( |
|
||||
|
private fun sendMedia( |
||||
|
threadId: String, |
||||
|
mediaId: String, |
||||
|
callback: (() -> Unit)?, |
||||
|
): LiveData<Resource<Any?>> { |
||||
|
val data = MutableLiveData<Resource<Any?>>() |
||||
|
data.postValue(loading(null)) |
||||
|
val request = service.broadcastMediaShare( |
||||
UUID.randomUUID().toString(), |
UUID.randomUUID().toString(), |
||||
ThreadIdOrUserIds.of(threadId), |
|
||||
|
of(threadId), |
||||
mediaId |
mediaId |
||||
); |
|
||||
request.enqueue(new Callback<DirectThreadBroadcastResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<DirectThreadBroadcastResponse> call, |
|
||||
@NonNull final Response<DirectThreadBroadcastResponse> response) { |
|
||||
if (response.isSuccessful()) { |
|
||||
data.postValue(Resource.success(new Object())); |
|
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
} |
|
||||
return; |
|
||||
} |
|
||||
if (response.errorBody() != null) { |
|
||||
|
) |
||||
|
request.enqueue(object : Callback<DirectThreadBroadcastResponse?> { |
||||
|
override fun onResponse( |
||||
|
call: Call<DirectThreadBroadcastResponse?>, |
||||
|
response: Response<DirectThreadBroadcastResponse?>, |
||||
|
) { |
||||
|
if (response.isSuccessful) { |
||||
|
data.postValue(success(Any())) |
||||
|
callback?.invoke() |
||||
|
return |
||||
|
} |
||||
|
val errorBody = response.errorBody() |
||||
|
if (errorBody != null) { |
||||
try { |
try { |
||||
final String string = response.errorBody().string(); |
|
||||
final String msg = String.format(Locale.US, |
|
||||
|
val string = errorBody.string() |
||||
|
val msg = String.format(Locale.US, |
||||
"onResponse: url: %s, responseCode: %d, errorBody: %s", |
"onResponse: url: %s, responseCode: %d, errorBody: %s", |
||||
call.request().url().toString(), |
call.request().url().toString(), |
||||
response.code(), |
response.code(), |
||||
string); |
|
||||
Log.e(TAG, msg); |
|
||||
data.postValue(Resource.error(msg, null)); |
|
||||
} catch (IOException e) { |
|
||||
Log.e(TAG, "onResponse: ", e); |
|
||||
data.postValue(Resource.error(e.getMessage(), null)); |
|
||||
} |
|
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
} |
|
||||
return; |
|
||||
} |
|
||||
final String msg = "onResponse: request was not successful and response error body was null"; |
|
||||
Log.e(TAG, msg); |
|
||||
data.postValue(Resource.error(msg, null)); |
|
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<DirectThreadBroadcastResponse> call, @NonNull final Throwable t) { |
|
||||
Log.e(TAG, "onFailure: ", t); |
|
||||
data.postValue(Resource.error(t.getMessage(), null)); |
|
||||
if (callback != null) { |
|
||||
callback.apply(null); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
return data; |
|
||||
|
string) |
||||
|
Log.e(TAG, msg) |
||||
|
data.postValue(error(msg, null)) |
||||
|
} catch (e: IOException) { |
||||
|
Log.e(TAG, "onResponse: ", e) |
||||
|
data.postValue(error(e.message, null)) |
||||
|
} |
||||
|
callback?.invoke() |
||||
|
return |
||||
|
} |
||||
|
val msg = "onResponse: request was not successful and response error body was null" |
||||
|
Log.e(TAG, msg) |
||||
|
data.postValue(error(msg, null)) |
||||
|
callback?.invoke() |
||||
|
} |
||||
|
|
||||
|
override fun onFailure(call: Call<DirectThreadBroadcastResponse?>, t: Throwable) { |
||||
|
Log.e(TAG, "onFailure: ", t) |
||||
|
data.postValue(error(t.message, null)) |
||||
|
callback?.invoke() |
||||
|
} |
||||
|
}) |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
init { |
||||
|
val cookie = Utils.settingsHelper.getString(Constants.COOKIE) |
||||
|
viewerId = getUserIdFromCookie(cookie) |
||||
|
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID) |
||||
|
val csrfToken = getCsrfTokenFromCookie(cookie) |
||||
|
require(!csrfToken.isNullOrBlank() && viewerId != 0L && deviceUuid.isNotBlank()) { "User is not logged in!" } |
||||
|
this.csrfToken = csrfToken |
||||
|
service = getInstance(csrfToken, viewerId, deviceUuid) |
||||
} |
} |
||||
} |
} |
@ -1,382 +1,349 @@ |
|||||
package awais.instagrabber.managers; |
|
||||
|
|
||||
import android.util.Log; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.lifecycle.LiveData; |
|
||||
import androidx.lifecycle.MutableLiveData; |
|
||||
import androidx.lifecycle.Transformations; |
|
||||
|
|
||||
import com.google.common.cache.CacheBuilder; |
|
||||
import com.google.common.cache.CacheLoader; |
|
||||
import com.google.common.cache.LoadingCache; |
|
||||
import com.google.common.collect.ImmutableList; |
|
||||
import com.google.common.collect.Iterables; |
|
||||
|
|
||||
import java.util.Collection; |
|
||||
import java.util.Collections; |
|
||||
import java.util.Comparator; |
|
||||
import java.util.LinkedList; |
|
||||
import java.util.List; |
|
||||
import java.util.Objects; |
|
||||
import java.util.concurrent.TimeUnit; |
|
||||
import java.util.stream.Collectors; |
|
||||
|
|
||||
import awais.instagrabber.R; |
|
||||
import awais.instagrabber.models.Resource; |
|
||||
import awais.instagrabber.repositories.responses.User; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectInbox; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectItem; |
|
||||
import awais.instagrabber.repositories.responses.directmessages.DirectThread; |
|
||||
import awais.instagrabber.utils.Constants; |
|
||||
import awais.instagrabber.utils.CookieUtils; |
|
||||
import awais.instagrabber.utils.TextUtils; |
|
||||
import awais.instagrabber.webservices.DirectMessagesService; |
|
||||
import retrofit2.Call; |
|
||||
import retrofit2.Callback; |
|
||||
import retrofit2.Response; |
|
||||
|
|
||||
import static androidx.lifecycle.Transformations.distinctUntilChanged; |
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|
||||
|
|
||||
public final class InboxManager { |
|
||||
private static final String TAG = InboxManager.class.getSimpleName(); |
|
||||
private static final LoadingCache<String, Object> THREAD_LOCKS = CacheBuilder |
|
||||
.newBuilder() |
|
||||
.expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected |
|
||||
.build(CacheLoader.from(Object::new)); |
|
||||
private static final Comparator<DirectThread> THREAD_COMPARATOR = (t1, t2) -> { |
|
||||
final DirectItem t1FirstDirectItem = t1.getFirstDirectItem(); |
|
||||
final DirectItem t2FirstDirectItem = t2.getFirstDirectItem(); |
|
||||
if (t1FirstDirectItem == null && t2FirstDirectItem == null) return 0; |
|
||||
if (t1FirstDirectItem == null) return 1; |
|
||||
if (t2FirstDirectItem == null) return -1; |
|
||||
return Long.compare(t2FirstDirectItem.getTimestamp(), t1FirstDirectItem.getTimestamp()); |
|
||||
}; |
|
||||
|
|
||||
private final MutableLiveData<Resource<DirectInbox>> inbox = new MutableLiveData<>(); |
|
||||
private final MutableLiveData<Resource<Integer>> unseenCount = new MutableLiveData<>(); |
|
||||
private final MutableLiveData<Integer> pendingRequestsTotal = new MutableLiveData<>(0); |
|
||||
|
|
||||
private final LiveData<List<DirectThread>> threads; |
|
||||
private final DirectMessagesService service; |
|
||||
private final boolean pending; |
|
||||
|
|
||||
private Call<DirectInboxResponse> inboxRequest; |
|
||||
private Call<DirectBadgeCount> unseenCountRequest; |
|
||||
private long seqId; |
|
||||
private String cursor; |
|
||||
private boolean hasOlder = true; |
|
||||
private User viewer; |
|
||||
|
|
||||
@NonNull |
|
||||
public static InboxManager getInstance(final boolean pending) { |
|
||||
return new InboxManager(pending); |
|
||||
} |
|
||||
|
|
||||
private InboxManager(final boolean pending) { |
|
||||
this.pending = pending; |
|
||||
final String cookie = settingsHelper.getString(Constants.COOKIE); |
|
||||
final long userId = CookieUtils.getUserIdFromCookie(cookie); |
|
||||
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); |
|
||||
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); |
|
||||
if (TextUtils.isEmpty(csrfToken)) { |
|
||||
throw new IllegalArgumentException("csrfToken is empty!"); |
|
||||
} else if (userId == 0) { |
|
||||
throw new IllegalArgumentException("user id invalid"); |
|
||||
} else if (TextUtils.isEmpty(deviceUuid)) { |
|
||||
throw new IllegalArgumentException("device uuid is empty!"); |
|
||||
} |
|
||||
service = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid); |
|
||||
|
|
||||
// Transformations |
|
||||
threads = distinctUntilChanged(Transformations.map(inbox, inboxResource -> { |
|
||||
if (inboxResource == null) { |
|
||||
return Collections.emptyList(); |
|
||||
|
package awais.instagrabber.managers |
||||
|
|
||||
|
import android.util.Log |
||||
|
import androidx.lifecycle.LiveData |
||||
|
import androidx.lifecycle.MutableLiveData |
||||
|
import androidx.lifecycle.Transformations |
||||
|
import awais.instagrabber.R |
||||
|
import awais.instagrabber.models.Resource |
||||
|
import awais.instagrabber.models.Resource.Companion.error |
||||
|
import awais.instagrabber.models.Resource.Companion.loading |
||||
|
import awais.instagrabber.models.Resource.Companion.success |
||||
|
import awais.instagrabber.repositories.responses.User |
||||
|
import awais.instagrabber.repositories.responses.directmessages.* |
||||
|
import awais.instagrabber.utils.Constants |
||||
|
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 com.google.common.cache.CacheBuilder |
||||
|
import com.google.common.cache.CacheLoader |
||||
|
import com.google.common.collect.ImmutableList |
||||
|
import retrofit2.Call |
||||
|
import retrofit2.Callback |
||||
|
import retrofit2.Response |
||||
|
import java.util.* |
||||
|
import java.util.concurrent.TimeUnit |
||||
|
|
||||
|
class InboxManager private constructor(private val pending: Boolean) { |
||||
|
private val inbox = MutableLiveData<Resource<DirectInbox?>>() |
||||
|
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 |
||||
|
private var cursor: String? = null |
||||
|
private var hasOlder = true |
||||
|
var viewer: User? = null |
||||
|
private set |
||||
|
|
||||
|
fun getInbox(): LiveData<Resource<DirectInbox?>> { |
||||
|
return Transformations.distinctUntilChanged(inbox) |
||||
|
} |
||||
|
|
||||
|
fun getUnseenCount(): LiveData<Resource<Int?>> { |
||||
|
return Transformations.distinctUntilChanged(unseenCount) |
||||
|
} |
||||
|
|
||||
|
fun getPendingRequestsTotal(): LiveData<Int> { |
||||
|
return Transformations.distinctUntilChanged(pendingRequestsTotal) |
||||
|
} |
||||
|
|
||||
|
fun fetchInbox() { |
||||
|
val inboxResource = inbox.value |
||||
|
if (inboxResource != null && inboxResource.status === Resource.Status.LOADING || !hasOlder) return |
||||
|
stopCurrentInboxRequest() |
||||
|
inbox.postValue(loading(currentDirectInbox)) |
||||
|
inboxRequest = if (pending) service.fetchPendingInbox(cursor, seqId) else service.fetchInbox(cursor, seqId) |
||||
|
inboxRequest?.enqueue(object : Callback<DirectInboxResponse?> { |
||||
|
override fun onResponse(call: Call<DirectInboxResponse?>, response: Response<DirectInboxResponse?>) { |
||||
|
val body = response.body() |
||||
|
if (body == null) { |
||||
|
Log.e(TAG, "parseInboxResponse: Response is null") |
||||
|
inbox.postValue(error(R.string.generic_null_response, currentDirectInbox)) |
||||
|
hasOlder = false |
||||
|
return |
||||
|
} |
||||
|
parseInboxResponse(body) |
||||
|
} |
||||
|
|
||||
|
override fun onFailure(call: Call<DirectInboxResponse?>, t: Throwable) { |
||||
|
Log.e(TAG, "Failed fetching dm inbox", t) |
||||
|
inbox.postValue(error(t.message, currentDirectInbox)) |
||||
|
hasOlder = false |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
fun fetchUnseenCount() { |
||||
|
val unseenCountResource = unseenCount.value |
||||
|
if (unseenCountResource != null && unseenCountResource.status === Resource.Status.LOADING) return |
||||
|
stopCurrentUnseenCountRequest() |
||||
|
unseenCount.postValue(loading(currentUnseenCount)) |
||||
|
unseenCountRequest = service.fetchUnseenCount() |
||||
|
unseenCountRequest?.enqueue(object : Callback<DirectBadgeCount?> { |
||||
|
override fun onResponse(call: Call<DirectBadgeCount?>, response: Response<DirectBadgeCount?>) { |
||||
|
val directBadgeCount = response.body() |
||||
|
if (directBadgeCount == null) { |
||||
|
Log.e(TAG, "onResponse: directBadgeCount Response is null") |
||||
|
unseenCount.postValue(error(R.string.dms_inbox_error_null_count, currentUnseenCount)) |
||||
|
return |
||||
} |
} |
||||
final DirectInbox inbox = inboxResource.data; |
|
||||
if (inbox == null) { |
|
||||
return Collections.emptyList(); |
|
||||
|
unseenCount.postValue(success(directBadgeCount.badgeCount)) |
||||
} |
} |
||||
return ImmutableList.sortedCopyOf(THREAD_COMPARATOR, inbox.getThreads()); |
|
||||
})); |
|
||||
|
|
||||
fetchInbox(); |
|
||||
if (!pending) { |
|
||||
fetchUnseenCount(); |
|
||||
|
override fun onFailure(call: Call<DirectBadgeCount?>, t: Throwable) { |
||||
|
Log.e(TAG, "Failed fetching unseen count", t) |
||||
|
unseenCount.postValue(error(t.message, currentUnseenCount)) |
||||
} |
} |
||||
|
}) |
||||
} |
} |
||||
|
|
||||
public LiveData<Resource<DirectInbox>> getInbox() { |
|
||||
return distinctUntilChanged(inbox); |
|
||||
|
fun refresh() { |
||||
|
cursor = null |
||||
|
seqId = 0 |
||||
|
hasOlder = true |
||||
|
fetchInbox() |
||||
|
if (!pending) { |
||||
|
fetchUnseenCount() |
||||
} |
} |
||||
|
|
||||
public LiveData<List<DirectThread>> getThreads() { |
|
||||
return threads; |
|
||||
} |
} |
||||
|
|
||||
public LiveData<Resource<Integer>> getUnseenCount() { |
|
||||
return distinctUntilChanged(unseenCount); |
|
||||
|
private val currentDirectInbox: DirectInbox? |
||||
|
get() { |
||||
|
val inboxResource = inbox.value |
||||
|
return inboxResource?.data |
||||
} |
} |
||||
|
|
||||
public LiveData<Integer> getPendingRequestsTotal() { |
|
||||
return distinctUntilChanged(pendingRequestsTotal); |
|
||||
|
private fun parseInboxResponse(response: DirectInboxResponse) { |
||||
|
if (response.status != "ok") { |
||||
|
Log.e(TAG, "DM inbox fetch response: status not ok") |
||||
|
inbox.postValue(error(R.string.generic_not_ok_response, currentDirectInbox)) |
||||
|
hasOlder = false |
||||
|
return |
||||
} |
} |
||||
|
|
||||
public User getViewer() { |
|
||||
return viewer; |
|
||||
|
seqId = response.seqId |
||||
|
if (viewer == null) { |
||||
|
viewer = response.viewer |
||||
|
} |
||||
|
val inbox = response.inbox ?: return |
||||
|
if (!cursor.isNullOrBlank()) { |
||||
|
val currentDirectInbox = currentDirectInbox |
||||
|
currentDirectInbox?.let { |
||||
|
val threads = it.threads |
||||
|
val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) |
||||
|
threadsCopy.addAll(inbox.threads ?: emptyList()) |
||||
|
inbox.threads = threads |
||||
|
} |
||||
|
} |
||||
|
this.inbox.postValue(success(inbox)) |
||||
|
cursor = inbox.oldestCursor |
||||
|
hasOlder = inbox.hasOlder |
||||
|
pendingRequestsTotal.postValue(response.pendingRequestsTotal) |
||||
|
} |
||||
|
|
||||
|
fun setThread( |
||||
|
threadId: String, |
||||
|
thread: DirectThread, |
||||
|
) { |
||||
|
val inbox = currentDirectInbox ?: return |
||||
|
val index = getThreadIndex(threadId, inbox) |
||||
|
setThread(inbox, index, thread) |
||||
|
} |
||||
|
|
||||
|
private fun setThread( |
||||
|
inbox: DirectInbox, |
||||
|
index: Int, |
||||
|
thread: DirectThread, |
||||
|
) { |
||||
|
if (index < 0) return |
||||
|
synchronized(this.inbox) { |
||||
|
val threads = inbox.threads |
||||
|
val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) |
||||
|
threadsCopy[index] = thread |
||||
|
try { |
||||
|
val clone = inbox.clone() as DirectInbox |
||||
|
clone.threads = threadsCopy |
||||
|
this.inbox.postValue(success(clone)) |
||||
|
} catch (e: CloneNotSupportedException) { |
||||
|
Log.e(TAG, "setThread: ", e) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fun addItemsToThread( |
||||
|
threadId: String, |
||||
|
insertIndex: Int, |
||||
|
items: Collection<DirectItem>, |
||||
|
) { |
||||
|
val inbox = currentDirectInbox ?: return |
||||
|
synchronized(THREAD_LOCKS.getUnchecked(threadId)) { |
||||
|
val index = getThreadIndex(threadId, inbox) |
||||
|
if (index < 0) return |
||||
|
val threads = inbox.threads ?: return |
||||
|
val thread = threads[index] |
||||
|
val threadItems = thread.items |
||||
|
val list = if (threadItems == null) LinkedList() else LinkedList(threadItems) |
||||
|
if (insertIndex >= 0) { |
||||
|
list.addAll(insertIndex, items) |
||||
|
} else { |
||||
|
list.addAll(items) |
||||
} |
} |
||||
|
|
||||
public void fetchInbox() { |
|
||||
final Resource<DirectInbox> inboxResource = inbox.getValue(); |
|
||||
if ((inboxResource != null && inboxResource.status == Resource.Status.LOADING) || !hasOlder) return; |
|
||||
stopCurrentInboxRequest(); |
|
||||
inbox.postValue(Resource.loading(getCurrentDirectInbox())); |
|
||||
inboxRequest = pending ? service.fetchPendingInbox(cursor, seqId) : service.fetchInbox(cursor, seqId); |
|
||||
inboxRequest.enqueue(new Callback<DirectInboxResponse>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<DirectInboxResponse> call, @NonNull final Response<DirectInboxResponse> response) { |
|
||||
parseInboxResponse(response.body()); |
|
||||
|
try { |
||||
|
val threadClone = thread.clone() as DirectThread |
||||
|
threadClone.items = list |
||||
|
setThread(inbox, index, threadClone) |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "addItemsToThread: ", e) |
||||
} |
} |
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<DirectInboxResponse> call, @NonNull final Throwable t) { |
|
||||
Log.e(TAG, "Failed fetching dm inbox", t); |
|
||||
inbox.postValue(Resource.error(t.getMessage(), getCurrentDirectInbox())); |
|
||||
hasOlder = false; |
|
||||
} |
} |
||||
}); |
|
||||
} |
} |
||||
|
|
||||
public void fetchUnseenCount() { |
|
||||
final Resource<Integer> unseenCountResource = unseenCount.getValue(); |
|
||||
if ((unseenCountResource != null && unseenCountResource.status == Resource.Status.LOADING)) return; |
|
||||
stopCurrentUnseenCountRequest(); |
|
||||
unseenCount.postValue(Resource.loading(getCurrentUnseenCount())); |
|
||||
unseenCountRequest = service.fetchUnseenCount(); |
|
||||
unseenCountRequest.enqueue(new Callback<DirectBadgeCount>() { |
|
||||
@Override |
|
||||
public void onResponse(@NonNull final Call<DirectBadgeCount> call, @NonNull final Response<DirectBadgeCount> response) { |
|
||||
final DirectBadgeCount directBadgeCount = response.body(); |
|
||||
if (directBadgeCount == null) { |
|
||||
Log.e(TAG, "onResponse: directBadgeCount Response is null"); |
|
||||
unseenCount.postValue(Resource.error(R.string.dms_inbox_error_null_count, getCurrentUnseenCount())); |
|
||||
return; |
|
||||
} |
|
||||
unseenCount.postValue(Resource.success(directBadgeCount.getBadgeCount())); |
|
||||
|
fun setItemsToThread( |
||||
|
threadId: String, |
||||
|
updatedItems: List<DirectItem>, |
||||
|
) { |
||||
|
val inbox = currentDirectInbox ?: return |
||||
|
synchronized(THREAD_LOCKS.getUnchecked(threadId)) { |
||||
|
val index = getThreadIndex(threadId, inbox) |
||||
|
if (index < 0) return |
||||
|
val threads = inbox.threads ?: return |
||||
|
val thread = threads[index] |
||||
|
try { |
||||
|
val threadClone = thread.clone() as DirectThread |
||||
|
threadClone.items = updatedItems |
||||
|
setThread(inbox, index, threadClone) |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "setItemsToThread: ", e) |
||||
} |
} |
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Call<DirectBadgeCount> call, @NonNull final Throwable t) { |
|
||||
Log.e(TAG, "Failed fetching unseen count", t); |
|
||||
unseenCount.postValue(Resource.error(t.getMessage(), getCurrentUnseenCount())); |
|
||||
} |
} |
||||
}); |
|
||||
} |
} |
||||
|
|
||||
public void refresh() { |
|
||||
cursor = null; |
|
||||
seqId = 0; |
|
||||
hasOlder = true; |
|
||||
fetchInbox(); |
|
||||
if (!pending) { |
|
||||
fetchUnseenCount(); |
|
||||
} |
|
||||
|
private fun getThreadIndex( |
||||
|
threadId: String, |
||||
|
inbox: DirectInbox, |
||||
|
): Int { |
||||
|
val threads = inbox.threads |
||||
|
return if (threads == null || threads.isEmpty()) { |
||||
|
-1 |
||||
|
} else threads.indexOfFirst { it.threadId == threadId } |
||||
} |
} |
||||
|
|
||||
private DirectInbox getCurrentDirectInbox() { |
|
||||
final Resource<DirectInbox> inboxResource = inbox.getValue(); |
|
||||
return inboxResource != null ? inboxResource.data : null; |
|
||||
|
private val currentUnseenCount: Int? |
||||
|
get() { |
||||
|
val unseenCountResource = unseenCount.value |
||||
|
return unseenCountResource?.data |
||||
} |
} |
||||
|
|
||||
private void parseInboxResponse(final DirectInboxResponse response) { |
|
||||
if (response == null) { |
|
||||
Log.e(TAG, "parseInboxResponse: Response is null"); |
|
||||
inbox.postValue(Resource.error(R.string.generic_null_response, getCurrentDirectInbox())); |
|
||||
hasOlder = false; |
|
||||
return; |
|
||||
} |
|
||||
if (!Objects.equals(response.getStatus(), "ok")) { |
|
||||
Log.e(TAG, "DM inbox fetch response: status not ok"); |
|
||||
inbox.postValue(Resource.error(R.string.generic_not_ok_response, getCurrentDirectInbox())); |
|
||||
hasOlder = false; |
|
||||
return; |
|
||||
} |
|
||||
seqId = response.getSeqId(); |
|
||||
if (viewer == null) { |
|
||||
viewer = response.getViewer(); |
|
||||
|
private fun stopCurrentInboxRequest() { |
||||
|
inboxRequest?.let { |
||||
|
if (it.isCanceled || it.isExecuted) return |
||||
|
it.cancel() |
||||
} |
} |
||||
final DirectInbox inbox = response.getInbox(); |
|
||||
if (inbox == null) return; |
|
||||
if (!TextUtils.isEmpty(cursor)) { |
|
||||
final DirectInbox currentDirectInbox = getCurrentDirectInbox(); |
|
||||
if (currentDirectInbox != null) { |
|
||||
List<DirectThread> threads = currentDirectInbox.getThreads(); |
|
||||
threads = threads == null ? new LinkedList<>() : new LinkedList<>(threads); |
|
||||
threads.addAll(inbox.getThreads() == null ? Collections.emptyList() : inbox.getThreads()); |
|
||||
inbox.setThreads(threads); |
|
||||
|
inboxRequest = null |
||||
} |
} |
||||
|
|
||||
|
private fun stopCurrentUnseenCountRequest() { |
||||
|
unseenCountRequest?.let { |
||||
|
if (it.isCanceled || it.isExecuted) return |
||||
|
it.cancel() |
||||
} |
} |
||||
this.inbox.postValue(Resource.success(inbox)); |
|
||||
cursor = inbox.getOldestCursor(); |
|
||||
hasOlder = inbox.getHasOlder(); |
|
||||
pendingRequestsTotal.postValue(response.getPendingRequestsTotal()); |
|
||||
|
unseenCountRequest = null |
||||
} |
} |
||||
|
|
||||
public void setThread(@NonNull final String threadId, |
|
||||
@NonNull final DirectThread thread) { |
|
||||
final DirectInbox inbox = getCurrentDirectInbox(); |
|
||||
if (inbox == null) return; |
|
||||
final int index = getThreadIndex(threadId, inbox); |
|
||||
setThread(inbox, index, thread); |
|
||||
|
fun onDestroy() { |
||||
|
stopCurrentInboxRequest() |
||||
|
stopCurrentUnseenCountRequest() |
||||
} |
} |
||||
|
|
||||
private void setThread(@NonNull final DirectInbox inbox, |
|
||||
final int index, |
|
||||
@NonNull final DirectThread thread) { |
|
||||
if (index < 0) return; |
|
||||
synchronized (this.inbox) { |
|
||||
final List<DirectThread> threadsCopy = new LinkedList<>(inbox.getThreads()); |
|
||||
threadsCopy.set(index, thread); |
|
||||
|
fun addThread(thread: DirectThread, insertIndex: Int) { |
||||
|
if (insertIndex < 0) return |
||||
|
synchronized(inbox) { |
||||
|
val currentDirectInbox = currentDirectInbox ?: return |
||||
|
val threads = currentDirectInbox.threads |
||||
|
val threadsCopy = if (threads == null) LinkedList() else LinkedList(threads) |
||||
|
threadsCopy.add(insertIndex, thread) |
||||
try { |
try { |
||||
final DirectInbox clone = (DirectInbox) inbox.clone(); |
|
||||
clone.setThreads(threadsCopy); |
|
||||
this.inbox.postValue(Resource.success(clone)); |
|
||||
} catch (CloneNotSupportedException e) { |
|
||||
Log.e(TAG, "setThread: ", e); |
|
||||
|
val clone = currentDirectInbox.clone() as DirectInbox |
||||
|
clone.threads = threadsCopy |
||||
|
inbox.setValue(success(clone)) |
||||
|
} catch (e: CloneNotSupportedException) { |
||||
|
Log.e(TAG, "setThread: ", e) |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public void addItemsToThread(@NonNull final String threadId, |
|
||||
final int insertIndex, |
|
||||
@NonNull final Collection<DirectItem> items) { |
|
||||
final DirectInbox inbox = getCurrentDirectInbox(); |
|
||||
if (inbox == null) return; |
|
||||
synchronized (THREAD_LOCKS.getUnchecked(threadId)) { |
|
||||
final int index = getThreadIndex(threadId, inbox); |
|
||||
if (index < 0) return; |
|
||||
final List<DirectThread> threads = inbox.getThreads(); |
|
||||
final DirectThread thread = threads.get(index); |
|
||||
List<DirectItem> list = thread.getItems(); |
|
||||
list = list == null ? new LinkedList<>() : new LinkedList<>(list); |
|
||||
if (insertIndex >= 0) { |
|
||||
list.addAll(insertIndex, items); |
|
||||
} else { |
|
||||
list.addAll(items); |
|
||||
} |
|
||||
|
fun removeThread(threadId: String) { |
||||
|
synchronized(inbox) { |
||||
|
val currentDirectInbox = currentDirectInbox ?: return |
||||
|
val threads = currentDirectInbox.threads ?: return |
||||
|
val threadsCopy = threads.asSequence().filter { it.threadId != threadId }.toList() |
||||
try { |
try { |
||||
final DirectThread threadClone = (DirectThread) thread.clone(); |
|
||||
threadClone.setItems(list); |
|
||||
setThread(inbox, index, threadClone); |
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "addItemsToThread: ", e); |
|
||||
|
val clone = currentDirectInbox.clone() as DirectInbox |
||||
|
clone.threads = threadsCopy |
||||
|
inbox.postValue(success(clone)) |
||||
|
} catch (e: CloneNotSupportedException) { |
||||
|
Log.e(TAG, "setThread: ", e) |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public void setItemsToThread(@NonNull final String threadId, |
|
||||
@NonNull final List<DirectItem> updatedItems) { |
|
||||
final DirectInbox inbox = getCurrentDirectInbox(); |
|
||||
if (inbox == null) return; |
|
||||
synchronized (THREAD_LOCKS.getUnchecked(threadId)) { |
|
||||
final int index = getThreadIndex(threadId, inbox); |
|
||||
if (index < 0) return; |
|
||||
final List<DirectThread> threads = inbox.getThreads(); |
|
||||
final DirectThread thread = threads.get(index); |
|
||||
try { |
|
||||
final DirectThread threadClone = (DirectThread) thread.clone(); |
|
||||
threadClone.setItems(updatedItems); |
|
||||
setThread(inbox, index, threadClone); |
|
||||
} catch (Exception e) { |
|
||||
Log.e(TAG, "setItemsToThread: ", e); |
|
||||
} |
|
||||
} |
|
||||
|
fun setPendingRequestsTotal(total: Int) { |
||||
|
pendingRequestsTotal.postValue(total) |
||||
} |
} |
||||
|
|
||||
private int getThreadIndex(@NonNull final String threadId, |
|
||||
@NonNull final DirectInbox inbox) { |
|
||||
final List<DirectThread> threads = inbox.getThreads(); |
|
||||
if (threads == null || threads.isEmpty()) { |
|
||||
return -1; |
|
||||
} |
|
||||
return Iterables.indexOf(threads, t -> { |
|
||||
if (t == null) return false; |
|
||||
return t.getThreadId().equals(threadId); |
|
||||
}); |
|
||||
|
fun containsThread(threadId: String?): Boolean { |
||||
|
if (threadId == null) return false |
||||
|
synchronized(inbox) { |
||||
|
val currentDirectInbox = currentDirectInbox ?: return false |
||||
|
val threads = currentDirectInbox.threads ?: return false |
||||
|
return threads.any { it.threadId == threadId } |
||||
} |
} |
||||
|
|
||||
private Integer getCurrentUnseenCount() { |
|
||||
final Resource<Integer> unseenCountResource = unseenCount.getValue(); |
|
||||
return unseenCountResource != null ? unseenCountResource.data : null; |
|
||||
} |
} |
||||
|
|
||||
private void stopCurrentInboxRequest() { |
|
||||
if (inboxRequest == null || inboxRequest.isCanceled() || inboxRequest.isExecuted()) return; |
|
||||
inboxRequest.cancel(); |
|
||||
inboxRequest = null; |
|
||||
} |
|
||||
|
|
||||
private void stopCurrentUnseenCountRequest() { |
|
||||
if (unseenCountRequest == null || unseenCountRequest.isCanceled() || unseenCountRequest.isExecuted()) return; |
|
||||
unseenCountRequest.cancel(); |
|
||||
unseenCountRequest = null; |
|
||||
} |
|
||||
|
|
||||
public void onDestroy() { |
|
||||
stopCurrentInboxRequest(); |
|
||||
stopCurrentUnseenCountRequest(); |
|
||||
|
companion object { |
||||
|
private val TAG = InboxManager::class.java.simpleName |
||||
|
private val THREAD_LOCKS = CacheBuilder |
||||
|
.newBuilder() |
||||
|
.expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected |
||||
|
.build<String, Any>(CacheLoader.from<Any> { Object() }) |
||||
|
private val THREAD_COMPARATOR = Comparator { t1: DirectThread, t2: DirectThread -> |
||||
|
val t1FirstDirectItem = t1.firstDirectItem |
||||
|
val t2FirstDirectItem = t2.firstDirectItem |
||||
|
if (t1FirstDirectItem == null && t2FirstDirectItem == null) return@Comparator 0 |
||||
|
if (t1FirstDirectItem == null) return@Comparator 1 |
||||
|
if (t2FirstDirectItem == null) return@Comparator -1 |
||||
|
t2FirstDirectItem.getTimestamp().compareTo(t1FirstDirectItem.getTimestamp()) |
||||
} |
} |
||||
|
|
||||
public void addThread(@NonNull final DirectThread thread, final int insertIndex) { |
|
||||
if (insertIndex < 0) return; |
|
||||
synchronized (this.inbox) { |
|
||||
final DirectInbox currentDirectInbox = getCurrentDirectInbox(); |
|
||||
if (currentDirectInbox == null) return; |
|
||||
final List<DirectThread> threadsCopy = new LinkedList<>(currentDirectInbox.getThreads()); |
|
||||
threadsCopy.add(insertIndex, thread); |
|
||||
try { |
|
||||
final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); |
|
||||
clone.setThreads(threadsCopy); |
|
||||
this.inbox.setValue(Resource.success(clone)); |
|
||||
} catch (CloneNotSupportedException e) { |
|
||||
Log.e(TAG, "setThread: ", e); |
|
||||
} |
|
||||
|
fun getInstance(pending: Boolean): InboxManager { |
||||
|
return InboxManager(pending) |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public void removeThread(@NonNull final String threadId) { |
|
||||
synchronized (this.inbox) { |
|
||||
final DirectInbox currentDirectInbox = getCurrentDirectInbox(); |
|
||||
if (currentDirectInbox == null) return; |
|
||||
final List<DirectThread> threadsCopy = currentDirectInbox.getThreads() |
|
||||
.stream() |
|
||||
.filter(t -> !t.getThreadId().equals(threadId)) |
|
||||
.collect(Collectors.toList()); |
|
||||
try { |
|
||||
final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); |
|
||||
clone.setThreads(threadsCopy); |
|
||||
this.inbox.postValue(Resource.success(clone)); |
|
||||
} catch (CloneNotSupportedException e) { |
|
||||
Log.e(TAG, "setThread: ", e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
init { |
||||
|
val cookie = Utils.settingsHelper.getString(Constants.COOKIE) |
||||
|
val viewerId = getUserIdFromCookie(cookie) |
||||
|
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) |
||||
|
|
||||
public void setPendingRequestsTotal(final int total) { |
|
||||
pendingRequestsTotal.postValue(total); |
|
||||
|
// Transformations |
||||
|
threads = Transformations.distinctUntilChanged(Transformations.map(inbox) { inboxResource: Resource<DirectInbox?>? -> |
||||
|
if (inboxResource == null) { |
||||
|
return@map emptyList() |
||||
} |
} |
||||
|
|
||||
public boolean containsThread(final String threadId) { |
|
||||
if (threadId == null) return false; |
|
||||
synchronized (this.inbox) { |
|
||||
final DirectInbox currentDirectInbox = getCurrentDirectInbox(); |
|
||||
if (currentDirectInbox == null) return false; |
|
||||
final List<DirectThread> threads = currentDirectInbox.getThreads(); |
|
||||
if (threads == null) return false; |
|
||||
return threads.stream().anyMatch(thread -> Objects.equals(thread.getThreadId(), threadId)); |
|
||||
|
val inbox = inboxResource.data |
||||
|
val threads = inbox?.threads ?: emptyList() |
||||
|
ImmutableList.sortedCopyOf(THREAD_COMPARATOR, threads) |
||||
|
}) |
||||
|
fetchInbox() |
||||
|
if (!pending) { |
||||
|
fetchUnseenCount() |
||||
} |
} |
||||
} |
} |
||||
} |
} |
1880
app/src/main/java/awais/instagrabber/managers/ThreadManager.java
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1675
app/src/main/java/awais/instagrabber/managers/ThreadManager.kt
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,193 +0,0 @@ |
|||||
package awais.instagrabber.utils; |
|
||||
|
|
||||
import android.content.ContentResolver; |
|
||||
import android.graphics.Bitmap; |
|
||||
import android.net.Uri; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.annotation.Nullable; |
|
||||
|
|
||||
import org.json.JSONObject; |
|
||||
|
|
||||
import java.io.File; |
|
||||
import java.io.FileInputStream; |
|
||||
import java.io.IOException; |
|
||||
import java.io.InputStream; |
|
||||
import java.util.Map; |
|
||||
|
|
||||
import awais.instagrabber.models.UploadPhotoOptions; |
|
||||
import awais.instagrabber.models.UploadVideoOptions; |
|
||||
import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor; |
|
||||
import okhttp3.Call; |
|
||||
import okhttp3.Headers; |
|
||||
import okhttp3.MediaType; |
|
||||
import okhttp3.OkHttpClient; |
|
||||
import okhttp3.Request; |
|
||||
import okhttp3.RequestBody; |
|
||||
import okhttp3.Response; |
|
||||
import okhttp3.ResponseBody; |
|
||||
import okio.BufferedSink; |
|
||||
import okio.Okio; |
|
||||
import okio.Source; |
|
||||
|
|
||||
public final class MediaUploader { |
|
||||
private static final String TAG = MediaUploader.class.getSimpleName(); |
|
||||
private static final String HOST = "https://i.instagram.com"; |
|
||||
private static final AppExecutors appExecutors = AppExecutors.INSTANCE; |
|
||||
|
|
||||
public static void uploadPhoto(@NonNull final Uri uri, |
|
||||
@NonNull final ContentResolver contentResolver, |
|
||||
@NonNull final OnMediaUploadCompleteListener listener) { |
|
||||
BitmapUtils.loadBitmap(contentResolver, uri, 1000, false, new BitmapUtils.ThumbnailLoadCallback() { |
|
||||
@Override |
|
||||
public void onLoad(@Nullable final Bitmap bitmap, final int width, final int height) { |
|
||||
if (bitmap == null) { |
|
||||
listener.onFailure(new RuntimeException("Bitmap result was null")); |
|
||||
return; |
|
||||
} |
|
||||
uploadPhoto(bitmap, listener); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onFailure(@NonNull final Throwable t) { |
|
||||
listener.onFailure(t); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static void uploadPhoto(@NonNull final Bitmap bitmap, |
|
||||
@NonNull final OnMediaUploadCompleteListener listener) { |
|
||||
appExecutors.getTasksThread().submit(() -> { |
|
||||
final File file; |
|
||||
final long byteLength; |
|
||||
try { |
|
||||
file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null); |
|
||||
byteLength = file.length(); |
|
||||
} catch (Exception e) { |
|
||||
listener.onFailure(e); |
|
||||
return; |
|
||||
} |
|
||||
final UploadPhotoOptions options = MediaUploadHelper.createUploadPhotoOptions(byteLength); |
|
||||
final Map<String, String> headers = MediaUploadHelper.getUploadPhotoHeaders(options); |
|
||||
final String url = HOST + "/rupload_igphoto/" + options.getName() + "/"; |
|
||||
appExecutors.getNetworkIO().execute(() -> { |
|
||||
try (FileInputStream input = new FileInputStream(file)) { |
|
||||
upload(input, url, headers, listener); |
|
||||
} catch (IOException e) { |
|
||||
listener.onFailure(e); |
|
||||
} finally { |
|
||||
//noinspection ResultOfMethodCallIgnored |
|
||||
file.delete(); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public static void uploadVideo(final Uri uri, |
|
||||
final ContentResolver contentResolver, |
|
||||
final UploadVideoOptions options, |
|
||||
final OnMediaUploadCompleteListener listener) { |
|
||||
appExecutors.getTasksThread().submit(() -> { |
|
||||
final Map<String, String> headers = MediaUploadHelper.getUploadVideoHeaders(options); |
|
||||
final String url = HOST + "/rupload_igvideo/" + options.getName() + "/"; |
|
||||
appExecutors.getNetworkIO().execute(() -> { |
|
||||
try (InputStream input = contentResolver.openInputStream(uri)) { |
|
||||
if (input == null) { |
|
||||
listener.onFailure(new RuntimeException("InputStream was null")); |
|
||||
return; |
|
||||
} |
|
||||
upload(input, url, headers, listener); |
|
||||
} catch (IOException e) { |
|
||||
listener.onFailure(e); |
|
||||
} |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private static void upload(@NonNull final InputStream input, |
|
||||
@NonNull final String url, |
|
||||
@NonNull final Map<String, String> headers, |
|
||||
@NonNull final OnMediaUploadCompleteListener listener) { |
|
||||
try { |
|
||||
final OkHttpClient client = new OkHttpClient.Builder() |
|
||||
// .addInterceptor(new LoggingInterceptor()) |
|
||||
.addInterceptor(new AddCookiesInterceptor()) |
|
||||
.followRedirects(false) |
|
||||
.followSslRedirects(false) |
|
||||
.build(); |
|
||||
final Request request = new Request.Builder() |
|
||||
.headers(Headers.of(headers)) |
|
||||
.url(url) |
|
||||
.post(create(MediaType.parse("application/octet-stream"), input)) |
|
||||
.build(); |
|
||||
final Call call = client.newCall(request); |
|
||||
final Response response = call.execute(); |
|
||||
final ResponseBody body = response.body(); |
|
||||
if (!response.isSuccessful()) { |
|
||||
listener.onFailure(new IOException("Unexpected code " + response + (body != null ? ": " + body.string() : ""))); |
|
||||
return; |
|
||||
} |
|
||||
listener.onUploadComplete(new MediaUploadResponse(response.code(), body != null ? new JSONObject(body.string()) : null)); |
|
||||
} catch (Exception e) { |
|
||||
listener.onFailure(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public interface OnMediaUploadCompleteListener { |
|
||||
void onUploadComplete(MediaUploadResponse response); |
|
||||
|
|
||||
void onFailure(Throwable t); |
|
||||
} |
|
||||
|
|
||||
private static RequestBody create(final MediaType mediaType, final InputStream inputStream) { |
|
||||
return new RequestBody() { |
|
||||
@Override |
|
||||
public MediaType contentType() { |
|
||||
return mediaType; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public long contentLength() { |
|
||||
try { |
|
||||
return inputStream.available(); |
|
||||
} catch (IOException e) { |
|
||||
return 0; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void writeTo(@NonNull BufferedSink sink) throws IOException { |
|
||||
try (Source source = Okio.source(inputStream)) { |
|
||||
sink.writeAll(source); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
public static class MediaUploadResponse { |
|
||||
private final int responseCode; |
|
||||
private final JSONObject response; |
|
||||
|
|
||||
public MediaUploadResponse(int responseCode, JSONObject response) { |
|
||||
this.responseCode = responseCode; |
|
||||
this.response = response; |
|
||||
} |
|
||||
|
|
||||
public int getResponseCode() { |
|
||||
return responseCode; |
|
||||
} |
|
||||
|
|
||||
public JSONObject getResponse() { |
|
||||
return response; |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
@Override |
|
||||
public String toString() { |
|
||||
return "MediaUploadResponse{" + |
|
||||
"responseCode=" + responseCode + |
|
||||
", response=" + response + |
|
||||
'}'; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,155 @@ |
|||||
|
package awais.instagrabber.utils |
||||
|
|
||||
|
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 okhttp3.* |
||||
|
import okio.BufferedSink |
||||
|
import okio.source |
||||
|
import org.json.JSONObject |
||||
|
import java.io.File |
||||
|
import java.io.FileInputStream |
||||
|
import java.io.IOException |
||||
|
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 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() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@JvmStatic |
||||
|
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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun upload( |
||||
|
input: InputStream, |
||||
|
url: String, |
||||
|
headers: Map<String, String>, |
||||
|
listener: OnMediaUploadCompleteListener, |
||||
|
) { |
||||
|
try { |
||||
|
val client = OkHttpClient.Builder() |
||||
|
// .addInterceptor(new LoggingInterceptor()) |
||||
|
.addInterceptor(AddCookiesInterceptor()) |
||||
|
.followRedirects(false) |
||||
|
.followSslRedirects(false) |
||||
|
.build() |
||||
|
val request = Request.Builder() |
||||
|
.headers(Headers.of(headers)) |
||||
|
.url(url) |
||||
|
.post(create(MediaType.parse("application/octet-stream"), 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 |
||||
|
} |
||||
|
listener.onUploadComplete(MediaUploadResponse(response.code(), if (body != null) JSONObject(body.string()) else null)) |
||||
|
} catch (e: Exception) { |
||||
|
listener.onFailure(e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody { |
||||
|
return object : RequestBody() { |
||||
|
override fun contentType(): MediaType? { |
||||
|
return mediaType |
||||
|
} |
||||
|
|
||||
|
override fun contentLength(): Long { |
||||
|
return try { |
||||
|
inputStream.available().toLong() |
||||
|
} catch (e: IOException) { |
||||
|
0 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Throws(IOException::class) |
||||
|
override fun writeTo(sink: BufferedSink) { |
||||
|
inputStream.source().use { sink.writeAll(it) } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
interface OnMediaUploadCompleteListener { |
||||
|
fun onUploadComplete(response: MediaUploadResponse) |
||||
|
fun onFailure(t: Throwable) |
||||
|
} |
||||
|
|
||||
|
data class MediaUploadResponse(val responseCode: Int, val response: JSONObject?) |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue