WIP: account stats implementation

This commit is contained in:
Mariotaku Lee 2018-05-17 01:19:46 +08:00
parent 42361c9fbe
commit 8094bbec4e
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
29 changed files with 1102 additions and 160 deletions

View File

@ -81,8 +81,10 @@ subprojects {
StethoBeanShellREPL : '0.5',
ArchLifecycleExtensions: '1.1.1',
ArchPaging : '1.0.0',
Room : '1.1.0',
ConstraintLayout : '1.1.0',
MessageBubbleView : '2.1',
WorkManager : '1.0.0-alpha01',
]
}

View File

@ -1 +1,2 @@
org.gradle.jvmargs=-Xmx3584m
org.gradle.jvmargs=-Xmx3584m
android.databinding.enableV2=true

View File

@ -38,6 +38,7 @@ import java.lang.annotation.RetentionPolicy;
CustomTabType.LIST_TIMELINE,
CustomTabType.PUBLIC_TIMELINE,
CustomTabType.NETWORK_PUBLIC_TIMELINE,
CustomTabType.ACCOUNT_STATS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CustomTabType {
@ -52,4 +53,5 @@ public @interface CustomTabType {
String LIST_TIMELINE = "list_timeline";
String PUBLIC_TIMELINE = "public_timeline";
String NETWORK_PUBLIC_TIMELINE = "network_public_timeline";
String ACCOUNT_STATS = "account_stats";
}

View File

@ -104,7 +104,8 @@ public class TabArguments implements Parcelable {
case CustomTabType.DIRECT_MESSAGES:
case CustomTabType.TRENDS_SUGGESTIONS:
case CustomTabType.PUBLIC_TIMELINE:
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
case CustomTabType.ACCOUNT_STATS: {
return LoganSquare.parse(json, TabArguments.class);
}
case CustomTabType.USER_TIMELINE:

View File

@ -57,6 +57,16 @@ android {
additionalParameters '--no-version-vectors'
}
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
dataBinding {
enabled = true
}
flavorDimensions 'channel'
productFlavors {
@ -164,6 +174,7 @@ dependencies {
kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}"
kapt "com.github.mariotaku.ObjectCursor:processor:${libVersions['ObjectCursor']}"
kapt "com.github.bumptech.glide:compiler:${libVersions['Glide']}"
kapt "android.arch.persistence.room:compiler:${libVersions['Room']}"
implementation project(':twidere.component.common')
implementation project(':twidere.component.nyan')
@ -204,6 +215,8 @@ dependencies {
implementation "android.arch.lifecycle:extensions:${libVersions['ArchLifecycleExtensions']}"
implementation "android.arch.paging:runtime:${libVersions['ArchPaging']}"
implementation "android.arch.persistence.room:runtime:${libVersions['Room']}"
implementation "android.arch.work:work-runtime-ktx:${libVersions['WorkManager']}"
implementation "com.android.support:multidex:${libVersions['MultiDex']}"
implementation "com.android.support:support-annotations:${libVersions['SupportLib']}"
implementation "com.android.support:support-compat:${libVersions['SupportLib']}"

View File

@ -0,0 +1,57 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "df1fe758472ff9d1d8e21da6f4d8b5fc",
"entities": [
{
"tableName": "account_daily_stats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `createdAt` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `followersCount` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountKey",
"columnName": "accountKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "statusesCount",
"columnName": "statusesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "followersCount",
"columnName": "followersCount",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"rowId"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"df1fe758472ff9d1d8e21da6f4d8b5fc\")"
]
}
}

View File

@ -0,0 +1,67 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "9ab8ea60785ca135c5f1dc50547c143f",
"entities": [
{
"tableName": "account_daily_stats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `createdAt` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `followersCount` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountKey",
"columnName": "accountKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "statusesCount",
"columnName": "statusesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "followersCount",
"columnName": "followersCount",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"rowId"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_account_daily_stats_createdAt_accountKey",
"unique": true,
"columnNames": [
"createdAt",
"accountKey"
],
"createSql": "CREATE UNIQUE INDEX `index_account_daily_stats_createdAt_accountKey` ON `${TABLE_NAME}` (`createdAt`, `accountKey`)"
}
],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9ab8ea60785ca135c5f1dc50547c143f\")"
]
}
}

View File

@ -126,7 +126,8 @@ public class CustomTabUtils implements Constants {
case CustomTabType.DIRECT_MESSAGES:
case CustomTabType.TRENDS_SUGGESTIONS:
case CustomTabType.PUBLIC_TIMELINE:
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
case CustomTabType.ACCOUNT_STATS: {
return new TabArguments();
}
case CustomTabType.USER_TIMELINE:

View File

@ -62,6 +62,8 @@ import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.work.WorkManager
import androidx.work.ktx.OneTimeWorkRequestBuilder
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.squareup.otto.Subscribe
@ -103,6 +105,7 @@ import org.mariotaku.twidere.receiver.NotificationReceiver
import org.mariotaku.twidere.service.StreamingService
import org.mariotaku.twidere.singleton.BusSingleton
import org.mariotaku.twidere.singleton.PreferencesSingleton
import org.mariotaku.twidere.task.worker.AccountDailyStatWorker
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
@ -293,6 +296,8 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
if (!showDrawerTutorial() && !PreferencesSingleton.get(this)[defaultAutoRefreshAskedKey]) {
showAutoRefreshConfirm()
}
WorkManager.getInstance().enqueue(OneTimeWorkRequestBuilder<AccountDailyStatWorker>().build())
}
override fun onPostCreate(savedInstanceState: Bundle?) {

View File

@ -0,0 +1,24 @@
package org.mariotaku.twidere.content.database
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.TypeConverters
import org.mariotaku.twidere.content.database.converter.UserKeyConverter
import org.mariotaku.twidere.content.database.dao.AccountDailyStats
import org.mariotaku.twidere.content.model.AccountStats
import org.mariotaku.twidere.util.lang.ApplicationContextSingletonHolder
@Database(entities = [AccountStats::class], version = 2)
@TypeConverters(UserKeyConverter::class)
abstract class TwidereDatabase : RoomDatabase() {
abstract fun accountDailyStats(): AccountDailyStats
companion object : ApplicationContextSingletonHolder<TwidereDatabase>({
Room.databaseBuilder(it, TwidereDatabase::class.java, "twidere.db")
.fallbackToDestructiveMigration()
.build()
})
}

View File

@ -0,0 +1,25 @@
package org.mariotaku.twidere.content.database.converter
import android.arch.persistence.room.TypeConverter
import org.mariotaku.twidere.extension.lang.threadLocal
import java.text.SimpleDateFormat
import java.util.*
class LocalDateConverter {
@TypeConverter
fun parse(time: String): Date {
return simpleDateFormat.parse(time)
}
@TypeConverter
fun serialize(date: Date): String {
return simpleDateFormat.format(date)
}
companion object {
val simpleDateFormat by threadLocal {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}
}

View File

@ -0,0 +1,17 @@
package org.mariotaku.twidere.content.database.converter
import android.arch.persistence.room.TypeConverter
import java.util.*
class TimestampDateTimeConverter {
@TypeConverter
fun parse(time: Long): Date {
return Date(time)
}
@TypeConverter
fun serialize(date: Date): Long {
return date.time
}
}

View File

@ -0,0 +1,18 @@
package org.mariotaku.twidere.content.database.converter
import android.arch.persistence.room.TypeConverter
import org.mariotaku.twidere.model.UserKey
class UserKeyConverter {
@TypeConverter
fun parse(str: String): UserKey {
return UserKey.valueOf(str)
}
@TypeConverter
fun serialize(key: UserKey): String {
return key.toString()
}
}

View File

@ -0,0 +1,153 @@
package org.mariotaku.twidere.content.database.dao
import android.arch.persistence.room.*
import org.mariotaku.twidere.content.database.converter.LocalDateConverter
import org.mariotaku.twidere.content.model.AccountStats
import org.mariotaku.twidere.extension.julianDay
import org.mariotaku.twidere.extension.time
import org.mariotaku.twidere.model.UserKey
import java.util.*
import kotlin.math.sign
@Dao
abstract class AccountDailyStats {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(stats: AccountStats)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(stats: Collection<AccountStats>)
@Query("SELECT * FROM `account_daily_stats` WHERE `createdAt` = :date")
abstract fun list(@TypeConverters(LocalDateConverter::class) date: Date): List<AccountStats>
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` BETWEEN :since AND :until ORDER BY `createdAt`")
abstract fun list(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
@TypeConverters(LocalDateConverter::class) until: Date): List<AccountStats>
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` >= :since ORDER BY `createdAt` LIMIT 1")
abstract fun firstSince(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date): AccountStats?
fun listSparse(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
@TypeConverters(LocalDateConverter::class) until: Date): Array<AccountStats?> {
val sinceJulianDay = Calendar.getInstance().time(since).julianDay()
val count = Calendar.getInstance().time(until).julianDay() - sinceJulianDay + 1
val result = arrayOfNulls<AccountStats>(count)
val tempCal = Calendar.getInstance()
list(accountKey, since, until).forEach {
tempCal.time = it.createdAt
val index = tempCal.julianDay() - sinceJulianDay
if (index in result.indices) {
result[index] = it
}
}
return result
}
fun monthlySummary(accountKey: UserKey, date: Date): AccountStats.Summaries {
val since = Calendar.getInstance().apply {
time = date
add(Calendar.DATE, -27)
}.time
val prevPeriodFirst = firstSince(accountKey, Calendar.getInstance().apply {
time = date
add(Calendar.DATE, -55)
}.time)
val currentPeriodStats = listSparse(accountKey, since, date)
val statuses = currentPeriodStats.statusesSummary(prevPeriodFirst)
val followers = currentPeriodStats.followersSummary()
return AccountStats.Summaries(statuses, followers)
}
private fun Array<AccountStats?>.statusesSummary(prevPeriodFirst: AccountStats?): AccountStats.DisplaySummary {
val firstNumber = numberAt(0, AccountStats::statusesCount)
val lastNumber = numberAt(lastIndex, AccountStats::statusesCount)
var growthText: String? = null
var growthSign = 0
if (prevPeriodFirst != null) {
val prevPeriodCount = firstNumber - prevPeriodFirst.statusesCount
val currPeriodCount = lastNumber - firstNumber
val growthPercent = currPeriodCount / prevPeriodCount.toFloat() - 1
growthText = String.format(Locale.US, "%.1f%%", Math.abs(growthPercent * 100))
growthSign = growthPercent.sign.toInt()
}
val firstNonNullIndex = indexOfFirst { it != null }
if (firstNonNullIndex < 0) throw DataNotReadyException()
val diffs = LongArray(size - firstNonNullIndex) item@{ index ->
val statIndex = index + firstNonNullIndex
if (statIndex == 0) return@item 0
return@item numberAt(statIndex, AccountStats::statusesCount) - numberAt(statIndex - 1,
AccountStats::statusesCount)
}
var maxDiff = diffs.max()!!
if (maxDiff <= 0L) {
maxDiff = Math.abs(diffs.min()!!)
}
if (maxDiff <= 0L) {
maxDiff = 10
}
val values = FloatArray(size - firstNonNullIndex) item@{ index ->
return@item diffs[index] / maxDiff.toFloat()
}
val periodSum = lastNumber - firstNumber
return AccountStats.DisplaySummary(periodSum, growthSign, growthText, size, values)
}
private fun Array<AccountStats?>.followersSummary(): AccountStats.DisplaySummary {
val firstNumber = numberAt(0, AccountStats::followersCount)
val lastNumber = numberAt(lastIndex, AccountStats::followersCount)
val maxNumber = maxBy { it?.followersCount ?: Long.MIN_VALUE }!!.followersCount
val valuesCount = size
val values = indices.map { index ->
val number = numberAt(index, AccountStats::followersCount)
return@map (number - firstNumber) / (maxNumber - firstNumber).toFloat()
}.toFloatArray()
val growth = lastNumber - firstNumber
return AccountStats.DisplaySummary(firstNumber, growth.sign, Math.abs(growth).toString(),
valuesCount, values)
}
private fun Array<AccountStats?>.numberAt(index: Int, selector: (AccountStats) -> Long): Long {
val itemAt = this[index]
if (itemAt != null) return selector(itemAt)
val firstNonNullIndex = indexOfFirst { it != null }
val lastNonNullIndex = indexOfLast { it != null }
if (firstNonNullIndex < 0 || lastNonNullIndex < 0 || firstNonNullIndex == lastNonNullIndex) {
throw DataNotReadyException()
}
val count = lastNonNullIndex - firstNonNullIndex
val firstNonNullNumber = selector(this[firstNonNullIndex]!!)
val lastNonNullNumber = selector(this[lastNonNullIndex]!!)
val delta = (lastNonNullNumber - firstNonNullNumber) / count
when {
index < firstNonNullIndex -> return firstNonNullNumber - delta * (firstNonNullIndex - index)
index > lastNonNullIndex -> return lastNonNullNumber + delta * (index - lastNonNullIndex)
else -> {
val startIndex = (0 until index).last { this[it] != null }
val endIndex = (index + 1..lastIndex).first { this[it] != null }
val start = selector(this[startIndex]!!)
val end = selector(this[endIndex]!!)
val delta2 = (end - start) / (endIndex - startIndex)
return start + delta2 * (index - startIndex)
}
}
}
class DataNotReadyException : Exception()
}

View File

@ -0,0 +1,47 @@
package org.mariotaku.twidere.content.model
import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey
import android.arch.persistence.room.TypeConverters
import org.mariotaku.twidere.content.database.converter.LocalDateConverter
import org.mariotaku.twidere.model.UserKey
import java.util.*
@Entity(tableName = "account_daily_stats",
indices = [Index("createdAt", "accountKey", unique = true)])
@TypeConverters(LocalDateConverter::class)
data class AccountStats(
@PrimaryKey(autoGenerate = true)
val rowId: Long = 0,
val createdAt: Date,
val accountKey: UserKey,
val statusesCount: Long,
val followersCount: Long
) {
data class Summaries(
val statuses: DisplaySummary,
val followers: DisplaySummary
)
data class DisplaySummary(
val number: Long,
val growthSign: Int,
val growth: String?,
val valuesCount: Int,
val values: FloatArray
) {
val numberDisplay: String
get() = number.toString()
val growthDisplay: String?
get() = when {
growthSign < 0 -> "-$growth"
growthSign > 0 -> "+$growth"
else -> growth
}
}
}

View File

@ -24,6 +24,7 @@ import android.support.annotation.WorkerThread
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.twidere.util.DebugLog
abstract class ComputableLiveData<T>(loadOnInstantiate: Boolean) : LiveData<T>() {
@ -37,6 +38,7 @@ abstract class ComputableLiveData<T>(loadOnInstantiate: Boolean) : LiveData<T>()
task(body = this::compute).successUi {
postValue(it)
}.failUi {
DebugLog.e(msg = "Exception in ComputableLiveData", tr = it)
postValue(null)
}
}

View File

@ -20,12 +20,54 @@
package org.mariotaku.twidere.extension
import java.util.*
import java.util.concurrent.TimeUnit
private val epochCalendar = Calendar.getInstance().timeInMillis(0)
val epochWeekDay: Int = epochCalendar.get(Calendar.DAY_OF_WEEK) - epochCalendar.getMinimum(Calendar.DAY_OF_WEEK)
val epochJulianDay: Int = epochCalendar.julianDay()
/**
* Created by mariotaku on 2017/9/25.
*/
fun Calendar.isSameDay(that: Calendar): Boolean {
return this[Calendar.ERA] == that[Calendar.ERA] &&
this[Calendar.YEAR] == that[Calendar.YEAR] &&
this[Calendar.DAY_OF_YEAR] == that[Calendar.DAY_OF_YEAR]
}
}
fun Calendar.timeInMillis(time: Long): Calendar = apply { timeInMillis = time }
fun Calendar.time(time: Date): Calendar = apply { this.time = time }
fun Calendar.daysSinceEpoch(day: Int): Calendar = apply {
timeInMillis = 0
add(Calendar.DATE, day)
}
fun Calendar.clearClockTime() {
set(Calendar.HOUR_OF_DAY, 0)
clear(Calendar.MINUTE)
clear(Calendar.SECOND)
clear(Calendar.MILLISECOND)
}
fun Calendar.dayProgress(): Float {
val offsetHour = (get(Calendar.HOUR_OF_DAY) - 6 + 24) % 24
val dayMillis = TimeUnit.HOURS.toMillis(offsetHour.toLong()) +
TimeUnit.MINUTES.toMillis(get(Calendar.MINUTE).toLong()) +
TimeUnit.SECONDS.toMillis(get(Calendar.SECOND).toLong()) +
get(Calendar.MILLISECOND)
return dayMillis / TimeUnit.HOURS.toMillis(24).toFloat()
}
// http://en.wikipedia.org/wiki/Julian_day
fun Calendar.julianDay(): Int {
val year = get(Calendar.YEAR)
val month = get(Calendar.MONTH) + 1
val day = get(Calendar.DATE)
val a = (14 - month) / 12
val y = year + 4800 - a
val m = month + 12 * a - 3
return day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045
}
fun Calendar.daysSinceEpoch(): Int = julianDay() - epochJulianDay
fun Calendar.weeksSinceEpoch(): Int = (daysSinceEpoch() + epochWeekDay) / 7

View File

@ -0,0 +1,51 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.extension.lang
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class ThreadLocalDelegate<T>(initial: () -> T) {
private var threadLocal: ThreadLocal<T> = PropertyThreadLocal(initial)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = threadLocal.get()
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
threadLocal.set(value)
}
private class PropertyThreadLocal<T>(val initial: () -> T) : ThreadLocal<T>() {
override fun initialValue(): T {
return initial()
}
}
}
private class ThreadLocalProperty<T>(private val ref: ThreadLocal<T>) : ReadOnlyProperty<Any, T?> {
override fun getValue(thisRef: Any, property: KProperty<*>): T? = ref.get()
}
fun <T> threadLocal(obj: () -> T): ThreadLocalDelegate<T> = ThreadLocalDelegate(obj)
operator fun <T> ThreadLocal<T>.provideDelegate(thisRef: Any, prop: KProperty<*>): ReadOnlyProperty<Any, T?> =
ThreadLocalProperty(this)

