implementing more streaming support
This commit is contained in:
Mariotaku Lee 2017-03-11 21:15:29 +08:00
parent 4367242282
commit 1e3a37f0a6
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
38 changed files with 1210 additions and 391 deletions

View File

@ -36,7 +36,7 @@ subprojects {
Kotlin : '1.1.0',
SupportLib : '25.2.0',
MariotakuCommons: '0.9.11',
RestFu : '0.9.40',
RestFu : '0.9.42',
ObjectCursor : '0.9.16',
PlayServices : '10.2.0',
MapsUtils : '0.4.4',

View File

@ -0,0 +1,34 @@
/*
* Twidere - Twitter client for Android
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.mariotaku.microblog.library.fanfou;
import org.mariotaku.microblog.library.fanfou.callback.FanfouUserStreamCallback;
import org.mariotaku.restfu.annotation.method.GET;
/**
* Created by mariotaku on 2017/3/11.
*/
public interface FanfouStream {
@GET("/1/user.json")
void getUserStream(FanfouUserStreamCallback callback);
}

View File

@ -0,0 +1,108 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.fanfou.callback;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.bluelinelabs.logansquare.LoganSquare;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.fanfou.model.FanfouStreamObject;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.microblog.library.util.CRLFLineReader;
import org.mariotaku.restfu.callback.RawCallback;
import org.mariotaku.restfu.http.HttpResponse;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
/**
* Created by mariotaku on 15/5/26.
*/
@SuppressWarnings({"WeakerAccess"})
public abstract class FanfouUserStreamCallback implements RawCallback<MicroBlogException> {
private boolean connected;
private boolean disconnected;
@Override
public final void result(@NonNull final HttpResponse response) throws MicroBlogException, IOException {
if (!response.isSuccessful()) {
final MicroBlogException cause = new MicroBlogException();
cause.setHttpResponse(response);
onException(cause);
return;
}
final CRLFLineReader reader = new CRLFLineReader(new InputStreamReader(response.getBody().stream(), "UTF-8"));
try {
for (String line; (line = reader.readLine()) != null && !disconnected; ) {
if (!connected) {
onConnected();
connected = true;
}
if (TextUtils.isEmpty(line)) continue;
FanfouStreamObject object = LoganSquare.parse(line, FanfouStreamObject.class);
if (!handleEvent(object, line)) {
onUnhandledEvent(object.getEvent(), line);
}
}
} catch (IOException e) {
onException(e);
} finally {
reader.close();
}
}
@Override
public final void error(@NonNull final MicroBlogException cause) {
onException(cause);
}
public final void disconnect() {
disconnected = true;
}
private boolean handleEvent(final FanfouStreamObject object, final String json) throws IOException {
switch (object.getEvent()) {
case "message.create": {
return onStatusCreation(object.getCreatedAt(), object.getSource(),
object.getTarget(), object.getObject(Status.class));
}
}
return false;
}
protected abstract boolean onConnected();
protected abstract boolean onDisconnect(int code, String reason);
protected abstract boolean onException(@NonNull Throwable ex);
protected abstract boolean onStatusCreation(@NonNull Date createdAt, @NonNull User source,
@Nullable User target, @NonNull Status object);
protected abstract void onUnhandledEvent(@NonNull String event, @NonNull String json)
throws IOException;
}

View File

@ -0,0 +1,62 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.fanfou.callback;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.microblog.library.twitter.model.User;
import java.io.IOException;
import java.util.Date;
/**
* Created by mariotaku on 2017/3/11.
*/
public abstract class SimpleFanfouUserStreamCallback extends FanfouUserStreamCallback {
@Override
protected boolean onConnected() {
return false;
}
@Override
protected boolean onDisconnect(final int code, final String reason) {
return false;
}
@Override
protected boolean onException(@NonNull final Throwable ex) {
return false;
}
@Override
protected boolean onStatusCreation(@NonNull final Date createdAt, @NonNull final User source,
@Nullable final User target, @NonNull final Status status) {
return false;
}
@Override
protected void onUnhandledEvent(@NonNull final String event, @NonNull final String json)
throws IOException {
}
}

View File

@ -0,0 +1,77 @@
/*
* Twidere - Twitter client for Android
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.mariotaku.microblog.library.fanfou.model;
import android.support.annotation.NonNull;
import com.bluelinelabs.logansquare.LoganSquare;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import org.mariotaku.commons.logansquare.JsonStringConverter;
import org.mariotaku.microblog.library.fanfou.model.util.StreamDateConverter;
import org.mariotaku.microblog.library.twitter.model.User;
import java.io.IOException;
import java.util.Date;
/**
* Created by mariotaku on 2017/3/11.
*/
@JsonObject
public class FanfouStreamObject {
@JsonField(name = "event")
String event;
@JsonField(name = "created_at", typeConverter = StreamDateConverter.class)
Date createdAt;
@JsonField(name = "source")
User source;
@JsonField(name = "target")
User target;
@JsonField(name = "object", typeConverter = JsonStringConverter.class)
String rawObject;
@NonNull
public String getEvent() {
if (event == null) return "";
return event;
}
public Date getCreatedAt() {
return createdAt;
}
public User getSource() {
return source;
}
public User getTarget() {
return target;
}
public <T> T getObject(Class<T> cls) throws IOException {
if (rawObject == null) return null;
return LoganSquare.parse(rawObject, cls);
}
}

View File

@ -0,0 +1,39 @@
/*
* Twidere - Twitter client for Android
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.mariotaku.microblog.library.fanfou.model.util;
import com.bluelinelabs.logansquare.typeconverters.DateTypeConverter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
/**
* Created by mariotaku on 2017/3/11.
*/
public class StreamDateConverter extends DateTypeConverter {
@Override
public DateFormat getDateFormat() {
return new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss", Locale.US);
}
}

View File

@ -19,6 +19,8 @@
package org.mariotaku.microblog.library.twitter;
import org.mariotaku.microblog.library.twitter.annotation.StreamWith;
import org.mariotaku.microblog.library.twitter.callback.UserStreamCallback;
import org.mariotaku.restfu.annotation.method.GET;
/**
@ -27,6 +29,6 @@ import org.mariotaku.restfu.annotation.method.GET;
public interface TwitterUserStream {
@GET("/user.json")
void getUserStream(String with, UserStreamCallback callback);
void getUserStream(@StreamWith String with, UserStreamCallback callback);
}

View File

@ -0,0 +1,33 @@
/*
* Twidere - Twitter client for Android
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.mariotaku.microblog.library.twitter.annotation;
import android.support.annotation.StringDef;
/**
* Created by mariotaku on 2017/3/11.
*/
@StringDef({StreamWith.USER, StreamWith.FOLLOWING})
public @interface StreamWith {
String USER = "user";
String FOLLOWING = "following";
}

View File

@ -0,0 +1,184 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.twitter.callback;
import android.support.annotation.NonNull;
import org.mariotaku.microblog.library.twitter.model.DeletionEvent;
import org.mariotaku.microblog.library.twitter.model.DirectMessage;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.microblog.library.twitter.model.TwitterStreamObject;
import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.microblog.library.twitter.model.UserList;
import org.mariotaku.microblog.library.twitter.model.Warning;
import java.io.IOException;
import java.util.Date;
/**
* Created by mariotaku on 2017/3/11.
*/
public abstract class SimpleUserStreamCallback extends UserStreamCallback {
@Override
protected boolean onConnected() {
return false;
}
@Override
protected boolean onDisconnect(final int code, final String reason) {
return false;
}
@Override
protected boolean onException(@NonNull final Throwable ex) {
return false;
}
@Override
protected boolean onStatus(@NonNull final Status status) {
return false;
}
@Override
protected boolean onDirectMessage(@NonNull final DirectMessage directMessage) {
return false;
}
@Override
protected boolean onBlock(final Date createdAt, final User source, final User blockedUser) {
return false;
}
@Override
protected boolean onDirectMessageDeleted(@NonNull final DeletionEvent event) {
return false;
}
@Override
protected boolean onStatusDeleted(@NonNull final DeletionEvent event) {
return false;
}
@Override
protected boolean onFavorite(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final Status targetObject) {
return false;
}
@Override
protected boolean onFollow(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target) {
return false;
}
@Override
protected boolean onUnfollow(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target) {
return false;
}
@Override
protected boolean onFriendList(@NonNull final String[] friendIds) {
return false;
}
@Override
protected boolean onScrubGeo(final String userId, final String upToStatusId) {
return false;
}
@Override
protected boolean onStallWarning(final Warning warn) {
return false;
}
@Override
protected boolean onTrackLimitationNotice(final int numberOfLimitedStatuses) {
return false;
}
@Override
protected boolean onUnblock(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User unblockedUser) {
return false;
}
@Override
protected boolean onUnfavorite(@NonNull final User source, @NonNull final User target, @NonNull final Status targetStatus) {
return false;
}
@Override
protected boolean onUserListCreation(@NonNull final Date createdAt, @NonNull final User source, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListDeletion(@NonNull final Date createdAt, @NonNull final User source, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListMemberAddition(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListMemberDeletion(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListSubscription(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListUnsubscription(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserListUpdate(@NonNull final Date createdAt, @NonNull final User source, @NonNull final UserList targetObject) {
return false;
}
@Override
protected boolean onUserProfileUpdate(@NonNull final Date createdAt, @NonNull final User updatedUser) {
return false;
}
@Override
protected boolean onQuotedTweet(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final Status targetObject) {
return false;
}
@Override
protected boolean onFavoritedRetweet(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final Status targetObject) {
return false;
}
@Override
protected boolean onRetweetedRetweet(@NonNull final Date createdAt, @NonNull final User source, @NonNull final User target, @NonNull final Status targetObject) {
return false;
}
@Override
protected void onUnhandledEvent(@NonNull final TwitterStreamObject obj, @NonNull final String json) throws IOException {
}
}

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.twitter;
package org.mariotaku.microblog.library.twitter.callback;
import android.support.annotation.NonNull;
import android.text.TextUtils;
@ -37,7 +37,7 @@ import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.microblog.library.twitter.model.UserList;
import org.mariotaku.microblog.library.twitter.model.UserListTargetObjectEvent;
import org.mariotaku.microblog.library.twitter.model.Warning;
import org.mariotaku.microblog.library.twitter.util.CRLFLineReader;
import org.mariotaku.microblog.library.util.CRLFLineReader;
import org.mariotaku.restfu.callback.RawCallback;
import org.mariotaku.restfu.http.HttpResponse;
@ -79,11 +79,19 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
} catch (IOException e) {
onException(e);
} finally {
Log.d("Twidere.Stream", "Cleaning up...");
reader.close();
}
}
@Override
public final void error(@NonNull final MicroBlogException cause) {
onException(cause);
}
public final void disconnect() {
disconnected = true;
}
private boolean handleEvent(final TwitterStreamObject object, final String json) throws IOException {
switch (object.determine()) {
case Type.FRIENDS: {
@ -200,139 +208,78 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
return false;
}
protected abstract boolean onConnected();
@Override
public final void error(@NonNull final MicroBlogException cause) {
onException(cause);
}
protected abstract boolean onDisconnect(int code, String reason);
public void disconnect() {
disconnected = true;
}
protected abstract boolean onException(@NonNull Throwable ex);
protected boolean onConnected() {
return false;
}
protected abstract boolean onStatus(@NonNull Status status);
protected boolean onDisconnect(int code, String reason) {
return false;
}
protected abstract boolean onDirectMessage(@NonNull DirectMessage directMessage);
protected boolean onStatus(@NonNull Status status) {
return false;
}
protected abstract boolean onBlock(Date createdAt, User source, User blockedUser);
protected boolean onDirectMessage(@NonNull DirectMessage directMessage) {
return false;
}
protected abstract boolean onDirectMessageDeleted(@NonNull DeletionEvent event);
protected boolean onBlock(final Date createdAt, User source, User blockedUser) {
return false;
}
protected abstract boolean onStatusDeleted(@NonNull DeletionEvent event);
protected boolean onDirectMessageDeleted(@NonNull DeletionEvent event) {
return false;
}
protected abstract boolean onFavorite(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject);
protected boolean onStatusDeleted(@NonNull DeletionEvent event) {
return false;
}
protected abstract boolean onFollow(@NonNull Date createdAt, @NonNull User source,
@NonNull User target);
protected boolean onException(@NonNull Throwable ex) {
return false;
}
protected abstract boolean onUnfollow(@NonNull Date createdAt, @NonNull User source,
@NonNull User target);
protected boolean onFavorite(@NonNull Date createdAt, @NonNull User source, @NonNull User target,
@NonNull Status targetObject) {
return false;
}
protected abstract boolean onFriendList(@NonNull String[] friendIds);
protected boolean onFollow(@NonNull Date createdAt, @NonNull User source, @NonNull User target) {
return false;
}
protected abstract boolean onScrubGeo(String userId, String upToStatusId);
protected boolean onUnfollow(@NonNull Date createdAt, @NonNull User source, @NonNull User target) {
return false;
}
protected abstract boolean onStallWarning(Warning warn);
protected boolean onFriendList(@NonNull String[] friendIds) {
return false;
}
protected abstract boolean onTrackLimitationNotice(int numberOfLimitedStatuses);
protected boolean onScrubGeo(String userId, String upToStatusId) {
return false;
}
protected abstract boolean onUnblock(@NonNull Date createdAt, @NonNull User source,
@NonNull User unblockedUser);
protected boolean onStallWarning(Warning warn) {
return false;
}
protected abstract boolean onUnfavorite(@NonNull User source, @NonNull User target,
@NonNull Status targetStatus);
protected boolean onTrackLimitationNotice(int numberOfLimitedStatuses) {
return false;
}
protected abstract boolean onUserListCreation(@NonNull Date createdAt, @NonNull User source,
@NonNull UserList targetObject);
protected boolean onUnblock(final Date createdAt, User source, User unblockedUser) {
return false;
}
protected abstract boolean onUserListDeletion(@NonNull Date createdAt, @NonNull User source,
@NonNull UserList targetObject);
protected boolean onUnfavorite(@NonNull User source, @NonNull User target, @NonNull Status targetStatus) {
return false;
}
protected abstract boolean onUserListMemberAddition(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject);
protected boolean onUserListCreation(@NonNull Date createdAt, @NonNull User source,
@NonNull UserList targetObject) {
return false;
}
protected abstract boolean onUserListMemberDeletion(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject);
protected boolean onUserListDeletion(@NonNull Date createdAt, @NonNull User source,
@NonNull UserList targetObject) {
return false;
}
protected abstract boolean onUserListSubscription(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject);
protected boolean onUserListMemberAddition(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject) {
return false;
}
protected abstract boolean onUserListUnsubscription(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject);
protected boolean onUserListMemberDeletion(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject) {
return false;
}
protected abstract boolean onUserListUpdate(@NonNull Date createdAt, @NonNull User source,
@NonNull UserList targetObject);
protected boolean onUserListSubscription(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject) {
return false;
}
protected abstract boolean onUserProfileUpdate(@NonNull Date createdAt,
@NonNull User updatedUser);
protected boolean onUserListUnsubscription(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull UserList targetObject) {
return false;
}
protected abstract boolean onQuotedTweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject);
protected boolean onUserListUpdate(@NonNull Date createdAt, @NonNull User source, @NonNull UserList targetObject) {
return false;
}
protected abstract boolean onFavoritedRetweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject);
protected boolean onUserProfileUpdate(@NonNull Date createdAt, @NonNull User updatedUser) {
return false;
}
protected abstract boolean onRetweetedRetweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject);
protected boolean onQuotedTweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject) {
return false;
}
protected boolean onFavoritedRetweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject) {
return false;
}
protected boolean onRetweetedRetweet(@NonNull Date createdAt, @NonNull User source,
@NonNull User target, @NonNull Status targetObject) {
return false;
}
protected void onUnhandledEvent(@NonNull final TwitterStreamObject obj, @NonNull final String json) throws IOException {
}
protected abstract void onUnhandledEvent(@NonNull TwitterStreamObject obj, @NonNull String json)
throws IOException;
}

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.microblog.library.twitter.util;
package org.mariotaku.microblog.library.util;
import java.io.BufferedReader;
import java.io.IOException;

View File

@ -43,6 +43,7 @@ import org.mariotaku.commons.logansquare.JsonStringConverter;
import org.mariotaku.twidere.annotation.AccountType;
import org.mariotaku.twidere.model.account.AccountExtras;
import org.mariotaku.twidere.model.account.cred.Credentials;
import org.mariotaku.twidere.model.util.RGBHexColorConverter;
import org.mariotaku.twidere.model.util.UserKeyConverter;
import org.mariotaku.twidere.util.model.AccountDetailsUtils;
@ -74,7 +75,7 @@ public class AccountDetails implements Parcelable, Comparable<AccountDetails> {
public ParcelableUser user;
@ColorInt
@JsonField(name = "color")
@JsonField(name = "color", typeConverter = RGBHexColorConverter.class)
public int color;
@JsonField(name = "position")

View File

@ -36,6 +36,7 @@ import org.mariotaku.commons.objectcursor.LoganSquareCursorFieldConverter;
import org.mariotaku.library.objectcursor.annotation.AfterCursorObjectCreated;
import org.mariotaku.library.objectcursor.annotation.CursorField;
import org.mariotaku.library.objectcursor.annotation.CursorObject;
import org.mariotaku.twidere.model.util.RGBHexColorConverter;
import org.mariotaku.twidere.model.util.UserKeyConverter;
import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter;
import org.mariotaku.twidere.provider.TwidereDataStore;
@ -160,15 +161,15 @@ public class ParcelableUser implements Parcelable, Comparable<ParcelableUser> {
public long media_count = -1;
@ParcelableThisPlease
@JsonField(name = "background_color")
@JsonField(name = "background_color", typeConverter = RGBHexColorConverter.class)
@CursorField(CachedUsers.BACKGROUND_COLOR)
public int background_color;
@ParcelableThisPlease
@JsonField(name = "link_color")
@JsonField(name = "link_color", typeConverter = RGBHexColorConverter.class)
@CursorField(CachedUsers.LINK_COLOR)
public int link_color;
@ParcelableThisPlease
@JsonField(name = "text_color")
@JsonField(name = "text_color", typeConverter = RGBHexColorConverter.class)
@CursorField(CachedUsers.TEXT_COLOR)
public int text_color;

View File

@ -0,0 +1,49 @@
/*
* Twidere - Twitter client for Android
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.mariotaku.twidere.model.util;
import android.graphics.Color;
import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter;
import java.util.Locale;
/**
* Created by mariotaku on 2017/3/11.
*/
public class RGBHexColorConverter extends StringBasedTypeConverter<Integer> {
@Override
public Integer getFromString(final String string) {
if (string == null) return 0;
if (string.startsWith("#")) {
return Color.parseColor(string);
}
return Integer.parseInt(string);
}
@Override
public String convertToString(final Integer object) {
if (object == null) return null;
return String.format(Locale.US, "#%06X", 0xFFFFFF & object);
}
}

View File

@ -35,8 +35,7 @@ import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.TypeRef
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider
import com.jayway.jsonpath.spi.mapper.MappingProvider
import org.apache.commons.cli.GnuParser
import org.apache.commons.cli.Options
import org.apache.commons.cli.*
import org.json.JSONArray
import org.json.JSONObject
import org.mariotaku.ktextension.HexColorFormat
@ -48,10 +47,8 @@ import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.Utils
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.PrintStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.GZIPInputStream
@ -70,191 +67,19 @@ import javax.crypto.spec.SecretKeySpec
class AccountsDumper(val context: Context) : DumperPlugin {
private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
private val salt = byteArrayOf(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
override fun getName() = "accounts"
override fun dump(dumpContext: DumperContext) {
val parser = GnuParser()
val argsAsList = dumpContext.argsAsList
when (argsAsList.firstOrNull()) {
"import" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
val options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
addRequiredOption("i", "input", true, "Accounts data file")
}
val commandLine = parser.parse(options, subCommandArgs)
try {
val password = commandLine.getOptionValue("password")
File(commandLine.getOptionValue("input")).inputStream().use { input ->
importAccounts(password, input, dumpContext.stdout)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
"export" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
val options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
addRequiredOption("o", "output", true, "Accounts data file")
}
val commandLine = parser.parse(options, subCommandArgs)
try {
val password = commandLine.getOptionValue("password")
File(commandLine.getOptionValue("output")).outputStream().use { output ->
exportAccounts(password, output)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
"list" -> {
val keys = DataStoreUtils.getAccountKeys(context)
keys.forEach {
dumpContext.stdout.println(it)
}
}
"get" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
if (subCommandArgs.isEmpty()) {
throw DumpException("Account key required")
}
val docContext = try {
accountDocContext(subCommandArgs[0])
} catch (e: Utils.NoAccountException) {
throw DumpException("Account not found")
}
if (subCommandArgs.size == 1) {
val result = docContext.read("$", Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
} else for (i in 1..subCommandArgs.lastIndex) {
val result = docContext.read(subCommandArgs[i], Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
}
}
"set" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
if (subCommandArgs.size != 3) {
throw DumpException("Usage: accounts set <account_key> <field> <value>")
}
val docContext = try {
accountDocContext(subCommandArgs[0])
} catch (e: Utils.NoAccountException) {
throw DumpException("Account not found")
}
val value = subCommandArgs[2]
val path = subCommandArgs[1]
docContext.set(path, value)
val details = docContext.read("$", Object::class.java)?.let {
LoganSquare.parse(it.toString(), AccountDetails::class.java)
} ?: return
val am = AccountManager.get(context)
details.account.updateDetails(am, details)
dumpContext.stdout.println("$path = ${docContext.read(path, Object::class.java)?.prettyPrint()}")
}
else -> {
dumpContext.stderr.println("Usage: accounts [import|export] -p <password>")
}
val subCommands = listOf(ExportCommand(context), ImportCommand(context),
ListCommand(context), GetCommand(context), SetCommand(context))
val subCommandName = argsAsList.firstOrNull()
val subCommand = subCommands.find { it.name == subCommandName } ?: run {
throw DumpException("Usage: accounts <${subCommands.joinToString("|", transform = SubCommand::name)}> <args>")
}
subCommand.execute(dumpContext, argsAsList.subArray(1..argsAsList.lastIndex))
}
private fun accountDocContext(forKey: String): DocumentContext {
val accountKey = UserKey.valueOf(forKey)
val am = AccountManager.get(context)
val details = AccountUtils.getAccountDetails(am, accountKey, true) ?: throw Utils.NoAccountException()
val configuration = Configuration.builder()
.jsonProvider(JsonOrgJsonProvider())
.mappingProvider(AsIsMappingProvider())
.build()
return JsonPath.parse(LoganSquare.serialize(details), configuration)
}
private fun Any.prettyPrint() = if (this is JSONObject) {
toString(4)
} else if (this is JSONArray) {
toString(4)
} else {
toString()
}
private fun exportAccounts(password: String, output: OutputStream) {
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secret)
val base64 = Base64OutputStream(output, Base64.NO_CLOSE)
val iv = cipher.parameters.getParameterSpec(IvParameterSpec::class.java).iv
// write IV size
base64.write(iv.size.toByteArray())
// write IV
base64.write(iv)
val gz = GZIPOutputStream(CipherOutputStream(base64, cipher))
// write accounts
val am = AccountManager.get(context)
val accounts = AccountUtils.getAllAccountDetails(am, true).toList()
LoganSquare.serialize(accounts, gz, AccountDetails::class.java)
}
private fun importAccounts(password: String, input: InputStream, output: PrintStream) {
val base64 = Base64InputStream(input, Base64.NO_CLOSE)
val ivSize = ByteArray(4).apply { base64.read(this) }.toInt()
val iv = ByteArray(ivSize).apply { base64.read(this) }
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(iv))
val gz = GZIPInputStream(CipherInputStream(base64, cipher))
val am = AccountManager.get(context)
val usedAccounts = AccountUtils.getAccounts(am)
val allDetails = LoganSquare.parseList(gz, AccountDetails::class.java)
allDetails.forEach { details ->
val account = details.account
if (account !in usedAccounts) {
am.addAccountExplicitly(account, null, null)
}
account.updateDetails(am, details)
}
output.println("Done.")
}
private fun Account.updateDetails(am: AccountManager, details: AccountDetails) {
am.setUserData(this, ACCOUNT_USER_DATA_KEY, details.key.toString())
am.setUserData(this, ACCOUNT_USER_DATA_TYPE, details.type)
am.setUserData(this, ACCOUNT_USER_DATA_CREDS_TYPE, details.credentials_type)
am.setUserData(this, ACCOUNT_USER_DATA_ACTIVATED, true.toString())
am.setUserData(this, ACCOUNT_USER_DATA_COLOR, toHexColor(details.color, format = HexColorFormat.RGB))
am.setUserData(this, ACCOUNT_USER_DATA_USER, LoganSquare.serialize(details.user))
am.setUserData(this, ACCOUNT_USER_DATA_EXTRAS, details.extras?.let { LoganSquare.serialize(it) })
am.setAuthToken(this, ACCOUNT_AUTH_TOKEN_TYPE, LoganSquare.serialize(details.credentials))
}
fun ByteArray.toInt(): Int {
val bb = ByteBuffer.wrap(this)
bb.order(ByteOrder.LITTLE_ENDIAN)
return bb.int
}
fun Int.toByteArray(): ByteArray {
val bb = ByteBuffer.allocate(Integer.SIZE / java.lang.Byte.SIZE)
bb.order(ByteOrder.LITTLE_ENDIAN)
bb.putInt(this)
return bb.array()
}
private fun generateSecret(password: String): SecretKeySpec {
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
}
internal class AsIsMappingProvider : MappingProvider {
override fun <T : Any?> map(source: Any?, type: Class<T>, configuration: Configuration): T {
@ -269,4 +94,223 @@ class AccountsDumper(val context: Context) : DumperPlugin {
}
abstract class SubCommand(val name: String) {
abstract fun execute(dumpContext: DumperContext, args: Array<String>)
}
class ExportCommand(val context: Context) : CmdLineSubCommand("export") {
override val options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
}
override val syntax: String = "[-p]"
override fun execute(dumpContext: DumperContext, commandLine: CommandLine) {
val am = AccountManager.get(context)
try {
val password = commandLine.getOptionValue("password")
am.exportAccounts(password, dumpContext.stdout)
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
}
abstract class CmdLineSubCommand(name: String) : SubCommand(name) {
protected abstract val options: Options
protected abstract val syntax: String
override final fun execute(dumpContext: DumperContext, args: Array<String>) {
val commandLine = try {
GnuParser().parse(options, args)
} catch (e: ParseException) {
val formatter = HelpFormatter()
formatter.printHelp(dumpContext.stderr, "$name $syntax", options)
return
}
execute(dumpContext, commandLine)
}
abstract fun execute(dumpContext: DumperContext, commandLine: CommandLine)
}
class ImportCommand(val context: Context) : CmdLineSubCommand("import") {
override val options: Options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
addOption("t", "test", false, "Dry-run without actual import")
}
override val syntax: String = "[-pt]"
override fun execute(dumpContext: DumperContext, commandLine: CommandLine) {
val am = AccountManager.get(context)
try {
val password = commandLine.getOptionValue("password")
val isTest = commandLine.hasOption("test")
val accounts = readAccounts(password, dumpContext.stdin)
if (isTest) {
accounts.forEach { dumpContext.stdout.println(it.key) }
} else {
am.importAccounts(accounts)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
}
class ListCommand(val context: Context) : SubCommand("list") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
val keys = DataStoreUtils.getAccountKeys(context)
keys.forEach {
dumpContext.stdout.println(it)
}
}
}
class GetCommand(val context: Context) : SubCommand("get-value") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
if (args.isEmpty()) {
throw DumpException("Usage: accounts $name <account_key> [value1] [value2] ...")
}
val am = AccountManager.get(context)
val docContext = try {
am.docContext(args[0])
} catch (e: Utils.NoAccountException) {
throw DumpException("Account not found")
}
if (args.size == 1) {
val result = docContext.read("$", Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
} else for (i in 1..args.lastIndex) {
val result = docContext.read(args[i], Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
}
}
}
class SetCommand(val context: Context) : SubCommand("set-value") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
if (args.size != 3) {
throw DumpException("Usage: accounts $name <account_key> <field> <value>")
}
val am = AccountManager.get(context)
val docContext = try {
am.docContext(args[0])
} catch (e: Utils.NoAccountException) {
throw DumpException("Account not found")
}
val value = args[2]
val path = args[1]
docContext.set(path, value)
val details = docContext.read("$", Object::class.java)?.let {
LoganSquare.parse(it.toString(), AccountDetails::class.java)
} ?: return
details.account.updateDetails(am, details)
dumpContext.stdout.println("$path = ${docContext.read(path, Object::class.java)?.prettyPrint()}")
}
}
companion object {
private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
private val salt = byteArrayOf(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
private fun AccountManager.exportAccounts(password: String, output: OutputStream) {
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secret)
val base64 = Base64OutputStream(output, Base64.NO_CLOSE)
val iv = cipher.parameters.getParameterSpec(IvParameterSpec::class.java).iv
// write IV size
base64.write(iv.size.toByteArray())
// write IV
base64.write(iv)
val gz = GZIPOutputStream(CipherOutputStream(base64, cipher))
// write accounts
val accounts = AccountUtils.getAllAccountDetails(this, true).toList()
LoganSquare.serialize(accounts, gz, AccountDetails::class.java)
}
private fun readAccounts(password: String, input: InputStream): List<AccountDetails> {
val base64 = Base64InputStream(input, Base64.NO_CLOSE)
val ivSize = ByteArray(4).apply { base64.read(this) }.toInt()
val iv = ByteArray(ivSize).apply { base64.read(this) }
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(iv))
val gz = GZIPInputStream(CipherInputStream(base64, cipher))
return LoganSquare.parseList(gz, AccountDetails::class.java)
}
private fun AccountManager.importAccounts(allDetails: List<AccountDetails>) {
val usedAccounts = AccountUtils.getAccounts(this)
allDetails.forEach { details ->
val account = details.account
if (account !in usedAccounts) {
this.addAccountExplicitly(account, null, null)
}
account.updateDetails(this, details)
}
}
fun ByteArray.toInt(): Int {
val bb = ByteBuffer.wrap(this)
bb.order(ByteOrder.LITTLE_ENDIAN)
return bb.int
}
fun Int.toByteArray(): ByteArray {
val bb = ByteBuffer.allocate(Integer.SIZE / java.lang.Byte.SIZE)
bb.order(ByteOrder.LITTLE_ENDIAN)
bb.putInt(this)
return bb.array()
}
private fun generateSecret(password: String): SecretKeySpec {
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
}
private fun Account.updateDetails(am: AccountManager, details: AccountDetails) {
am.setUserData(this, ACCOUNT_USER_DATA_KEY, details.key.toString())
am.setUserData(this, ACCOUNT_USER_DATA_TYPE, details.type)
am.setUserData(this, ACCOUNT_USER_DATA_CREDS_TYPE, details.credentials_type)
am.setUserData(this, ACCOUNT_USER_DATA_ACTIVATED, true.toString())
am.setUserData(this, ACCOUNT_USER_DATA_COLOR, toHexColor(details.color, format = HexColorFormat.RGB))
am.setUserData(this, ACCOUNT_USER_DATA_USER, LoganSquare.serialize(details.user))
am.setUserData(this, ACCOUNT_USER_DATA_EXTRAS, details.extras?.let { LoganSquare.serialize(it) })
am.setAuthToken(this, ACCOUNT_AUTH_TOKEN_TYPE, LoganSquare.serialize(details.credentials))
}
private fun AccountManager.docContext(forKey: String): DocumentContext {
val accountKey = UserKey.valueOf(forKey)
val details = AccountUtils.getAccountDetails(this, accountKey, true) ?: throw Utils.NoAccountException()
val configuration = Configuration.builder()
.jsonProvider(JsonOrgJsonProvider())
.mappingProvider(AsIsMappingProvider())
.build()
return JsonPath.parse(LoganSquare.serialize(details), configuration)
}
private fun Any.prettyPrint() = if (this is JSONObject) {
toString(4)
} else if (this is JSONArray) {
toString(4)
} else {
toString()
}
}
}

View File

@ -19,8 +19,11 @@
package org.mariotaku.twidere.util.stetho
import org.apache.commons.cli.HelpFormatter
import org.apache.commons.cli.Option
import org.apache.commons.cli.Options
import java.io.OutputStream
import java.io.PrintWriter
/**
* Created by mariotaku on 2017/3/9.
@ -31,4 +34,12 @@ internal fun Options.addRequiredOption(opt: String, longOpt: String? = null, has
val option = Option(opt, longOpt, hasArg, description)
option.isRequired = true
addOption(option)
}
internal fun HelpFormatter.printHelp(output: OutputStream, syntax: String, options: Options,
header: String? = null, footer: String? = null, autoUsage: Boolean = false) {
val writer = PrintWriter(output)
printHelp(writer, width, syntax, header, options, leftPadding, descPadding, footer,
autoUsage)
writer.flush()
}

View File

@ -21,68 +21,92 @@ package org.mariotaku.twidere.util.stetho
import android.accounts.AccountManager
import android.content.Context
import com.facebook.stetho.dumpapp.DumpException
import com.facebook.stetho.dumpapp.DumperContext
import com.facebook.stetho.dumpapp.DumperPlugin
import org.apache.commons.cli.GnuParser
import org.apache.commons.cli.HelpFormatter
import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException
import org.mariotaku.ktextension.subArray
import org.mariotaku.microblog.library.fanfou.FanfouStream
import org.mariotaku.microblog.library.twitter.TwitterUserStream
import org.mariotaku.microblog.library.twitter.annotation.StreamWith
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.microblog.library.twitter.model.DirectMessage
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.extension.model.getCredentials
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ActivityTitleSummaryMessage
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.util.UserColorNameManager
import org.mariotaku.twidere.util.dagger.DependencyHolder
import org.mariotaku.twidere.util.streaming.TimelineStreamCallback
import org.mariotaku.twidere.util.streaming.FanfouTimelineStreamCallback
import org.mariotaku.twidere.util.streaming.TwitterTimelineStreamCallback
/**
* Created by mariotaku on 2017/3/9.
*/
class UserStreamDumper(val context: Context) : DumperPlugin {
private val syntax = "$name <account_key> [-ti]"
override fun dump(dumpContext: DumperContext) {
val parser = GnuParser()
val options = Options()
options.addRequiredOption("a", "account", true, "Account key")
options.addOption("t", "timeline", false, "Include timeline")
options.addOption("i", "interactions", false, "Include interactions")
val cmdLine = try {
parser.parse(options, dumpContext.argsAsList.toTypedArray())
} catch (e: ParseException) {
throw DumpException(e.message)
val argsList = dumpContext.argsAsList
val formatter = HelpFormatter()
if (argsList.isEmpty()) {
formatter.printHelp(dumpContext.stderr, syntax, options)
return
}
val cmdLine = parser.parse(options, argsList.subArray(1..argsList.lastIndex))
val manager = DependencyHolder.get(context).userColorNameManager
val includeTimeline = cmdLine.hasOption("timeline")
val includeInteractions = cmdLine.hasOption("interactions")
val accountKey = UserKey.valueOf(cmdLine.getOptionValue("account"))
val accountKey = UserKey.valueOf(argsList[0])
val am = AccountManager.get(context)
val account = AccountUtils.findByAccountKey(am, accountKey) ?: return
val credentials = account.getCredentials(am)
val userStream = credentials.newMicroBlogInstance(context, account.type,
cls = TwitterUserStream::class.java)
val account = AccountUtils.getAccountDetails(am, accountKey, true) ?: return
when (account.type) {
AccountType.TWITTER -> {
beginTwitterStream(account, dumpContext, includeInteractions, includeTimeline, manager)
}
AccountType.FANFOU -> {
beginFanfouStream(account, dumpContext, includeInteractions, includeTimeline, manager)
}
else -> {
dumpContext.stderr.println("Unsupported account type ${account.type}")
dumpContext.stderr.flush()
}
}
}
private fun beginTwitterStream(account: AccountDetails, dumpContext: DumperContext, includeInteractions: Boolean,
includeTimeline: Boolean, manager: UserColorNameManager) {
val userStream = account.newMicroBlogInstance(context, cls = TwitterUserStream::class.java)
dumpContext.stdout.println("Beginning user stream...")
dumpContext.stdout.flush()
val callback = object : TimelineStreamCallback(accountKey.id) {
val callback = object : TwitterTimelineStreamCallback(account.key.id) {
override fun onException(ex: Throwable): Boolean {
ex.printStackTrace(dumpContext.stderr)
dumpContext.stderr.flush()
return true
}
override fun onHomeTimeline(status: Status) {
if (!includeTimeline && includeInteractions) return
override fun onHomeTimeline(status: Status): Boolean {
if (!includeTimeline && includeInteractions) return true
dumpContext.stdout.println("Home: @${status.user.screenName}: ${status.text.trim('\n')}")
dumpContext.stdout.flush()
return true
}
override fun onActivityAboutMe(activity: Activity) {
if (!includeInteractions && includeTimeline) return
val pActivity = ParcelableActivityUtils.fromActivity(activity, accountKey)
override fun onActivityAboutMe(activity: Activity): Boolean {
if (!includeInteractions && includeTimeline) return true
val pActivity = ParcelableActivityUtils.fromActivity(activity, account.key)
val message = ActivityTitleSummaryMessage.get(context, manager, pActivity, pActivity.sources, 0,
true, true)
if (message != null) {
@ -91,6 +115,7 @@ class UserStreamDumper(val context: Context) : DumperPlugin {
dumpContext.stdout.println("Activity unsupported: ${activity.action}")
}
dumpContext.stdout.flush()
return true
}
override fun onDirectMessage(directMessage: DirectMessage): Boolean {
@ -100,12 +125,58 @@ class UserStreamDumper(val context: Context) : DumperPlugin {
}
}
try {
userStream.getUserStream("user", callback)
userStream.getUserStream(StreamWith.USER, callback)
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
override fun getName() = "user_stream"
private fun beginFanfouStream(account: AccountDetails, dumpContext: DumperContext, includeInteractions: Boolean,
includeTimeline: Boolean, manager: UserColorNameManager) {
val userStream = account.newMicroBlogInstance(context, cls = FanfouStream::class.java)
dumpContext.stdout.println("Beginning user stream...")
dumpContext.stdout.flush()
val callback = object : FanfouTimelineStreamCallback(account.key.id) {
override fun onException(ex: Throwable): Boolean {
ex.printStackTrace(dumpContext.stderr)
dumpContext.stderr.flush()
return true
}
override fun onHomeTimeline(status: Status): Boolean {
if (!includeTimeline && includeInteractions) return true
dumpContext.stdout.println("Home: @${status.user.screenName}: ${status.text.trim('\n')}")
dumpContext.stdout.flush()
return true
}
override fun onActivityAboutMe(activity: Activity): Boolean {
if (!includeInteractions && includeTimeline) return true
val pActivity = ParcelableActivityUtils.fromActivity(activity, account.key)
val message = ActivityTitleSummaryMessage.get(context, manager, pActivity, pActivity.sources, 0,
true, true)
if (message != null) {
dumpContext.stdout.println("Activity: ${message.title}: ${message.summary}")
} else {
dumpContext.stdout.println("Activity unsupported: ${activity.action}")
}
dumpContext.stdout.flush()
return true
}
override fun onUnhandledEvent(event: String, json: String) {
dumpContext.stdout.println("Unhandled: $event: $json")
dumpContext.stdout.flush()
}
}
try {
userStream.getUserStream(callback)
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
override fun getName() = "userstream"
}

View File

@ -35,7 +35,6 @@ import org.mariotaku.restfu.http.mime.Body;
import org.mariotaku.restfu.oauth.OAuthEndpoint;
import org.mariotaku.restfu.oauth.OAuthToken;
import org.mariotaku.twidere.TwidereConstants;
import org.mariotaku.twidere.annotation.AccountType;
import org.mariotaku.twidere.extension.model.AccountExtensionsKt;
import org.mariotaku.twidere.extension.model.CredentialsExtensionsKt;
import org.mariotaku.twidere.model.ConsumerKeyType;
@ -46,7 +45,6 @@ import org.mariotaku.twidere.util.api.TwitterAndroidExtraHeaders;
import org.mariotaku.twidere.util.api.UserAgentExtraHeaders;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@ -100,26 +98,8 @@ public class MicroBlogAPIFactory implements TwidereConstants {
if (account == null) return null;
final Credentials credentials = AccountExtensionsKt.getCredentials(account, am);
final String accountType = AccountExtensionsKt.getAccountType(account, am);
final HashMap<String, String> extraParams = getExtraParams(accountType, true, true);
return CredentialsExtensionsKt.newMicroBlogInstance(credentials, context, accountType,
extraParams, MicroBlog.class);
}
@NonNull
public static HashMap<String, String> getExtraParams(@NonNull @AccountType String accountType,
boolean includeEntities, boolean includeRetweets) {
final HashMap<String, String> extraParams = new HashMap<>();
switch (accountType) {
case AccountType.FANFOU: {
extraParams.put("format", "html");
break;
}
case AccountType.TWITTER: {
extraParams.put("include_entities", String.valueOf(includeEntities));
break;
}
}
return extraParams;
MicroBlog.class);
}
public static boolean verifyApiFormat(@NonNull String format) {
@ -163,14 +143,24 @@ public class MicroBlogAPIFactory implements TwidereConstants {
if (startOfHost < 0) return getApiBaseUrl("https://[DOMAIN.]twitter.com/", domain);
final int endOfHost = format.indexOf('/', startOfHost);
final String host = endOfHost != -1 ? format.substring(startOfHost, endOfHost) : format.substring(startOfHost);
if (!host.equalsIgnoreCase("api.twitter.com")) return format;
final StringBuilder sb = new StringBuilder();
sb.append(format.substring(0, startOfHost));
if (domain != null) {
sb.append(domain);
sb.append(".twitter.com");
if (host.equalsIgnoreCase("api.twitter.com")) {
if (domain != null) {
sb.append(domain);
sb.append(".twitter.com");
} else {
sb.append("twitter.com");
}
} else if (host.equalsIgnoreCase("api.fanfou.com")) {
if (domain != null) {
sb.append(domain);
sb.append(".fanfou.com");
} else {
sb.append("fanfou.com");
}
} else {
sb.append("twitter.com");
return format;
}
if (endOfHost != -1) {
sb.append(format.substring(endOfHost));

View File

@ -203,8 +203,7 @@ class BrowserSignInActivity : BaseActivity() {
val endpoint = MicroBlogAPIFactory.getOAuthSignInEndpoint(apiConfig.apiUrlFormat,
apiConfig.isSameOAuthUrl)
val auth = OAuthAuthorization(apiConfig.consumerKey, apiConfig.consumerSecret)
val oauth = newMicroBlogInstance(activity, endpoint, auth, apiConfig.type, null,
TwitterOAuth::class.java)
val oauth = newMicroBlogInstance(activity, endpoint, auth, apiConfig.type, TwitterOAuth::class.java)
return SingleResponse(oauth.getRequestToken(TwidereConstants.OAUTH_CALLBACK_OOB))
} catch (e: MicroBlogException) {
return SingleResponse(exception = e)

View File

@ -10,7 +10,6 @@ import org.mariotaku.twidere.model.account.TwitterAccountExtras
import org.mariotaku.twidere.model.account.cred.Credentials
import org.mariotaku.twidere.model.account.cred.OAuthCredentials
import org.mariotaku.twidere.task.twitter.UpdateStatusTask
import org.mariotaku.twidere.util.MicroBlogAPIFactory
import org.mariotaku.twidere.util.TwitterContentUtils
fun AccountDetails.isOfficial(context: Context): Boolean {
@ -40,11 +39,9 @@ fun <T> AccountDetails.newMicroBlogInstance(
context: Context,
includeEntities: Boolean = true,
includeRetweets: Boolean = true,
extraRequestParams: Map<String, String>? = MicroBlogAPIFactory.getExtraParams(type,
includeEntities, includeRetweets),
cls: Class<T>
): T {
return credentials.newMicroBlogInstance(context, type, extraRequestParams, cls)
return credentials.newMicroBlogInstance(context, type, cls)
}
val AccountDetails.isOAuth: Boolean

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.text.TextUtils
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.fanfou.FanfouStream
import org.mariotaku.microblog.library.twitter.*
import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization
import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization
@ -90,6 +91,10 @@ fun Credentials.getEndpoint(cls: Class<*>): Endpoint {
domain = "caps"
versionSuffix = null
}
FanfouStream::class.java.isAssignableFrom(cls) -> {
domain = "stream"
versionSuffix = null
}
else -> throw TwitterConverterFactory.UnsupportedTypeException(cls)
}
val endpointUrl = MicroBlogAPIFactory.getApiUrl(apiUrlFormat, domain, versionSuffix)
@ -106,14 +111,13 @@ fun Credentials.getEndpoint(cls: Class<*>): Endpoint {
}
fun <T> Credentials.newMicroBlogInstance(context: Context, @AccountType accountType: String? = null,
extraRequestParams: Map<String, String>? = null, cls: Class<T>): T {
cls: Class<T>): T {
return newMicroBlogInstance(context, getEndpoint(cls), getAuthorization(), accountType,
extraRequestParams, cls)
cls)
}
fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authorization,
@AccountType accountType: String? = null, extraRequestParams: Map<String, String>? = null,
cls: Class<T>): T {
@AccountType accountType: String? = null, cls: Class<T>): T {
val factory = RestAPIFactory<MicroBlogException>()
val extraHeaders = if (auth is OAuthAuthorization) {
val officialKeyType = TwitterContentUtils.getOfficialKeyType(context,
@ -134,7 +138,7 @@ fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authori
holder.connectionPool, holder.cache)
factory.setHttpClient(uploadHttpClient)
}
TwitterUserStream::class.java -> {
TwitterUserStream::class.java, FanfouStream::class.java -> {
val conf = HttpClientFactory.HttpClientConfiguration(holder.preferences)
// Use longer read timeout for streaming
conf.readTimeoutSecs = 300
@ -158,7 +162,7 @@ fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authori
}
val converterFactory = TwitterConverterFactory()
factory.setRestConverterFactory(converterFactory)
factory.setRestRequestFactory(MicroBlogAPIFactory.TwidereRestRequestFactory(extraRequestParams))
factory.setRestRequestFactory(MicroBlogAPIFactory.TwidereRestRequestFactory(null))
factory.setHttpRequestFactory(MicroBlogAPIFactory.TwidereHttpRequestFactory(extraHeaders))
factory.setExceptionFactory(MicroBlogAPIFactory.TwidereExceptionFactory(converterFactory))
return factory.build<T>(cls)

View File

@ -61,6 +61,9 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
// Data fields
private val systemWindowsInsets = Rect()
private val refreshCompleteListener: RefreshCompleteListener?
get() = parentFragment as? RefreshCompleteListener
override fun canScroll(dy: Float): Boolean {
return drawerCallback.canScroll(dy)
}
@ -140,6 +143,9 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
if (!currentRefreshing) {
updateRefreshProgressOffset()
}
if (!value) {
refreshCompleteListener?.onRefreshComplete(this)
}
if (value == currentRefreshing) return
val layoutRefreshing = value && adapter.loadMoreIndicatorPosition != ILoadMoreSupportAdapter.NONE
swipeLayout.isRefreshing = layoutRefreshing
@ -264,9 +270,8 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
protected abstract fun onCreateAdapter(context: Context): A
protected open fun createItemDecoration(context: Context,
recyclerView: RecyclerView,
layoutManager: L): ItemDecoration? {
protected open fun createItemDecoration(context: Context, recyclerView: RecyclerView,
layoutManager: L): ItemDecoration? {
return null
}
@ -313,4 +318,8 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
val swipeDistance = Math.round(64 * density)
swipeLayout.setProgressViewOffset(false, swipeStart, swipeStart + swipeDistance)
}
interface RefreshCompleteListener {
fun onRefreshComplete(fragment: AbsContentRecyclerViewFragment<*, *>)
}
}

View File

@ -0,0 +1,33 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment
import org.mariotaku.twidere.R
class AccountStreamingSettingsFragment : BaseAccountPreferenceFragment() {
override val preferencesResource: Int
get() = R.xml.preferences_account_streaming
override val switchPreferenceDefault: Boolean = false
override val switchPreferenceKey: String? = "streaming"
}

View File

@ -147,7 +147,8 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
OnSizeChangedListener, OnTouchListener, DrawerCallback, SupportFragmentCallback,
SystemWindowsInsetsCallback, RefreshScrollTopInterface, OnPageChangeListener,
KeyboardShortcutCallback, UserColorChangedListener, UserNicknameChangedListener,
IToolBarSupportFragment, StatusesFragmentDelegate, UserTimelineFragmentDelegate {
IToolBarSupportFragment, StatusesFragmentDelegate, UserTimelineFragmentDelegate,
AbsContentRecyclerViewFragment.RefreshCompleteListener {
override val toolbar: Toolbar
get() = profileContentContainer.toolbar
@ -724,6 +725,11 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
profileBanner.setOnSizeChangedListener(this)
profileBannerSpace.setOnTouchListener(this)
userProfileSwipeLayout.setOnRefreshListener {
if (!triggerRefresh()) {
userProfileSwipeLayout.isRefreshing = false
}
}
profileNameBackground.setBackgroundColor(cardBackgroundColor)
profileDetailsContainer.setBackgroundColor(cardBackgroundColor)
@ -1294,6 +1300,10 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
return true
}
override fun onRefreshComplete(fragment: AbsContentRecyclerViewFragment<*, *>) {
userProfileSwipeLayout.isRefreshing = false
}
private fun getFriendship() {
val user = user ?: return
relationship = null

View File

@ -22,13 +22,13 @@ package org.mariotaku.twidere.preference
import android.content.Context
import android.util.AttributeSet
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT
import org.mariotaku.twidere.fragment.AccountNotificationSettingsFragment
import org.mariotaku.twidere.fragment.AccountStreamingSettingsFragment
import org.mariotaku.twidere.model.AccountDetails
class StreamingAccountsListPreference(context: Context, attrs: AttributeSet? = null) : AccountsListPreference(context, attrs) {
override fun setupPreference(preference: AccountsListPreference.AccountItemPreference, account: AccountDetails) {
preference.fragment = AccountNotificationSettingsFragment::class.java.name
preference.fragment = AccountStreamingSettingsFragment::class.java.name
val args = preference.extras
args.putParcelable(EXTRA_ACCOUNT, account)
}

View File

@ -14,7 +14,11 @@ import android.util.Log
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
import org.mariotaku.microblog.library.twitter.TwitterUserStream
import org.mariotaku.microblog.library.twitter.UserStreamCallback
import org.mariotaku.microblog.library.twitter.annotation.StreamWith
import org.mariotaku.microblog.library.twitter.callback.UserStreamCallback
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.microblog.library.twitter.model.DirectMessage
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
@ -28,6 +32,7 @@ import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.TwidereArrayUtils
import org.mariotaku.twidere.util.streaming.TwitterTimelineStreamCallback
class StreamingService : Service() {
@ -97,7 +102,7 @@ class StreamingService : Service() {
callbacks.put(account.key, callback)
object : Thread() {
override fun run() {
twitter.getUserStream("user", callback)
twitter.getUserStream(StreamWith.USER, callback)
Log.d(LOGTAG, String.format("Stream %s disconnected", account.key))
callbacks.remove(account.key)
updateStreamState()
@ -140,7 +145,12 @@ class StreamingService : Service() {
internal class TwidereUserStreamCallback(
private val context: Context,
private val account: AccountDetails
) : UserStreamCallback() {
) : TwitterTimelineStreamCallback(account.key.id) {
override fun onHomeTimeline(status: Status): Boolean = true
override fun onActivityAboutMe(activity: Activity): Boolean = true
override fun onDirectMessage(directMessage: DirectMessage): Boolean = true
private var statusStreamStarted: Boolean = false
private val mentionsStreamStarted: Boolean = false

View File

@ -49,7 +49,7 @@ class RetweetStatusTask(
val details = AccountUtils.getAccountDetails(AccountManager.get(context),
accountKey, true) ?: return SingleResponse.getInstance<ParcelableStatus>(MicroBlogException("No account"))
val microBlog = details.newMicroBlogInstance(
context, false, false, null, MicroBlog::class.java)
context, false, false, MicroBlog::class.java)
try {
val result = ParcelableStatusUtils.fromStatus(microBlog.retweetStatus(statusId),
accountKey, false)

View File

@ -0,0 +1,51 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.streaming
import org.mariotaku.microblog.library.fanfou.callback.SimpleFanfouUserStreamCallback
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.microblog.library.twitter.model.InternalActivityCreator
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.User
import java.util.*
/**
* Created by mariotaku on 2017/3/11.
*/
abstract class FanfouTimelineStreamCallback(
val accountId: String
) : SimpleFanfouUserStreamCallback() {
override fun onStatusCreation(createdAt: Date, source: User, target: User?, status: Status): Boolean {
var handled = false
if (target == null) {
handled = handled or onHomeTimeline(status)
}
if (target?.id == accountId) {
handled = handled or onActivityAboutMe(InternalActivityCreator.status(accountId, status))
}
return handled
}
protected abstract fun onHomeTimeline(status: Status): Boolean
protected abstract fun onActivityAboutMe(activity: Activity): Boolean
}

View File

@ -19,7 +19,7 @@
package org.mariotaku.twidere.util.streaming
import org.mariotaku.microblog.library.twitter.UserStreamCallback
import org.mariotaku.microblog.library.twitter.callback.SimpleUserStreamCallback
import org.mariotaku.microblog.library.twitter.model.*
import java.util.*
@ -27,7 +27,7 @@ import java.util.*
* Created by mariotaku on 2017/3/10.
*/
abstract class TimelineStreamCallback(val accountId: String) : UserStreamCallback() {
abstract class TwitterTimelineStreamCallback(val accountId: String) : SimpleUserStreamCallback() {
private val friends = mutableSetOf<String>()
@ -38,39 +38,41 @@ abstract class TimelineStreamCallback(val accountId: String) : UserStreamCallbac
override final fun onStatus(status: Status): Boolean {
val userId = status.user.id
var handled = false
if (accountId == userId || userId in friends) {
onHomeTimeline(status)
handled = handled or onHomeTimeline(status)
}
if (status.inReplyToUserId == accountId) {
// Reply
onActivityAboutMe(InternalActivityCreator.status(accountId, status))
handled = handled or onActivityAboutMe(InternalActivityCreator.status(accountId, status))
} else if (userId != accountId && status.retweetedStatus?.user?.id == accountId) {
// Retweet
onActivityAboutMe(InternalActivityCreator.retweet(status))
handled = handled or onActivityAboutMe(InternalActivityCreator.retweet(status))
} else if (status.userMentionEntities?.find { it.id == accountId } != null) {
// Mention
onActivityAboutMe(InternalActivityCreator.status(accountId, status))
handled = handled or onActivityAboutMe(InternalActivityCreator.status(accountId, status))
}
return true
return handled
}
override final fun onFollow(createdAt: Date, source: User, target: User): Boolean {
if (source.id == accountId) {
friends.add(target.id)
return true
} else if (target.id == accountId) {
// Dispatch follow activity
onActivityAboutMe(InternalActivityCreator.follow(createdAt, source, target))
return onActivityAboutMe(InternalActivityCreator.follow(createdAt, source, target))
}
return true
return false
}
override final fun onFavorite(createdAt: Date, source: User, target: User,
targetObject: Status): Boolean {
if (source.id == accountId) {
// Update my favorite status
// TODO Update my favorite status
} else if (target.id == accountId) {
// Dispatch favorite activity
onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.FAVORITE,
return onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.FAVORITE,
createdAt, source, targetObject))
}
return true
@ -79,15 +81,17 @@ abstract class TimelineStreamCallback(val accountId: String) : UserStreamCallbac
override final fun onUnfollow(createdAt: Date, source: User, followedUser: User): Boolean {
if (source.id == accountId) {
friends.remove(followedUser.id)
return true
}
return true
return false
}
override final fun onQuotedTweet(createdAt: Date, source: User, target: User, targetObject: Status): Boolean {
if (source.id == accountId) {
return false
} else if (target.id == accountId) {
// Dispatch activity
onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.QUOTE,
return onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.QUOTE,
createdAt, source, targetObject))
}
return true
@ -95,9 +99,10 @@ abstract class TimelineStreamCallback(val accountId: String) : UserStreamCallbac
override final fun onFavoritedRetweet(createdAt: Date, source: User, target: User, targetObject: Status): Boolean {
if (source.id == accountId) {
return false
} else if (target.id == accountId) {
// Dispatch activity
onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.FAVORITED_RETWEET,
return onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.FAVORITED_RETWEET,
createdAt, source, targetObject))
}
return true
@ -105,25 +110,29 @@ abstract class TimelineStreamCallback(val accountId: String) : UserStreamCallbac
override final fun onRetweetedRetweet(createdAt: Date, source: User, target: User, targetObject: Status): Boolean {
if (source.id == accountId) {
return false
} else if (target.id == accountId) {
// Dispatch activity
onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.RETWEETED_RETWEET,
return onActivityAboutMe(InternalActivityCreator.targetStatus(Activity.Action.RETWEETED_RETWEET,
createdAt, source, targetObject))
}
return true
return false
}
override final fun onUserListMemberAddition(createdAt: Date, source: User, target: User, targetObject: UserList): Boolean {
if (source.id == accountId) {
return false
} else if (target.id == accountId) {
// Dispatch activity
onActivityAboutMe(InternalActivityCreator.targetObject(Activity.Action.LIST_MEMBER_ADDED,
return onActivityAboutMe(InternalActivityCreator.targetObject(Activity.Action.LIST_MEMBER_ADDED,
createdAt, source, target, targetObject))
}
return true
return false
}
protected abstract fun onHomeTimeline(status: Status)
override abstract fun onDirectMessage(directMessage: DirectMessage): Boolean
protected abstract fun onActivityAboutMe(activity: Activity)
protected abstract fun onHomeTimeline(status: Status): Boolean
protected abstract fun onActivityAboutMe(activity: Activity): Boolean
}

View File

@ -56,7 +56,7 @@
android:layout_height="match_parent">
<org.mariotaku.twidere.view.ExtendedSwipeRefreshLayout
android:id="@+id/detailsContainer"
android:id="@+id/userProfileSwipeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -976,8 +976,8 @@
<string name="settings">Settings</string>
<string name="settings_interface">Interface</string>
<string name="settings_notifications">Notifications</string>
<string name="settings_streaming">Streaming</string>
<string name="settings_refresh">Refresh</string>
<string name="settings_streaming">Streaming</string>
<string name="share_format">Share format</string>
<string name="share_format_summary">\"[TITLE]\" = Content title\n\"[TEXT]\" = Content text</string>
@ -1122,6 +1122,7 @@
<string name="title_set_nickname">Set nickname</string>
<string name="title_status">Tweet</string>
<string name="title_statuses">Tweets</string>
<string name="title_streaming">Streaming</string>
<string name="title_subscription_name">Name</string>
<string name="title_subscription_url">URL</string>
<string name="title_summary_line_format"><xliff:g id="title">%1$s</xliff:g>: <xliff:g id="summary">%2$s</xliff:g></string>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ 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.
~
~ This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/title_streaming">
</PreferenceScreen>

View File

@ -11,4 +11,22 @@
app:switchDefault="false"
app:switchKey="streaming"/>
<org.mariotaku.twidere.preference.TintedPreferenceCategory
android:key="cat_general"
android:title="@string/general">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="combined_notifications"
android:summaryOff="@string/combined_notifications_summary_off"
android:summaryOn="@string/combined_notifications_summary_on"
android:title="@string/combined_notifications"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pebble_notifications"
android:summary="@string/pebble_notifications_summary"
android:title="@string/pebble_notifications"/>
</org.mariotaku.twidere.preference.TintedPreferenceCategory>
</PreferenceScreen>