Twidere-App-Android-Twitter.../twidere/src/main/java/org/mariotaku/twidere/service/StreamingService.java

423 lines
17 KiB
Java

package org.mariotaku.twidere.service;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
import android.util.Log;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.TwitterUserStream;
import org.mariotaku.microblog.library.twitter.UserStreamCallback;
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.User;
import org.mariotaku.microblog.library.twitter.model.UserList;
import org.mariotaku.microblog.library.twitter.model.Warning;
import org.mariotaku.restfu.http.Authorization;
import org.mariotaku.restfu.http.ContentType;
import org.mariotaku.restfu.http.Endpoint;
import org.mariotaku.restfu.http.HttpResponse;
import org.mariotaku.restfu.http.mime.Body;
import org.mariotaku.sqliteqb.library.Expression;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.activity.SettingsActivity;
import org.mariotaku.twidere.model.AccountPreferences;
import org.mariotaku.twidere.model.ParcelableAccount;
import org.mariotaku.twidere.model.ParcelableCredentials;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns;
import org.mariotaku.twidere.provider.TwidereDataStore.Accounts;
import org.mariotaku.twidere.provider.TwidereDataStore.Activities;
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages;
import org.mariotaku.twidere.provider.TwidereDataStore.Mentions;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
import org.mariotaku.twidere.util.ContentValuesCreator;
import org.mariotaku.twidere.util.DataStoreUtils;
import org.mariotaku.twidere.util.MicroBlogAPIFactory;
import org.mariotaku.twidere.util.TwidereArrayUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
public class StreamingService extends Service implements Constants {
private static final int NOTIFICATION_SERVICE_STARTED = 1;
private final SimpleArrayMap<UserKey, UserStreamCallback> mCallbacks = new SimpleArrayMap<>();
private ContentResolver mResolver;
private NotificationManager mNotificationManager;
private UserKey[] mAccountKeys;
private static final Uri[] MESSAGES_URIS = new Uri[]{DirectMessages.Inbox.CONTENT_URI,
DirectMessages.Outbox.CONTENT_URI};
private final ContentObserver mAccountChangeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(final boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(final boolean selfChange, final Uri uri) {
if (!TwidereArrayUtils.contentMatch(mAccountKeys, DataStoreUtils.getActivatedAccountKeys(StreamingService.this))) {
initStreaming();
}
}
};
@Override
public IBinder onBind(final Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mResolver = getContentResolver();
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (BuildConfig.DEBUG) {
Log.d(Constants.LOGTAG, "Stream service started.");
}
initStreaming();
mResolver.registerContentObserver(Accounts.CONTENT_URI, true, mAccountChangeObserver);
}
@Override
public void onDestroy() {
clearTwitterInstances();
mResolver.unregisterContentObserver(mAccountChangeObserver);
if (BuildConfig.DEBUG) {
Log.d(Constants.LOGTAG, "Stream service stopped.");
}
super.onDestroy();
}
private void clearTwitterInstances() {
for (int i = 0, j = mCallbacks.size(); i < j; i++) {
new Thread(new ShutdownStreamTwitterRunnable(mCallbacks.valueAt(i))).start();
}
mCallbacks.clear();
mNotificationManager.cancel(NOTIFICATION_SERVICE_STARTED);
}
@SuppressWarnings("deprecation")
private void initStreaming() {
if (!BuildConfig.DEBUG) return;
setTwitterInstances();
updateStreamState();
}
private boolean setTwitterInstances() {
final List<ParcelableCredentials> accountsList = DataStoreUtils.getCredentialsList(this, true);
final UserKey[] accountKeys = new UserKey[accountsList.size()];
for (int i = 0, j = accountKeys.length; i < j; i++) {
final ParcelableCredentials credentials = accountsList.get(i);
accountKeys[i] = credentials.account_key;
}
final AccountPreferences[] activatedPreferences = AccountPreferences.getAccountPreferences(this, accountKeys);
if (BuildConfig.DEBUG) {
Log.d(Constants.LOGTAG, "Setting up twitter stream instances");
}
mAccountKeys = accountKeys;
clearTwitterInstances();
boolean result = false;
for (int i = 0, j = accountsList.size(); i < j; i++) {
final AccountPreferences preferences = activatedPreferences[i];
if (!preferences.isStreamingEnabled()) continue;
final ParcelableCredentials account = accountsList.get(i);
final Endpoint endpoint = MicroBlogAPIFactory.getEndpoint(account, TwitterUserStream.class);
final Authorization authorization = MicroBlogAPIFactory.getAuthorization(account);
final TwitterUserStream twitter = MicroBlogAPIFactory.getInstance(this, endpoint, authorization, TwitterUserStream.class);
final TwidereUserStreamCallback callback = new TwidereUserStreamCallback(this, account);
mCallbacks.put(account.account_key, callback);
new Thread() {
@Override
public void run() {
twitter.getUserStream(callback);
Log.d(Constants.LOGTAG, String.format("Stream %s disconnected", account.account_key));
mCallbacks.remove(account.account_key);
updateStreamState();
}
}.start();
result |= true;
}
return result;
}
private void updateStreamState() {
if (mCallbacks.size() > 0) {
final Intent intent = new Intent(this, SettingsActivity.class);
final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
final CharSequence contentTitle = getString(R.string.app_name);
final CharSequence contentText = getString(R.string.timeline_streaming_running);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setOngoing(true);
builder.setSmallIcon(R.drawable.ic_stat_refresh);
builder.setContentTitle(contentTitle);
builder.setContentText(contentText);
builder.setContentIntent(contentIntent);
mNotificationManager.notify(NOTIFICATION_SERVICE_STARTED, builder.build());
} else {
mNotificationManager.cancel(NOTIFICATION_SERVICE_STARTED);
}
}
static class ShutdownStreamTwitterRunnable implements Runnable {
private final UserStreamCallback callback;
ShutdownStreamTwitterRunnable(final UserStreamCallback callback) {
this.callback = callback;
}
@Override
public void run() {
if (callback == null) return;
Log.d(Constants.LOGTAG, "Disconnecting stream");
callback.disconnect();
}
}
static class TwidereUserStreamCallback extends UserStreamCallback {
private final Context context;
private final ParcelableAccount account;
private final ContentResolver resolver;
private boolean statusStreamStarted, mentionsStreamStarted;
public TwidereUserStreamCallback(final Context context, final ParcelableAccount account) {
this.context = context;
this.account = account;
resolver = context.getContentResolver();
}
@Override
public void onConnected() {
}
@Override
public void onBlock(final User source, final User blockedUser) {
final String message = String.format("%s blocked %s", source.getScreenName(), blockedUser.getScreenName());
Log.d(LOGTAG, message);
}
@Override
public void onDirectMessageDeleted(final DeletionEvent event) {
final String where = Expression.equalsArgs(DirectMessages.MESSAGE_ID).getSQL();
final String[] whereArgs = {event.getId()};
for (final Uri uri : MESSAGES_URIS) {
resolver.delete(uri, where, whereArgs);
}
}
@Override
public void onStatusDeleted(final DeletionEvent event) {
final String statusId = event.getId();
resolver.delete(Statuses.CONTENT_URI, Expression.equalsArgs(Statuses.STATUS_ID).getSQL(),
new String[]{statusId});
resolver.delete(Activities.AboutMe.CONTENT_URI, Expression.equalsArgs(Activities.STATUS_ID).getSQL(),
new String[]{statusId});
}
@Override
public void onDirectMessage(final DirectMessage directMessage) throws IOException {
if (directMessage == null || directMessage.getId() == null) return;
final String where = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY),
Expression.equalsArgs(DirectMessages.MESSAGE_ID)).getSQL();
final String[] whereArgs = {account.account_key.toString(), directMessage.getId()};
for (final Uri uri : MESSAGES_URIS) {
resolver.delete(uri, where, whereArgs);
}
final User sender = directMessage.getSender(), recipient = directMessage.getRecipient();
if (TextUtils.equals(sender.getId(), account.account_key.getId())) {
final ContentValues values = ContentValuesCreator.createDirectMessage(directMessage,
account.account_key, true);
if (values != null) {
resolver.insert(DirectMessages.Outbox.CONTENT_URI, values);
}
}
if (TextUtils.equals(recipient.getId(), account.account_key.getId())) {
final ContentValues values = ContentValuesCreator.createDirectMessage(directMessage,
account.account_key, false);
final Uri.Builder builder = DirectMessages.Inbox.CONTENT_URI.buildUpon();
builder.appendQueryParameter(QUERY_PARAM_NOTIFY, "true");
if (values != null) {
resolver.insert(builder.build(), values);
}
}
}
@Override
public void onException(final Throwable ex) {
if (ex instanceof MicroBlogException) {
Log.w(LOGTAG, String.format("Error %d", ((MicroBlogException) ex).getStatusCode()), ex);
final HttpResponse response = ((MicroBlogException) ex).getHttpResponse();
if (response != null) {
try {
final Body body = response.getBody();
if (body != null) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
body.writeTo(os);
final String charsetName;
final ContentType contentType = body.contentType();
if (contentType != null) {
final Charset charset = contentType.getCharset();
if (charset != null) {
charsetName = charset.name();
} else {
charsetName = Charset.defaultCharset().name();
}
} else {
charsetName = Charset.defaultCharset().name();
}
Log.w(LOGTAG, os.toString(charsetName));
}
} catch (IOException e) {
Log.w(LOGTAG, e);
}
}
} else {
Log.w(Constants.LOGTAG, ex);
}
}
@Override
public void onFavorite(final User source, final User target, final Status targetStatus) {
final String message = String.format("%s favorited %s's tweet: %s", source.getScreenName(),
target.getScreenName(), targetStatus.getExtendedText());
Log.d(LOGTAG, message);
}
@Override
public void onFollow(final User source, final User followedUser) {
final String message = String
.format("%s followed %s", source.getScreenName(), followedUser.getScreenName());
Log.d(LOGTAG, message);
}
@Override
public void onFriendList(final long[] friendIds) {
}
@Override
public void onScrubGeo(final long userId, final long upToStatusId) {
final String where = Expression.and(Expression.equalsArgs(Statuses.USER_KEY),
Expression.greaterEqualsArgs(Statuses.SORT_ID)).getSQL();
final String[] whereArgs = {String.valueOf(userId), String.valueOf(upToStatusId)};
final ContentValues values = new ContentValues();
values.putNull(Statuses.LOCATION);
resolver.update(Statuses.CONTENT_URI, values, where, whereArgs);
}
@Override
public void onStallWarning(final Warning warn) {
}
@Override
public void onStatus(final Status status) throws IOException {
final ContentValues values = ContentValuesCreator.createStatus(status, account.account_key);
if (!statusStreamStarted) {
statusStreamStarted = true;
values.put(Statuses.IS_GAP, true);
}
final String where = Expression.and(Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).getSQL();
final String[] whereArgs = {account.account_key.toString(), String.valueOf(status.getId())};
resolver.delete(Statuses.CONTENT_URI, where, whereArgs);
resolver.delete(Mentions.CONTENT_URI, where, whereArgs);
resolver.insert(Statuses.CONTENT_URI, values);
final Status rt = status.getRetweetedStatus();
if (rt != null && rt.getExtendedText().contains("@" + account.screen_name) || rt == null
&& status.getExtendedText().contains("@" + account.screen_name)) {
resolver.insert(Mentions.CONTENT_URI, values);
}
}
@Override
public void onTrackLimitationNotice(final int numberOfLimitedStatuses) {
}
@Override
public void onUnblock(final User source, final User unblockedUser) {
final String message = String.format("%s unblocked %s", source.getScreenName(),
unblockedUser.getScreenName());
Log.d(LOGTAG, message);
}
@Override
public void onUnfavorite(final User source, final User target, final Status targetStatus) {
final String message = String.format("%s unfavorited %s's tweet: %s", source.getScreenName(),
target.getScreenName(), targetStatus.getExtendedText());
Log.d(LOGTAG, message);
}
@Override
public void onUserListCreation(final User listOwner, final UserList list) {
}
@Override
public void onUserListDeletion(final User listOwner, final UserList list) {
}
@Override
public void onUserListMemberAddition(final User addedMember, final User listOwner, final UserList list) {
}
@Override
public void onUserListMemberDeletion(final User deletedMember, final User listOwner, final UserList list) {
}
@Override
public void onUserListSubscription(final User subscriber, final User listOwner, final UserList list) {
}
@Override
public void onUserListUnsubscription(final User subscriber, final User listOwner, final UserList list) {
}
@Override
public void onUserListUpdate(final User listOwner, final UserList list) {
}
@Override
public void onUserProfileUpdate(final User updatedUser) {
}
}
}