1
0
mirror of https://github.com/TwidereProject/Twidere-Android synced 2025-02-01 09:16:47 +01:00

implementing user stream

This commit is contained in:
Mariotaku Lee 2017-03-12 21:27:55 +08:00
parent 0ad6692efa
commit 112e8fdb7d
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
25 changed files with 436 additions and 357 deletions

View File

@ -58,6 +58,9 @@ public abstract class FanfouUserStreamCallback implements RawCallback<MicroBlogE
final CRLFLineReader reader = new CRLFLineReader(new InputStreamReader(response.getBody().stream(), "UTF-8"));
try {
for (String line; (line = reader.readLine()) != null && !disconnected; ) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (!connected) {
onConnected();
connected = true;

View File

@ -21,7 +21,6 @@ package org.mariotaku.microblog.library.twitter.callback;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.bluelinelabs.logansquare.LoganSquare;
@ -66,6 +65,9 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
final CRLFLineReader reader = new CRLFLineReader(new InputStreamReader(response.getBody().stream(), "UTF-8"));
try {
for (String line; (line = reader.readLine()) != null && !disconnected; ) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (!connected) {
onConnected();
connected = true;

View File

@ -13,6 +13,5 @@ class UtilsTest {
@Test
fun testHasAutoRefreshAccounts() {
val context = InstrumentationRegistry.getTargetContext()
Utils.hasAutoRefreshAccounts(context)
}
}

View File

@ -30,8 +30,8 @@ import com.squareup.leakcanary.RefWatcher;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.util.net.NoIntercept;
import org.mariotaku.twidere.util.stetho.AccountsDumper;
import org.mariotaku.twidere.util.stetho.UserStreamDumper;
import org.mariotaku.twidere.util.stetho.AccountsDumperPlugin;
import org.mariotaku.twidere.util.stetho.UserStreamDumperPlugin;
import java.io.IOException;
@ -69,8 +69,8 @@ public class DebugModeUtils {
@Override
public Iterable<DumperPlugin> get() {
return new Stetho.DefaultDumperPluginsBuilder(application)
.provide(new AccountsDumper(application))
.provide(new UserStreamDumper(application))
.provide(new AccountsDumperPlugin(application))
.provide(new UserStreamDumperPlugin(application))
.finish();
}
})

View File

@ -65,7 +65,7 @@ import javax.crypto.spec.SecretKeySpec
* Created by mariotaku on 2017/3/6.
*/
class AccountsDumper(val context: Context) : DumperPlugin {
class AccountsDumperPlugin(val context: Context) : DumperPlugin {
override fun getName() = "accounts"

View File

@ -48,7 +48,7 @@ import org.mariotaku.twidere.util.streaming.TwitterTimelineStreamCallback
/**
* Created by mariotaku on 2017/3/9.
*/
class UserStreamDumper(val context: Context) : DumperPlugin {
class UserStreamDumperPlugin(val context: Context) : DumperPlugin {
private val syntax = "$name <account_key> [-ti]"

View File

@ -98,7 +98,7 @@ public class SessionEvent extends BaseEvent implements Parcelable {
public void dumpPreferences(Context context) {
final HashMap<String, String> preferences = new HashMap<>();
for (AccountPreferences pref : AccountPreferences.getAccountPreferences(context, DataStoreUtils.INSTANCE.getAccountKeys(context))) {
for (AccountPreferences pref : AccountPreferences.Companion.getAccountPreferences(context, DataStoreUtils.INSTANCE.getAccountKeys(context))) {
final UserKey accountKey = pref.getAccountKey();
preferences.put("notification_" + accountKey + "_home", String.valueOf(pref.isHomeTimelineNotificationEnabled()));
preferences.put("notification_" + accountKey + "_interactions", String.valueOf(pref.isInteractionsNotificationEnabled()));

View File

@ -1,192 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 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.model;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.model.util.AccountUtils;
public class AccountPreferences implements Constants {
private final Context mContext;
private final UserKey mAccountKey;
private final SharedPreferences mPreferences;
public AccountPreferences(final Context context, final UserKey accountKey) {
mContext = context;
mAccountKey = accountKey;
final String name = ACCOUNT_PREFERENCES_NAME_PREFIX + accountKey;
mPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE);
}
public UserKey getAccountKey() {
return mAccountKey;
}
public int getDefaultNotificationLightColor() {
final AccountDetails a = AccountUtils.getAccountDetails(AccountManager.get(mContext), mAccountKey, true);
if (a != null) {
return a.color;
} else {
return ContextCompat.getColor(mContext, R.color.branding_color);
}
}
public int getDirectMessagesNotificationType() {
return mPreferences.getInt(KEY_NOTIFICATION_TYPE_DIRECT_MESSAGES, DEFAULT_NOTIFICATION_TYPE_DIRECT_MESSAGES);
}
public int getHomeTimelineNotificationType() {
return mPreferences.getInt(KEY_NOTIFICATION_TYPE_HOME, DEFAULT_NOTIFICATION_TYPE_HOME);
}
public int getMentionsNotificationType() {
return mPreferences.getInt(KEY_NOTIFICATION_TYPE_MENTIONS, DEFAULT_NOTIFICATION_TYPE_MENTIONS);
}
public int getNotificationLightColor() {
return mPreferences.getInt(KEY_NOTIFICATION_LIGHT_COLOR, getDefaultNotificationLightColor());
}
public Uri getNotificationRingtone() {
final String ringtone = mPreferences.getString(KEY_NOTIFICATION_RINGTONE, null);
if (TextUtils.isEmpty(ringtone)) {
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
} else {
return Uri.parse(ringtone);
}
}
public boolean isAutoRefreshDirectMessagesEnabled() {
return mPreferences.getBoolean(KEY_AUTO_REFRESH_DIRECT_MESSAGES, DEFAULT_AUTO_REFRESH_DIRECT_MESSAGES);
}
public boolean isAutoRefreshEnabled() {
return mPreferences.getBoolean(KEY_AUTO_REFRESH, mPreferences.getBoolean(KEY_DEFAULT_AUTO_REFRESH, false));
}
public boolean isAutoRefreshHomeTimelineEnabled() {
return mPreferences.getBoolean(KEY_AUTO_REFRESH_HOME_TIMELINE, DEFAULT_AUTO_REFRESH_HOME_TIMELINE);
}
public boolean isAutoRefreshMentionsEnabled() {
return mPreferences.getBoolean(KEY_AUTO_REFRESH_MENTIONS, DEFAULT_AUTO_REFRESH_MENTIONS);
}
public boolean isAutoRefreshTrendsEnabled() {
return mPreferences.getBoolean(KEY_AUTO_REFRESH_TRENDS, DEFAULT_AUTO_REFRESH_TRENDS);
}
public boolean isStreamingEnabled() {
return mPreferences.getBoolean(KEY_ENABLE_STREAMING, false);
}
public boolean isDirectMessagesNotificationEnabled() {
return mPreferences.getBoolean(KEY_DIRECT_MESSAGES_NOTIFICATION, DEFAULT_DIRECT_MESSAGES_NOTIFICATION);
}
public boolean isHomeTimelineNotificationEnabled() {
return mPreferences.getBoolean(KEY_HOME_TIMELINE_NOTIFICATION, DEFAULT_HOME_TIMELINE_NOTIFICATION);
}
public boolean isInteractionsNotificationEnabled() {
return mPreferences.getBoolean(KEY_MENTIONS_NOTIFICATION, DEFAULT_MENTIONS_NOTIFICATION);
}
public boolean isNotificationFollowingOnly() {
return mPreferences.getBoolean(KEY_NOTIFICATION_FOLLOWING_ONLY, false);
}
public boolean isNotificationMentionsOnly() {
return mPreferences.getBoolean(KEY_NOTIFICATION_MENTIONS_ONLY, false);
}
public boolean isNotificationEnabled() {
return mPreferences.getBoolean(KEY_NOTIFICATION, DEFAULT_NOTIFICATION);
}
public static AccountPreferences getAccountPreferences(final AccountPreferences[] prefs, final UserKey accountKey) {
for (final AccountPreferences pref : prefs) {
if (pref.getAccountKey() == accountKey) return pref;
}
return null;
}
public static AccountPreferences[] getAccountPreferences(final Context context, final UserKey[] accountKeys) {
if (context == null || accountKeys == null) return null;
final AccountPreferences[] preferences = new AccountPreferences[accountKeys.length];
for (int i = 0, j = preferences.length; i < j; i++) {
preferences[i] = new AccountPreferences(context, accountKeys[i]);
}
return preferences;
}
@NonNull
public static UserKey[] getAutoRefreshEnabledAccountIds(final Context context, final UserKey[] accountKeys) {
if (context == null || accountKeys == null) return new UserKey[0];
final UserKey[] temp = new UserKey[accountKeys.length];
int i = 0;
for (final UserKey accountKey : accountKeys) {
if (new AccountPreferences(context, accountKey).isAutoRefreshEnabled()) {
temp[i++] = accountKey;
}
}
final UserKey[] enabledIds = new UserKey[i];
System.arraycopy(temp, 0, enabledIds, 0, i);
return enabledIds;
}
@NonNull
public static AccountPreferences[] getNotificationEnabledPreferences(final Context context, final UserKey[] accountKeys) {
if (context == null || accountKeys == null) return new AccountPreferences[0];
final AccountPreferences[] temp = new AccountPreferences[accountKeys.length];
int i = 0;
for (final UserKey accountKey : accountKeys) {
final AccountPreferences preference = new AccountPreferences(context, accountKey);
if (preference.isNotificationEnabled()) {
temp[i++] = preference;
}
}
final AccountPreferences[] enabledIds = new AccountPreferences[i];
System.arraycopy(temp, 0, enabledIds, 0, i);
return enabledIds;
}
public static boolean isNotificationHasLight(final int flags) {
return (flags & VALUE_NOTIFICATION_FLAG_LIGHT) != 0;
}
public static boolean isNotificationHasRingtone(final int flags) {
return (flags & VALUE_NOTIFICATION_FLAG_RINGTONE) != 0;
}
public static boolean isNotificationHasVibration(final int flags) {
return (flags & VALUE_NOTIFICATION_FLAG_VIBRATION) != 0;
}
}

View File

@ -634,11 +634,6 @@ public final class Utils implements Constants {
return 0;
}
public static boolean hasAutoRefreshAccounts(final Context context) {
final UserKey[] accountKeys = DataStoreUtils.INSTANCE.getAccountKeys(context);
return !ArrayUtils.isEmpty(AccountPreferences.getAutoRefreshEnabledAccountIds(context, accountKeys));
}
public static boolean isBatteryOkay(final Context context) {
if (context == null) return false;
final Context app = context.getApplicationContext();

View File

@ -271,9 +271,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
val initialTabPosition = handleIntent(intent, savedInstanceState == null)
setTabPosition(initialTabPosition)
if (Utils.isStreamingEnabled()) {
startService(Intent(this, StreamingService::class.java))
}
startService(Intent(this, StreamingService::class.java))
if (!showDrawerTutorial() && !kPreferences[defaultAutoRefreshAskedKey]) {
showAutoRefreshConfirm()
@ -313,10 +311,12 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
}
override fun onDestroy() {
stopService(Intent(this, StreamingService::class.java))
if (isFinishing) {
// Stop only when exiting explicitly
stopService(Intent(this, StreamingService::class.java))
}
// Delete unused items in databases.
val context = applicationContext
TaskStarter.execute(object : AbstractTask<Any?, Any?, Any?>() {
override fun doLongOperation(params: Any?): Any? {

View File

@ -32,7 +32,6 @@ import org.mariotaku.twidere.extension.model.user
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.glide.RoundedRectTransformation
import org.mariotaku.twidere.view.ShapedImageView
fun RequestManager.loadProfileImage(context: Context, url: String?, @ImageShapeStyle style: Int,
cornerRadius: Float = 0f, cornerRadiusRatio: Float = 0f, size: String? = null): DrawableRequestBuilder<String?> {
@ -142,15 +141,13 @@ internal inline fun <T> configureLoadProfileImage(context: Context, @ImageShapeS
val builder = create()
builder.diskCacheStrategy(DiskCacheStrategy.RESULT)
builder.dontAnimate()
if (!ShapedImageView.OUTLINE_DRAW) {
when (shapeStyle) {
ImageShapeStyle.SHAPE_CIRCLE -> {
builder.bitmapTransform(CropCircleTransformation(context))
}
ImageShapeStyle.SHAPE_RECTANGLE -> {
builder.bitmapTransform(RoundedRectTransformation(context, cornerRadius,
cornerRadiusRatio))
}
when (shapeStyle) {
ImageShapeStyle.SHAPE_CIRCLE -> {
builder.bitmapTransform(CropCircleTransformation(context))
}
ImageShapeStyle.SHAPE_RECTANGLE -> {
builder.bitmapTransform(RoundedRectTransformation(context, cornerRadius,
cornerRadiusRatio))
}
}
return builder

View File

@ -88,4 +88,8 @@ val Array<AccountDetails>.textLimit: Int
}
}
return limit
}
}
val AccountDetails.isStreamingSupported: Boolean
get() = type == AccountType.TWITTER

View File

@ -20,6 +20,7 @@
package org.mariotaku.twidere.fragment
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_ENABLE_STREAMING
class AccountStreamingSettingsFragment : BaseAccountPreferenceFragment() {
@ -28,6 +29,6 @@ class AccountStreamingSettingsFragment : BaseAccountPreferenceFragment() {
override val switchPreferenceDefault: Boolean = false
override val switchPreferenceKey: String? = "streaming"
override val switchPreferenceKey: String? = KEY_ENABLE_STREAMING
}

View File

@ -0,0 +1,123 @@
/*
* 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.model
import android.accounts.AccountManager
import android.content.Context
import android.media.RingtoneManager
import android.net.Uri
import android.support.v4.content.ContextCompat
import android.text.TextUtils
import org.mariotaku.ktextension.contains
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.ACCOUNT_PREFERENCES_NAME_PREFIX
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
import org.mariotaku.twidere.model.util.AccountUtils
class AccountPreferences(private val context: Context, val accountKey: UserKey) {
private val preferences = context.getSharedPreferences("$ACCOUNT_PREFERENCES_NAME_PREFIX$accountKey", Context.MODE_PRIVATE)
val defaultNotificationLightColor: Int
get() {
val a = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true)
if (a != null) {
return a.color
} else {
return ContextCompat.getColor(context, R.color.branding_color)
}
}
val directMessagesNotificationType: Int
get() = preferences.getInt(KEY_NOTIFICATION_TYPE_DIRECT_MESSAGES, DEFAULT_NOTIFICATION_TYPE_DIRECT_MESSAGES)
val homeTimelineNotificationType: Int
get() = preferences.getInt(KEY_NOTIFICATION_TYPE_HOME, DEFAULT_NOTIFICATION_TYPE_HOME)
val mentionsNotificationType: Int
get() = preferences.getInt(KEY_NOTIFICATION_TYPE_MENTIONS, DEFAULT_NOTIFICATION_TYPE_MENTIONS)
val notificationLightColor: Int
get() = preferences.getInt(KEY_NOTIFICATION_LIGHT_COLOR, defaultNotificationLightColor)
val notificationRingtone: Uri
get() {
val ringtone = preferences.getString(KEY_NOTIFICATION_RINGTONE, null)
if (TextUtils.isEmpty(ringtone)) {
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
} else {
return Uri.parse(ringtone)
}
}
val isAutoRefreshDirectMessagesEnabled: Boolean
get() = preferences.getBoolean(KEY_AUTO_REFRESH_DIRECT_MESSAGES, DEFAULT_AUTO_REFRESH_DIRECT_MESSAGES)
val isAutoRefreshEnabled: Boolean
get() = preferences.getBoolean(KEY_AUTO_REFRESH, preferences.getBoolean(KEY_DEFAULT_AUTO_REFRESH, false))
val isAutoRefreshHomeTimelineEnabled: Boolean
get() = preferences.getBoolean(KEY_AUTO_REFRESH_HOME_TIMELINE, DEFAULT_AUTO_REFRESH_HOME_TIMELINE)
val isAutoRefreshMentionsEnabled: Boolean
get() = preferences.getBoolean(KEY_AUTO_REFRESH_MENTIONS, DEFAULT_AUTO_REFRESH_MENTIONS)
val isAutoRefreshTrendsEnabled: Boolean
get() = preferences.getBoolean(KEY_AUTO_REFRESH_TRENDS, DEFAULT_AUTO_REFRESH_TRENDS)
val isStreamingEnabled: Boolean
get() = preferences.getBoolean(KEY_ENABLE_STREAMING, false)
val isDirectMessagesNotificationEnabled: Boolean
get() = preferences.getBoolean(KEY_DIRECT_MESSAGES_NOTIFICATION, DEFAULT_DIRECT_MESSAGES_NOTIFICATION)
val isHomeTimelineNotificationEnabled: Boolean
get() = preferences.getBoolean(KEY_HOME_TIMELINE_NOTIFICATION, DEFAULT_HOME_TIMELINE_NOTIFICATION)
val isInteractionsNotificationEnabled: Boolean
get() = preferences.getBoolean(KEY_MENTIONS_NOTIFICATION, DEFAULT_MENTIONS_NOTIFICATION)
val isNotificationFollowingOnly: Boolean
get() = preferences.getBoolean(KEY_NOTIFICATION_FOLLOWING_ONLY, false)
val isNotificationMentionsOnly: Boolean
get() = preferences.getBoolean(KEY_NOTIFICATION_MENTIONS_ONLY, false)
val isNotificationEnabled: Boolean
get() = preferences.getBoolean(KEY_NOTIFICATION, DEFAULT_NOTIFICATION)
companion object {
fun getAccountPreferences(context: Context, accountKeys: Array<UserKey>): Array<AccountPreferences> {
return Array(accountKeys.size) { AccountPreferences(context, accountKeys[it]) }
}
fun isNotificationHasLight(flags: Int): Boolean {
return VALUE_NOTIFICATION_FLAG_LIGHT in flags
}
fun isNotificationHasRingtone(flags: Int): Boolean {
return VALUE_NOTIFICATION_FLAG_RINGTONE in flags
}
fun isNotificationHasVibration(flags: Int): Boolean {
return VALUE_NOTIFICATION_FLAG_VIBRATION in flags
}
}
}

View File

@ -89,7 +89,7 @@ abstract class AccountsListPreference(context: Context, attrs: AttributeSet? = n
init {
GeneralComponentHelper.build(context).inject(this)
val switchPreferenceName = ACCOUNT_PREFERENCES_NAME_PREFIX + account.key
val switchPreferenceName = "$ACCOUNT_PREFERENCES_NAME_PREFIX${account.key}"
switchPreference = context.getSharedPreferences(switchPreferenceName, Context.MODE_PRIVATE)
switchPreference.registerOnSharedPreferenceChangeListener(this)
title = account.user.name

View File

@ -21,16 +21,18 @@ package org.mariotaku.twidere.preference
import android.content.Context
import android.util.AttributeSet
import org.mariotaku.ktextension.set
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT
import org.mariotaku.twidere.extension.model.isStreamingSupported
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.isEnabled = account.isStreamingSupported
preference.fragment = AccountStreamingSettingsFragment::class.java.name
val args = preference.extras
args.putParcelable(EXTRA_ACCOUNT, account)
preference.extras[EXTRA_ACCOUNT] = account
}
}

View File

@ -508,9 +508,8 @@ class TwidereDataProvider : ContentProvider(), LazyLoadCallback {
TABLE_ID_STATUSES -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
backgroundExecutor.execute {
val prefs = AccountPreferences.getNotificationEnabledPreferences(context,
DataStoreUtils.getAccountKeys(context))
prefs.filter(AccountPreferences::isHomeTimelineNotificationEnabled).forEach {
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isHomeTimelineNotificationEnabled }.forEach {
val positionTag = getPositionTag(CustomTabType.HOME_TIMELINE, it.accountKey)
contentNotificationManager.showTimeline(it, positionTag)
}
@ -520,9 +519,8 @@ class TwidereDataProvider : ContentProvider(), LazyLoadCallback {
TABLE_ID_ACTIVITIES_ABOUT_ME -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
backgroundExecutor.execute {
val prefs = AccountPreferences.getNotificationEnabledPreferences(context,
DataStoreUtils.getAccountKeys(context))
prefs.filter(AccountPreferences::isInteractionsNotificationEnabled).forEach {
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isInteractionsNotificationEnabled }.forEach {
val positionTag = getPositionTag(ReadPositionTag.ACTIVITIES_ABOUT_ME, it.accountKey)
contentNotificationManager.showInteractions(it, positionTag)
}
@ -532,9 +530,8 @@ class TwidereDataProvider : ContentProvider(), LazyLoadCallback {
TABLE_ID_MESSAGES_CONVERSATIONS -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
backgroundExecutor.execute {
val prefs = AccountPreferences.getNotificationEnabledPreferences(context,
DataStoreUtils.getAccountKeys(context))
prefs.filter(AccountPreferences::isDirectMessagesNotificationEnabled).forEach {
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isDirectMessagesNotificationEnabled }.forEach {
contentNotificationManager.showMessages(it)
}
notifyUnreadCountChanged(NOTIFICATION_ID_DIRECT_MESSAGES)

View File

@ -2,158 +2,277 @@ package org.mariotaku.twidere.service
import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.Handler
import android.os.Looper
import android.support.v4.app.NotificationCompat
import android.support.v4.util.SimpleArrayMap
import android.util.Log
import org.apache.commons.lang3.concurrent.BasicThreadFactory
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterUserStream
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
import org.mariotaku.twidere.activity.SettingsActivity
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.isStreamingSupported
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.AccountPreferences
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.account.cred.OAuthCredentials
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.twitter.GetActivitiesAboutMeTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.TwidereArrayUtils
import org.mariotaku.twidere.util.NotificationManagerWrapper
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import org.mariotaku.twidere.util.streaming.TwitterTimelineStreamCallback
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class StreamingService : Service() {
private val callbacks = SimpleArrayMap<UserKey, UserStreamCallback>()
@Inject
internal lateinit var notificationManager: NotificationManagerWrapper
internal lateinit var threadPoolExecutor: ExecutorService
internal lateinit var handler: Handler
private var notificationManager: NotificationManager? = null
private var accountKeys: Array<UserKey>? = null
private val stateMap = WeakHashMap<UserKey, Future<*>>()
private val accountChangeObserver = OnAccountsUpdateListener {
if (!TwidereArrayUtils.contentMatch(accountKeys, DataStoreUtils.getActivatedAccountKeys(this@StreamingService))) {
initStreaming()
}
setupStreaming()
}
override fun onCreate() {
super.onCreate()
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
DebugLog.d(LOGTAG, "Stream service started.")
initStreaming()
GeneralComponentHelper.build(this).inject(this)
threadPoolExecutor = Executors.newCachedThreadPool(BasicThreadFactory.Builder().priority(Thread.NORM_PRIORITY - 1).build())
handler = Handler(Looper.getMainLooper())
AccountManager.get(this).addOnAccountsUpdatedListenerSafe(accountChangeObserver, updateImmediately = false)
}
override fun onDestroy() {
clearTwitterInstances()
threadPoolExecutor.shutdown()
stateMap.clear()
removeNotification()
AccountManager.get(this).removeOnAccountsUpdatedListenerSafe(accountChangeObserver)
DebugLog.d(LOGTAG, "Stream service stopped.")
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (setupStreaming()) {
return START_STICKY
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent) = throw UnsupportedOperationException()
private fun setupStreaming(): Boolean {
if (updateStreamingInstances()) {
showNotification()
return true
} else {
stopSelf()
return false
}
}
private val Future<*>.isRunning: Boolean
get() = !isCancelled && !isDone
private fun updateStreamingInstances(): Boolean {
val am = AccountManager.get(this)
val supportedAccounts = AccountUtils.getAllAccountDetails(am, true).filter { it.isStreamingSupported }
val enabledPrefs = supportedAccounts.map { AccountPreferences(this, it.key) }
val enabledAccounts = supportedAccounts.filter { account ->
return@filter enabledPrefs.any {
account.key == it.accountKey
}
}
if (enabledAccounts.isEmpty()) return false
// Remove all disabled instances
stateMap.forEach { k, v ->
if (enabledAccounts.none { k == it.key } && v.isRunning) {
v.cancel(true)
}
}
// Add instances if not running
enabledAccounts.forEach { account ->
val existing = stateMap[account.key]
if (existing == null || !existing.isRunning) {
val runnable = account.newStreamingRunnable() ?: return@forEach
stateMap[account.key] = threadPoolExecutor.submit(runnable)
}
}
return true
}
private fun showNotification() {
val intent = Intent(this, SettingsActivity::class.java)
val contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val contentTitle = getString(R.string.app_name)
val contentText = getString(R.string.timeline_streaming_running)
val builder = NotificationCompat.Builder(this)
builder.setOngoing(true)
builder.setSmallIcon(R.drawable.ic_stat_streaming)
builder.setContentTitle(contentTitle)
builder.setContentText(contentText)
builder.setContentIntent(contentIntent)
builder.setCategory(NotificationCompat.CATEGORY_STATUS)
builder.priority = NotificationCompat.PRIORITY_MIN
startForeground(NOTIFICATION_SERVICE_STARTED, builder.build())
}
private fun removeNotification() {
stopForeground(true)
}
private fun AccountDetails.newStreamingRunnable(): StreamingRunnable<*>? {
when (type) {
AccountType.TWITTER -> {
return TwitterStreamingRunnable(this@StreamingService, handler, this)
}
}
return null
}
private fun clearTwitterInstances() {
var i = 0
val j = callbacks.size()
while (i < j) {
Thread(ShutdownStreamTwitterRunnable(callbacks.valueAt(i))).start()
i++
}
callbacks.clear()
notificationManager!!.cancel(NOTIFICATION_SERVICE_STARTED)
}
private fun initStreaming() {
if (!BuildConfig.DEBUG) return
setTwitterInstances()
updateStreamState()
}
private fun setTwitterInstances(): Boolean {
val accountsList = AccountUtils.getAllAccountDetails(AccountManager.get(this), true).filter { it.credentials is OAuthCredentials }
val accountKeys = accountsList.map { it.key }.toTypedArray()
val activatedPreferences = AccountPreferences.getAccountPreferences(this, accountKeys)
DebugLog.d(LOGTAG, "Setting up twitter stream instances")
this.accountKeys = accountKeys
clearTwitterInstances()
var result = false
accountsList.forEachIndexed { i, account ->
val preferences = activatedPreferences[i]
if (!preferences.isStreamingEnabled) {
return@forEachIndexed
}
val twitter = account.newMicroBlogInstance(context = this, cls = TwitterUserStream::class.java)
val callback = TwidereUserStreamCallback(this, account)
callbacks.put(account.key, callback)
object : Thread() {
override fun run() {
twitter.getUserStream(StreamWith.USER, callback)
Log.d(LOGTAG, String.format("Stream %s disconnected", account.key))
callbacks.remove(account.key)
updateStreamState()
}
}.start()
result = result or true
}
return result
}
private fun updateStreamState() {
if (callbacks.size() > 0) {
val intent = Intent(this, SettingsActivity::class.java)
val contentIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val contentTitle = getString(R.string.app_name)
val contentText = getString(R.string.timeline_streaming_running)
val builder = NotificationCompat.Builder(this)
builder.setOngoing(true)
builder.setSmallIcon(R.drawable.ic_stat_refresh)
builder.setContentTitle(contentTitle)
builder.setContentText(contentText)
builder.setContentIntent(contentIntent)
notificationManager!!.notify(NOTIFICATION_SERVICE_STARTED, builder.build())
} else {
notificationManager!!.cancel(NOTIFICATION_SERVICE_STARTED)
}
}
internal class ShutdownStreamTwitterRunnable(private val callback: UserStreamCallback?) : Runnable {
internal abstract class StreamingRunnable<T>(val context: Context, val account: AccountDetails) : Runnable {
override fun run() {
if (callback == null) return
Log.d(LOGTAG, "Disconnecting stream")
callback.disconnect()
val instance = createStreamingInstance()
while (!Thread.currentThread().isInterrupted) {
try {
instance.beginStreaming()
} catch (e: MicroBlogException) {
}
Thread.sleep(TimeUnit.MINUTES.toMillis(1))
}
}
abstract fun createStreamingInstance(): T
abstract fun T.beginStreaming()
}
internal class TwidereUserStreamCallback(
private val context: Context,
private val account: AccountDetails
) : TwitterTimelineStreamCallback(account.key.id) {
override fun onHomeTimeline(status: Status): Boolean = true
internal class TwitterStreamingRunnable(context: Context, val handler: Handler, account: AccountDetails) :
StreamingRunnable<TwitterUserStream>(context, account) {
override fun onActivityAboutMe(activity: Activity): Boolean = true
private val profileImageSize = context.getString(R.string.profile_image_size)
private val isOfficial = account.isOfficial(context)
override fun onDirectMessage(directMessage: DirectMessage): Boolean = true
private var canGetInteractions: Boolean = true
private var canGetMessages: Boolean = true
private var statusStreamStarted: Boolean = false
private val mentionsStreamStarted: Boolean = false
private val interactionsTimeoutRunnable = Runnable {
canGetInteractions = true
}
private val messagesTimeoutRunnable = Runnable {
canGetMessages = true
}
val callback = object : TwitterTimelineStreamCallback(account.key.id) {
private var homeInsertGap = false
private var interactionsInsertGap = false
override fun onConnected(): Boolean {
homeInsertGap = true
interactionsInsertGap = true
return true
}
override fun onHomeTimeline(status: Status): Boolean {
val values = ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java)
.create(ParcelableStatusUtils.fromStatus(status, account.key, homeInsertGap,
profileImageSize))
context.contentResolver.insert(Statuses.CONTENT_URI, values)
homeInsertGap = false
return true
}
override fun onActivityAboutMe(activity: Activity): Boolean {
if (isOfficial) {
// Wait for 30 seconds to avoid rate limit
if (canGetInteractions) {
getInteractions()
canGetInteractions = false
handler.postDelayed(interactionsTimeoutRunnable, TimeUnit.SECONDS.toMillis(30))
}
} else {
val values = ObjectCursor.valuesCreatorFrom(ParcelableActivity::class.java)
.create(ParcelableActivityUtils.fromActivity(activity, account.key,
interactionsInsertGap, profileImageSize))
context.contentResolver.insert(Activities.AboutMe.CONTENT_URI, values)
interactionsInsertGap = false
}
return true
}
override fun onDirectMessage(directMessage: DirectMessage): Boolean {
if (canGetMessages) {
getMessages()
canGetMessages = false
val timeout = TimeUnit.SECONDS.toMillis(if (isOfficial) 30 else 90)
handler.postDelayed(messagesTimeoutRunnable, timeout)
}
return true
}
private fun getInteractions() {
val task = GetActivitiesAboutMeTask(context)
task.params = object : SimpleRefreshTaskParam() {
override val accountKeys: Array<UserKey> = arrayOf(account.key)
override val sinceIds: Array<String?>?
get() = DataStoreUtils.getNewestActivityMaxPositions(context,
Activities.AboutMe.CONTENT_URI, arrayOf(account.key))
override val sinceSortIds: LongArray?
get() = DataStoreUtils.getNewestActivityMaxSortPositions(context,
Activities.AboutMe.CONTENT_URI, arrayOf(account.key))
override val hasSinceIds: Boolean = true
}
TaskStarter.execute(task)
}
private fun getMessages() {
val task = GetMessagesTask(context)
task.params = object : GetMessagesTask.RefreshMessagesTaskParam(context) {
override val accountKeys: Array<UserKey> = arrayOf(account.key)
override val hasSinceIds: Boolean = true
}
TaskStarter.execute(task)
}
}
override fun createStreamingInstance(): TwitterUserStream {
return account.newMicroBlogInstance(context, cls = TwitterUserStream::class.java)
}
override fun TwitterUserStream.beginStreaming() {
getUserStream(StreamWith.USER, callback)
}
}
@ -164,3 +283,4 @@ class StreamingService : Service() {
}
}

View File

@ -15,10 +15,10 @@ import org.mariotaku.twidere.model.SimpleRefreshTaskParam
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.filter.RefreshFiltersSubscriptionsTask
import org.mariotaku.twidere.task.twitter.GetActivitiesAboutMeTask
import org.mariotaku.twidere.task.twitter.GetHomeTimelineTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask
import org.mariotaku.twidere.task.filter.RefreshFiltersSubscriptionsTask
/**
* Created by mariotaku on 2017/1/6.
@ -74,7 +74,7 @@ class TaskServiceRunner(
override val accountKeys: Array<UserKey> by lazy {
AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context)).filter {
it.isAutoRefreshEnabled && it.isAutoRefreshDirectMessagesEnabled
}.map(AccountPreferences::getAccountKey).toTypedArray()
}.map(AccountPreferences::accountKey).toTypedArray()
}
}
return task
@ -100,7 +100,7 @@ class TaskServiceRunner(
override val accountKeys: Array<UserKey> by lazy {
return@lazy AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context)).filter {
it.isAutoRefreshEnabled && refreshable(it)
}.map(AccountPreferences::getAccountKey).toTypedArray()
}.map(AccountPreferences::accountKey).toTypedArray()
}
override val sinceIds: Array<String?>?

View File

@ -41,6 +41,7 @@ import org.mariotaku.twidere.provider.TwidereDataProvider
import org.mariotaku.twidere.service.BaseIntentService
import org.mariotaku.twidere.service.JobTaskService
import org.mariotaku.twidere.service.LegacyTaskService
import org.mariotaku.twidere.service.StreamingService
import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.task.ManagedAsyncTask
import org.mariotaku.twidere.text.util.EmojiEditableFactory
@ -141,4 +142,6 @@ interface GeneralComponent {
fun inject(controller: PremiumDashboardActivity.ExtraFeatureViewController)
fun inject(fragment: ExoPlayerPageFragment)
fun inject(service: StreamingService)
}

View File

@ -130,9 +130,9 @@ abstract class TwitterTimelineStreamCallback(val accountId: String) : SimpleUser
return false
}
override abstract fun onDirectMessage(directMessage: DirectMessage): Boolean
protected abstract fun onHomeTimeline(status: Status): Boolean
protected abstract fun onActivityAboutMe(activity: Activity): Boolean
override abstract fun onDirectMessage(directMessage: DirectMessage): Boolean
}

View File

@ -780,14 +780,18 @@
<string name="preference_randomize_account_rename_accounts_confirm">Rename existing accounts?</string>
<string name="preference_summary_auto_refresh_compatibility_mode">Enable for faster refresh interval, increases power usage on Android 5.0+</string>
<string name="preference_summary_auto_refresh_power_saving">Stop auto refresh when battery is low</string>
<string name="preference_summary_background_streaming">Press HOME instead of BACK to keep streaming open</string>
<string name="preference_summary_chrome_custom_tab">Open links with in-app browser (Powered by Chrome)</string>
<string name="preference_summary_database_item_limit">Upper limit of items stored in databases for each account, set to a smaller value to save space and increase loading speed.</string>
<string name="preference_summary_media_preload_non_metered_network">Preload media only on free networks like Wi-Fi</string>
<string name="preference_summary_streaming_non_metered_network">Streaming only on free networks like Wi-Fi</string>
<string name="preference_summary_streaming_power_saving">Streaming only when charging</string>
<string name="preference_summary_trends_location">Now you can set location separately in tab settings.</string>
<string name="preference_title_accounts">Accounts</string>
<string name="preference_title_advanced">Advanced</string>
<string name="preference_title_auto_refresh_compatibility_mode">Compatibility mode</string>
<string name="preference_title_auto_refresh_power_saving">Power saving mode</string>
<string name="preference_title_background_streaming">Background streaming</string>
<string name="preference_title_chrome_custom_tab">In-app browser</string>
<string name="preference_title_database_item_limit">Database size limit</string>
<string name="preference_title_filter_manage_subscriptions">Manage</string>
@ -800,6 +804,8 @@
<string name="preference_title_multi_column_tabs">Multi column tabs</string>
<string name="preference_title_portrait">Portrait</string>
<string name="preference_title_storage">Storage</string>
<string name="preference_title_streaming_non_metered_network">Streaming on free network</string>
<string name="preference_title_streaming_power_saving">Power saving mode</string>
<string name="preference_title_tablet_mode">Tablet mode</string>
<string name="preference_title_text_size">Text size</string>
<string name="preference_title_translate">Translate</string>

View File

@ -9,24 +9,28 @@
android:key="cat_accounts"
android:title="@string/preference_title_accounts"
app:switchDefault="false"
app:switchKey="streaming"/>
app:switchKey="enable_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"/>
android:defaultValue="true"
android:key="streaming_power_saving"
android:summary="@string/preference_summary_streaming_power_saving"
android:title="@string/preference_title_streaming_power_saving"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pebble_notifications"
android:summary="@string/pebble_notifications_summary"
android:title="@string/pebble_notifications"/>
android:defaultValue="true"
android:key="streaming_non_metered_network"
android:summary="@string/preference_summary_streaming_non_metered_network"
android:title="@string/preference_title_streaming_non_metered_network"/>
<Preference
android:key="background_streaming_hint"
android:summary="@string/preference_summary_background_streaming"
android:title="@string/preference_title_background_streaming"/>
</org.mariotaku.twidere.preference.TintedPreferenceCategory>
</PreferenceScreen>

View File

@ -7,9 +7,9 @@
<g id="Action-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ic_action_streaming-mdpi">
<rect id="Rectangle-2" x="4" y="4" width="24" height="24"></rect>
<path d="M17.873855,14.2816795 L22,5 L10,17.7183205 L14.126145,17.7183205 L10,27 L22,14.2816795 L17.873855,14.2816795 Z" id="Triangle" fill="#FFFFFF"></path>
<path d="M15,3 L17,3 L17,8 L15,8 L15,3 Z M29,15 L29,17 L24,17 L24,15 L29,15 Z M17,29 L15,29 L15,24 L17,24 L17,29 Z M3,17 L3,15 L8,15 L8,17 L3,17 Z" id="Rectangle" fill="#FFFFFF"></path>
<path d="M23.7781746,6.80761184 L25.1923882,8.22182541 L22.363961,11.0502525 L20.9497475,9.63603897 L23.7781746,6.80761184 Z M25.1923882,23.7781746 L23.7781746,25.1923882 L20.9497475,22.363961 L22.363961,20.9497475 L25.1923882,23.7781746 Z M8.22182541,25.1923882 L6.80761184,23.7781746 L9.63603897,20.9497475 L11.0502525,22.363961 L8.22182541,25.1923882 Z M6.80761184,8.22182541 L8.22182541,6.80761184 L11.0502525,9.63603897 L9.63603897,11.0502525 L6.80761184,8.22182541 Z" id="Rectangle-3" fill="#FFFFFF"></path>
<path d="M17.6036432,14.2816795 L21,5 L10,17.7183205 L14.3963568,17.7183205 L11,27 L22,14.2816795 L17.6036432,14.2816795 Z" id="Triangle" fill="#FFFFFF"></path>
<path d="M15.25,3 L16.75,3 L16.75,8 L15.25,8 L15.25,3 Z M29,15.25 L29,16.75 L24,16.75 L24,15.25 L29,15.25 Z M16.75,29 L15.25,29 L15.25,24 L16.75,24 L16.75,29 Z M3,16.75 L3,15.25 L8,15.25 L8,16.75 L3,16.75 Z" id="Rectangle-4" fill="#FFFFFF"></path>
<path d="M24.6620581,6.27728176 L25.7227182,7.33794193 L23.6013979,9.45926227 L22.5407377,8.3986021 L24.6620581,6.27728176 Z M25.7227182,24.6620581 L24.6620581,25.7227182 L22.5407377,23.6013979 L23.6013979,22.5407377 L25.7227182,24.6620581 Z M7.33794193,25.7227182 L6.27728176,24.6620581 L8.3986021,22.5407377 L9.45926227,23.6013979 L7.33794193,25.7227182 Z M6.27728176,7.33794193 L7.33794193,6.27728176 L9.45926227,8.3986021 L8.3986021,9.45926227 L6.27728176,7.33794193 Z" id="Rectangle-3" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>ic_stat_streaming-mdpi</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Miscellaneous" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ic_stat_streaming-mdpi">
<rect id="Rectangle-2" x="2.76923077" y="2.76923077" width="18.4615385" height="18.4615385"></rect>
<path d="M13.2335717,10.678215 L15.8461538,3.53846154 L7.38461538,13.321785 L10.7664283,13.321785 L8.15384615,20.4615385 L16.6153846,10.678215 L13.2335717,10.678215 Z" id="Triangle" fill="#FFFFFF"></path>
<path d="M11.4230769,2 L12.5769231,2 L12.5769231,5.84615385 L11.4230769,5.84615385 L11.4230769,2 Z M22,11.4230769 L22,12.5769231 L18.1538462,12.5769231 L18.1538462,11.4230769 L22,11.4230769 Z M12.5769231,22 L11.4230769,22 L11.4230769,18.1538462 L12.5769231,18.1538462 L12.5769231,22 Z M2,12.5769231 L2,11.4230769 L5.84615385,11.4230769 L5.84615385,12.5769231 L2,12.5769231 Z" id="Rectangle-4" fill="#FFFFFF"></path>
<path d="M18.6631216,4.52098597 L19.479014,5.33687841 L17.8472292,6.96866329 L17.0313367,6.15277085 L18.6631216,4.52098597 Z M19.479014,18.6631216 L18.6631216,19.479014 L17.0313367,17.8472292 L17.8472292,17.0313367 L19.479014,18.6631216 Z M5.33687841,19.479014 L4.52098597,18.6631216 L6.15277085,17.0313367 L6.96866329,17.8472292 L5.33687841,19.479014 Z M4.52098597,5.33687841 L5.33687841,4.52098597 L6.96866329,6.15277085 L6.15277085,6.96866329 L4.52098597,5.33687841 Z" id="Rectangle-3" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB