parent
4367242282
commit
1e3a37f0a6
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<*, *>)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue