Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/provider/TwidereDataProvider.kt

524 lines
22 KiB
Kotlin
Raw Normal View History

2017-02-16 12:07:39 +01:00
/*
* 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.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.database.SQLException
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteFullException
import android.net.Uri
import android.os.Binder
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.support.v4.text.BidiFormatter
import com.squareup.otto.Bus
2017-03-01 09:27:45 +01:00
import okhttp3.Dns
2017-02-16 12:07:39 +01:00
import org.mariotaku.ktextension.isNullOrEmpty
import org.mariotaku.ktextension.toNulls
import org.mariotaku.sqliteqb.library.Columns.Column
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.RawItemArray
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.annotation.CustomTabType
import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.app.TwidereApplication
2017-04-08 16:06:04 +02:00
import org.mariotaku.twidere.extension.withAppendedPath
2017-02-16 12:07:39 +01:00
import org.mariotaku.twidere.model.AccountPreferences
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.UnreadCountUpdatedEvent
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.SQLiteDatabaseWrapper.LazyLoadCallback
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
2017-02-17 11:42:39 +01:00
import org.mariotaku.twidere.util.database.CachedUsersQueryBuilder
2017-02-16 12:07:39 +01:00
import org.mariotaku.twidere.util.database.SuggestionsCursorCreator
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import javax.inject.Inject
class TwidereDataProvider : ContentProvider(), LazyLoadCallback {
@Inject
lateinit internal var readStateManager: ReadStateManager
@Inject
lateinit internal var twitterWrapper: AsyncTwitterWrapper
@Inject
lateinit internal var notificationManager: NotificationManagerWrapper
@Inject
lateinit internal var preferences: SharedPreferencesWrapper
@Inject
2017-03-01 09:27:45 +01:00
lateinit internal var dns: Dns
2017-02-16 12:07:39 +01:00
@Inject
lateinit internal var bus: Bus
@Inject
lateinit internal var userColorNameManager: UserColorNameManager
@Inject
lateinit internal var bidiFormatter: BidiFormatter
@Inject
lateinit internal var permissionsManager: PermissionsManager
@Inject
lateinit internal var contentNotificationManager: ContentNotificationManager
private lateinit var databaseWrapper: SQLiteDatabaseWrapper
2017-02-17 15:27:53 +01:00
private lateinit var backgroundExecutor: Executor
private lateinit var handler: Handler
2017-02-16 12:07:39 +01:00
2017-02-17 11:42:39 +01:00
override fun onCreate(): Boolean {
val context = context!!
GeneralComponentHelper.build(context).inject(this)
handler = Handler(Looper.getMainLooper())
databaseWrapper = SQLiteDatabaseWrapper(this)
backgroundExecutor = Executors.newSingleThreadExecutor()
// final GetWritableDatabaseTask task = new
// GetWritableDatabaseTask(context, helper, mDatabaseWrapper);
// task.executeTask();
return true
}
override fun onCreateSQLiteDatabase(): SQLiteDatabase {
val app = TwidereApplication.getInstance(context!!)
val helper = app.sqLiteOpenHelper
return helper.writableDatabase
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
try {
return insertInternal(uri, values)
} catch (e: SQLException) {
if (handleSQLException(e)) {
try {
return insertInternal(uri, values)
} catch (e1: SQLException) {
throw IllegalStateException(e1)
}
}
throw IllegalStateException(e)
}
}
2017-02-16 12:07:39 +01:00
override fun bulkInsert(uri: Uri, valuesArray: Array<ContentValues>): Int {
try {
return bulkInsertInternal(uri, valuesArray)
} catch (e: SQLException) {
if (handleSQLException(e)) {
try {
return bulkInsertInternal(uri, valuesArray)
} catch (e1: SQLException) {
throw IllegalStateException(e1)
}
}
throw IllegalStateException(e)
}
}
2017-02-17 11:42:39 +01:00
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?,
sortOrder: String?): Cursor? {
try {
val tableId = DataStoreUtils.getTableId(uri)
val table = DataStoreUtils.getTableNameById(tableId)
when (tableId) {
VIRTUAL_TABLE_ID_DATABASE_PREPARE -> {
databaseWrapper.prepare()
return MatrixCursor(projection ?: arrayOfNulls<String>(0))
}
VIRTUAL_TABLE_ID_PERMISSIONS -> {
val context = context ?: return null
val c = MatrixCursor(Permissions.MATRIX_COLUMNS)
val pm = context.packageManager
if (Binder.getCallingUid() == Process.myUid()) {
val map = permissionsManager.all
for ((key, value) in map) {
c.addRow(arrayOf<Any>(key, value))
}
} else {
val map = permissionsManager.all
val callingPackages = pm.getPackagesForUid(Binder.getCallingUid())
for ((key, value) in map) {
2017-04-07 06:12:34 +02:00
if (key in callingPackages) {
2017-02-17 11:42:39 +01:00
c.addRow(arrayOf<Any>(key, value))
}
}
}
return c
}
VIRTUAL_TABLE_ID_CACHED_USERS_WITH_RELATIONSHIP -> {
val accountKey = UserKey.valueOf(uri.lastPathSegment)
val query = CachedUsersQueryBuilder.withRelationship(projection,
selection, selectionArgs, sortOrder, accountKey)
val c = databaseWrapper.rawQuery(query.first.sql, query.second)
c?.setNotificationUri(context.contentResolver, CachedUsers.CONTENT_URI)
return c
}
VIRTUAL_TABLE_ID_CACHED_USERS_WITH_SCORE -> {
val accountKey = UserKey.valueOf(uri.lastPathSegment)
val query = CachedUsersQueryBuilder.withScore(projection,
selection, selectionArgs, sortOrder, accountKey, 0)
val c = databaseWrapper.rawQuery(query.first.sql, query.second)
c?.setNotificationUri(context.contentResolver, CachedUsers.CONTENT_URI)
return c
}
VIRTUAL_TABLE_ID_DRAFTS_UNSENT -> {
val twitter = twitterWrapper
val sendingIds = RawItemArray(twitter.getSendingDraftIds())
val where: Expression
if (selection != null) {
where = Expression.and(Expression(selection),
Expression.notIn(Column(Drafts._ID), sendingIds))
} else {
where = Expression.and(Expression.notIn(Column(Drafts._ID), sendingIds))
}
val c = databaseWrapper.query(Drafts.TABLE_NAME, projection,
where.sql, selectionArgs, null, null, sortOrder)
c?.setNotificationUri(context.contentResolver, uri)
return c
}
VIRTUAL_TABLE_ID_SUGGESTIONS_AUTO_COMPLETE -> {
return SuggestionsCursorCreator.forAutoComplete(databaseWrapper,
userColorNameManager, uri, projection)
}
VIRTUAL_TABLE_ID_SUGGESTIONS_SEARCH -> {
return SuggestionsCursorCreator.forSearch(databaseWrapper,
userColorNameManager, uri, projection)
}
VIRTUAL_TABLE_ID_NULL -> {
return null
}
VIRTUAL_TABLE_ID_EMPTY -> {
return MatrixCursor(projection ?: arrayOfNulls<String>(0))
}
VIRTUAL_TABLE_ID_RAW_QUERY -> {
if (projection != null || selection != null || sortOrder != null) {
throw IllegalArgumentException()
}
val c = databaseWrapper.rawQuery(uri.lastPathSegment, selectionArgs)
uri.getQueryParameter(QUERY_PARAM_NOTIFY_URI)?.let {
c?.setNotificationUri(context.contentResolver, Uri.parse(it))
2017-02-17 11:42:39 +01:00
}
return c
}
}
if (table == null) return null
val c = databaseWrapper.query(table, projection, selection, selectionArgs,
null, null, sortOrder)
c?.setNotificationUri(context.contentResolver, uri)
return c
} catch (e: SQLException) {
throw IllegalStateException(e)
}
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
try {
return updateInternal(uri, values, selection, selectionArgs)
} catch (e: SQLException) {
if (handleSQLException(e)) {
try {
return updateInternal(uri, values, selection, selectionArgs)
} catch (e1: SQLException) {
throw IllegalStateException(e1)
}
}
throw IllegalStateException(e)
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
try {
return deleteInternal(uri, selection, selectionArgs)
} catch (e: SQLException) {
if (handleSQLException(e)) {
try {
return deleteInternal(uri, selection, selectionArgs)
} catch (e1: SQLException) {
throw IllegalStateException(e1)
}
}
throw IllegalStateException(e)
}
}
override fun getType(uri: Uri): String? {
return null
}
2017-02-16 12:07:39 +01:00
private fun handleSQLException(e: SQLException): Boolean {
try {
if (e is SQLiteFullException) {
// Drop cached databases
databaseWrapper.delete(CachedUsers.TABLE_NAME, null, null)
databaseWrapper.delete(CachedStatuses.TABLE_NAME, null, null)
databaseWrapper.delete(CachedHashtags.TABLE_NAME, null, null)
databaseWrapper.execSQL("VACUUM")
return true
}
} catch (ee: SQLException) {
throw IllegalStateException(ee)
}
throw IllegalStateException(e)
}
private fun bulkInsertInternal(uri: Uri, valuesArray: Array<ContentValues>): Int {
val tableId = DataStoreUtils.getTableId(uri)
val table = DataStoreUtils.getTableNameById(tableId)
var result = 0
val newIds = LongArray(valuesArray.size)
if (table != null && valuesArray.isNotEmpty()) {
databaseWrapper.beginTransaction()
if (tableId == TABLE_ID_CACHED_USERS) {
for (values in valuesArray) {
val where = Expression.equalsArgs(CachedUsers.USER_KEY)
databaseWrapper.update(table, values, where.sql, arrayOf(values.getAsString(CachedUsers.USER_KEY)))
newIds[result++] = databaseWrapper.insertWithOnConflict(table, null,
values, SQLiteDatabase.CONFLICT_REPLACE)
}
} else if (tableId == TABLE_ID_SEARCH_HISTORY) {
for (values in valuesArray) {
values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis())
val where = Expression.equalsArgs(SearchHistory.QUERY)
val args = arrayOf(values.getAsString(SearchHistory.QUERY))
databaseWrapper.update(table, values, where.sql, args)
newIds[result++] = databaseWrapper.insertWithOnConflict(table, null,
values, SQLiteDatabase.CONFLICT_IGNORE)
}
} else {
val conflictAlgorithm = getConflictAlgorithm(tableId)
if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) {
for (values in valuesArray) {
newIds[result++] = databaseWrapper.insertWithOnConflict(table, null,
values, conflictAlgorithm)
}
} else {
for (values in valuesArray) {
newIds[result++] = databaseWrapper.insert(table, null, values)
}
}
}
databaseWrapper.setTransactionSuccessful()
databaseWrapper.endTransaction()
}
if (result > 0) {
onDatabaseUpdated(tableId, uri)
}
onNewItemsInserted(uri, tableId, valuesArray.toNulls())
return result
}
private fun deleteInternal(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val tableId = DataStoreUtils.getTableId(uri)
2017-04-08 16:06:04 +02:00
when (tableId) {
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS -> {
notificationManager.cancel(uri.toString(), NOTIFICATION_ID_DRAFTS)
return 1
}
else -> {
val table = DataStoreUtils.getTableNameById(tableId) ?: return 0
val result = databaseWrapper.delete(table, selection, selectionArgs)
if (result > 0) {
onDatabaseUpdated(tableId, uri)
}
onItemDeleted(uri, tableId)
return result
}
2017-02-16 12:07:39 +01:00
}
}
2017-02-17 11:42:39 +01:00
2017-02-16 12:07:39 +01:00
private fun insertInternal(uri: Uri, values: ContentValues?): Uri? {
val tableId = DataStoreUtils.getTableId(uri)
val table = DataStoreUtils.getTableNameById(tableId)
var rowId: Long = -1
when (tableId) {
TABLE_ID_CACHED_USERS -> {
if (values != null) {
val where = Expression.equalsArgs(CachedUsers.USER_KEY)
val whereArgs = arrayOf(values.getAsString(CachedUsers.USER_KEY))
databaseWrapper.update(table, values, where.sql, whereArgs)
}
rowId = databaseWrapper.insertWithOnConflict(table, null, values,
SQLiteDatabase.CONFLICT_IGNORE)
}
TABLE_ID_SEARCH_HISTORY -> {
if (values != null) {
values.put(SearchHistory.RECENT_QUERY, System.currentTimeMillis())
val where = Expression.equalsArgs(SearchHistory.QUERY)
val args = arrayOf(values.getAsString(SearchHistory.QUERY))
databaseWrapper.update(table, values, where.sql, args)
}
rowId = databaseWrapper.insertWithOnConflict(table, null, values,
SQLiteDatabase.CONFLICT_IGNORE)
}
TABLE_ID_CACHED_RELATIONSHIPS -> {
var updated = false
if (values != null) {
val accountKey = values.getAsString(CachedRelationships.ACCOUNT_KEY)
val userId = values.getAsString(CachedRelationships.USER_KEY)
val where = Expression.and(Expression.equalsArgs(CachedRelationships.ACCOUNT_KEY),
Expression.equalsArgs(CachedRelationships.USER_KEY))
if (databaseWrapper.update(table, values, where.sql, arrayOf(accountKey, userId)) > 0) {
val projection = arrayOf(CachedRelationships._ID)
val c = databaseWrapper.query(table, projection, where.sql, null,
null, null, null)
if (c.moveToFirst()) {
rowId = c.getLong(0)
}
c.close()
updated = true
}
}
if (!updated) {
rowId = databaseWrapper.insertWithOnConflict(table, null, values,
SQLiteDatabase.CONFLICT_IGNORE)
}
}
VIRTUAL_TABLE_ID_DRAFTS_NOTIFICATIONS -> {
2017-04-08 16:06:04 +02:00
rowId = contentNotificationManager.showDraft(uri)
2017-02-16 12:07:39 +01:00
}
else -> {
val conflictAlgorithm = getConflictAlgorithm(tableId)
if (conflictAlgorithm != SQLiteDatabase.CONFLICT_NONE) {
rowId = databaseWrapper.insertWithOnConflict(table, null, values,
conflictAlgorithm)
} else if (table != null) {
rowId = databaseWrapper.insert(table, null, values)
} else {
return null
}
}
}
onDatabaseUpdated(tableId, uri)
onNewItemsInserted(uri, tableId, arrayOf(values))
2017-04-08 16:06:04 +02:00
return uri.withAppendedPath(rowId.toString())
2017-02-16 12:07:39 +01:00
}
private fun updateInternal(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
val tableId = DataStoreUtils.getTableId(uri)
val table = DataStoreUtils.getTableNameById(tableId)
var result = 0
if (table != null) {
result = databaseWrapper.update(table, values, selection, selectionArgs)
}
if (result > 0) {
onDatabaseUpdated(tableId, uri)
}
return result
}
private fun notifyContentObserver(uri: Uri) {
2017-03-03 05:20:52 +01:00
if (!uri.getBooleanQueryParameter(QUERY_PARAM_NOTIFY_CHANGE, true)) return
handler.post {
2017-02-17 11:42:39 +01:00
context?.contentResolver?.notifyChange(uri, null)
}
2017-02-16 12:07:39 +01:00
}
private fun notifyUnreadCountChanged(position: Int) {
handler.post { bus.post(UnreadCountUpdatedEvent(position)) }
2017-02-16 12:07:39 +01:00
}
private fun onDatabaseUpdated(tableId: Int, uri: Uri?) {
if (uri == null) return
2017-02-17 11:42:39 +01:00
notifyContentObserver(uri)
2017-02-16 12:07:39 +01:00
}
2017-04-08 16:06:04 +02:00
private fun onItemDeleted(uri: Uri, tableId: Int) {
}
2017-02-16 12:07:39 +01:00
private fun onNewItemsInserted(uri: Uri, tableId: Int, valuesArray: Array<ContentValues?>?) {
val context = context ?: return
if (valuesArray.isNullOrEmpty()) return
when (tableId) {
TABLE_ID_STATUSES -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
2017-02-17 15:27:53 +01:00
backgroundExecutor.execute {
2017-03-12 14:27:55 +01:00
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isHomeTimelineNotificationEnabled }.forEach {
2017-02-16 12:07:39 +01:00
val positionTag = getPositionTag(CustomTabType.HOME_TIMELINE, it.accountKey)
2017-02-16 12:19:04 +01:00
contentNotificationManager.showTimeline(it, positionTag)
2017-02-16 12:07:39 +01:00
}
notifyUnreadCountChanged(NOTIFICATION_ID_HOME_TIMELINE)
}
}
TABLE_ID_ACTIVITIES_ABOUT_ME -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
2017-02-17 15:27:53 +01:00
backgroundExecutor.execute {
2017-03-12 14:27:55 +01:00
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isInteractionsNotificationEnabled }.forEach {
2017-02-17 15:27:53 +01:00
val positionTag = getPositionTag(ReadPositionTag.ACTIVITIES_ABOUT_ME, it.accountKey)
contentNotificationManager.showInteractions(it, positionTag)
2017-02-16 12:07:39 +01:00
}
notifyUnreadCountChanged(NOTIFICATION_ID_INTERACTIONS_TIMELINE)
}
}
TABLE_ID_MESSAGES_CONVERSATIONS -> {
if (!uri.getBooleanQueryParameter(QUERY_PARAM_SHOW_NOTIFICATION, true)) return
2017-02-17 15:27:53 +01:00
backgroundExecutor.execute {
2017-03-12 14:27:55 +01:00
val prefs = AccountPreferences.getAccountPreferences(context, DataStoreUtils.getAccountKeys(context))
prefs.filter { it.isNotificationEnabled && it.isDirectMessagesNotificationEnabled }.forEach {
2017-02-17 15:27:53 +01:00
contentNotificationManager.showMessages(it)
}
notifyUnreadCountChanged(NOTIFICATION_ID_DIRECT_MESSAGES)
2017-02-16 12:07:39 +01:00
}
}
TABLE_ID_DRAFTS -> {
}
}
}
private fun getPositionTag(tag: String, accountKey: UserKey): Long {
val position = readStateManager.getPosition(Utils.getReadPositionTagWithAccount(tag,
accountKey))
if (position != -1L) return position
return readStateManager.getPosition(tag)
}
companion object {
private fun getConflictAlgorithm(tableId: Int): Int {
when (tableId) {
2017-02-16 12:19:04 +01:00
TABLE_ID_CACHED_HASHTAGS, TABLE_ID_CACHED_STATUSES, TABLE_ID_CACHED_USERS,
TABLE_ID_CACHED_RELATIONSHIPS, TABLE_ID_SEARCH_HISTORY, TABLE_ID_MESSAGES,
TABLE_ID_MESSAGES_CONVERSATIONS -> {
return SQLiteDatabase.CONFLICT_REPLACE
}
TABLE_ID_FILTERED_USERS, TABLE_ID_FILTERED_KEYWORDS, TABLE_ID_FILTERED_SOURCES,
TABLE_ID_FILTERED_LINKS -> {
return SQLiteDatabase.CONFLICT_IGNORE
}
2017-02-16 12:07:39 +01:00
}
return SQLiteDatabase.CONFLICT_NONE
}
}
}