Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt

984 lines
46 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.*
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.provider.BaseColumns
import android.support.annotation.WorkerThread
import android.text.TextUtils
import com.bluelinelabs.logansquare.LoganSquare
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.StringUtils
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.useCursor
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Activity
import org.mariotaku.sqliteqb.library.*
import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.query.SQLSelectQuery
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.constant.databaseItemLimitKey
import org.mariotaku.twidere.extension.model.*
import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.tab.extra.HomeTabExtras
import org.mariotaku.twidere.model.tab.extra.InteractionsTabExtras
import org.mariotaku.twidere.model.tab.extra.TabExtras
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.provider.TwidereDataStore
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.util.content.ContentResolverUtils
import java.io.IOException
import java.util.*
/**
* Created by mariotaku on 15/11/28.
*/
object DataStoreUtils {
val STATUSES_URIS = arrayOf(Statuses.CONTENT_URI, CachedStatuses.CONTENT_URI)
val CACHE_URIS = arrayOf(CachedUsers.CONTENT_URI, CachedStatuses.CONTENT_URI, CachedHashtags.CONTENT_URI, CachedTrends.Local.CONTENT_URI)
val MESSAGES_URIS = arrayOf(Messages.CONTENT_URI, Conversations.CONTENT_URI)
val ACTIVITIES_URIS = arrayOf(Activities.AboutMe.CONTENT_URI)
private val CONTENT_PROVIDER_URI_MATCHER = UriMatcher(UriMatcher.NO_MATCH)
init {
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Statuses.CONTENT_PATH,
TABLE_ID_STATUSES)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Activities.AboutMe.CONTENT_PATH,
TABLE_ID_ACTIVITIES_ABOUT_ME)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH,
TABLE_ID_DRAFTS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedUsers.CONTENT_PATH,
TABLE_ID_CACHED_USERS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Filters.Users.CONTENT_PATH,
TABLE_ID_FILTERED_USERS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Filters.Keywords.CONTENT_PATH,
TABLE_ID_FILTERED_KEYWORDS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Filters.Sources.CONTENT_PATH,
TABLE_ID_FILTERED_SOURCES)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Filters.Links.CONTENT_PATH,
TABLE_ID_FILTERED_LINKS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Filters.Subscriptions.CONTENT_PATH,
TABLE_ID_FILTERS_SUBSCRIPTIONS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Messages.CONTENT_PATH,
TABLE_ID_MESSAGES)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Conversations.CONTENT_PATH,
TABLE_ID_MESSAGES_CONVERSATIONS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedTrends.Local.CONTENT_PATH,
TABLE_ID_TRENDS_LOCAL)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Tabs.CONTENT_PATH,
TABLE_ID_TABS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedStatuses.CONTENT_PATH,
TABLE_ID_CACHED_STATUSES)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedHashtags.CONTENT_PATH,
TABLE_ID_CACHED_HASHTAGS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedRelationships.CONTENT_PATH,
TABLE_ID_CACHED_RELATIONSHIPS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, SavedSearches.CONTENT_PATH,
TABLE_ID_SAVED_SEARCHES)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, SearchHistory.CONTENT_PATH,
TABLE_ID_SEARCH_HISTORY)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Permissions.CONTENT_PATH,
VIRTUAL_TABLE_ID_PERMISSIONS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedUsers.CONTENT_PATH_WITH_RELATIONSHIP + "/*",
VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, CachedUsers.CONTENT_PATH_WITH_SCORE + "/*",
VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH_UNSENT,
VIRTUAL_TABLE_ID_DRAFTS_UNSENT)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH_NOTIFICATIONS,
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Drafts.CONTENT_PATH_NOTIFICATIONS,
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Suggestions.AutoComplete.CONTENT_PATH,
VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, Suggestions.Search.CONTENT_PATH,
VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, TwidereDataStore.CONTENT_PATH_DATABASE_PREPARE,
VIRTUAL_TABLE_ID_DATABASE_PREPARE)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, TwidereDataStore.CONTENT_PATH_NULL,
VIRTUAL_TABLE_ID_NULL)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, TwidereDataStore.CONTENT_PATH_EMPTY,
VIRTUAL_TABLE_ID_EMPTY)
CONTENT_PROVIDER_URI_MATCHER.addURI(TwidereDataStore.AUTHORITY, TwidereDataStore.CONTENT_PATH_RAW_QUERY + "/*",
VIRTUAL_TABLE_ID_RAW_QUERY)
}
fun getNewestStatusIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>): Array<String?> {
return getStringFieldArray(context, uri, accountKeys, Statuses.ACCOUNT_KEY, Statuses.STATUS_ID,
OrderBy(SQLFunctions.MAX(Statuses.STATUS_TIMESTAMP)), null, null)
}
fun getNewestMessageIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>, outgoing: Boolean): Array<String?> {
val having: Expression = Expression.equals(Messages.IS_OUTGOING, if (outgoing) 1 else 0)
return getStringFieldArray(context, uri, accountKeys, Messages.ACCOUNT_KEY, Messages.MESSAGE_ID,
OrderBy(SQLFunctions.MAX(Messages.LOCAL_TIMESTAMP)), having, null)
}
fun getOldestMessageIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>, outgoing: Boolean): Array<String?> {
if (accountKeys.all { it == null }) return arrayOfNulls(accountKeys.size)
val having: Expression = Expression.equals(Messages.IS_OUTGOING, if (outgoing) 1 else 0)
return getStringFieldArray(context, uri, accountKeys, Messages.ACCOUNT_KEY, Messages.MESSAGE_ID,
OrderBy(SQLFunctions.MIN(Messages.LOCAL_TIMESTAMP)), having, null)
}
fun getOldestConversations(context: Context, uri: Uri, accountKeys: Array<UserKey?>): Array<ParcelableMessageConversation?> {
if (accountKeys.all { it == null }) return arrayOfNulls(accountKeys.size)
return getObjectFieldArray(context, uri, accountKeys, Conversations.ACCOUNT_KEY, Conversations.COLUMNS,
OrderBy(SQLFunctions.MIN(Messages.LOCAL_TIMESTAMP)), null, null,
{ ObjectCursor.indicesFrom(it, ParcelableMessageConversation::class.java) },
{ arrayOfNulls<ParcelableMessageConversation>(it) })
}
fun getNewestConversations(context: Context, uri: Uri, accountKeys: Array<UserKey?>,
extraWhere: Expression? = null, extraWhereArgs: Array<String>? = null): Array<ParcelableMessageConversation?> {
if (accountKeys.all { it == null }) return arrayOfNulls(accountKeys.size)
return getObjectFieldArray(context, uri, accountKeys, Conversations.ACCOUNT_KEY, Conversations.COLUMNS,
OrderBy(SQLFunctions.MAX(Messages.LOCAL_TIMESTAMP)), extraWhere, extraWhereArgs,
{ ObjectCursor.indicesFrom(it, ParcelableMessageConversation::class.java) },
{ arrayOfNulls<ParcelableMessageConversation>(it) })
}
fun getNewestStatusSortIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>): LongArray {
return getLongFieldArray(context, uri, accountKeys, Statuses.ACCOUNT_KEY, Statuses.SORT_ID,
OrderBy(SQLFunctions.MAX(Statuses.STATUS_TIMESTAMP)), null, null)
}
fun getOldestStatusIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>): Array<String?> {
return getStringFieldArray(context, uri, accountKeys, Statuses.ACCOUNT_KEY, Statuses.STATUS_ID,
OrderBy(SQLFunctions.MIN(Statuses.STATUS_TIMESTAMP)), null, null)
}
fun getOldestStatusSortIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>): LongArray {
return getLongFieldArray(context, uri, accountKeys, Statuses.ACCOUNT_KEY, Statuses.SORT_ID,
OrderBy(SQLFunctions.MIN(Statuses.STATUS_TIMESTAMP)), null, null)
}
fun getNewestActivityMaxPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>,
extraWhere: Expression?, extraWhereArgs: Array<String>?): Array<String?> {
return getStringFieldArray(context, uri, accountKeys, Activities.ACCOUNT_KEY,
Activities.MAX_REQUEST_POSITION, OrderBy(SQLFunctions.MAX(Activities.TIMESTAMP)),
extraWhere, extraWhereArgs)
}
fun getRefreshNewestActivityMaxPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>):
Array<String?> {
return getOfficialSeparatedIds(context, { accountKeys, isOfficial ->
val (where, whereArgs) = getIdsWhere(isOfficial)
DataStoreUtils.getNewestActivityMaxPositions(context, uri, accountKeys,
where, whereArgs)
}, { arr1, arr2 ->
Array(accountKeys.size) { arr1[it] ?: arr2[it] }
}, accountKeys)
}
fun getOldestActivityMaxPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>,
extraWhere: Expression?, extraWhereArgs: Array<String>?): Array<String?> {
return getStringFieldArray(context, uri, accountKeys, Activities.ACCOUNT_KEY,
Activities.MAX_REQUEST_POSITION, OrderBy(SQLFunctions.MIN(Activities.TIMESTAMP)),
extraWhere, extraWhereArgs)
}
fun getRefreshOldestActivityMaxPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>):
Array<String?> {
return getOfficialSeparatedIds(context, { accountKeys, isOfficial ->
val (where, whereArgs) = getIdsWhere(isOfficial)
DataStoreUtils.getOldestActivityMaxPositions(context, uri, accountKeys,
where, whereArgs)
}, { arr1, arr2 ->
Array(accountKeys.size) { arr1[it] ?: arr2[it] }
}, accountKeys)
}
fun getNewestActivityMaxSortPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>,
extraWhere: Expression?, extraWhereArgs: Array<String>?): LongArray {
return getLongFieldArray(context, uri, accountKeys, Activities.ACCOUNT_KEY,
Activities.MAX_SORT_POSITION, OrderBy(SQLFunctions.MAX(Activities.TIMESTAMP)),
extraWhere, extraWhereArgs)
}
fun getRefreshNewestActivityMaxSortPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>):
LongArray {
return getOfficialSeparatedIds(context, { accountKeys, isOfficial ->
val (where, whereArgs) = getIdsWhere(isOfficial)
DataStoreUtils.getNewestActivityMaxSortPositions(context, uri, accountKeys,
where, whereArgs)
}, { arr1, arr2 ->
LongArray(accountKeys.size) { arr1[it].takeIf { it > 0 } ?: arr2[it] }
}, accountKeys)
}
fun getOldestActivityMaxSortPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>,
extraWhere: Expression?, extraWhereArgs: Array<String>?): LongArray {
return getLongFieldArray(context, uri, accountKeys, Activities.ACCOUNT_KEY,
Activities.MAX_SORT_POSITION, OrderBy(SQLFunctions.MIN(Activities.TIMESTAMP)),
extraWhere, extraWhereArgs)
}
fun getRefreshOldestActivityMaxSortPositions(context: Context, uri: Uri, accountKeys: Array<UserKey?>):
LongArray {
return getOfficialSeparatedIds(context, { accountKeys, isOfficial ->
val (where, whereArgs) = getIdsWhere(isOfficial)
DataStoreUtils.getOldestActivityMaxSortPositions(context, uri, accountKeys,
where, whereArgs)
}, { arr1, arr2 ->
LongArray(accountKeys.size) { arr1[it].takeIf { it > 0 } ?: arr2[it] }
}, accountKeys)
}
fun getStatusCount(context: Context, uri: Uri, accountId: UserKey): Int {
val where = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY).sql
val whereArgs = arrayOf(accountId.toString())
return queryCount(context.contentResolver, uri, where, whereArgs)
}
fun getActivitiesCount(context: Context, uri: Uri,
accountKey: UserKey): Int {
val where = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY).sql
return queryCount(context.contentResolver, uri, where, arrayOf(accountKey.toString()))
}
@SuppressLint("Recycle")
fun getFilteredUserIds(context: Context?): Array<UserKey> {
if (context == null) return emptyArray()
val resolver = context.contentResolver
val projection = arrayOf(Filters.Users.USER_KEY)
return resolver.query(Filters.Users.CONTENT_URI, projection, null, null, null)?.useCursor { cur ->
return@useCursor Array(cur.count) { i ->
cur.moveToPosition(i)
UserKey.valueOf(cur.getString(0))
}
} ?: emptyArray()
}
fun getAccountDisplayName(context: Context, accountKey: UserKey, nameFirst: Boolean): String? {
val name: String?
if (nameFirst) {
name = getAccountName(context, accountKey)
} else {
name = "@${getAccountScreenName(context, accountKey)}"
}
return name
}
fun getAccountName(context: Context, accountKey: UserKey): String? {
val am = AccountManager.get(context)
val account = AccountUtils.findByAccountKey(am, accountKey) ?: return null
return account.getAccountUser(am).name
}
fun getAccountScreenName(context: Context, accountKey: UserKey): String? {
val am = AccountManager.get(context)
val account = AccountUtils.findByAccountKey(am, accountKey) ?: return null
return account.getAccountUser(am).screen_name
}
fun getActivatedAccountKeys(context: Context): Array<UserKey> {
val am = AccountManager.get(context)
val keys = ArrayList<UserKey>()
for (account in AccountUtils.getAccounts(am)) {
if (account.isActivated(am)) {
keys.add(account.getAccountKey(am))
}
}
return keys.toTypedArray()
}
fun getStatusesCount(context: Context, preferences: SharedPreferences, uri: Uri,
extraArgs: Bundle?, compare: Long, compareColumn: String, greaterThan: Boolean,
accountKeys: Array<UserKey>?): Int {
val keys = accountKeys ?: getActivatedAccountKeys(context)
val expressions = ArrayList<Expression>()
val expressionArgs = ArrayList<String>()
expressions.add(Expression.inArgs(Column(Statuses.ACCOUNT_KEY), keys.size))
for (accountKey in keys) {
expressionArgs.add(accountKey.toString())
}
if (greaterThan) {
expressions.add(Expression.greaterThanArgs(compareColumn))
} else {
expressions.add(Expression.lesserThanArgs(compareColumn))
}
expressionArgs.add(compare.toString())
expressions.add(buildStatusFilterWhereClause(preferences, getTableNameByUri(uri)!!, null))
if (extraArgs != null) {
val extras = extraArgs.getParcelable<Parcelable>(EXTRA_EXTRAS)
if (extras is HomeTabExtras) {
processTabExtras(expressions, expressionArgs, extras)
}
}
val selection = Expression.and(*expressions.toTypedArray())
return queryCount(context.contentResolver, uri, selection.sql, expressionArgs.toTypedArray())
}
fun getActivitiesCount(context: Context, uri: Uri, compare: Long,
compareColumn: String, greaterThan: Boolean, accountKeys: Array<UserKey>?): Int {
val keys = accountKeys ?: getActivatedAccountKeys(context)
val selection = Expression.and(
Expression.inArgs(Column(Activities.ACCOUNT_KEY), keys.size),
if (greaterThan) Expression.greaterThanArgs(compareColumn) else Expression.lesserThanArgs(compareColumn),
buildActivityFilterWhereClause(getTableNameByUri(uri)!!, null)
)
val whereArgs = arrayListOf<String>()
keys.mapTo(whereArgs) { it.toString() }
whereArgs.add(compare.toString())
return queryCount(context.contentResolver, uri, selection.sql, whereArgs.toTypedArray())
}
fun getActivitiesCount(context: Context, uri: Uri,
extraWhere: Expression?, extraWhereArgs: Array<String>?,
since: Long, sinceColumn: String, followingOnly: Boolean,
accountKeys: Array<UserKey>?): Int {
val keys = (accountKeys ?: getActivatedAccountKeys(context)).map { it.toString() }.toTypedArray()
val expressions = ArrayList<Expression>()
expressions.add(Expression.inArgs(Column(Activities.ACCOUNT_KEY), keys.size))
expressions.add(Expression.greaterThanArgs(sinceColumn))
expressions.add(buildActivityFilterWhereClause(getTableNameByUri(uri)!!, null))
if (extraWhere != null) {
expressions.add(extraWhere)
}
val selection = Expression.and(*expressions.toTypedArray())
val selectionArgs: Array<String>
if (extraWhereArgs != null) {
selectionArgs = keys + since.toString() + extraWhereArgs
} else {
selectionArgs = keys + since.toString()
}
// If followingOnly option is on, we have to iterate over items
val resolver = context.contentResolver
if (followingOnly) {
val projection = arrayOf(Activities.SOURCES)
val cur = resolver.query(uri, projection, selection.sql, selectionArgs, null) ?: return -1
cur.useCursor { cur ->
var total = 0
cur.moveToFirst()
while (!cur.isAfterLast) {
val string = cur.getString(0)
if (TextUtils.isEmpty(string)) continue
var hasFollowing = false
try {
for (state in JsonSerializer.parseList(string, UserFollowState::class.java)) {
if (state.is_following) {
hasFollowing = true
break
}
}
} catch (e: IOException) {
continue
}
if (hasFollowing) {
total++
}
cur.moveToNext()
}
return total
}
}
return queryCount(resolver, uri, selection.sql, selectionArgs)
}
fun getTableId(uri: Uri?): Int {
if (uri == null) return -1
return CONTENT_PROVIDER_URI_MATCHER.match(uri)
}
fun getTableNameById(id: Int): String? {
when (id) {
TABLE_ID_STATUSES -> return Statuses.TABLE_NAME
TABLE_ID_ACTIVITIES_ABOUT_ME -> return Activities.AboutMe.TABLE_NAME
TABLE_ID_DRAFTS -> return Drafts.TABLE_NAME
TABLE_ID_FILTERED_USERS -> return Filters.Users.TABLE_NAME
TABLE_ID_FILTERED_KEYWORDS -> return Filters.Keywords.TABLE_NAME
TABLE_ID_FILTERED_SOURCES -> return Filters.Sources.TABLE_NAME
TABLE_ID_FILTERED_LINKS -> return Filters.Links.TABLE_NAME
TABLE_ID_FILTERS_SUBSCRIPTIONS -> return Filters.Subscriptions.TABLE_NAME
TABLE_ID_MESSAGES -> return Messages.TABLE_NAME
TABLE_ID_MESSAGES_CONVERSATIONS -> return Conversations.TABLE_NAME
TABLE_ID_TRENDS_LOCAL -> return CachedTrends.Local.TABLE_NAME
TABLE_ID_TABS -> return Tabs.TABLE_NAME
TABLE_ID_CACHED_STATUSES -> return CachedStatuses.TABLE_NAME
TABLE_ID_CACHED_USERS -> return CachedUsers.TABLE_NAME
TABLE_ID_CACHED_HASHTAGS -> return CachedHashtags.TABLE_NAME
TABLE_ID_CACHED_RELATIONSHIPS -> return CachedRelationships.TABLE_NAME
TABLE_ID_SAVED_SEARCHES -> return SavedSearches.TABLE_NAME
TABLE_ID_SEARCH_HISTORY -> return SearchHistory.TABLE_NAME
else -> return null
}
}
fun getTableNameByUri(uri: Uri?): String? {
if (uri == null) return null
return getTableNameById(getTableId(uri))
}
fun buildActivityFilterWhereClause(table: String, extraSelection: Expression?): Expression {
val filteredUsersQuery = SQLQueryBuilder
.select(Column(Table(Filters.Users.TABLE_NAME), Filters.Users.USER_KEY))
.from(Tables(Filters.Users.TABLE_NAME))
.build()
val filteredUsersWhere = Expression.or(
Expression.`in`(Column(Table(table), Activities.STATUS_USER_KEY), filteredUsersQuery),
Expression.`in`(Column(Table(table), Activities.STATUS_RETWEETED_BY_USER_KEY), filteredUsersQuery),
Expression.`in`(Column(Table(table), Activities.STATUS_QUOTED_USER_KEY), filteredUsersQuery)
)
val filteredIdsQueryBuilder = SQLQueryBuilder
.select(Column(Table(table), Activities._ID))
.from(Tables(table))
.where(filteredUsersWhere)
.union()
.select(Columns(Column(Table(table), Activities._ID)))
.from(Tables(table, Filters.Sources.TABLE_NAME))
.where(Expression.or(
Expression.likeRaw(Column(Table(table), Activities.STATUS_SOURCE),
"'%>'||${Filters.Sources.TABLE_NAME}.${Filters.Sources.VALUE}||'</a>%'"),
Expression.likeRaw(Column(Table(table), Activities.STATUS_QUOTE_SOURCE),
"'%>'||${Filters.Sources.TABLE_NAME}.${Filters.Sources.VALUE}||'</a>%'")
))
.union()
.select(Columns(Column(Table(table), Activities._ID)))
.from(Tables(table, Filters.Keywords.TABLE_NAME))
.where(Expression.or(
Expression.likeRaw(Column(Table(table), Activities.STATUS_TEXT_PLAIN),
"'%'||${Filters.Keywords.TABLE_NAME}.${Filters.Keywords.VALUE}||'%'"),
Expression.likeRaw(Column(Table(table), Activities.STATUS_QUOTE_TEXT_PLAIN),
"'%'||${Filters.Keywords.TABLE_NAME}.${Filters.Keywords.VALUE}||'%'")
))
.union()
.select(Columns(Column(Table(table), Activities._ID)))
.from(Tables(table, Filters.Links.TABLE_NAME))
.where(Expression.or(
Expression.likeRaw(Column(Table(table), Activities.STATUS_SPANS),
"'%'||${Filters.Links.TABLE_NAME}.${Filters.Links.VALUE}||'%'"),
Expression.likeRaw(Column(Table(table), Activities.STATUS_QUOTE_SPANS),
"'%'||${Filters.Links.TABLE_NAME}.${Filters.Links.VALUE}||'%'")
))
val filterExpression = Expression.or(
Expression.notIn(Column(Table(table), Activities._ID), filteredIdsQueryBuilder.build()),
Expression.equals(Column(Table(table), Activities.IS_GAP), 1)
)
if (extraSelection != null) {
return Expression.and(filterExpression, extraSelection)
}
return filterExpression
}
fun getAccountColors(context: Context, accountKeys: Array<UserKey>): IntArray {
val am = AccountManager.get(context)
val colors = IntArray(accountKeys.size)
for (i in accountKeys.indices) {
val account = AccountUtils.findByAccountKey(am, accountKeys[i])
if (account != null) {
colors[i] = account.getColor(am)
}
}
return colors
}
fun findAccountKeyByScreenName(context: Context, screenName: String): UserKey? {
val am = AccountManager.get(context)
for (account in AccountUtils.getAccounts(am)) {
val user = account.getAccountUser(am)
if (StringUtils.equalsIgnoreCase(screenName, user.screen_name)) {
return user.key
}
}
return null
}
fun getAccountKeys(context: Context): Array<UserKey> {
val am = AccountManager.get(context)
val accounts = AccountUtils.getAccounts(am)
val keys = ArrayList<UserKey>(accounts.size)
for (account in accounts) {
val keyString = am.getUserData(account, ACCOUNT_USER_DATA_KEY) ?: continue
keys.add(UserKey.valueOf(keyString))
}
return keys.toTypedArray()
}
fun findAccountKey(context: Context, accountId: String): UserKey? {
val am = AccountManager.get(context)
for (account in AccountUtils.getAccounts(am)) {
val key = account.getAccountKey(am)
if (accountId == key.id) {
return key
}
}
return null
}
fun hasAccount(context: Context): Boolean {
return AccountUtils.getAccounts(AccountManager.get(context)).isNotEmpty()
}
@Synchronized fun cleanDatabasesByItemLimit(context: Context) {
val resolver = context.contentResolver
val preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
val itemLimit = preferences[databaseItemLimitKey]
for (accountKey in getAccountKeys(context)) {
// Clean statuses.
for (uri in STATUSES_URIS) {
if (CachedStatuses.CONTENT_URI == uri) {
continue
}
val table = getTableNameByUri(uri)
val qb = SQLSelectQuery.Builder()
qb.select(Column(Statuses._ID))
.from(Tables(table))
.where(Expression.equalsArgs(Statuses.ACCOUNT_KEY))
.orderBy(OrderBy(Statuses.POSITION_KEY, false))
.limit(itemLimit)
val where = Expression.and(
Expression.notIn(Column(Statuses._ID), qb.build()),
Expression.equalsArgs(Statuses.ACCOUNT_KEY)
)
val whereArgs = arrayOf(accountKey.toString(), accountKey.toString())
resolver.delete(uri, where.sql, whereArgs)
}
for (uri in ACTIVITIES_URIS) {
val table = getTableNameByUri(uri)
val qb = SQLSelectQuery.Builder()
qb.select(Column(Activities._ID))
.from(Tables(table))
.where(Expression.equalsArgs(Activities.ACCOUNT_KEY))
.orderBy(OrderBy(Activities.TIMESTAMP, false))
.limit(itemLimit)
val where = Expression.and(
Expression.notIn(Column(Activities._ID), qb.build()),
Expression.equalsArgs(Activities.ACCOUNT_KEY)
)
val whereArgs = arrayOf(accountKey.toString(), accountKey.toString())
resolver.delete(uri, where.sql, whereArgs)
}
}
// Clean cached values.
for (uri in CACHE_URIS) {
val table = getTableNameByUri(uri) ?: continue
val qb = SQLSelectQuery.Builder()
qb.select(Column(BaseColumns._ID))
.from(Tables(table))
.orderBy(OrderBy(BaseColumns._ID, false))
.limit(itemLimit * 20)
val where = Expression.notIn(Column(BaseColumns._ID), qb.build())
resolver.delete(uri, where.sql, null)
}
}
fun isFilteringUser(context: Context, userKey: UserKey): Boolean {
return isFilteringUser(context, userKey.toString())
}
fun isFilteringUser(context: Context, userKey: String): Boolean {
val cr = context.contentResolver
val where = Expression.equalsArgs(Filters.Users.USER_KEY)
val c = cr.query(Filters.Users.CONTENT_URI, arrayOf(SQLFunctions.COUNT()),
where.sql, arrayOf(userKey), null) ?: return false
try {
if (c.moveToFirst()) {
return c.getLong(0) > 0
}
} finally {
c.close()
}
return false
}
private fun <T> getObjectFieldArray(context: Context, uri: Uri, keys: Array<UserKey?>,
keyField: String, valueFields: Array<String>, sortExpression: OrderBy?, extraWhere: Expression?,
extraWhereArgs: Array<String>?, createIndices: (Cursor) -> ObjectCursor.CursorIndices<T>,
createArray: (Int) -> Array<T?>): Array<T?> {
return getFieldsArray(context, uri, keys, keyField, valueFields, sortExpression,
extraWhere, extraWhereArgs, object : FieldArrayCreator<Array<T?>, ObjectCursor.CursorIndices<T>> {
override fun newArray(size: Int): Array<T?> {
return createArray(size)
}
override fun newIndex(cur: Cursor): ObjectCursor.CursorIndices<T> {
return createIndices(cur)
}
override fun assign(array: Array<T?>, arrayIdx: Int, cur: Cursor, colIdx: ObjectCursor.CursorIndices<T>) {
array[arrayIdx] = colIdx.newObject(cur)
}
})
}
private fun getStringFieldArray(context: Context, uri: Uri, keys: Array<UserKey?>,
keyField: String, valueField: String, sortExpression: OrderBy?, extraWhere: Expression?,
extraWhereArgs: Array<String>?): Array<String?> {
return getFieldsArray(context, uri, keys, keyField, arrayOf(valueField), sortExpression,
extraWhere, extraWhereArgs, object : FieldArrayCreator<Array<String?>, Int> {
override fun newArray(size: Int): Array<String?> {
return arrayOfNulls(size)
}
override fun newIndex(cur: Cursor): Int {
return cur.getColumnIndex(valueField)
}
override fun assign(array: Array<String?>, arrayIdx: Int, cur: Cursor, colIdx: Int) {
array[arrayIdx] = cur.getString(colIdx)
}
})
}
private fun getLongFieldArray(context: Context, uri: Uri, keys: Array<UserKey?>,
keyField: String, valueField: String, sortExpression: OrderBy?, extraWhere: Expression?,
extraWhereArgs: Array<String>?): LongArray {
return getFieldsArray(context, uri, keys, keyField, arrayOf(valueField), sortExpression,
extraWhere, extraWhereArgs, object : FieldArrayCreator<LongArray, Int> {
override fun newArray(size: Int): LongArray {
return LongArray(size)
}
override fun newIndex(cur: Cursor): Int {
return cur.getColumnIndex(valueField)
}
override fun assign(array: LongArray, arrayIdx: Int, cur: Cursor, colIdx: Int) {
array[arrayIdx] = cur.getLong(colIdx)
}
})
}
@SuppressLint("Recycle")
private fun <T, I> getFieldsArray(
context: Context, uri: Uri,
keys: Array<UserKey?>, keyField: String,
valueFields: Array<String>, sortExpression: OrderBy?,
extraWhere: Expression?, extraWhereArgs: Array<String>?,
creator: FieldArrayCreator<T, I>
): T {
val resolver = context.contentResolver
val resultArray = creator.newArray(keys.size)
val nonNullKeys = keys.mapNotNull { it?.toString() }.toTypedArray()
val tableName = getTableNameByUri(uri) ?: throw NullPointerException()
val having = Expression.inArgs(keyField, nonNullKeys.size)
val bindingArgs: Array<String>
if (extraWhereArgs != null) {
bindingArgs = extraWhereArgs + nonNullKeys
} else {
bindingArgs = nonNullKeys
}
val builder = SQLQueryBuilder.select(Columns(keyField, *valueFields))
builder.from(Table(tableName))
if (extraWhere != null) {
builder.where(extraWhere)
}
builder.groupBy(Column(keyField))
builder.having(having)
if (sortExpression != null) {
builder.orderBy(sortExpression)
}
resolver.rawQuery(builder.buildSQL(), bindingArgs)?.useCursor { cur ->
cur.moveToFirst()
val colIdx = creator.newIndex(cur)
while (!cur.isAfterLast) {
val keyString = cur.getString(cur.getColumnIndex(keyField))
if (keyString != null) {
val accountKey = UserKey.valueOf(keyString)
val arrayIdx = ArrayUtils.indexOf(keys, accountKey)
if (arrayIdx >= 0) {
creator.assign(resultArray, arrayIdx, cur, colIdx)
}
}
cur.moveToNext()
}
}
return resultArray
}
fun deleteStatus(cr: ContentResolver, accountKey: UserKey,
statusId: String, status: ParcelableStatus?) {
val host = accountKey.host
val deleteWhere: String
val updateWhere: String
val deleteWhereArgs: Array<String>
val updateWhereArgs: Array<String>
if (host != null) {
deleteWhere = Expression.and(
Expression.likeRaw(Column(Statuses.ACCOUNT_KEY), "'%@'||?"),
Expression.or(
Expression.equalsArgs(Statuses.STATUS_ID),
Expression.equalsArgs(Statuses.RETWEET_ID)
)).sql
deleteWhereArgs = arrayOf(host, statusId, statusId)
updateWhere = Expression.and(
Expression.likeRaw(Column(Statuses.ACCOUNT_KEY), "'%@'||?"),
Expression.equalsArgs(Statuses.MY_RETWEET_ID)
).sql
updateWhereArgs = arrayOf(host, statusId)
} else {
deleteWhere = Expression.or(
Expression.equalsArgs(Statuses.STATUS_ID),
Expression.equalsArgs(Statuses.RETWEET_ID)
).sql
deleteWhereArgs = arrayOf(statusId, statusId)
updateWhere = Expression.equalsArgs(Statuses.MY_RETWEET_ID).sql
updateWhereArgs = arrayOf(statusId)
}
for (uri in STATUSES_URIS) {
cr.delete(uri, deleteWhere, deleteWhereArgs)
if (status != null) {
val values = ContentValues()
values.putNull(Statuses.MY_RETWEET_ID)
values.put(Statuses.RETWEET_COUNT, status.retweet_count - 1)
cr.update(uri, values, updateWhere, updateWhereArgs)
}
}
}
fun processTabExtras(expressions: MutableList<Expression>, expressionArgs: MutableList<String>, extras: HomeTabExtras) {
if (extras.isHideRetweets) {
expressions.add(Expression.equalsArgs(Statuses.IS_RETWEET))
expressionArgs.add("0")
}
if (extras.isHideQuotes) {
expressions.add(Expression.equalsArgs(Statuses.IS_QUOTE))
expressionArgs.add("0")
}
if (extras.isHideReplies) {
expressions.add(Expression.isNull(Column(Statuses.IN_REPLY_TO_STATUS_ID)))
}
}
fun prepareDatabase(context: Context) {
val cr = context.contentResolver
val cursor = cr.query(TwidereDataStore.CONTENT_URI_DATABASE_PREPARE, null, null,
null, null) ?: return
cursor.close()
}
internal interface FieldArrayCreator<T, I> {
fun newArray(size: Int): T
fun newIndex(cur: Cursor): I
fun assign(array: T, arrayIdx: Int, cur: Cursor, colIdx: I)
}
fun queryCount(cr: ContentResolver, uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val projection = arrayOf(SQLFunctions.COUNT())
val cur = cr.query(uri, projection, selection, selectionArgs, null) ?: return -1
try {
if (cur.moveToFirst()) {
return cur.getInt(0)
}
return -1
} finally {
cur.close()
}
}
fun getInteractionsCount(context: Context, extraArgs: Bundle?,
accountIds: Array<UserKey>, since: Long, sinceColumn: String): Int {
var extraWhere: Expression? = null
var extraWhereArgs: Array<String>? = null
var followingOnly = false
if (extraArgs != null) {
val extras = extraArgs.getParcelable<TabExtras>(IntentConstants.EXTRA_EXTRAS)
if (extras is InteractionsTabExtras) {
if (extras.isMentionsOnly) {
extraWhere = Expression.inArgs(Activities.ACTION, 3)
extraWhereArgs = arrayOf(Activity.Action.MENTION, Activity.Action.REPLY, Activity.Action.QUOTE)
}
if (extras.isMyFollowingOnly) {
followingOnly = true
}
}
}
return getActivitiesCount(context, Activities.AboutMe.CONTENT_URI, extraWhere, extraWhereArgs,
since, sinceColumn, followingOnly, accountIds)
}
fun addToFilter(context: Context, users: Collection<ParcelableUser>, filterAnywhere: Boolean) {
val cr = context.contentResolver
try {
val baseCreator = ObjectCursor.valuesCreatorFrom(FiltersData.BaseItem::class.java)
val userCreator = ObjectCursor.valuesCreatorFrom(FiltersData.UserItem::class.java)
val userValues = ArrayList<ContentValues>()
val keywordValues = ArrayList<ContentValues>()
val linkValues = ArrayList<ContentValues>()
for (user in users) {
val userItem = FiltersData.UserItem()
userItem.userKey = user.key
userItem.screenName = user.screen_name
userItem.name = user.name
userValues.add(userCreator.create(userItem))
val keywordItem = FiltersData.BaseItem()
keywordItem.value = "@" + user.screen_name
keywordItem.userKey = user.key
keywordValues.add(baseCreator.create(keywordItem))
// Insert user link (without scheme) to links
val linkItem = FiltersData.BaseItem()
val userLink = LinkCreator.getUserWebLink(user)
val linkWithoutScheme = userLink.toString().substringAfter("://")
linkItem.value = linkWithoutScheme
linkItem.userKey = user.key
linkValues.add(baseCreator.create(linkItem))
}
ContentResolverUtils.bulkInsert(cr, Filters.Users.CONTENT_URI, userValues)
if (filterAnywhere) {
// Insert to filtered users
ContentResolverUtils.bulkInsert(cr, Filters.Keywords.CONTENT_URI, keywordValues)
// Insert user mention to keywords
ContentResolverUtils.bulkInsert(cr, Filters.Links.CONTENT_URI, linkValues)
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
fun removeFromFilter(context: Context, users: Collection<ParcelableUser>) {
val cr = context.contentResolver
// Delete from filtered users
val userKeyValues = users.map { it.key.toString() }
ContentResolverUtils.bulkDelete(cr, Filters.Users.CONTENT_URI, Filters.Users.USER_KEY,
false, userKeyValues, null, null)
ContentResolverUtils.bulkDelete(cr, Filters.Keywords.CONTENT_URI, Filters.Keywords.USER_KEY,
false, userKeyValues, null, null)
ContentResolverUtils.bulkDelete(cr, Filters.Links.CONTENT_URI, Filters.Links.USER_KEY,
false, userKeyValues, null, null)
}
@WorkerThread
fun findStatusInDatabases(context: Context,
accountKey: UserKey,
statusId: String): ParcelableStatus? {
val resolver = context.contentResolver
var status: ParcelableStatus? = null
val where = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).sql
val whereArgs = arrayOf(accountKey.toString(), statusId)
for (uri in DataStoreUtils.STATUSES_URIS) {
val cur = resolver.query(uri, Statuses.COLUMNS, where, whereArgs, null) ?: continue
try {
if (cur.moveToFirst()) {
val indices = ObjectCursor.indicesFrom(cur, ParcelableStatus::class.java)
status = indices.newObject(cur)
}
} finally {
cur.close()
}
}
return status
}
@WorkerThread
@Throws(MicroBlogException::class)
fun findStatus(context: Context, accountKey: UserKey, statusId: String): ParcelableStatus {
val cached = findStatusInDatabases(context, accountKey, statusId)
if (cached != null) return cached
val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey,
true) ?: throw MicroBlogException("No account")
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
val result = microBlog.showStatus(statusId)
val where = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).sql
val whereArgs = arrayOf(accountKey.toString(), statusId)
val resolver = context.contentResolver
val status = ParcelableStatusUtils.fromStatus(result, accountKey, details.type, false)
resolver.delete(CachedStatuses.CONTENT_URI, where, whereArgs)
resolver.insert(CachedStatuses.CONTENT_URI, ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java).create(status))
return status
}
@WorkerThread
fun findMessageConversation(context: Context, accountKey: UserKey, conversationId: String): ParcelableMessageConversation? {
val resolver = context.contentResolver
val where = Expression.and(Expression.equalsArgs(Conversations.ACCOUNT_KEY),
Expression.equalsArgs(Conversations.CONVERSATION_ID)).sql
val whereArgs = arrayOf(accountKey.toString(), conversationId)
val cur = resolver.query(Conversations.CONTENT_URI, Conversations.COLUMNS, where, whereArgs, null) ?: return null
try {
if (cur.moveToFirst()) {
val indices = ObjectCursor.indicesFrom(cur, ParcelableMessageConversation::class.java)
return indices.newObject(cur)
}
} finally {
cur.close()
}
return null
}
private fun getIdsWhere(official: Boolean): Pair<Expression?, Array<String>?> {
if (official) return Pair(null, null)
return Pair(Expression.inArgs(Activities.ACTION, Activity.Action.MENTION_ACTIONS.size)
, Activity.Action.MENTION_ACTIONS)
}
private fun <T> getOfficialSeparatedIds(context: Context, getFromDatabase: (Array<UserKey?>, Boolean) -> T,
mergeResult: (T, T) -> T, accountKeys: Array<UserKey?>): T {
val officialKeys = Array(accountKeys.size) {
val key = accountKeys[it]
if (Utils.isOfficialCredentials(context, key)) {
return@Array key
}
return@Array null
}
val notOfficialKeys = Array(accountKeys.size) {
val key = accountKeys[it]
if (Utils.isOfficialCredentials(context, key)) {
return@Array null
}
return@Array key
}
val officialMaxPositions = getFromDatabase(officialKeys, true)
val notOfficialMaxPositions = getFromDatabase(notOfficialKeys, false)
return mergeResult(officialMaxPositions, notOfficialMaxPositions)
}
}