View File

@ -0,0 +1,66 @@
package org.mariotaku.twidere.fragment.stats
import android.arch.lifecycle.Observer
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_account_stats.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.content.database.TwidereDatabase
import org.mariotaku.twidere.content.model.AccountStats
import org.mariotaku.twidere.data.ComputableLiveData
import org.mariotaku.twidere.databinding.AdapterItemAccountStatCardBinding
import org.mariotaku.twidere.fragment.BaseFragment
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.Utils
import java.util.*
class AccountStatsFragment : BaseFragment() {
private lateinit var liveStats: AccountStatsLiveData
private lateinit var statusesBinding: AdapterItemAccountStatCardBinding
private lateinit var followersBinding: AdapterItemAccountStatCardBinding
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
liveStats = AccountStatsLiveData(context!!, Utils.getAccountKeys(context!!,
arguments)!!.first())
statusesBinding = AdapterItemAccountStatCardBinding.inflate(layoutInflater,
statsScrollContent, true)
followersBinding = AdapterItemAccountStatCardBinding.inflate(layoutInflater,
statsScrollContent, true)
liveStats.observe(this, Observer {
displaySummaries(it)
})
liveStats.load()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_account_stats, container, false)
}
private fun displaySummaries(summaries: AccountStats.Summaries?) {
if (summaries == null) return
statusesBinding.statTitle.text = getString(R.string.title_statuses)
followersBinding.statTitle.text = getString(R.string.title_followers)
statusesBinding.summary = summaries.statuses
followersBinding.summary = summaries.followers
}
class AccountStatsLiveData(
val context: Context,
val accountKey: UserKey
) : ComputableLiveData<AccountStats.Summaries>(false) {
override fun compute(): AccountStats.Summaries {
return TwidereDatabase.get(context).accountDailyStats().monthlySummary(accountKey, Date())
}
}
}

View File

@ -342,8 +342,9 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
}
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
val context = context ?: return
val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context!!)[readFromBottomKey]) {
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context)[readFromBottomKey]) {
scrollToPositionWithOffset(0, 0)
} else {
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)

View File

@ -150,7 +150,8 @@ abstract class TabConfiguration {
CustomTabType.TRENDS_SUGGESTIONS, CustomTabType.DIRECT_MESSAGES,
CustomTabType.FAVORITES, CustomTabType.USER_TIMELINE, CustomTabType.USER_MEDIA_TIMELINE,
CustomTabType.SEARCH_STATUSES, CustomTabType.LIST_TIMELINE,
CustomTabType.PUBLIC_TIMELINE, CustomTabType.NETWORK_PUBLIC_TIMELINE)
CustomTabType.PUBLIC_TIMELINE, CustomTabType.NETWORK_PUBLIC_TIMELINE,
CustomTabType.ACCOUNT_STATS)
val all: List<Pair<String, TabConfiguration>> = allTypes.mapNotNull {
val conf = ofType(it) ?: return@mapNotNull null
@ -170,6 +171,7 @@ abstract class TabConfiguration {
CustomTabType.SEARCH_STATUSES -> SearchTabConfiguration()
CustomTabType.PUBLIC_TIMELINE -> PublicTimelineTabConfiguration()
CustomTabType.NETWORK_PUBLIC_TIMELINE -> NetworkPublicTimelineTabConfiguration()
CustomTabType.ACCOUNT_STATS -> AccountStatsConfiguration()
else -> null
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.tab.impl
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.TabAccountFlags
import org.mariotaku.twidere.fragment.stats.AccountStatsFragment
import org.mariotaku.twidere.model.tab.DrawableHolder
import org.mariotaku.twidere.model.tab.StringHolder
import org.mariotaku.twidere.model.tab.TabConfiguration
class AccountStatsConfiguration : TabConfiguration() {
override val name = StringHolder.resource(R.string.title_account_stats)
override val icon = DrawableHolder.Builtin.TRENDS
override val accountFlags = TabAccountFlags.FLAG_HAS_ACCOUNT or
TabAccountFlags.FLAG_ACCOUNT_REQUIRED or TabAccountFlags.FLAG_ACCOUNT_MUTABLE
override val fragmentClass = AccountStatsFragment::class.java
}

View File

@ -0,0 +1,68 @@
package org.mariotaku.twidere.task.worker
import android.accounts.AccountManager
import androidx.work.Worker
import org.mariotaku.microblog.library.Mastodon
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.content.database.TwidereDatabase
import org.mariotaku.twidere.content.database.dao.AccountDailyStats
import org.mariotaku.twidere.content.model.AccountStats
import org.mariotaku.twidere.extension.getAllDetails
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import java.text.SimpleDateFormat
import java.util.*
class AccountDailyStatWorker : Worker() {
override fun doWork(): WorkerResult {
val am = AccountManager.get(applicationContext)
val dao = TwidereDatabase.get(applicationContext).accountDailyStats()
val date = Date()
val existingStats = dao.list(date)
val accounts = am.getAllDetails(true).filterNot { account ->
existingStats.any { it.accountKey == account.key }
}
val stats = accounts.mapNotNull {
val user = try {
when (it.type) {
AccountType.MASTODON -> it.newMicroBlogInstance(applicationContext, Mastodon::class.java)
.verifyCredentials().toParcelable(it.key)
else -> it.newMicroBlogInstance(applicationContext, MicroBlog::class.java)
.verifyCredentials().toParcelable(it.key, it.type)
}
} catch (e: MicroBlogException) {
return@mapNotNull null
}
return@mapNotNull AccountStats(createdAt = date, accountKey = it.key,
statusesCount = user.statuses_count, followersCount = user.followers_count)
}
dao.insert(stats)
return WorkerResult.SUCCESS
}
private fun AccountDailyStats.insertTestData(stats: List<AccountStats>) {
val cal = Calendar.getInstance()
val date = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse("2018-05-14")
var i = 1
var s = stats
while (i < 90) {
cal.time = date
cal.add(Calendar.DATE, -i)
s = s.map {
it.copy(
rowId = 0,
createdAt = cal.time,
statusesCount = (it.statusesCount - Math.random() * 30).toLong().coerceAtLeast(0),
followersCount = ((it.followersCount + Math.random() * 20).toLong() - 10).coerceAtLeast(0)
)
}
insert(s)
i++
}
}
}

View File

@ -0,0 +1,131 @@
package org.mariotaku.twidere.view.chart
import android.content.Context
import android.databinding.BindingMethod
import android.databinding.BindingMethods
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import org.mariotaku.twidere.R
@BindingMethods(
BindingMethod(type = LineChartView::class, attribute = "chartValues", method = "setValues"),
BindingMethod(type = LineChartView::class, attribute = "chartValuesCount", method = "setValuesCount")
)
class LineChartView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var values: FloatArray? = null
set(value) {
field = value
updatePath()
invalidate()
}
var valuesCount: Int = 0
set(value) {
field = value
updatePath()
invalidate()
}
private var chartLineColor: Int = Color.LTGRAY
set(value) {
field = value
linePaint.color = value
}
private var chartLineSize: Float = 1f
set(value) {
field = value
linePaint.strokeWidth = value
}
private var chartBaseline: Float
private var chartBaselineColor: Int = Color.LTGRAY
set(value) {
field = value
baselinePaint.color = value
}
private var chartBaselineSize: Float = 1f
set(value) {
field = value
baselinePaint.strokeWidth = value
}
private var chartEndpointColor: Int = Color.LTGRAY
set(value) {
field = value
endpointPaint.color = value
}
private var chartEndpointSize: Float = 2f
set(value) {
field = value
endpointPaint.strokeWidth = value
invalidate()
}
private val linePaint = createPaint()
private val baselinePaint = createPaint()
private val endpointPaint = createPaint()
private val path: Path = Path()
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.LineChartView)
chartBaseline = a.getDimension(R.styleable.LineChartView_chartBaseline, 0f)
chartBaselineColor = a.getColor(R.styleable.LineChartView_chartBaselineColor, Color.LTGRAY)
chartBaselineSize = a.getDimension(R.styleable.LineChartView_chartBaselineSize, 1f)
chartLineColor = a.getColor(R.styleable.LineChartView_chartLineColor, Color.LTGRAY)
chartLineSize = a.getDimension(R.styleable.LineChartView_chartLineSize, 1f)
chartEndpointColor = a.getColor(R.styleable.LineChartView_chartEndpointColor, Color.DKGRAY)
chartEndpointSize = a.getDimension(R.styleable.LineChartView_chartEndpointSize, 2f)
a.recycle()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawPath(path, linePaint)
canvas.drawLine(0f, height - chartBaseline, width.toFloat(),
height - chartBaseline, baselinePaint)
val values = this.values
if (values != null && values.isNotEmpty()) {
val x = width * (values.lastIndex / (valuesCount - 1f))
val y = (height - chartBaseline) * (1 - values.last())
canvas.drawPoint(x, y, endpointPaint)
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
updatePath()
}
private fun updatePath() {
if (width <= 0 || height <= 0) return
path.reset()
if (valuesCount <= 1) return
val values = this.values ?: return
values.forEachIndexed { index, value ->
val x = width * (index / (valuesCount - 1f))
val y = (height - chartBaseline) * (1 - value)
if (index == 0) {
path.moveTo(x, y)
} else {
path.lineTo(x, y)
}
}
}
companion object {
private fun createPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
}
}
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="summary"
type="org.mariotaku.twidere.content.model.AccountStats.DisplaySummary" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="16dp">
<TextView
android:id="@+id/statTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceSmall"
app:layout_constraintStart_toStartOf="parent"
tools:text="Tweets" />
<TextView
android:id="@+id/statValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@{summary.numberDisplay}"
android:textAppearance="?android:textAppearanceLarge"
app:layout_constraintTop_toBottomOf="@+id/statTitle"
tools:text="335" />
<TextView
android:id="@+id/statGrowth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{summary.growthDisplay}"
android:textAppearance="?android:textAppearanceMedium"
app:layout_constraintBaseline_toBaselineOf="@+id/statValue"
app:layout_constraintEnd_toEndOf="parent"
tools:text="+204.5%"
tools:textColor="@color/material_light_green" />
<android.support.constraint.Barrier
android:id="@+id/statBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="statValue,statGrowth" />
<org.mariotaku.twidere.view.chart.LineChartView
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginTop="8dp"
app:chartBaseline="16dp"
app:chartBaselineColor="#ccd6dd"
app:chartBaselineSize="1dp"
app:chartEndpointColor="#a6d388"
app:chartEndpointSize="6dp"
app:chartLineColor="#ccd6dd"
app:chartLineSize="2dp"
app:layout_constraintTop_toBottomOf="@+id/statBarrier"
bind:chartValues="@{summary.values}"
bind:chartValuesCount="@{summary.valuesCount}" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</layout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:id="@+id/statsScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/statsScrollContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</android.support.v4.widget.NestedScrollView>
</android.support.constraint.ConstraintLayout>

