[streaming] Home timeline streaming

This commit is contained in:
kyori19 2018-12-19 16:05:51 +09:00
parent 54dfc854d4
commit 9bb15e943d
9 changed files with 234 additions and 5 deletions

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -25,6 +26,7 @@ import android.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
@ -118,6 +120,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
private Drawer drawer;
private TabLayout tabLayout;
private ViewPager viewPager;
private SharedPreferences defPrefs;
private int notificationTabPosition;
private MainPagerAdapter adapter;
@ -204,12 +207,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
setupTabs(showNotificationTab);
defPrefs = PreferenceManager.getDefaultSharedPreferences(this);
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
viewPager.setPageMargin(pageMargin);
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
R.drawable.tab_page_margin_dark);
viewPager.setPageMarginDrawable(pageMarginDrawable);
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean("viewPagerOffScreenLimit", false)) {
if (defPrefs.getBoolean("viewPagerOffScreenLimit", false)) {
viewPager.setOffscreenPageLimit(9);
}
@ -270,6 +275,28 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
}
@Override
protected void onStart() {
super.onStart();
startStreaming();
}
private void startStreaming() {
if (defPrefs.getBoolean("useHTLStream", false)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
@Override
protected void onStop() {
super.onStop();
stopStreaming();
}
private void stopStreaming() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
public void onBackPressed() {
if (drawer != null && drawer.isDrawerOpen()) {
@ -538,6 +565,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
SFragment.flushFilters();
accountManager.setActiveAccount(newSelectedId);
stopStreaming();
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (forward != null) {

View File

@ -17,4 +17,5 @@ data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
data class DomainMuteEvent(val instance: String): Dispatchable
data class QuickReplyEvent(val status: Status) : Dispatchable
data class QuickReplyEvent(val status: Status) : Dispatchable
data class StreamUpdateEvent(val status: Status, val first: Boolean) : Dispatchable

View File

@ -0,0 +1,20 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
data class StreamEvent(
var event: EventType,
var payload: String
) {
enum class EventType(val num: Int) {
UNKNOWN(0),
@SerializedName("update")
UPDATE(1),
@SerializedName("notification")
NOTIFICATION(2),
@SerializedName("delete")
DELETE(3);
}
}

View File

@ -45,7 +45,9 @@ import com.keylesspalace.tusky.appstore.QuickReplyEvent;
import com.keylesspalace.tusky.appstore.ReblogEvent;
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
import com.keylesspalace.tusky.appstore.UnfollowEvent;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Filter;
@ -71,8 +73,11 @@ import com.keylesspalace.tusky.view.BackgroundMessageView;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import net.accelf.yuito.TimelineStreamingListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@ -104,6 +109,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.WebSocket;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -178,6 +186,8 @@ public class TimelineFragment extends SFragment implements
private boolean reduceTimelineRoading;
private boolean checkMobileNetwork;
private WebSocket webSocket;
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
@Override
@ -263,6 +273,56 @@ public class TimelineFragment extends SFragment implements
return rootView;
}
@Override
public void onStart() {
super.onStart();
startStreaming();
}
@Override
public void onStop() {
super.onStop();
stopStreaming();
}
private void startStreaming() {
if (preferences.getBoolean("useHTLStream", false) && kind == Kind.HOME) {
connectWebsocket(buildStreamingUrl());
}
}
private String buildStreamingUrl() {
AccountEntity activeAccount = accountManager.getActiveAccount();
if (activeAccount != null) {
return "wss://" + activeAccount.getDomain() + "/api/v1/streaming/?" + "stream=user" + "&" + "access_token" + "=" + activeAccount.getAccessToken();
} else {
return null;
}
}
private void connectWebsocket(String endpoint) {
if (webSocket != null) {
stopStreaming();
}
Request request = new Request.Builder()
.url(endpoint)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.build();
webSocket = client.newWebSocket(request, new TimelineStreamingListener(eventHub));
}
private void stopStreaming() {
if (webSocket == null) {
return;
}
webSocket.close(1000, null);
webSocket = null;
}
private void sendInitialRequest() {
if (this.kind == Kind.HOME) {
this.tryCache();
@ -535,6 +595,10 @@ public class TimelineFragment extends SFragment implements
handleStatusComposeEvent(status);
} else if (event instanceof PreferenceChangedEvent) {
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
} else if (event instanceof StreamUpdateEvent) {
if (kind == Kind.HOME) {
handleStreamUpdateEvent((StreamUpdateEvent) event);
}
}
});
eventRegistered = true;
@ -1222,6 +1286,24 @@ public class TimelineFragment extends SFragment implements
}
}
private void addStatus(Either<Placeholder, Status> item) {
if (item.isRight()) {
Status status = item.asRight();
if (!((status.getInReplyToId() != null && filterRemoveReplies)
|| (status.getReblog() != null && filterRemoveReblogs)
|| shouldFilterStatus(status))) {
if (findStatusOrReblogPositionById(status.getId()) < 0) {
statuses.add(0, item);
updateAdapter();
timelineRepo.addSingleStatusToDb(status);
}
}
} else {
statuses.add(0, item);
updateAdapter();
}
}
/**
* For certain requests we don't want to see placeholders, they will be removed some other way
*/
@ -1308,6 +1390,9 @@ public class TimelineFragment extends SFragment implements
private void handleStatusComposeEvent(@NonNull Status status) {
switch (kind) {
case HOME:
if (preferences.getBoolean("useHTLStream", false)){
return;
}
case PUBLIC_FEDERATED:
case PUBLIC_LOCAL:
break;
@ -1343,6 +1428,16 @@ public class TimelineFragment extends SFragment implements
}
}
private void handleStreamUpdateEvent(StreamUpdateEvent event) {
Status status = event.getStatus();
if (event.getFirst() && statuses.get(0).isRight()) {
Placeholder placeholder = new Placeholder(statuses.get(0).asRight().getId() + 1);
updateStatuses(Arrays.asList(new Either.Right<>(status), new Either.Left<>(placeholder)), false);
} else {
addStatus(new Either.Right<>(status));
}
}
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {
return CollectionsKt.map(list, statusLifter);
}
@ -1357,11 +1452,14 @@ public class TimelineFragment extends SFragment implements
if (isAdded()) {
adapter.notifyItemRangeInserted(position, count);
Context context = getContext();
if (position == 0 && context != null) {
if (isSwipeToRefreshEnabled)
if (position == 0 && context != null && layoutManager.findFirstVisibleItemPosition() == 0) {
if (count == 1) {
jumpToTop();
} else if (isSwipeToRefreshEnabled) {
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
else
} else {
recyclerView.scrollToPosition(0);
}
}
}
}

View File

@ -30,6 +30,7 @@ enum class TimelineRequestMode {
interface TimelineRepository {
fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
requestMode: TimelineRequestMode): Single<out List<TimelineStatus>>
fun addSingleStatusToDb(status: Status)
companion object {
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
@ -61,6 +62,17 @@ class TimelineRepositoryImpl(
}
}
override fun addSingleStatusToDb(status: Status) {
val acc = accountManager.activeAccount ?: throw IllegalStateException()
val accountId = acc.id
timelineDao.insertInTransaction(
status.toEntity(accountId, htmlConverter, gson),
status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson)
)
}
private fun getStatusesFromNetwork(maxId: String?, sinceId: String?,
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode

View File

@ -0,0 +1,62 @@
package net.accelf.yuito;
import android.text.Spanned;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StreamEvent;
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class TimelineStreamingListener extends WebSocketListener {
private Gson gson = buildGson();
private boolean isFirstStatus = true;
private EventHub eventHub;
private static Gson buildGson() {
return new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
}
public TimelineStreamingListener(EventHub eventHub) {
this.eventHub = eventHub;
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
Log.d("StreamingListener", "Stream connected.");
}
@Override
public void onMessage(WebSocket webSocket, String text) {
StreamEvent event = gson.fromJson(text, StreamEvent.class);
String payload = event.getPayload();
switch (event.getEvent()) {
case UPDATE:
Status status = gson.fromJson(payload, Status.class);
eventHub.dispatch(new StreamUpdateEvent(status, isFirstStatus));
isFirstStatus = false;
break;
case DELETE:
eventHub.dispatch(new StatusDeletedEvent(payload));
break;
}
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
Log.d("StreamingListener", "Stream closed.");
}
}

View File

@ -175,6 +175,7 @@
<string name="pref_title_limited_bandwidth_mobile">モバイルネットワークでのみ有効</string>
<string name="pref_title_limited_bandwidth_timeline">トゥート後のタイムラインの自動更新をしない</string>
<string name="pref_title_experimental_viewpager_offscreen">ViewPagerのoffscreenPageLimitを増やす</string>
<string name="pref_title_experimental_htl_streaming">HTLのストリーミングを利用する</string>
<string name="app_them_dark">ダーク</string>
<string name="app_theme_light">ライト</string>
<string name="app_theme_black">ブラック</string>

View File

@ -223,6 +223,7 @@
<string name="pref_title_limited_bandwidth_timeline">Reduce timeline auto reload after toot</string>
<string name="pref_title_experimental">Experimental Settings</string>
<string name="pref_title_experimental_viewpager_offscreen">Increase offscreenPageLimit of ViewPager</string>
<string name="pref_title_experimental_htl_streaming">Use home timeline streaming</string>
<string name="app_them_dark">Dark</string>
<string name="app_theme_light">Light</string>

View File

@ -119,5 +119,10 @@
android:defaultValue="false"
android:key="viewPagerOffScreenLimit"
android:title="@string/pref_title_experimental_viewpager_offscreen" />
<CheckBoxPreference
android:defaultValue="false"
android:key="useHTLStream"
android:title="@string/pref_title_experimental_htl_streaming" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>