supported notification channels

fixed some memory leaks
This commit is contained in:
Mariotaku Lee 2017-08-25 19:59:43 +08:00
parent 5a095a9178
commit 1d81c8cdf8
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
14 changed files with 296 additions and 98 deletions

View File

@ -38,7 +38,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.annotation.StringRes import android.support.annotation.StringRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.app.NotificationCompat
import android.support.v4.view.GravityCompat import android.support.v4.view.GravityCompat
import android.support.v4.view.ViewCompat import android.support.v4.view.ViewCompat
import android.support.v4.view.ViewPager.OnPageChangeListener import android.support.v4.view.ViewPager.OnPageChangeListener
@ -79,6 +78,7 @@ import org.mariotaku.twidere.annotation.NavbarStyle
import org.mariotaku.twidere.annotation.ReadPositionTag import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.extension.applyTheme import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.extension.model.notificationBuilder
import org.mariotaku.twidere.extension.onShow import org.mariotaku.twidere.extension.onShow
import org.mariotaku.twidere.fragment.AccountsDashboardFragment import org.mariotaku.twidere.fragment.AccountsDashboardFragment
import org.mariotaku.twidere.fragment.BaseDialogFragment import org.mariotaku.twidere.fragment.BaseDialogFragment
@ -91,6 +91,7 @@ import org.mariotaku.twidere.model.SupportTabSpec
import org.mariotaku.twidere.model.Tab import org.mariotaku.twidere.model.Tab
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.UnreadCountUpdatedEvent import org.mariotaku.twidere.model.event.UnreadCountUpdatedEvent
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
import org.mariotaku.twidere.provider.TwidereDataStore.Activities import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
@ -99,6 +100,7 @@ import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
import org.mariotaku.twidere.view.HomeDrawerLayout import org.mariotaku.twidere.view.HomeDrawerLayout
import org.mariotaku.twidere.view.TabPagerIndicator import org.mariotaku.twidere.view.TabPagerIndicator
import java.lang.ref.WeakReference
class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, SupportFragmentCallback, class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, SupportFragmentCallback,
OnLongClickListener, DrawerLayout.DrawerListener { OnLongClickListener, DrawerLayout.DrawerListener {
@ -118,6 +120,18 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
private val readStateChangeListener = OnSharedPreferenceChangeListener { _, _ -> updateUnreadCount() } private val readStateChangeListener = OnSharedPreferenceChangeListener { _, _ -> updateUnreadCount() }
private val controlBarShowHideHelper = ControlBarShowHideHelper(this) private val controlBarShowHideHelper = ControlBarShowHideHelper(this)
override val controlBarHeight: Int
get() {
return mainTabs.height - mainTabs.stripHeight
}
override val currentVisibleFragment: Fragment?
get() {
val currentItem = mainPager.currentItem
if (currentItem < 0 || currentItem >= pagerAdapter.count) return null
return pagerAdapter.instantiateItem(mainPager, currentItem)
}
private val homeDrawerToggleDelegate = object : ActionBarDrawerToggle.Delegate { private val homeDrawerToggleDelegate = object : ActionBarDrawerToggle.Delegate {
override fun setActionBarUpIndicator(upDrawable: Drawable, @StringRes contentDescRes: Int) { override fun setActionBarUpIndicator(upDrawable: Drawable, @StringRes contentDescRes: Int) {
drawerToggleButton.setImageDrawable(upDrawable) drawerToggleButton.setImageDrawable(upDrawable)
@ -146,34 +160,17 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
} }
override val controlBarHeight: Int private val keyboardShortcutRecipient: Fragment?
get() { get() = when {
return mainTabs.height - mainTabs.stripHeight homeMenu.isDrawerOpen(GravityCompat.START) -> leftDrawerFragment
homeMenu.isDrawerOpen(GravityCompat.END) -> null
else -> currentVisibleFragment
} }
fun closeAccountsDrawer() {
if (homeMenu == null) return
homeMenu.closeDrawers()
}
private val activatedAccountKeys: Array<UserKey> private val activatedAccountKeys: Array<UserKey>
get() = DataStoreUtils.getActivatedAccountKeys(this) get() = DataStoreUtils.getActivatedAccountKeys(this)
override val currentVisibleFragment: Fragment? private val leftDrawerFragment: Fragment?
get() {
val currentItem = mainPager.currentItem
if (currentItem < 0 || currentItem >= pagerAdapter.count) return null
return pagerAdapter.instantiateItem(mainPager, currentItem)
}
override fun triggerRefresh(position: Int): Boolean {
val f = pagerAdapter.instantiateItem(mainPager, position)
if (f.activity == null || f.isDetached) return false
if (f !is RefreshScrollTopInterface) return false
return f.triggerRefresh()
}
val leftDrawerFragment: Fragment?
get() = supportFragmentManager.findFragmentById(R.id.leftDrawer) get() = supportFragmentManager.findFragmentById(R.id.leftDrawer)
private val isDrawerOpen: Boolean private val isDrawerOpen: Boolean
@ -220,7 +217,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
ViewCompat.setOnApplyWindowInsetsListener(homeContent, this) ViewCompat.setOnApplyWindowInsetsListener(homeContent, this)
homeMenu.fitsSystemWindows = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || homeMenu.fitsSystemWindows = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ||
preferences[navbarStyleKey] != NavbarStyle.TRANSPARENT preferences[navbarStyleKey] != NavbarStyle.TRANSPARENT
if (!homeMenu.fitsSystemWindows) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !ViewCompat.getFitsSystemWindows(homeMenu)) {
ViewCompat.setOnApplyWindowInsetsListener(homeMenu, null) ViewCompat.setOnApplyWindowInsetsListener(homeMenu, null)
} }
@ -544,6 +541,12 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
override fun triggerRefresh(position: Int): Boolean {
val f = pagerAdapter.instantiateItem(mainPager, position)
if (f.activity == null || f.isDetached) return false
if (f !is RefreshScrollTopInterface) return false
return f.triggerRefresh()
}
fun notifyAccountsChanged() { fun notifyAccountsChanged() {
} }
@ -553,11 +556,6 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
updateUnreadCount() updateUnreadCount()
} }
fun openSearchView(account: AccountDetails?) {
selectedAccountToSearch = account
onSearchRequested()
}
fun updateUnreadCount() { fun updateUnreadCount() {
if (mainTabs == null || updateUnreadCountTask != null && updateUnreadCountTask!!.status == AsyncTask.Status.RUNNING) if (mainTabs == null || updateUnreadCountTask != null && updateUnreadCountTask!!.status == AsyncTask.Status.RUNNING)
return return
@ -580,10 +578,10 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
if (mainTabs.columns > 1) { if (mainTabs.columns > 1) {
val lp = actionsButton.layoutParams val lp = actionsButton.layoutParams
val total: Float val total: Float
if (lp is MarginLayoutParams) { total = if (lp is MarginLayoutParams) {
total = (lp.bottomMargin + actionsButton.height).toFloat() (lp.bottomMargin + actionsButton.height).toFloat()
} else { } else {
total = actionsButton.height.toFloat() actionsButton.height.toFloat()
} }
return 1 - actionsButton.translationY / total return 1 - actionsButton.translationY / total
} }
@ -626,16 +624,15 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
return homeDrawerToggleDelegate return homeDrawerToggleDelegate
} }
private val keyboardShortcutRecipient: Fragment? fun closeAccountsDrawer() {
get() { if (homeMenu == null) return
if (homeMenu.isDrawerOpen(GravityCompat.START)) { homeMenu.closeDrawers()
return leftDrawerFragment }
} else if (homeMenu.isDrawerOpen(GravityCompat.END)) {
return null private fun openSearchView(account: AccountDetails?) {
} else { selectedAccountToSearch = account
return currentVisibleFragment onSearchRequested()
} }
}
private fun handleFragmentKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler, private fun handleFragmentKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler,
keyCode: Int, repeatCount: Int, keyCode: Int, repeatCount: Int,
@ -678,11 +675,10 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
if (Intent.ACTION_SEARCH == action) { if (Intent.ACTION_SEARCH == action) {
val query = intent.getStringExtra(SearchManager.QUERY) val query = intent.getStringExtra(SearchManager.QUERY)
val appSearchData = intent.getBundleExtra(SearchManager.APP_DATA) val appSearchData = intent.getBundleExtra(SearchManager.APP_DATA)
val accountKey: UserKey? val accountKey = if (appSearchData != null && appSearchData.containsKey(EXTRA_ACCOUNT_KEY)) {
if (appSearchData != null && appSearchData.containsKey(EXTRA_ACCOUNT_KEY)) { appSearchData.getParcelable(EXTRA_ACCOUNT_KEY)
accountKey = appSearchData.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
} else { } else {
accountKey = Utils.getDefaultAccountKey(this) Utils.getDefaultAccountKey(this)
} }
IntentUtils.openSearch(this, accountKey, query) IntentUtils.openSearch(this, accountKey, query)
return -1 return -1
@ -847,7 +843,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
val intent = Intent(this, UsageStatisticsActivity::class.java) val intent = Intent(this, UsageStatisticsActivity::class.java)
val contentIntent = PendingIntent.getActivity(this, 0, intent, 0) val contentIntent = PendingIntent.getActivity(this, 0, intent, 0)
val builder = NotificationCompat.Builder(this) val builder = NotificationChannelSpec.appNotices.notificationBuilder(this)
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setSmallIcon(R.drawable.ic_stat_info) builder.setSmallIcon(R.drawable.ic_stat_info)
builder.setTicker(getString(R.string.usage_statistics)) builder.setTicker(getString(R.string.usage_statistics))
@ -889,7 +885,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
fun hasMultiColumns(): Boolean { private fun hasMultiColumns(): Boolean {
if (!DeviceUtils.isDeviceTablet(this) || !DeviceUtils.isScreenTablet(this)) return false if (!DeviceUtils.isDeviceTablet(this) || !DeviceUtils.isScreenTablet(this)) return false
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
return preferences.getBoolean("multi_column_tabs_landscape", resources.getBoolean(R.bool.default_multi_column_tabs_land)) return preferences.getBoolean("multi_column_tabs_landscape", resources.getBoolean(R.bool.default_multi_column_tabs_land))
@ -907,17 +903,20 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
private class UpdateUnreadCountTask( private class UpdateUnreadCountTask(
private val context: Context, context: Context,
private val preferences: SharedPreferences, private val preferences: SharedPreferences,
private val readStateManager: ReadStateManager, private val readStateManager: ReadStateManager,
private val indicator: TabPagerIndicator, indicator: TabPagerIndicator,
private val tabs: Array<SupportTabSpec> private val tabs: Array<SupportTabSpec>
) : AsyncTask<Any, UpdateUnreadCountTask.TabBadge, SparseIntArray>() { ) : AsyncTask<Any, UpdateUnreadCountTask.TabBadge, SparseIntArray>() {
private val activatedKeys = DataStoreUtils.getActivatedAccountKeys(context) private val activatedKeys = DataStoreUtils.getActivatedAccountKeys(context)
private val contextRef = WeakReference(context)
private val indicatorRef = WeakReference(indicator)
override fun doInBackground(vararg params: Any): SparseIntArray { override fun doInBackground(vararg params: Any): SparseIntArray {
val result = SparseIntArray() val result = SparseIntArray()
val context = contextRef.get() ?: return result
tabs.forEachIndexed { i, spec -> tabs.forEachIndexed { i, spec ->
if (spec.type == null) { if (spec.type == null) {
publishProgress(TabBadge(i, -1)) publishProgress(TabBadge(i, -1))
@ -969,6 +968,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
override fun onPostExecute(result: SparseIntArray) { override fun onPostExecute(result: SparseIntArray) {
val indicator = indicatorRef.get() ?: return
indicator.clearBadge() indicator.clearBadge()
for (i in 0 until result.size()) { for (i in 0 until result.size()) {
indicator.setBadge(result.keyAt(i), result.valueAt(i)) indicator.setBadge(result.keyAt(i), result.valueAt(i))
@ -976,6 +976,7 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
} }
override fun onProgressUpdate(vararg values: TabBadge) { override fun onProgressUpdate(vararg values: TabBadge) {
val indicator = indicatorRef.get() ?: return
for (value in values) { for (value in values) {
indicator.setBadge(value.index, value.count) indicator.setBadge(value.index, value.count)
} }

View File

@ -35,9 +35,6 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.load.resource.drawable.GlideDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import org.mariotaku.chameleon.Chameleon import org.mariotaku.chameleon.Chameleon
@ -52,7 +49,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.SHARED_PREFERENCES_NAME import org.mariotaku.twidere.TwidereConstants.SHARED_PREFERENCES_NAME
import org.mariotaku.twidere.activity.iface.IBaseActivity import org.mariotaku.twidere.activity.iface.IBaseActivity
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_INTENT import org.mariotaku.twidere.constant.IntentConstants.EXTRA_INTENT
import org.mariotaku.twidere.constant.lastLaunchPresentationTimeKey import org.mariotaku.twidere.constant.lastLaunchTimeKey
import org.mariotaku.twidere.constant.promotionsEnabledKey import org.mariotaku.twidere.constant.promotionsEnabledKey
import org.mariotaku.twidere.constant.themeColorKey import org.mariotaku.twidere.constant.themeColorKey
import org.mariotaku.twidere.constant.themeKey import org.mariotaku.twidere.constant.themeKey
@ -148,14 +145,14 @@ open class MainActivity : ChameleonActivity(), IBaseActivity<MainActivity> {
} }
private fun showPresentationOrLaunch() { private fun showPresentationOrLaunch() {
val lastLaunchPresentationTime = preferences[lastLaunchPresentationTimeKey] val lastLaunchTime = preferences[lastLaunchTimeKey]
val maximumDuration = if (BuildConfig.DEBUG) { val maximumDuration = if (BuildConfig.DEBUG) {
TimeUnit.SECONDS.toMillis(30) TimeUnit.SECONDS.toMillis(30)
} else { } else {
TimeUnit.HOURS.toMillis(6) TimeUnit.HOURS.toMillis(6)
} }
// Show again at least 6 hours later (30 secs for debug builds) // Show again at least 6 hours later (30 secs for debug builds)
if (lastLaunchPresentationTime >= 0 && System.currentTimeMillis() - lastLaunchPresentationTime < maximumDuration) { if (lastLaunchTime >= 0 && System.currentTimeMillis() - lastLaunchTime < maximumDuration) {
launchDirectly() launchDirectly()
return return
} }
@ -212,20 +209,6 @@ open class MainActivity : ChameleonActivity(), IBaseActivity<MainActivity> {
skipPresentation.visibility = View.VISIBLE skipPresentation.visibility = View.VISIBLE
controlOverlay.tag = presentation controlOverlay.tag = presentation
Glide.with(this).load(presentation.images.first().url) Glide.with(this).load(presentation.images.first().url)
.listener(object : RequestListener<String, GlideDrawable> {
override fun onException(e: Exception?, model: String?,
target: Target<GlideDrawable>?, isFirstResource: Boolean): Boolean {
return false
}
override fun onResourceReady(resource: GlideDrawable?, model: String?,
target: Target<GlideDrawable>?, isFromMemoryCache: Boolean,
isFirstResource: Boolean): Boolean {
preferences[lastLaunchPresentationTimeKey] = System.currentTimeMillis()
return false
}
})
.priority(Priority.HIGH) .priority(Priority.HIGH)
.into(presentationView) .into(presentationView)
} }
@ -245,6 +228,7 @@ open class MainActivity : ChameleonActivity(), IBaseActivity<MainActivity> {
} }
private fun performLaunch() { private fun performLaunch() {
preferences[lastLaunchTimeKey] = System.currentTimeMillis()
val am = AccountManager.get(this) val am = AccountManager.get(this)
if (!DeviceUtils.checkCompatibility()) { if (!DeviceUtils.checkCompatibility()) {
startActivity(Intent(this, IncompatibleAlertActivity::class.java)) startActivity(Intent(this, IncompatibleAlertActivity::class.java))

View File

@ -65,6 +65,8 @@ import org.mariotaku.twidere.util.kovenant.stopKovenant
import org.mariotaku.twidere.util.media.MediaPreloader import org.mariotaku.twidere.util.media.MediaPreloader
import org.mariotaku.twidere.util.media.ThumborWrapper import org.mariotaku.twidere.util.media.ThumborWrapper
import org.mariotaku.twidere.util.net.TwidereDns import org.mariotaku.twidere.util.net.TwidereDns
import org.mariotaku.twidere.util.notification.ContentNotificationManager
import org.mariotaku.twidere.util.notification.NotificationChannelsManager
import org.mariotaku.twidere.util.premium.ExtraFeaturesService import org.mariotaku.twidere.util.premium.ExtraFeaturesService
import org.mariotaku.twidere.util.refresh.AutoRefreshController import org.mariotaku.twidere.util.refresh.AutoRefreshController
import org.mariotaku.twidere.util.sync.DataSyncProvider import org.mariotaku.twidere.util.sync.DataSyncProvider
@ -132,6 +134,7 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
} }
super.onCreate() super.onCreate()
EmojioneTranslator.init(this) EmojioneTranslator.init(this)
NotificationChannelsManager.createChannels(this)
applyLanguageSettings() applyLanguageSettings()
startKovenant() startKovenant()
initializeAsyncTask() initializeAsyncTask()

View File

@ -85,7 +85,7 @@ val homeRefreshDirectMessagesKey = KBooleanKey(KEY_HOME_REFRESH_DIRECT_MESSAGES,
val homeRefreshSavedSearchesKey = KBooleanKey(KEY_HOME_REFRESH_SAVED_SEARCHES, true) val homeRefreshSavedSearchesKey = KBooleanKey(KEY_HOME_REFRESH_SAVED_SEARCHES, true)
val composeStatusVisibilityKey = KNullableStringKey("compose_status_visibility", null) val composeStatusVisibilityKey = KNullableStringKey("compose_status_visibility", null)
val navbarStyleKey = KStringKey(KEY_NAVBAR_STYLE, NavbarStyle.DEFAULT) val navbarStyleKey = KStringKey(KEY_NAVBAR_STYLE, NavbarStyle.DEFAULT)
val lastLaunchPresentationTimeKey = KLongKey("last_launch_presentation_time", -1) val lastLaunchTimeKey = KLongKey("last_launch_time", -1)
val promotionsEnabledKey = KBooleanKey("promotions_enabled", false) val promotionsEnabledKey = KBooleanKey("promotions_enabled", false)
object cacheSizeLimitKey : KSimpleKey<Int>(KEY_CACHE_SIZE_LIMIT, 300) { object cacheSizeLimitKey : KSimpleKey<Int>(KEY_CACHE_SIZE_LIMIT, 300) {

View File

@ -0,0 +1,31 @@
/*
* 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.extension.model
import android.content.Context
import android.support.v4.app.NotificationCompat
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
/**
* Created by mariotaku on 2017/8/25.
*/
fun NotificationChannelSpec.notificationBuilder(context: Context): NotificationCompat.Builder {
return NotificationCompat.Builder(context, id)
}

View File

@ -0,0 +1,89 @@
/*
* 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.notification
import android.app.NotificationManager
import android.support.annotation.StringRes
import org.mariotaku.twidere.R
/**
* Created by mariotaku on 2017/8/25.
*/
enum class NotificationChannelSpec(
val id: String,
@StringRes val nameRes: Int,
@StringRes val descriptionRes: Int = 0,
val importance: Int,
val showBadge: Boolean = false) {
/**
* For notifications send by app itself.
* Such as "what's new"
*/
appNotices("app_notices", R.string.notification_channel_name_app_notices,
importance = NotificationManager.IMPORTANCE_LOW, showBadge = true),
/**
* For notifications indicate that some lengthy operations are performing in the background.
* Such as sending attachment process.
*/
backgroundProgresses("background_progresses", R.string.notification_channel_name_background_progresses,
importance = NotificationManager.IMPORTANCE_MIN),
/**
* For ongoing notifications indicating service statuses.
* Such as notification showing streaming service running
*/
serviceStatuses("service_statuses", R.string.notification_channel_name_service_statuses,
importance = NotificationManager.IMPORTANCE_MIN),
/**
* For import notifications related to micro-blogging features.
* Such as failure to update status.
*/
contentNotices("content_notices", R.string.notification_channel_name_content_notices,
importance = NotificationManager.IMPORTANCE_HIGH, showBadge = true),
/**
* For updates related to micro-blogging features.
* Such as new statuses posted by friends.
*/
contentUpdates("content_updates", R.string.notification_channel_name_content_updates,
importance = NotificationManager.IMPORTANCE_DEFAULT, showBadge = true),
/**
* For updates related to micro-blogging features.
* Such as new statuses posted by friends user subscribed to.
*/
contentSubscriptions("content_subscriptions", R.string.notification_channel_name_content_subscriptions,
importance = NotificationManager.IMPORTANCE_HIGH, showBadge = true),
/**
* For interactions related to micro-blogging features.
* Such as replies and likes.
*/
contentInteractions("content_interactions", R.string.notification_channel_name_content_interactions,
descriptionRes = R.string.notification_channel_description_content_interactions,
importance = NotificationManager.IMPORTANCE_HIGH, showBadge = true),
/**
* For messages related to micro-blogging features.
* Such as direct messages.
*/
contentMessages("content_messages", R.string.notification_channel_name_content_messages,
descriptionRes = R.string.notification_channel_description_content_messages,
importance = NotificationManager.IMPORTANCE_HIGH, showBadge = true)
}

View File

@ -54,6 +54,7 @@ import org.mariotaku.twidere.util.SQLiteDatabaseWrapper.LazyLoadCallback
import org.mariotaku.twidere.util.dagger.GeneralComponent import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.database.CachedUsersQueryBuilder import org.mariotaku.twidere.util.database.CachedUsersQueryBuilder
import org.mariotaku.twidere.util.database.SuggestionsCursorCreator import org.mariotaku.twidere.util.database.SuggestionsCursorCreator
import org.mariotaku.twidere.util.notification.ContentNotificationManager
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject

View File

@ -26,6 +26,7 @@ import com.twitter.Extractor
import com.twitter.Validator import com.twitter.Validator
import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.dagger.GeneralComponent import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.notification.ContentNotificationManager
import javax.inject.Inject import javax.inject.Inject
abstract class BaseService : Service() { abstract class BaseService : Service() {

View File

@ -23,16 +23,13 @@ import android.accounts.AccountManager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.Service import android.app.Service
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.BaseColumns
import android.support.annotation.UiThread import android.support.annotation.UiThread
import android.support.annotation.WorkerThread import android.support.annotation.WorkerThread
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.Builder
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@ -41,7 +38,6 @@ import nl.komponents.kovenant.ui.successUi
import org.mariotaku.abstask.library.AbstractTask import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.abstask.library.ManualTaskStarter import org.mariotaku.abstask.library.ManualTaskStarter
import org.mariotaku.kpreferences.get import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.configure
import org.mariotaku.ktextension.getNullableTypedArrayExtra import org.mariotaku.ktextension.getNullableTypedArrayExtra
import org.mariotaku.ktextension.toLongOr import org.mariotaku.ktextension.toLongOr
import org.mariotaku.ktextension.useCursor import org.mariotaku.ktextension.useCursor
@ -58,9 +54,12 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.constant.refreshAfterTweetKey import org.mariotaku.twidere.constant.refreshAfterTweetKey
import org.mariotaku.twidere.extension.getErrorMessage import org.mariotaku.twidere.extension.getErrorMessage
import org.mariotaku.twidere.extension.model.notificationBuilder
import org.mariotaku.twidere.extension.withAppendedPath
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
import org.mariotaku.twidere.model.draft.StatusObjectActionExtras import org.mariotaku.twidere.model.draft.StatusObjectActionExtras
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
import org.mariotaku.twidere.model.schedule.ScheduleInfo import org.mariotaku.twidere.model.schedule.ScheduleInfo
import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils
@ -183,7 +182,7 @@ class LengthyOperationsService : BaseIntentService("lengthy_operations") {
private fun sendMessage(message: ParcelableNewMessage) { private fun sendMessage(message: ParcelableNewMessage) {
val title = getString(R.string.sending_direct_message) val title = getString(R.string.sending_direct_message)
val builder = Builder(this) val builder = NotificationChannelSpec.backgroundProgresses.notificationBuilder(this)
builder.setSmallIcon(R.drawable.ic_stat_send) builder.setSmallIcon(R.drawable.ic_stat_send)
builder.setProgress(100, 0, true) builder.setProgress(100, 0, true)
builder.setTicker(title) builder.setTicker(title)
@ -239,7 +238,7 @@ class LengthyOperationsService : BaseIntentService("lengthy_operations") {
private fun updateStatuses(statuses: Array<ParcelableStatusUpdate>, scheduleInfo: ScheduleInfo? = null) { private fun updateStatuses(statuses: Array<ParcelableStatusUpdate>, scheduleInfo: ScheduleInfo? = null) {
val context = this val context = this
val builder = Builder(context) val builder = NotificationChannelSpec.backgroundProgresses.notificationBuilder(context)
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
builder, 0, null)) builder, 0, null))
for (item in statuses) { for (item in statuses) {
@ -321,9 +320,7 @@ class LengthyOperationsService : BaseIntentService("lengthy_operations") {
invokeAfterExecute(task, result) invokeAfterExecute(task, result)
if (!result.succeed) { if (!result.succeed) {
contentResolver.insert(Drafts.CONTENT_URI_NOTIFICATIONS, configure(ContentValues()) { contentResolver.insert(Drafts.CONTENT_URI_NOTIFICATIONS.withAppendedPath(result.draftId.toString()), null)
put(BaseColumns._ID, result.draftId)
})
} }
} }
if (preferences[refreshAfterTweetKey]) { if (preferences[refreshAfterTweetKey]) {

View File

@ -36,6 +36,7 @@ import org.mariotaku.twidere.extension.model.api.key
import org.mariotaku.twidere.extension.model.api.microblog.toParcelable import org.mariotaku.twidere.extension.model.api.microblog.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
import org.mariotaku.twidere.model.pagination.SinceMaxPagination import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.* import org.mariotaku.twidere.provider.TwidereDataStore.*
@ -172,7 +173,7 @@ class StreamingService : BaseService() {
val contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val contentTitle = getString(R.string.app_name) val contentTitle = getString(R.string.app_name)
val contentText = getString(R.string.timeline_streaming_running) val contentText = getString(R.string.timeline_streaming_running)
val builder = NotificationCompat.Builder(this) val builder = NotificationChannelSpec.serviceStatuses.notificationBuilder(this)
builder.setOngoing(true) builder.setOngoing(true)
builder.setSmallIcon(R.drawable.ic_stat_streaming) builder.setSmallIcon(R.drawable.ic_stat_streaming)
builder.setContentTitle(contentTitle) builder.setContentTitle(contentTitle)

View File

@ -58,6 +58,7 @@ import org.mariotaku.twidere.util.media.MediaPreloader
import org.mariotaku.twidere.util.media.ThumborWrapper import org.mariotaku.twidere.util.media.ThumborWrapper
import org.mariotaku.twidere.util.media.TwidereMediaDownloader import org.mariotaku.twidere.util.media.TwidereMediaDownloader
import org.mariotaku.twidere.util.net.TwidereDns import org.mariotaku.twidere.util.net.TwidereDns
import org.mariotaku.twidere.util.notification.ContentNotificationManager
import org.mariotaku.twidere.util.premium.ExtraFeaturesService import org.mariotaku.twidere.util.premium.ExtraFeaturesService
import org.mariotaku.twidere.util.refresh.AutoRefreshController import org.mariotaku.twidere.util.refresh.AutoRefreshController
import org.mariotaku.twidere.util.refresh.JobSchedulerAutoRefreshController import org.mariotaku.twidere.util.refresh.JobSchedulerAutoRefreshController

View File

@ -17,7 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.mariotaku.twidere.util package org.mariotaku.twidere.util.notification
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
@ -48,21 +48,22 @@ import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.api.formattedTextWithIndices import org.mariotaku.twidere.extension.model.api.formattedTextWithIndices
import org.mariotaku.twidere.extension.model.getSummaryText import org.mariotaku.twidere.extension.model.getSummaryText
import org.mariotaku.twidere.extension.model.getTitle import org.mariotaku.twidere.extension.model.getTitle
import org.mariotaku.twidere.extension.model.notificationBuilder
import org.mariotaku.twidere.extension.model.notificationDisabled import org.mariotaku.twidere.extension.model.notificationDisabled
import org.mariotaku.twidere.extension.rawQuery import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
import org.mariotaku.twidere.model.util.ParcelableActivityUtils import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.provider.TwidereDataStore.* import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.receiver.NotificationReceiver import org.mariotaku.twidere.receiver.NotificationReceiver
import org.mariotaku.twidere.service.LengthyOperationsService import org.mariotaku.twidere.service.LengthyOperationsService
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.database.FilterQueryBuilder import org.mariotaku.twidere.util.database.FilterQueryBuilder
import org.oshkimaadziig.george.androidutils.SpanFormatter import org.oshkimaadziig.george.androidutils.SpanFormatter
import java.io.IOException import java.io.IOException
/**
* Created by mariotaku on 2017/2/16.
*/
class ContentNotificationManager( class ContentNotificationManager(
val context: Context, val context: Context,
val activityTracker: ActivityTracker, val activityTracker: ActivityTracker,
@ -134,7 +135,7 @@ class ContentNotificationManager(
} }
// Setup notification // Setup notification
val builder = NotificationCompat.Builder(context) val builder = NotificationChannelSpec.contentUpdates.notificationBuilder(context)
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setSmallIcon(R.drawable.ic_stat_twitter) builder.setSmallIcon(R.drawable.ic_stat_twitter)
builder.setTicker(notificationTitle) builder.setTicker(notificationTitle)
@ -175,7 +176,7 @@ class ContentNotificationManager(
@SuppressLint("Recycle") @SuppressLint("Recycle")
val c = cr.query(Activities.AboutMe.CONTENT_URI, Activities.COLUMNS, where, whereArgs, val c = cr.query(Activities.AboutMe.CONTENT_URI, Activities.COLUMNS, where, whereArgs,
OrderBy(Activities.TIMESTAMP, false).sql) ?: return OrderBy(Activities.TIMESTAMP, false).sql) ?: return
val builder = NotificationCompat.Builder(context) val builder = NotificationChannelSpec.contentInteractions.notificationBuilder(context)
val pebbleNotificationStringBuilder = StringBuilder() val pebbleNotificationStringBuilder = StringBuilder()
try { try {
val count = c.count val count = c.count
@ -277,18 +278,18 @@ class ContentNotificationManager(
var messageSum: Int = 0 var messageSum: Int = 0
var newLastReadTimestamp = -1L var newLastReadTimestamp = -1L
cur.forEachRow { cur, _ -> cur.forEachRow { c, _ ->
val unreadCount = cur.getInt(indices[Conversations.UNREAD_COUNT]) val unreadCount = c.getInt(indices[Conversations.UNREAD_COUNT])
if (unreadCount <= 0) return@forEachRow false if (unreadCount <= 0) return@forEachRow false
if (newLastReadTimestamp != -1L) { if (newLastReadTimestamp != -1L) {
newLastReadTimestamp = cur.getLong(indices[Conversations.LAST_READ_TIMESTAMP]) newLastReadTimestamp = c.getLong(indices[Conversations.LAST_READ_TIMESTAMP])
} }
messageSum += unreadCount messageSum += unreadCount
return@forEachRow true return@forEachRow true
} }
if (messageSum == 0) return if (messageSum == 0) return
val builder = NotificationCompat.Builder(context) val builder = NotificationChannelSpec.contentMessages.notificationBuilder(context)
applyNotificationPreferences(builder, pref, pref.directMessagesNotificationType) applyNotificationPreferences(builder, pref, pref.directMessagesNotificationType)
builder.setSmallIcon(R.drawable.ic_stat_message) builder.setSmallIcon(R.drawable.ic_stat_message)
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL) builder.setCategory(NotificationCompat.CATEGORY_SOCIAL)
@ -305,8 +306,8 @@ class ContentNotificationManager(
builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.DIRECT_MESSAGES, builder.setDeleteIntent(getMarkReadDeleteIntent(context, NotificationType.DIRECT_MESSAGES,
accountKey, newLastReadTimestamp, false)) accountKey, newLastReadTimestamp, false))
val remaining = cur.forEachRow(5) { cur, pos -> val remaining = cur.forEachRow(5) { c, pos ->
val conversation = indices.newObject(cur) val conversation = indices.newObject(c)
if (conversation.notificationDisabled) return@forEachRow false if (conversation.notificationDisabled) return@forEachRow false
val title = conversation.getTitle(context, userColorNameManager, nameFirst) val title = conversation.getTitle(context, userColorNameManager, nameFirst)
val summary = conversation.getSummaryText(context, userColorNameManager, nameFirst) val summary = conversation.getSummaryText(context, userColorNameManager, nameFirst)
@ -336,7 +337,7 @@ class ContentNotificationManager(
val userDisplayName = userColorNameManager.getDisplayName(status.user, val userDisplayName = userColorNameManager.getDisplayName(status.user,
preferences[nameFirstKey]) preferences[nameFirstKey])
val statusUri = LinkCreator.getTwidereStatusLink(accountKey, status.id) val statusUri = LinkCreator.getTwidereStatusLink(accountKey, status.id)
val builder = NotificationCompat.Builder(context) val builder = NotificationChannelSpec.contentSubscriptions.notificationBuilder(context)
builder.color = userColorNameManager.getUserColor(userKey) builder.color = userColorNameManager.getUserColor(userKey)
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setWhen(status.createdAt?.time ?: 0) builder.setWhen(status.createdAt?.time ?: 0)
@ -381,7 +382,7 @@ class ContentNotificationManager(
uriBuilder.scheme(SCHEME_TWIDERE) uriBuilder.scheme(SCHEME_TWIDERE)
uriBuilder.authority(AUTHORITY_DRAFTS) uriBuilder.authority(AUTHORITY_DRAFTS)
intent.data = uriBuilder.build() intent.data = uriBuilder.build()
val nb = NotificationCompat.Builder(context) val nb = NotificationChannelSpec.contentNotices.notificationBuilder(context)
nb.setTicker(message) nb.setTicker(message)
nb.setContentTitle(title) nb.setContentTitle(title)
nb.setContentText(item.text) nb.setContentText(item.text)
@ -401,7 +402,6 @@ class ContentNotificationManager(
PendingIntent.getService(context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT)) PendingIntent.getService(context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
nb.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)) nb.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT))
nb.setGroup("drafts")
notificationManager.notify(draftUri.toString(), NOTIFICATION_ID_DRAFTS, nb.build()) notificationManager.notify(draftUri.toString(), NOTIFICATION_ID_DRAFTS, nb.build())
return draftId return draftId
} }

View File

@ -0,0 +1,79 @@
/*
* 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.notification
import android.annotation.TargetApi
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.notification.NotificationChannelSpec
import org.mariotaku.twidere.util.dagger.DependencyHolder
/**
* Created by mariotaku on 2017/8/25.
*/
object NotificationChannelsManager {
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
NotificationChannelCreatorImpl.createChannels(context)
}
fun createAccountGroup(context: Context, account: AccountDetails) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
NotificationChannelCreatorImpl.createAccountGroup(context, account)
}
@TargetApi(Build.VERSION_CODES.O)
private object NotificationChannelCreatorImpl {
fun createChannels(context: Context) {
val nm = context.getSystemService(NotificationManager::class.java)
val values = NotificationChannelSpec.values()
nm.notificationChannels.filterNot { channel ->
values.any { channel.id == it.id }
}.forEach {
nm.deleteNotificationChannel(it.id)
}
for (spec in values) {
val channel = NotificationChannel(spec.id, context.getString(spec.nameRes), spec.importance)
if (spec.descriptionRes != 0) {
channel.description = context.getString(spec.descriptionRes)
}
channel.setShowBadge(spec.showBadge)
nm.createNotificationChannel(channel)
}
}
fun createAccountGroup(context: Context, account: AccountDetails) {
val nm = context.getSystemService(NotificationManager::class.java)
val holder = DependencyHolder.get(context)
val pref = holder.preferences
val ucnm = holder.userColorNameManager
val group = NotificationChannelGroup(account.key.toString(),
ucnm.getDisplayName(account.user, pref[nameFirstKey]))
nm.createNotificationChannelGroup(group)
}
}
}

View File

@ -44,6 +44,7 @@
<!-- [verb] Action for deleting a file or a twitter object like tweet--> <!-- [verb] Action for deleting a file or a twitter object like tweet-->
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="action_delete_messages">Delete messages</string> <string name="action_delete_messages">Delete messages</string>
<string name="action_disable_promotions">Disable promotions</string>
<string name="action_dont_restart">Don\'t restart</string> <string name="action_dont_restart">Don\'t restart</string>
<string name="action_dont_terminate">Don\'t quit</string> <string name="action_dont_terminate">Don\'t quit</string>
<!-- [verb] Edit image/settings etc. --> <!-- [verb] Edit image/settings etc. -->
@ -769,6 +770,16 @@
<string name="none">None</string> <string name="none">None</string>
<string name="notification_channel_description_content_interactions">Interactions like mentions and retweets</string>
<string name="notification_channel_description_content_messages">Important messages like DMs</string>
<string name="notification_channel_name_app_notices">App notices</string>
<string name="notification_channel_name_background_progresses">Background operations</string>
<string name="notification_channel_name_content_interactions">Content interactions</string>
<string name="notification_channel_name_content_messages">Content messages</string>
<string name="notification_channel_name_content_notices">Content notices</string>
<string name="notification_channel_name_content_subscriptions">Content subscriptions</string>
<string name="notification_channel_name_content_updates">Content updates</string>
<string name="notification_channel_name_service_statuses">Service statuses</string>
<string name="notification_direct_message"><xliff:g id="user">%s</xliff:g> sent you a direct message.</string> <string name="notification_direct_message"><xliff:g id="user">%s</xliff:g> sent you a direct message.</string>
<string name="notification_direct_message_multiple_messages"><xliff:g id="user">%1$s</xliff:g> sent you <xliff:g id="messages_count">%2$d</xliff:g> direct messages.</string> <string name="notification_direct_message_multiple_messages"><xliff:g id="user">%1$s</xliff:g> sent you <xliff:g id="messages_count">%2$d</xliff:g> direct messages.</string>
<string name="notification_direct_message_multiple_users"><xliff:g id="user">%1$s</xliff:g> and <xliff:g id="users_count">%2$d</xliff:g> others sent you <xliff:g id="messages_count">%3$d</xliff:g> direct messages.</string> <string name="notification_direct_message_multiple_users"><xliff:g id="user">%1$s</xliff:g> and <xliff:g id="users_count">%2$d</xliff:g> others sent you <xliff:g id="messages_count">%3$d</xliff:g> direct messages.</string>
@ -1329,5 +1340,4 @@
<string name="users_blocked">Blocked these users.</string> <string name="users_blocked">Blocked these users.</string>
<string name="users_lists_with_name"><xliff:g id="name">%s</xliff:g>\'s lists</string> <string name="users_lists_with_name"><xliff:g id="name">%s</xliff:g>\'s lists</string>
<string name="users_statuses">User\'s tweets</string> <string name="users_statuses">User\'s tweets</string>
<string name="action_disable_promotions">Disable promotions</string>
</resources> </resources>