View File

@ -246,7 +246,7 @@
android:baselineAligned="false"
android:orientation="horizontal"
android:splitMotionEvents="false"
app:layout_constraintTop_toBottomOf="@+id/createdAt">
app:layout_constraintTop_toBottomOf="@+id/date">
<org.mariotaku.twidere.view.TwoLineTextView
android:id="@+id/followersCount"

View File

@ -2,231 +2,240 @@
<resources>
<declare-styleable name="Twidere">
<attr name="cardActionButtonStyle" format="reference"/>
<attr name="profileImageStyle" format="reference"/>
<attr name="profileImageStyleLarge" format="reference"/>
<attr name="menuIconColor" format="color"/>
<attr name="menuIconColorDisabled" format="color"/>
<attr name="menuIconColorActionBar" format="color"/>
<attr name="messageBubbleColor" format="color"/>
<attr name="cardItemBackgroundColor" format="color"/>
<attr name="quoteIndicatorBackgroundColor" format="color"/>
<attr name="linePageIndicatorStyle" format="reference"/>
<attr name="mediaLabelBackground" format="color"/>
<attr name="isDialogTheme" format="boolean"/>
<attr name="cardActionButtonStyle" format="reference" />
<attr name="profileImageStyle" format="reference" />
<attr name="profileImageStyleLarge" format="reference" />
<attr name="menuIconColor" format="color" />
<attr name="menuIconColorDisabled" format="color" />
<attr name="menuIconColorActionBar" format="color" />
<attr name="messageBubbleColor" format="color" />
<attr name="cardItemBackgroundColor" format="color" />
<attr name="quoteIndicatorBackgroundColor" format="color" />
<attr name="linePageIndicatorStyle" format="reference" />
<attr name="mediaLabelBackground" format="color" />
<attr name="isDialogTheme" format="boolean" />
</declare-styleable>
<declare-styleable name="TwidereTheme">
<attr name="darkThemeResource" format="reference"/>
<attr name="lightThemeResource" format="reference"/>
<attr name="darkThemeResource" format="reference" />
<attr name="lightThemeResource" format="reference" />
</declare-styleable>
<declare-styleable name="ColorLabelView">
<attr name="ignorePadding" format="boolean"/>
<attr name="backgroundColor" format="color"/>
<attr name="ignorePadding" format="boolean" />
<attr name="backgroundColor" format="color" />
</declare-styleable>
<declare-styleable name="ColorPickerPreferences">
<attr name="defaultColor" format="color"/>
<attr name="alphaSlider" format="boolean"/>
<attr name="defaultColor" format="color" />
<attr name="alphaSlider" format="boolean" />
</declare-styleable>
<declare-styleable name="TabPagerIndicator">
<attr name="tabStripColor" format="color"/>
<attr name="tabIconColor" format="color"/>
<attr name="tabLabelColor" format="color"/>
<attr name="tabStripHeight" format="dimension"/>
<attr name="tabHorizontalPadding" format="dimension"/>
<attr name="tabVerticalPadding" format="dimension"/>
<attr name="tabDividerVerticalPadding" format="dimension"/>
<attr name="tabDividerHorizontalPadding" format="dimension"/>
<attr name="tabExpandEnabled" format="boolean"/>
<attr name="tabShowDivider" format="boolean"/>
<attr name="tabDisplayOption"/>
<attr name="tabStripColor" format="color" />
<attr name="tabIconColor" format="color" />
<attr name="tabLabelColor" format="color" />
<attr name="tabStripHeight" format="dimension" />
<attr name="tabHorizontalPadding" format="dimension" />
<attr name="tabVerticalPadding" format="dimension" />
<attr name="tabDividerVerticalPadding" format="dimension" />
<attr name="tabDividerHorizontalPadding" format="dimension" />
<attr name="tabExpandEnabled" format="boolean" />
<attr name="tabShowDivider" format="boolean" />
<attr name="tabDisplayOption" />
</declare-styleable>
<declare-styleable name="NameView">
<attr name="nvNameFirst" format="boolean"/>
<attr name="nvNameFirst" format="boolean" />
</declare-styleable>
<declare-styleable name="TwoLineTextView">
<attr name="tltvTwoLine" format="boolean"/>
<attr name="tltvPrimaryTextAppearance" format="reference"/>
<attr name="tltvSecondaryTextAppearance" format="reference"/>
<attr name="tltvPrimaryText" format="string"/>
<attr name="tltvSecondaryText" format="string"/>
<attr name="tltvPrimaryTextColor" format="color"/>
<attr name="tltvSecondaryTextColor" format="color"/>
<attr name="tltvPrimaryLinkTextColor" format="color"/>
<attr name="tltvSecondaryLinkTextColor" format="color"/>
<attr name="tltvPrimaryTextStyle"/>
<attr name="tltvSecondaryTextStyle"/>
<attr name="tltvPrimaryTextSize" format="dimension"/>
<attr name="tltvSecondaryTextSize" format="dimension"/>
<attr name="tltvTwoLine" format="boolean" />
<attr name="tltvPrimaryTextAppearance" format="reference" />
<attr name="tltvSecondaryTextAppearance" format="reference" />
<attr name="tltvPrimaryText" format="string" />
<attr name="tltvSecondaryText" format="string" />
<attr name="tltvPrimaryTextColor" format="color" />
<attr name="tltvSecondaryTextColor" format="color" />
<attr name="tltvPrimaryLinkTextColor" format="color" />
<attr name="tltvSecondaryLinkTextColor" format="color" />
<attr name="tltvPrimaryTextStyle" />
<attr name="tltvSecondaryTextStyle" />
<attr name="tltvPrimaryTextSize" format="dimension" />
<attr name="tltvSecondaryTextSize" format="dimension" />
</declare-styleable>
<declare-styleable name="ValueDependencyPreference">
<attr name="dependencyKey" format="string"/>
<attr name="dependencyValues" format="reference"/>
<attr name="dependencyValueDefault" format="string"/>
<attr name="dependencyKey" format="string" />
<attr name="dependencyValues" format="reference" />
<attr name="dependencyValueDefault" format="string" />
</declare-styleable>
<declare-styleable name="AccountsListPreference">
<attr name="switchKey" format="string"/>
<attr name="switchDefault" format="boolean"/>
<attr name="switchKey" format="string" />
<attr name="switchDefault" format="boolean" />
</declare-styleable>
<declare-styleable name="NotificationTypePreference">
<attr name="notificationType"/>
<attr name="notificationType" />
</declare-styleable>
<declare-styleable name="SeekBarDialogPreference">
<attr name="max" format="integer"/>
<attr name="min" format="integer"/>
<attr name="step" format="integer"/>
<attr name="progressTextSuffix" format="string"/>
<attr name="max" format="integer" />
<attr name="min" format="integer" />
<attr name="step" format="integer" />
<attr name="progressTextSuffix" format="string" />
</declare-styleable>
<declare-styleable name="KeyboardShortcutPreference">
<attr name="android:action"/>
<attr name="android:tag"/>
<attr name="android:action" />
<attr name="android:tag" />
</declare-styleable>
<declare-styleable name="SyncItemPreference">
<attr name="syncType" format="string"/>
<attr name="syncType" format="string" />
</declare-styleable>
<declare-styleable name="DrawableTintTextView">
<attr name="drawableTint" format="reference"/>
<attr name="drawableTint" format="reference" />
</declare-styleable>
<declare-styleable name="ShapedImageView">
<attr name="sivBorder" format="boolean"/>
<attr name="sivBorderWidth" format="dimension"/>
<attr name="sivBorderColor" format="color"/>
<attr name="sivBackgroundColor" format="color"/>
<attr name="sivElevation" format="dimension"/>
<attr name="sivCornerRadius" format="dimension"/>
<attr name="sivCornerRadiusRatio" format="fraction"/>
<attr name="sivDrawShadow" format="boolean"/>
<attr name="sivShape"/>
<attr name="sivBorder" format="boolean" />
<attr name="sivBorderWidth" format="dimension" />
<attr name="sivBorderColor" format="color" />
<attr name="sivBackgroundColor" format="color" />
<attr name="sivElevation" format="dimension" />
<attr name="sivCornerRadius" format="dimension" />
<attr name="sivCornerRadiusRatio" format="fraction" />
<attr name="sivDrawShadow" format="boolean" />
<attr name="sivShape" />
</declare-styleable>
<declare-styleable name="TintedStatusLayout">
<attr name="setPadding" format="boolean"/>
<attr name="setPadding" format="boolean" />
</declare-styleable>
<declare-styleable name="BadgeView">
<attr name="android:textColor"/>
<attr name="android:text"/>
<attr name="android:textColor" />
<attr name="android:text" />
</declare-styleable>
<declare-styleable name="CardMediaContainer">
<attr name="android:horizontalSpacing"/>
<attr name="android:verticalSpacing"/>
<attr name="android:layout"/>
<attr name="android:horizontalSpacing" />
<attr name="android:verticalSpacing" />
<attr name="android:layout" />
</declare-styleable>
<declare-styleable name="MessageViewHolder">
<attr name="android:textColorPrimary"/>
<attr name="android:textColorPrimaryInverse"/>
<attr name="android:textColorSecondary"/>
<attr name="android:textColorSecondaryInverse"/>
<attr name="android:textColorPrimary" />
<attr name="android:textColorPrimaryInverse" />
<attr name="android:textColorSecondary" />
<attr name="android:textColorSecondaryInverse" />
</declare-styleable>
<declare-styleable name="RingtonePreference">
<attr name="android:ringtoneType"/>
<attr name="android:showDefault"/>
<attr name="android:showSilent"/>
<attr name="android:ringtoneType" />
<attr name="android:showDefault" />
<attr name="android:showSilent" />
</declare-styleable>
<declare-styleable name="ForegroundView">
<attr name="android:foreground"/>
<attr name="android:foregroundGravity"/>
<attr name="android:foreground" />
<attr name="android:foregroundGravity" />
</declare-styleable>
<declare-styleable name="AccountDashboardHeaderView">
<attr name="sizeMeasurementId" format="reference"/>
<attr name="sizeMeasurementId" format="reference" />
</declare-styleable>
<declare-styleable name="PremiumEntryPreference">
<attr name="requiredFeature" format="string"/>
<attr name="requiredFeature" format="string" />
</declare-styleable>
<declare-styleable name="ProfileBannerSpace">
<attr name="bannerAspectRatio"/>
<attr name="bannerAspectRatio" />
</declare-styleable>
<declare-styleable name="ProfileBannerImageView">
<attr name="bannerAspectRatio"/>
<attr name="bannerAspectRatio" />
</declare-styleable>
<declare-styleable name="AppBarChildBehavior">
<attr name="behavior_appBarId" format="reference"/>
<attr name="behavior_toolbarId" format="reference"/>
<attr name="behavior_dependencyViewId" format="reference"/>
<attr name="behavior_targetViewId" format="reference"/>
<attr name="behavior_appBarId" format="reference" />
<attr name="behavior_toolbarId" format="reference" />
<attr name="behavior_dependencyViewId" format="reference" />
<attr name="behavior_targetViewId" format="reference" />
<attr name="behavior_marginLeft" format="dimension"/>
<attr name="behavior_marginRight" format="dimension"/>
<attr name="behavior_marginStart" format="dimension"/>
<attr name="behavior_marginEnd" format="dimension"/>
<attr name="behavior_marginTop" format="dimension"/>
<attr name="behavior_marginBottom" format="dimension"/>
<attr name="behavior_marginLeft" format="dimension" />
<attr name="behavior_marginRight" format="dimension" />
<attr name="behavior_marginStart" format="dimension" />
<attr name="behavior_marginEnd" format="dimension" />
<attr name="behavior_marginTop" format="dimension" />
<attr name="behavior_marginBottom" format="dimension" />
<attr name="behavior_alignmentRule"/>
<attr name="behavior_alignmentRule" />
<attr name="behavior_childTransformation" format="string"/>
<attr name="behavior_childTransformation" format="string" />
</declare-styleable>
<declare-styleable name="IExtendedViewPadding">
<attr name="android:paddingTop"/>
<attr name="android:paddingBottom"/>
<attr name="android:paddingLeft"/>
<attr name="android:paddingRight"/>
<attr name="android:paddingStart"/>
<attr name="android:paddingEnd"/>
<attr name="android:padding"/>
<attr name="android:paddingTop" />
<attr name="android:paddingBottom" />
<attr name="android:paddingLeft" />
<attr name="android:paddingRight" />
<attr name="android:paddingStart" />
<attr name="android:paddingEnd" />
<attr name="android:padding" />
</declare-styleable>
<declare-styleable name="MediaLayoutParams">
<attr name="layout_isMediaItemView" format="boolean"/>
<attr name="layout_videoViewId" format="reference"/>
<attr name="layout_isMediaItemView" format="boolean" />
<attr name="layout_videoViewId" format="reference" />
</declare-styleable>
<declare-styleable name="MaxHeightScrollView">
<attr name="android:maxHeight"/>
<attr name="android:maxHeight" />
</declare-styleable>
<declare-styleable name="LabeledImageButton">
<attr name="android:text"/>
<attr name="android:textAppearance"/>
<attr name="android:textColor"/>
<attr name="android:textSize"/>
<attr name="android:textStyle"/>
<attr name="android:drawablePadding"/>
<attr name="android:text" />
<attr name="android:textAppearance" />
<attr name="android:textColor" />
<attr name="android:textSize" />
<attr name="android:textStyle" />
<attr name="android:drawablePadding" />
</declare-styleable>
<declare-styleable name="BannerBehavior">
<attr name="bannerAspectRatio"/>
<attr name="bannerAspectRatio" />
</declare-styleable>
<declare-styleable name="LineChartView">
<attr name="chartBaseline" format="dimension" />
<attr name="chartBaselineColor" format="color" />
<attr name="chartLineColor" format="color" />
<attr name="chartLineSize" format="dimension" />
<attr name="chartBaselineSize" format="dimension" />
<attr name="chartEndpointColor" format="color" />
<attr name="chartEndpointSize" format="dimension" />
</declare-styleable>
<attr name="behavior_alignmentRule">
<flag name="left" value="0x00000001"/>
<flag name="right" value="0x00000002"/>
<flag name="toLeftOf" value="0x00000004"/>
<flag name="toRightOf" value="0x00000008"/>
<flag name="toLeftOfCenter" value="0x00000010"/>
<flag name="toRightOfCenter" value="0x00000020"/>
<flag name="centerHorizontal" value="0x00000003"/>
<flag name="start" value="0x00001001"/>
<flag name="end" value="0x00001002"/>
<flag name="toStartOf" value="0x00001004"/>
<flag name="toEndOf" value="0x00001008"/>
<flag name="toStartOfCenter" value="0x00001010"/>
<flag name="toEndOfCenter" value="0x00001020"/>
<flag name="top" value="0x00010000"/>
<flag name="bottom" value="0x00020000"/>
<flag name="above" value="0x00040000"/>
<flag name="below" value="0x00080000"/>
<flag name="aboveCenter" value="0x00100000"/>
<flag name="belowCenter" value="0x00200000"/>
<flag name="centerVertical" value="0x00030000"/>
<flag name="center" value="0x00030003"/>
<flag name="left" value="0x00000001" />
<flag name="right" value="0x00000002" />
<flag name="toLeftOf" value="0x00000004" />
<flag name="toRightOf" value="0x00000008" />
<flag name="toLeftOfCenter" value="0x00000010" />
<flag name="toRightOfCenter" value="0x00000020" />
<flag name="centerHorizontal" value="0x00000003" />
<flag name="start" value="0x00001001" />
<flag name="end" value="0x00001002" />
<flag name="toStartOf" value="0x00001004" />
<flag name="toEndOf" value="0x00001008" />
<flag name="toStartOfCenter" value="0x00001010" />
<flag name="toEndOfCenter" value="0x00001020" />
<flag name="top" value="0x00010000" />
<flag name="bottom" value="0x00020000" />
<flag name="above" value="0x00040000" />
<flag name="below" value="0x00080000" />
<flag name="aboveCenter" value="0x00100000" />
<flag name="belowCenter" value="0x00200000" />
<flag name="centerVertical" value="0x00030000" />
<flag name="center" value="0x00030003" />
</attr>
<attr name="notificationType">
<flag name="none" value="0"/>
<flag name="ringtone" value="1"/>
<flag name="vibration" value="2"/>
<flag name="light" value="4"/>
<flag name="none" value="0" />
<flag name="ringtone" value="1" />
<flag name="vibration" value="2" />
<flag name="light" value="4" />
</attr>
<attr name="tabDisplayOption">
<flag name="label" value="0x1"/>
<flag name="icon" value="0x2"/>
<flag name="label" value="0x1" />
<flag name="icon" value="0x2" />
</attr>
<attr name="sivShape">
<enum name="circle" value="0x1"/>
<enum name="rectangle" value="0x2"/>
<enum name="circle" value="0x1" />
<enum name="rectangle" value="0x2" />
</attr>
<attr name="tltvPrimaryTextStyle">
<flag name="normal" value="0"/>
<flag name="bold" value="1"/>
<flag name="italic" value="2"/>
<flag name="normal" value="0" />
<flag name="bold" value="1" />
<flag name="italic" value="2" />
</attr>
<attr name="tltvSecondaryTextStyle">
<flag name="normal" value="0"/>
<flag name="bold" value="1"/>
<flag name="italic" value="2"/>
<flag name="normal" value="0" />
<flag name="bold" value="1" />
<flag name="italic" value="2" />
</attr>
<attr name="bannerAspectRatio" format="fraction"/>
<attr name="bannerAspectRatio" format="fraction" />
</resources>

View File

@ -1316,6 +1316,7 @@
<string name="translators">Translators</string>
<string name="trends">Trends</string>
<string name="title_account_stats">Stats</string>
<string name="trends_location">Trends location</string>
<string name="trends_location_summary">Set location for local trends.</string>