WIP: account stats implementation
This commit is contained in:
parent
42361c9fbe
commit
8094bbec4e
|
@ -81,8 +81,10 @@ subprojects {
|
||||||
StethoBeanShellREPL : '0.5',
|
StethoBeanShellREPL : '0.5',
|
||||||
ArchLifecycleExtensions: '1.1.1',
|
ArchLifecycleExtensions: '1.1.1',
|
||||||
ArchPaging : '1.0.0',
|
ArchPaging : '1.0.0',
|
||||||
|
Room : '1.1.0',
|
||||||
ConstraintLayout : '1.1.0',
|
ConstraintLayout : '1.1.0',
|
||||||
MessageBubbleView : '2.1',
|
MessageBubbleView : '2.1',
|
||||||
|
WorkManager : '1.0.0-alpha01',
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
org.gradle.jvmargs=-Xmx3584m
|
org.gradle.jvmargs=-Xmx3584m
|
||||||
|
android.databinding.enableV2=true
|
|
@ -38,6 +38,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
CustomTabType.LIST_TIMELINE,
|
CustomTabType.LIST_TIMELINE,
|
||||||
CustomTabType.PUBLIC_TIMELINE,
|
CustomTabType.PUBLIC_TIMELINE,
|
||||||
CustomTabType.NETWORK_PUBLIC_TIMELINE,
|
CustomTabType.NETWORK_PUBLIC_TIMELINE,
|
||||||
|
CustomTabType.ACCOUNT_STATS,
|
||||||
})
|
})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface CustomTabType {
|
public @interface CustomTabType {
|
||||||
|
@ -52,4 +53,5 @@ public @interface CustomTabType {
|
||||||
String LIST_TIMELINE = "list_timeline";
|
String LIST_TIMELINE = "list_timeline";
|
||||||
String PUBLIC_TIMELINE = "public_timeline";
|
String PUBLIC_TIMELINE = "public_timeline";
|
||||||
String NETWORK_PUBLIC_TIMELINE = "network_public_timeline";
|
String NETWORK_PUBLIC_TIMELINE = "network_public_timeline";
|
||||||
|
String ACCOUNT_STATS = "account_stats";
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,8 @@ public class TabArguments implements Parcelable {
|
||||||
case CustomTabType.DIRECT_MESSAGES:
|
case CustomTabType.DIRECT_MESSAGES:
|
||||||
case CustomTabType.TRENDS_SUGGESTIONS:
|
case CustomTabType.TRENDS_SUGGESTIONS:
|
||||||
case CustomTabType.PUBLIC_TIMELINE:
|
case CustomTabType.PUBLIC_TIMELINE:
|
||||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
|
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
|
||||||
|
case CustomTabType.ACCOUNT_STATS: {
|
||||||
return LoganSquare.parse(json, TabArguments.class);
|
return LoganSquare.parse(json, TabArguments.class);
|
||||||
}
|
}
|
||||||
case CustomTabType.USER_TIMELINE:
|
case CustomTabType.USER_TIMELINE:
|
||||||
|
|
|
@ -57,6 +57,16 @@ android {
|
||||||
additionalParameters '--no-version-vectors'
|
additionalParameters '--no-version-vectors'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
flavorDimensions 'channel'
|
flavorDimensions 'channel'
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
|
@ -164,6 +174,7 @@ dependencies {
|
||||||
kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}"
|
kapt "com.google.dagger:dagger-compiler:${libVersions['Dagger']}"
|
||||||
kapt "com.github.mariotaku.ObjectCursor:processor:${libVersions['ObjectCursor']}"
|
kapt "com.github.mariotaku.ObjectCursor:processor:${libVersions['ObjectCursor']}"
|
||||||
kapt "com.github.bumptech.glide:compiler:${libVersions['Glide']}"
|
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.common')
|
||||||
implementation project(':twidere.component.nyan')
|
implementation project(':twidere.component.nyan')
|
||||||
|
@ -204,6 +215,8 @@ dependencies {
|
||||||
|
|
||||||
implementation "android.arch.lifecycle:extensions:${libVersions['ArchLifecycleExtensions']}"
|
implementation "android.arch.lifecycle:extensions:${libVersions['ArchLifecycleExtensions']}"
|
||||||
implementation "android.arch.paging:runtime:${libVersions['ArchPaging']}"
|
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:multidex:${libVersions['MultiDex']}"
|
||||||
implementation "com.android.support:support-annotations:${libVersions['SupportLib']}"
|
implementation "com.android.support:support-annotations:${libVersions['SupportLib']}"
|
||||||
implementation "com.android.support:support-compat:${libVersions['SupportLib']}"
|
implementation "com.android.support:support-compat:${libVersions['SupportLib']}"
|
||||||
|
|
|
@ -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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,7 +126,8 @@ public class CustomTabUtils implements Constants {
|
||||||
case CustomTabType.DIRECT_MESSAGES:
|
case CustomTabType.DIRECT_MESSAGES:
|
||||||
case CustomTabType.TRENDS_SUGGESTIONS:
|
case CustomTabType.TRENDS_SUGGESTIONS:
|
||||||
case CustomTabType.PUBLIC_TIMELINE:
|
case CustomTabType.PUBLIC_TIMELINE:
|
||||||
case CustomTabType.NETWORK_PUBLIC_TIMELINE: {
|
case CustomTabType.NETWORK_PUBLIC_TIMELINE:
|
||||||
|
case CustomTabType.ACCOUNT_STATS: {
|
||||||
return new TabArguments();
|
return new TabArguments();
|
||||||
}
|
}
|
||||||
case CustomTabType.USER_TIMELINE:
|
case CustomTabType.USER_TIMELINE:
|
||||||
|
|
|
@ -62,6 +62,8 @@ import android.view.View.OnClickListener
|
||||||
import android.view.View.OnLongClickListener
|
import android.view.View.OnLongClickListener
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.ktx.OneTimeWorkRequestBuilder
|
||||||
import com.getkeepsafe.taptargetview.TapTarget
|
import com.getkeepsafe.taptargetview.TapTarget
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.squareup.otto.Subscribe
|
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.service.StreamingService
|
||||||
import org.mariotaku.twidere.singleton.BusSingleton
|
import org.mariotaku.twidere.singleton.BusSingleton
|
||||||
import org.mariotaku.twidere.singleton.PreferencesSingleton
|
import org.mariotaku.twidere.singleton.PreferencesSingleton
|
||||||
|
import org.mariotaku.twidere.task.worker.AccountDailyStatWorker
|
||||||
import org.mariotaku.twidere.util.*
|
import org.mariotaku.twidere.util.*
|
||||||
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
|
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
|
||||||
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
|
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
|
||||||
|
@ -293,6 +296,8 @@ class HomeActivity : BaseActivity(), OnClickListener, OnPageChangeListener, Supp
|
||||||
if (!showDrawerTutorial() && !PreferencesSingleton.get(this)[defaultAutoRefreshAskedKey]) {
|
if (!showDrawerTutorial() && !PreferencesSingleton.get(this)[defaultAutoRefreshAskedKey]) {
|
||||||
showAutoRefreshConfirm()
|
showAutoRefreshConfirm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkManager.getInstance().enqueue(OneTimeWorkRequestBuilder<AccountDailyStatWorker>().build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import android.support.annotation.WorkerThread
|
||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
import nl.komponents.kovenant.ui.failUi
|
import nl.komponents.kovenant.ui.failUi
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
|
import org.mariotaku.twidere.util.DebugLog
|
||||||
|
|
||||||
abstract class ComputableLiveData<T>(loadOnInstantiate: Boolean) : LiveData<T>() {
|
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 {
|
task(body = this::compute).successUi {
|
||||||
postValue(it)
|
postValue(it)
|
||||||
}.failUi {
|
}.failUi {
|
||||||
|
DebugLog.e(msg = "Exception in ComputableLiveData", tr = it)
|
||||||
postValue(null)
|
postValue(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,54 @@
|
||||||
package org.mariotaku.twidere.extension
|
package org.mariotaku.twidere.extension
|
||||||
|
|
||||||
import java.util.*
|
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 {
|
fun Calendar.isSameDay(that: Calendar): Boolean {
|
||||||
return this[Calendar.ERA] == that[Calendar.ERA] &&
|
return this[Calendar.ERA] == that[Calendar.ERA] &&
|
||||||
this[Calendar.YEAR] == that[Calendar.YEAR] &&
|
this[Calendar.YEAR] == that[Calendar.YEAR] &&
|
||||||
this[Calendar.DAY_OF_YEAR] == that[Calendar.DAY_OF_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
|
|
@ -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)
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -342,8 +342,9 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
|
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
|
||||||
|
val context = context ?: return
|
||||||
val firstVisiblePosition = positionBackup.getAndSet(null) ?: 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)
|
scrollToPositionWithOffset(0, 0)
|
||||||
} else {
|
} else {
|
||||||
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
|
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
|
||||||
|
|
|
@ -150,7 +150,8 @@ abstract class TabConfiguration {
|
||||||
CustomTabType.TRENDS_SUGGESTIONS, CustomTabType.DIRECT_MESSAGES,
|
CustomTabType.TRENDS_SUGGESTIONS, CustomTabType.DIRECT_MESSAGES,
|
||||||
CustomTabType.FAVORITES, CustomTabType.USER_TIMELINE, CustomTabType.USER_MEDIA_TIMELINE,
|
CustomTabType.FAVORITES, CustomTabType.USER_TIMELINE, CustomTabType.USER_MEDIA_TIMELINE,
|
||||||
CustomTabType.SEARCH_STATUSES, CustomTabType.LIST_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 all: List<Pair<String, TabConfiguration>> = allTypes.mapNotNull {
|
||||||
val conf = ofType(it) ?: return@mapNotNull null
|
val conf = ofType(it) ?: return@mapNotNull null
|
||||||
|
@ -170,6 +171,7 @@ abstract class TabConfiguration {
|
||||||
CustomTabType.SEARCH_STATUSES -> SearchTabConfiguration()
|
CustomTabType.SEARCH_STATUSES -> SearchTabConfiguration()
|
||||||
CustomTabType.PUBLIC_TIMELINE -> PublicTimelineTabConfiguration()
|
CustomTabType.PUBLIC_TIMELINE -> PublicTimelineTabConfiguration()
|
||||||
CustomTabType.NETWORK_PUBLIC_TIMELINE -> NetworkPublicTimelineTabConfiguration()
|
CustomTabType.NETWORK_PUBLIC_TIMELINE -> NetworkPublicTimelineTabConfiguration()
|
||||||
|
CustomTabType.ACCOUNT_STATS -> AccountStatsConfiguration()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -246,7 +246,7 @@
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:splitMotionEvents="false"
|
android:splitMotionEvents="false"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/createdAt">
|
app:layout_constraintTop_toBottomOf="@+id/date">
|
||||||
|
|
||||||
<org.mariotaku.twidere.view.TwoLineTextView
|
<org.mariotaku.twidere.view.TwoLineTextView
|
||||||
android:id="@+id/followersCount"
|
android:id="@+id/followersCount"
|
||||||
|
|
|
@ -2,231 +2,240 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<declare-styleable name="Twidere">
|
<declare-styleable name="Twidere">
|
||||||
<attr name="cardActionButtonStyle" format="reference"/>
|
<attr name="cardActionButtonStyle" format="reference" />
|
||||||
<attr name="profileImageStyle" format="reference"/>
|
<attr name="profileImageStyle" format="reference" />
|
||||||
<attr name="profileImageStyleLarge" format="reference"/>
|
<attr name="profileImageStyleLarge" format="reference" />
|
||||||
<attr name="menuIconColor" format="color"/>
|
<attr name="menuIconColor" format="color" />
|
||||||
<attr name="menuIconColorDisabled" format="color"/>
|
<attr name="menuIconColorDisabled" format="color" />
|
||||||
<attr name="menuIconColorActionBar" format="color"/>
|
<attr name="menuIconColorActionBar" format="color" />
|
||||||
<attr name="messageBubbleColor" format="color"/>
|
<attr name="messageBubbleColor" format="color" />
|
||||||
<attr name="cardItemBackgroundColor" format="color"/>
|
<attr name="cardItemBackgroundColor" format="color" />
|
||||||
<attr name="quoteIndicatorBackgroundColor" format="color"/>
|
<attr name="quoteIndicatorBackgroundColor" format="color" />
|
||||||
<attr name="linePageIndicatorStyle" format="reference"/>
|
<attr name="linePageIndicatorStyle" format="reference" />
|
||||||
<attr name="mediaLabelBackground" format="color"/>
|
<attr name="mediaLabelBackground" format="color" />
|
||||||
<attr name="isDialogTheme" format="boolean"/>
|
<attr name="isDialogTheme" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="TwidereTheme">
|
<declare-styleable name="TwidereTheme">
|
||||||
<attr name="darkThemeResource" format="reference"/>
|
<attr name="darkThemeResource" format="reference" />
|
||||||
<attr name="lightThemeResource" format="reference"/>
|
<attr name="lightThemeResource" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="ColorLabelView">
|
<declare-styleable name="ColorLabelView">
|
||||||
<attr name="ignorePadding" format="boolean"/>
|
<attr name="ignorePadding" format="boolean" />
|
||||||
<attr name="backgroundColor" format="color"/>
|
<attr name="backgroundColor" format="color" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ColorPickerPreferences">
|
<declare-styleable name="ColorPickerPreferences">
|
||||||
<attr name="defaultColor" format="color"/>
|
<attr name="defaultColor" format="color" />
|
||||||
<attr name="alphaSlider" format="boolean"/>
|
<attr name="alphaSlider" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="TabPagerIndicator">
|
<declare-styleable name="TabPagerIndicator">
|
||||||
<attr name="tabStripColor" format="color"/>
|
<attr name="tabStripColor" format="color" />
|
||||||
<attr name="tabIconColor" format="color"/>
|
<attr name="tabIconColor" format="color" />
|
||||||
<attr name="tabLabelColor" format="color"/>
|
<attr name="tabLabelColor" format="color" />
|
||||||
<attr name="tabStripHeight" format="dimension"/>
|
<attr name="tabStripHeight" format="dimension" />
|
||||||
<attr name="tabHorizontalPadding" format="dimension"/>
|
<attr name="tabHorizontalPadding" format="dimension" />
|
||||||
<attr name="tabVerticalPadding" format="dimension"/>
|
<attr name="tabVerticalPadding" format="dimension" />
|
||||||
<attr name="tabDividerVerticalPadding" format="dimension"/>
|
<attr name="tabDividerVerticalPadding" format="dimension" />
|
||||||
<attr name="tabDividerHorizontalPadding" format="dimension"/>
|
<attr name="tabDividerHorizontalPadding" format="dimension" />
|
||||||
<attr name="tabExpandEnabled" format="boolean"/>
|
<attr name="tabExpandEnabled" format="boolean" />
|
||||||
<attr name="tabShowDivider" format="boolean"/>
|
<attr name="tabShowDivider" format="boolean" />
|
||||||
<attr name="tabDisplayOption"/>
|
<attr name="tabDisplayOption" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="NameView">
|
<declare-styleable name="NameView">
|
||||||
<attr name="nvNameFirst" format="boolean"/>
|
<attr name="nvNameFirst" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="TwoLineTextView">
|
<declare-styleable name="TwoLineTextView">
|
||||||
<attr name="tltvTwoLine" format="boolean"/>
|
<attr name="tltvTwoLine" format="boolean" />
|
||||||
<attr name="tltvPrimaryTextAppearance" format="reference"/>
|
<attr name="tltvPrimaryTextAppearance" format="reference" />
|
||||||
<attr name="tltvSecondaryTextAppearance" format="reference"/>
|
<attr name="tltvSecondaryTextAppearance" format="reference" />
|
||||||
<attr name="tltvPrimaryText" format="string"/>
|
<attr name="tltvPrimaryText" format="string" />
|
||||||
<attr name="tltvSecondaryText" format="string"/>
|
<attr name="tltvSecondaryText" format="string" />
|
||||||
<attr name="tltvPrimaryTextColor" format="color"/>
|
<attr name="tltvPrimaryTextColor" format="color" />
|
||||||
<attr name="tltvSecondaryTextColor" format="color"/>
|
<attr name="tltvSecondaryTextColor" format="color" />
|
||||||
<attr name="tltvPrimaryLinkTextColor" format="color"/>
|
<attr name="tltvPrimaryLinkTextColor" format="color" />
|
||||||
<attr name="tltvSecondaryLinkTextColor" format="color"/>
|
<attr name="tltvSecondaryLinkTextColor" format="color" />
|
||||||
<attr name="tltvPrimaryTextStyle"/>
|
<attr name="tltvPrimaryTextStyle" />
|
||||||
<attr name="tltvSecondaryTextStyle"/>
|
<attr name="tltvSecondaryTextStyle" />
|
||||||
<attr name="tltvPrimaryTextSize" format="dimension"/>
|
<attr name="tltvPrimaryTextSize" format="dimension" />
|
||||||
<attr name="tltvSecondaryTextSize" format="dimension"/>
|
<attr name="tltvSecondaryTextSize" format="dimension" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ValueDependencyPreference">
|
<declare-styleable name="ValueDependencyPreference">
|
||||||
<attr name="dependencyKey" format="string"/>
|
<attr name="dependencyKey" format="string" />
|
||||||
<attr name="dependencyValues" format="reference"/>
|
<attr name="dependencyValues" format="reference" />
|
||||||
<attr name="dependencyValueDefault" format="string"/>
|
<attr name="dependencyValueDefault" format="string" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="AccountsListPreference">
|
<declare-styleable name="AccountsListPreference">
|
||||||
<attr name="switchKey" format="string"/>
|
<attr name="switchKey" format="string" />
|
||||||
<attr name="switchDefault" format="boolean"/>
|
<attr name="switchDefault" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="NotificationTypePreference">
|
<declare-styleable name="NotificationTypePreference">
|
||||||
<attr name="notificationType"/>
|
<attr name="notificationType" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="SeekBarDialogPreference">
|
<declare-styleable name="SeekBarDialogPreference">
|
||||||
<attr name="max" format="integer"/>
|
<attr name="max" format="integer" />
|
||||||
<attr name="min" format="integer"/>
|
<attr name="min" format="integer" />
|
||||||
<attr name="step" format="integer"/>
|
<attr name="step" format="integer" />
|
||||||
<attr name="progressTextSuffix" format="string"/>
|
<attr name="progressTextSuffix" format="string" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="KeyboardShortcutPreference">
|
<declare-styleable name="KeyboardShortcutPreference">
|
||||||
<attr name="android:action"/>
|
<attr name="android:action" />
|
||||||
<attr name="android:tag"/>
|
<attr name="android:tag" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="SyncItemPreference">
|
<declare-styleable name="SyncItemPreference">
|
||||||
<attr name="syncType" format="string"/>
|
<attr name="syncType" format="string" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="DrawableTintTextView">
|
<declare-styleable name="DrawableTintTextView">
|
||||||
<attr name="drawableTint" format="reference"/>
|
<attr name="drawableTint" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ShapedImageView">
|
<declare-styleable name="ShapedImageView">
|
||||||
<attr name="sivBorder" format="boolean"/>
|
<attr name="sivBorder" format="boolean" />
|
||||||
<attr name="sivBorderWidth" format="dimension"/>
|
<attr name="sivBorderWidth" format="dimension" />
|
||||||
<attr name="sivBorderColor" format="color"/>
|
<attr name="sivBorderColor" format="color" />
|
||||||
<attr name="sivBackgroundColor" format="color"/>
|
<attr name="sivBackgroundColor" format="color" />
|
||||||
<attr name="sivElevation" format="dimension"/>
|
<attr name="sivElevation" format="dimension" />
|
||||||
<attr name="sivCornerRadius" format="dimension"/>
|
<attr name="sivCornerRadius" format="dimension" />
|
||||||
<attr name="sivCornerRadiusRatio" format="fraction"/>
|
<attr name="sivCornerRadiusRatio" format="fraction" />
|
||||||
<attr name="sivDrawShadow" format="boolean"/>
|
<attr name="sivDrawShadow" format="boolean" />
|
||||||
<attr name="sivShape"/>
|
<attr name="sivShape" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="TintedStatusLayout">
|
<declare-styleable name="TintedStatusLayout">
|
||||||
<attr name="setPadding" format="boolean"/>
|
<attr name="setPadding" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="BadgeView">
|
<declare-styleable name="BadgeView">
|
||||||
<attr name="android:textColor"/>
|
<attr name="android:textColor" />
|
||||||
<attr name="android:text"/>
|
<attr name="android:text" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="CardMediaContainer">
|
<declare-styleable name="CardMediaContainer">
|
||||||
<attr name="android:horizontalSpacing"/>
|
<attr name="android:horizontalSpacing" />
|
||||||
<attr name="android:verticalSpacing"/>
|
<attr name="android:verticalSpacing" />
|
||||||
<attr name="android:layout"/>
|
<attr name="android:layout" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="MessageViewHolder">
|
<declare-styleable name="MessageViewHolder">
|
||||||
<attr name="android:textColorPrimary"/>
|
<attr name="android:textColorPrimary" />
|
||||||
<attr name="android:textColorPrimaryInverse"/>
|
<attr name="android:textColorPrimaryInverse" />
|
||||||
<attr name="android:textColorSecondary"/>
|
<attr name="android:textColorSecondary" />
|
||||||
<attr name="android:textColorSecondaryInverse"/>
|
<attr name="android:textColorSecondaryInverse" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="RingtonePreference">
|
<declare-styleable name="RingtonePreference">
|
||||||
<attr name="android:ringtoneType"/>
|
<attr name="android:ringtoneType" />
|
||||||
<attr name="android:showDefault"/>
|
<attr name="android:showDefault" />
|
||||||
<attr name="android:showSilent"/>
|
<attr name="android:showSilent" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ForegroundView">
|
<declare-styleable name="ForegroundView">
|
||||||
<attr name="android:foreground"/>
|
<attr name="android:foreground" />
|
||||||
<attr name="android:foregroundGravity"/>
|
<attr name="android:foregroundGravity" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="AccountDashboardHeaderView">
|
<declare-styleable name="AccountDashboardHeaderView">
|
||||||
<attr name="sizeMeasurementId" format="reference"/>
|
<attr name="sizeMeasurementId" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="PremiumEntryPreference">
|
<declare-styleable name="PremiumEntryPreference">
|
||||||
<attr name="requiredFeature" format="string"/>
|
<attr name="requiredFeature" format="string" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ProfileBannerSpace">
|
<declare-styleable name="ProfileBannerSpace">
|
||||||
<attr name="bannerAspectRatio"/>
|
<attr name="bannerAspectRatio" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="ProfileBannerImageView">
|
<declare-styleable name="ProfileBannerImageView">
|
||||||
<attr name="bannerAspectRatio"/>
|
<attr name="bannerAspectRatio" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="AppBarChildBehavior">
|
<declare-styleable name="AppBarChildBehavior">
|
||||||
<attr name="behavior_appBarId" format="reference"/>
|
<attr name="behavior_appBarId" format="reference" />
|
||||||
<attr name="behavior_toolbarId" format="reference"/>
|
<attr name="behavior_toolbarId" format="reference" />
|
||||||
<attr name="behavior_dependencyViewId" format="reference"/>
|
<attr name="behavior_dependencyViewId" format="reference" />
|
||||||
<attr name="behavior_targetViewId" format="reference"/>
|
<attr name="behavior_targetViewId" format="reference" />
|
||||||
|
|
||||||
<attr name="behavior_marginLeft" format="dimension"/>
|
<attr name="behavior_marginLeft" format="dimension" />
|
||||||
<attr name="behavior_marginRight" format="dimension"/>
|
<attr name="behavior_marginRight" format="dimension" />
|
||||||
<attr name="behavior_marginStart" format="dimension"/>
|
<attr name="behavior_marginStart" format="dimension" />
|
||||||
<attr name="behavior_marginEnd" format="dimension"/>
|
<attr name="behavior_marginEnd" format="dimension" />
|
||||||
<attr name="behavior_marginTop" format="dimension"/>
|
<attr name="behavior_marginTop" format="dimension" />
|
||||||
<attr name="behavior_marginBottom" 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>
|
||||||
<declare-styleable name="IExtendedViewPadding">
|
<declare-styleable name="IExtendedViewPadding">
|
||||||
<attr name="android:paddingTop"/>
|
<attr name="android:paddingTop" />
|
||||||
<attr name="android:paddingBottom"/>
|
<attr name="android:paddingBottom" />
|
||||||
<attr name="android:paddingLeft"/>
|
<attr name="android:paddingLeft" />
|
||||||
<attr name="android:paddingRight"/>
|
<attr name="android:paddingRight" />
|
||||||
<attr name="android:paddingStart"/>
|
<attr name="android:paddingStart" />
|
||||||
<attr name="android:paddingEnd"/>
|
<attr name="android:paddingEnd" />
|
||||||
<attr name="android:padding"/>
|
<attr name="android:padding" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="MediaLayoutParams">
|
<declare-styleable name="MediaLayoutParams">
|
||||||
<attr name="layout_isMediaItemView" format="boolean"/>
|
<attr name="layout_isMediaItemView" format="boolean" />
|
||||||
<attr name="layout_videoViewId" format="reference"/>
|
<attr name="layout_videoViewId" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="MaxHeightScrollView">
|
<declare-styleable name="MaxHeightScrollView">
|
||||||
<attr name="android:maxHeight"/>
|
<attr name="android:maxHeight" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="LabeledImageButton">
|
<declare-styleable name="LabeledImageButton">
|
||||||
<attr name="android:text"/>
|
<attr name="android:text" />
|
||||||
<attr name="android:textAppearance"/>
|
<attr name="android:textAppearance" />
|
||||||
<attr name="android:textColor"/>
|
<attr name="android:textColor" />
|
||||||
<attr name="android:textSize"/>
|
<attr name="android:textSize" />
|
||||||
<attr name="android:textStyle"/>
|
<attr name="android:textStyle" />
|
||||||
<attr name="android:drawablePadding"/>
|
<attr name="android:drawablePadding" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
<declare-styleable name="BannerBehavior">
|
<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>
|
</declare-styleable>
|
||||||
<attr name="behavior_alignmentRule">
|
<attr name="behavior_alignmentRule">
|
||||||
<flag name="left" value="0x00000001"/>
|
<flag name="left" value="0x00000001" />
|
||||||
<flag name="right" value="0x00000002"/>
|
<flag name="right" value="0x00000002" />
|
||||||
<flag name="toLeftOf" value="0x00000004"/>
|
<flag name="toLeftOf" value="0x00000004" />
|
||||||
<flag name="toRightOf" value="0x00000008"/>
|
<flag name="toRightOf" value="0x00000008" />
|
||||||
<flag name="toLeftOfCenter" value="0x00000010"/>
|
<flag name="toLeftOfCenter" value="0x00000010" />
|
||||||
<flag name="toRightOfCenter" value="0x00000020"/>
|
<flag name="toRightOfCenter" value="0x00000020" />
|
||||||
<flag name="centerHorizontal" value="0x00000003"/>
|
<flag name="centerHorizontal" value="0x00000003" />
|
||||||
<flag name="start" value="0x00001001"/>
|
<flag name="start" value="0x00001001" />
|
||||||
<flag name="end" value="0x00001002"/>
|
<flag name="end" value="0x00001002" />
|
||||||
<flag name="toStartOf" value="0x00001004"/>
|
<flag name="toStartOf" value="0x00001004" />
|
||||||
<flag name="toEndOf" value="0x00001008"/>
|
<flag name="toEndOf" value="0x00001008" />
|
||||||
<flag name="toStartOfCenter" value="0x00001010"/>
|
<flag name="toStartOfCenter" value="0x00001010" />
|
||||||
<flag name="toEndOfCenter" value="0x00001020"/>
|
<flag name="toEndOfCenter" value="0x00001020" />
|
||||||
<flag name="top" value="0x00010000"/>
|
<flag name="top" value="0x00010000" />
|
||||||
<flag name="bottom" value="0x00020000"/>
|
<flag name="bottom" value="0x00020000" />
|
||||||
<flag name="above" value="0x00040000"/>
|
<flag name="above" value="0x00040000" />
|
||||||
<flag name="below" value="0x00080000"/>
|
<flag name="below" value="0x00080000" />
|
||||||
<flag name="aboveCenter" value="0x00100000"/>
|
<flag name="aboveCenter" value="0x00100000" />
|
||||||
<flag name="belowCenter" value="0x00200000"/>
|
<flag name="belowCenter" value="0x00200000" />
|
||||||
<flag name="centerVertical" value="0x00030000"/>
|
<flag name="centerVertical" value="0x00030000" />
|
||||||
<flag name="center" value="0x00030003"/>
|
<flag name="center" value="0x00030003" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="notificationType">
|
<attr name="notificationType">
|
||||||
<flag name="none" value="0"/>
|
<flag name="none" value="0" />
|
||||||
<flag name="ringtone" value="1"/>
|
<flag name="ringtone" value="1" />
|
||||||
<flag name="vibration" value="2"/>
|
<flag name="vibration" value="2" />
|
||||||
<flag name="light" value="4"/>
|
<flag name="light" value="4" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="tabDisplayOption">
|
<attr name="tabDisplayOption">
|
||||||
<flag name="label" value="0x1"/>
|
<flag name="label" value="0x1" />
|
||||||
<flag name="icon" value="0x2"/>
|
<flag name="icon" value="0x2" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="sivShape">
|
<attr name="sivShape">
|
||||||
<enum name="circle" value="0x1"/>
|
<enum name="circle" value="0x1" />
|
||||||
<enum name="rectangle" value="0x2"/>
|
<enum name="rectangle" value="0x2" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="tltvPrimaryTextStyle">
|
<attr name="tltvPrimaryTextStyle">
|
||||||
<flag name="normal" value="0"/>
|
<flag name="normal" value="0" />
|
||||||
<flag name="bold" value="1"/>
|
<flag name="bold" value="1" />
|
||||||
<flag name="italic" value="2"/>
|
<flag name="italic" value="2" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="tltvSecondaryTextStyle">
|
<attr name="tltvSecondaryTextStyle">
|
||||||
<flag name="normal" value="0"/>
|
<flag name="normal" value="0" />
|
||||||
<flag name="bold" value="1"/>
|
<flag name="bold" value="1" />
|
||||||
<flag name="italic" value="2"/>
|
<flag name="italic" value="2" />
|
||||||
</attr>
|
</attr>
|
||||||
<attr name="bannerAspectRatio" format="fraction"/>
|
<attr name="bannerAspectRatio" format="fraction" />
|
||||||
</resources>
|
</resources>
|
|
@ -1316,6 +1316,7 @@
|
||||||
<string name="translators">Translators</string>
|
<string name="translators">Translators</string>
|
||||||
|
|
||||||
<string name="trends">Trends</string>
|
<string name="trends">Trends</string>
|
||||||
|
<string name="title_account_stats">Stats</string>
|
||||||
<string name="trends_location">Trends location</string>
|
<string name="trends_location">Trends location</string>
|
||||||
<string name="trends_location_summary">Set location for local trends.</string>
|
<string name="trends_location_summary">Set location for local trends.</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue