From 886ff2f06b69e67ca459e02d0633f4d69c5b21da Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 31 Jan 2021 19:34:33 +0100 Subject: [PATCH] get rid of BaseFragment by using RxJava instead of Retrofit Calls (#2055) * get rid of BaseFragment by using RxJava instead of Retrofit Calls * fix tests --- .../components/drafts/DraftsViewModel.kt | 2 +- .../fragment/InstanceListFragment.kt | 8 +- .../tusky/fragment/AccountListFragment.kt | 52 ++++------- .../tusky/fragment/AccountMediaFragment.kt | 54 +++++------ .../tusky/fragment/BaseFragment.java | 43 --------- .../tusky/fragment/NotificationsFragment.java | 89 ++++++++----------- .../tusky/fragment/SFragment.java | 14 ++- .../tusky/fragment/TimelineFragment.java | 63 ++++++------- .../tusky/fragment/ViewMediaFragment.kt | 3 +- .../tusky/fragment/ViewThreadFragment.java | 65 +++++--------- .../tusky/network/MastodonApi.kt | 49 +++------- .../tusky/repository/TimelineRepository.kt | 8 +- .../tusky/fragment/TimelineRepositoryTest.kt | 29 +++--- 13 files changed, 170 insertions(+), 309 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index 9eca963aa..f928b6d03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -57,7 +57,7 @@ class DraftsViewModel @Inject constructor( } fun getToot(tootId: String): Single { - return api.statusSingle(tootId) + return api.status(tootId) } override fun onCleared() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index bb850bdcf..093fc42d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -5,6 +5,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -14,7 +15,6 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.fragment.BaseFragment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide @@ -30,7 +30,7 @@ import retrofit2.Response import java.io.IOException import javax.inject.Inject -class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { +class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { @Inject lateinit var api: MastodonApi @@ -39,10 +39,6 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { private var adapter = DomainMutesAdapter(this) private lateinit var scrollListener: EndlessOnScrollListener - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_instance_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index d5785ae36..93a6162b7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -17,9 +17,8 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -45,14 +44,12 @@ import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_account_list.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException import java.util.* import javax.inject.Inject -class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { +class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable { @Inject lateinit var api: MastodonApi @@ -71,10 +68,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { id = arguments?.getString(ARG_ID) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_account_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -202,27 +195,23 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) { - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onRespondToFollowRequestSuccess(position) - } else { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - val call = if (accept) { + if (accept) { api.authorizeFollowRequest(accountId) } else { api.rejectFollowRequest(accountId) - } - callList.add(call) - call.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ + onRespondToFollowRequestSuccess(position) + }, { throwable -> + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $accountId.", throwable) + }) + } private fun onRespondToFollowRequestSuccess(position: Int) { @@ -230,15 +219,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { followRequestsAdapter.removeItem(position) } - private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { - val verb = if (accept) { - "accept" - } else { - "reject" - } - Log.e(TAG, "Failed to $verb account id $accountId.") - } - private fun getFetchCallByListType(fromId: String?): Single>> { return when (type) { Type.FOLLOWS -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index 0ae207587..b85a87f31 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -18,12 +18,13 @@ package com.keylesspalace.tusky.fragment import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -40,9 +41,11 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.fragment_timeline.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException import java.util.* @@ -54,7 +57,7 @@ import javax.inject.Inject * Fragment with multiple columns of media previews for the specified account. */ -class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { +class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { companion object { @JvmStatic fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { @@ -78,14 +81,13 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { lateinit var api: MastodonApi private val adapter = MediaGridAdapter() - private var currentCall: Call>? = null private val statuses = mutableListOf() private var fetchingStatus = FetchingStatus.NOT_FETCHING private lateinit var accountId: String - private val callback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val callback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { @@ -107,7 +109,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { swipeRefreshLayout.isRefreshing = false @@ -128,22 +130,23 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { if (statuses.isEmpty()) { statusView.show() - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) } } } } + + override fun onSubscribe(d: Disposable) {} } - private val bottomCallback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val bottomCallback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING val body = response.body() body?.let { fetched -> @@ -160,6 +163,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } } + override fun onSubscribe(d: Disposable) { } } override fun onCreate(savedInstanceState: Bundle?) { @@ -167,10 +171,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true accountId = arguments?.getString(ACCOUNT_ID_ARG)!! } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timeline, container, false) - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -202,8 +202,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { statuses.lastOrNull()?.let { (id) -> Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)") fetchingStatus = FetchingStatus.FETCHING_BOTTOM - currentCall = api.accountStatuses(accountId, id, null, null, null, true, null) - currentCall?.enqueue(bottomCallback) + api.accountStatuses(accountId, id, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(bottomCallback) } } } @@ -216,14 +218,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { private fun refresh() { statusView.hide() if (fetchingStatus != FetchingStatus.NOT_FETCHING) return - currentCall = if (statuses.isEmpty()) { + if (statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING api.accountStatuses(accountId, null, null, null, null, true, null) } else { fetchingStatus = FetchingStatus.REFRESHING api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) - } - currentCall?.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) if (!isSwipeToRefreshEnabled) topProgressBar?.show() @@ -235,8 +238,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING - currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) - currentCall?.enqueue(callback) + api.accountStatuses(accountId, null, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) } else if (needToRefresh) refresh() @@ -339,5 +344,4 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { needToRefresh = true } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java deleted file mode 100644 index b674b8ba5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment; - -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import java.util.ArrayList; -import java.util.List; - -import retrofit2.Call; - -public class BaseFragment extends Fragment { - protected List callList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - callList = new ArrayList<>(); - } - - @Override - public void onDestroy() { - for (Call call : callList) { - call.cancel(); - } - super.onDestroy(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index fe0c75f55..216ee2286 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -102,13 +102,11 @@ import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.keylesspalace.tusky.util.StringUtils.isLessThan; import static com.uber.autodispose.AutoDispose.autoDisposable; @@ -125,8 +123,9 @@ public class NotificationsFragment extends SFragment implements private static final int LOAD_AT_ONCE = 30; private int maxPlaceholderId = 0; + private final Set notificationFilter = new HashSet<>(); - private Set notificationFilter = new HashSet<>(); + private final CompositeDisposable disposables = new CompositeDisposable(); private enum FetchEnd { TOP, @@ -685,32 +684,21 @@ public class NotificationsFragment extends SFragment implements updateAdapter(); //Execute clear notifications request - Call call = mastodonApi.clearNotifications(); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (isAdded()) { - if (!response.isSuccessful()) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - }); - callList.add(call); + mastodonApi.clearNotifications() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + // nothing to do + }, + throwable -> { + //Reload notifications on failure + fullyRefreshWithProgressBar(true); + }); } private void resetNotificationsLoad() { - for (Call callItem : callList) { - callItem.cancel(); - } - callList.clear(); + disposables.clear(); bottomLoading = false; topLoading = false; @@ -840,8 +828,8 @@ public class NotificationsFragment extends SFragment implements @Override public void onRespondToFollowRequest(boolean accept, String id, int position) { Single request = accept ? - mastodonApi.authorizeFollowRequestObservable(id) : - mastodonApi.rejectFollowRequestObservable(id); + mastodonApi.authorizeFollowRequest(id) : + mastodonApi.rejectFollowRequest(id); request.observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( @@ -959,27 +947,20 @@ public class NotificationsFragment extends SFragment implements bottomLoading = true; } - Call> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null); - - call.enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful()) { - String linkHeader = response.headers().get("Link"); - onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); - } else { - onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - if (!call.isCanceled()) - onFetchNotificationsFailure((Exception) t, fetchEnd, pos); - } - }); - callList.add(call); + Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + String linkHeader = response.headers().get("Link"); + onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); + } else { + onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + throwable -> onFetchNotificationsFailure(throwable, fetchEnd, pos)); + disposables.add(notificationCall); } private void onFetchNotificationsSuccess(List notifications, String linkHeader, @@ -1038,7 +1019,7 @@ public class NotificationsFragment extends SFragment implements progressBar.setVisibility(View.GONE); } - private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); @@ -1050,7 +1031,7 @@ public class NotificationsFragment extends SFragment implements this.statusView.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); this.showingError = true; - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1065,7 +1046,7 @@ public class NotificationsFragment extends SFragment implements } updateFilterVisibility(); } - Log.e(TAG, "Fetch failure: " + exception.getMessage()); + Log.e(TAG, "Fetch failure: " + throwable.getMessage()); if (fetchEnd == FetchEnd.TOP) { topLoading = false; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 4c50740b7..9d4b45b3e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -20,7 +20,6 @@ import android.app.DownloadManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -30,8 +29,6 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -41,14 +38,14 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; -import androidx.preference.PreferenceManager; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BottomSheetActivity; import com.keylesspalace.tusky.MainActivity; -import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.PostLookupFallbackBehavior; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity; @@ -76,9 +73,8 @@ import java.util.regex.Pattern; import javax.inject.Inject; -import kotlin.Unit; - import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -92,7 +88,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear * up what needs to be where. */ -public abstract class SFragment extends BaseFragment implements Injectable { +public abstract class SFragment extends Fragment implements Injectable { protected abstract void removeItem(int position); @@ -103,7 +99,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { private static List filters; private boolean filterRemoveRegex; private Matcher filterRemoveRegexMatcher; - private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); + private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); @Inject public MastodonApi mastodonApi; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index d6910a4c4..06ca56f32 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -101,12 +101,11 @@ import javax.inject.Inject; import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import retrofit2.Call; -import retrofit2.Callback; import retrofit2.Response; import static com.uber.autodispose.AutoDispose.autoDisposable; @@ -1004,7 +1003,7 @@ public class TimelineFragment extends SFragment implements } } - private Call> getFetchCallByTimelineType(String fromId, String uptoId) { + private Single>> getFetchCallByTimelineType(String fromId, String uptoId) { MastodonApi api = mastodonApi; switch (kind) { default: @@ -1051,37 +1050,31 @@ public class TimelineFragment extends SFragment implements .observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( - (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), - (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) + result -> onFetchTimelineSuccess(result, fetchEnd, pos), + err -> onFetchTimelineFailure(err, fetchEnd, pos) ); } else { - Callback> callback = new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful()) { - @Nullable - String newNextId = extractNextId(response); - if (newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId; - } - onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); - } else { - onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - onFetchTimelineFailure((Exception) t, fetchEnd, pos); - } - }; - - Call> listCall = getFetchCallByTimelineType(maxId, sinceId); - callList.add(listCall); - listCall.enqueue(callback); + getFetchCallByTimelineType(maxId, sinceId) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + @Nullable + String newNextId = extractNextId(response); + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId; + } + onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); + } else { + onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + err -> onFetchTimelineFailure(err, fetchEnd, pos) + ); } } @@ -1158,7 +1151,7 @@ public class TimelineFragment extends SFragment implements } } - private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); topProgressBar.hide(); @@ -1177,7 +1170,7 @@ public class TimelineFragment extends SFragment implements } else if (this.statuses.isEmpty()) { swipeRefreshLayout.setEnabled(false); this.statusView.setVisibility(View.VISIBLE); - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1192,7 +1185,7 @@ public class TimelineFragment extends SFragment implements } } - Log.e(TAG, "Fetch Failure: " + exception.getMessage()); + Log.e(TAG, "Fetch Failure: " + throwable.getMessage()); updateBottomLoadingState(fetchEnd); progressBar.setVisibility(View.GONE); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index 86b3d09be..b25fec26f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -17,10 +17,11 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.text.TextUtils +import androidx.fragment.app.Fragment import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.entity.Attachment -abstract class ViewMediaFragment : BaseFragment() { +abstract class ViewMediaFragment : Fragment() { private var toolbarVisibiltyDisposable: Function0? = null abstract fun setupMediaView( diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index bf28301ff..5ce66eee2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -55,7 +55,6 @@ import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.settings.PrefKeys; @@ -75,9 +74,6 @@ import java.util.Locale; import javax.inject.Inject; import io.reactivex.android.schedulers.AndroidSchedulers; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; @@ -463,49 +459,32 @@ public final class ViewThreadFragment extends SFragment implements } private void sendStatusRequest(final String id) { - Call call = mastodonApi.status(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - int position = setStatus(response.body()); - recyclerView.scrollToPosition(position); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.status(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + status -> { + int position = setStatus(status); + recyclerView.scrollToPosition(position); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } private void sendThreadRequest(final String id) { - Call call = mastodonApi.statusContext(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - StatusContext context = response.body(); - if (response.isSuccessful() && context != null) { - swipeRefreshLayout.setRefreshing(false); - setContext(context.getAncestors(), context.getDescendants()); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.statusContext(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + context -> { + swipeRefreshLayout.setRefreshing(false); + setContext(context.getAncestors(), context.getDescendants()); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } - private void onThreadRequestFailure(final String id) { + private void onThreadRequestFailure(final String id, final Throwable throwable) { View view = getView(); swipeRefreshLayout.setRefreshing(false); if (view != null) { @@ -516,7 +495,7 @@ public final class ViewThreadFragment extends SFragment implements }) .show(); } else { - Log.e(TAG, "Couldn't display thread fetch error message"); + Log.e(TAG, "Network request failed", throwable); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 58caec85e..28ac77b6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -56,14 +56,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> - - @GET("api/v1/timelines/home") - fun homeTimelineSingle( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? - ): Single> + ): Single>> @GET("api/v1/timelines/public") fun publicTimeline( @@ -71,7 +64,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/tag/{hashtag}") fun hashtagTimeline( @@ -81,7 +74,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/list/{listId}") fun listTimeline( @@ -89,7 +82,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/notifications") fun notifications( @@ -97,7 +90,7 @@ interface MastodonApi { @Query("since_id") sinceId: String?, @Query("limit") limit: Int?, @Query("exclude_types[]") excludes: Set? - ): Call> + ): Single>> @GET("api/v1/markers") fun markersWithAuth( @@ -114,12 +107,7 @@ interface MastodonApi { ): Single> @POST("api/v1/notifications/clear") - fun clearNotifications(): Call - - @GET("api/v1/notifications/{id}") - fun notification( - @Path("id") notificationId: String - ): Call + fun clearNotifications(): Single @Multipart @POST("api/v1/media") @@ -146,17 +134,12 @@ interface MastodonApi { @GET("api/v1/statuses/{id}") fun status( @Path("id") statusId: String - ): Call - - @GET("api/v1/statuses/{id}") - fun statusSingle( - @Path("id") statusId: String ): Single @GET("api/v1/statuses/{id}/context") fun statusContext( @Path("id") statusId: String - ): Call + ): Single @GET("api/v1/statuses/{id}/reblogged_by") fun statusRebloggedBy( @@ -295,7 +278,7 @@ interface MastodonApi { @Query("exclude_replies") excludeReplies: Boolean?, @Query("only_media") onlyMedia: Boolean?, @Query("pinned") pinned: Boolean? - ): Call> + ): Single>> @GET("api/v1/accounts/{id}/followers") fun accountFollowers( @@ -398,14 +381,14 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/bookmarks") fun bookmarks( @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/follow_requests") fun followRequests( @@ -415,20 +398,10 @@ interface MastodonApi { @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequest( - @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/authorize") - fun authorizeFollowRequestObservable( - @Path("id") accountId: String ): Single @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequestObservable( + fun rejectFollowRequest( @Path("id") accountId: String ): Single diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index 8aa3eb769..945c55d33 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -66,9 +66,9 @@ class TimelineRepositoryImpl( sinceIdMinusOne: String?, limit: Int, accountId: Long, requestMode: TimelineRequestMode ): Single> { - return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) - .map { statuses -> - this.saveStatusesToDb(accountId, statuses, maxId, sinceId) + return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) + .map { response -> + this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId) } .flatMap { statuses -> this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) @@ -85,7 +85,7 @@ class TimelineRepositoryImpl( private fun addFromDbIfNeeded(accountId: Long, statuses: List>, maxId: String?, sinceId: String?, limit: Int, requestMode: TimelineRequestMode - ): Single>? { + ): Single> { return if (requestMode != NETWORK && statuses.size < 2) { val newMaxID = if (statuses.isEmpty()) { maxId diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index 16d0e2717..7a7c3f7d2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -28,6 +28,7 @@ import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config +import retrofit2.Response import java.util.* import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList @@ -76,8 +77,8 @@ class TimelineRepositoryTest { makeStatus("3"), makeStatus("2") ) - whenever(mastodonApi.homeTimelineSingle(isNull(), isNull(), anyInt())) - .thenReturn(Single.just(statuses)) + whenever(mastodonApi.homeTimeline(isNull(), isNull(), anyInt())) + .thenReturn(Single.just(Response.success(statuses))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -107,8 +108,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -141,8 +142,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -181,8 +182,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "3" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -224,8 +225,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "4" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -263,8 +264,8 @@ class TimelineRepositoryTest { dbResult.status = dbStatus.toEntity(account.id, gson) dbResult.account = status.account.toEntity(account.id, gson) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success((listOf(status))))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) @@ -281,8 +282,8 @@ class TimelineRepositoryTest { val dbResult2 = TimelineStatusWithAccount() dbResult2.status = Placeholder("1").toEntity(account.id) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success(listOf(status)))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult, dbResult2))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY)