First step syncronization implementation
This commit is contained in:
12
app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt
Normal file
12
app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.sync
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel
|
||||
|
||||
val syncModule
|
||||
get() = module {
|
||||
|
||||
viewModel { SyncAuthViewModel(androidContext(), get()) }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.sync.data
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AccountInterceptor(
|
||||
context: Context,
|
||||
private val account: Account,
|
||||
) : Interceptor {
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val password = accountManager.getPassword(account)
|
||||
val request = if (password != null) {
|
||||
val credential: String = Credentials.basic(account.name, password)
|
||||
chain.request().newBuilder()
|
||||
.header("Authorization", credential)
|
||||
.build()
|
||||
} else {
|
||||
chain.request()
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.sync.domain
|
||||
|
||||
class SyncAuthResult(
|
||||
val email: String,
|
||||
val password: String,
|
||||
val token: String,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SyncAuthResult
|
||||
|
||||
if (email != other.email) return false
|
||||
if (password != other.password) return false
|
||||
if (token != other.token) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = email.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + token.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package org.koitharu.kotatsu.sync.domain
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA_TAGS
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_TAGS
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.sync.data.AccountInterceptor
|
||||
import org.koitharu.kotatsu.utils.ext.toContentValues
|
||||
import org.koitharu.kotatsu.utils.ext.toJson
|
||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||
|
||||
private const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
|
||||
private const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
|
||||
/**
|
||||
* Warning! This class may be used in another process
|
||||
*/
|
||||
class SyncRepository(
|
||||
context: Context,
|
||||
account: Account,
|
||||
private val provider: ContentProviderClient,
|
||||
) {
|
||||
|
||||
private val httpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(AccountInterceptor(context, account))
|
||||
.build()
|
||||
private val baseUrl = context.getString(R.string.url_sync_server)
|
||||
|
||||
@WorkerThread
|
||||
fun syncFavouriteCategories(syncResult: SyncResult) {
|
||||
val uri = uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)
|
||||
val data = JSONObject()
|
||||
provider.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val favourites = JSONArray()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
favourites.put(cursor.toJson())
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
data.put(TABLE_FAVOURITES, favourites)
|
||||
}
|
||||
data.put("timestamp", System.currentTimeMillis())
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/resource/$TABLE_FAVOURITE_CATEGORIES")
|
||||
.post(data.toRequestBody())
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute().parseJson()
|
||||
val operations = ArrayList<ContentProviderOperation>()
|
||||
val timestamp = response.getLong("timestamp")
|
||||
operations += ContentProviderOperation.newDelete(uri)
|
||||
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
|
||||
.build()
|
||||
val ja = response.getJSONArray(TABLE_FAVOURITE_CATEGORIES)
|
||||
ja.mapJSONTo(operations) { jo ->
|
||||
ContentProviderOperation.newInsert(uri)
|
||||
.withValues(jo.toContentValues())
|
||||
.build()
|
||||
}
|
||||
|
||||
val result = provider.applyBatch(operations)
|
||||
syncResult.stats.numDeletes = result.first().count?.toLong() ?: 0L
|
||||
syncResult.stats.numInserts = result.drop(1).sumOf { it.count?.toLong() ?: 0L }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun syncFavourites(syncResult: SyncResult) {
|
||||
val uri = uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITES)
|
||||
val data = JSONObject()
|
||||
provider.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val jsonArray = JSONArray()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val jo = cursor.toJson()
|
||||
jo.put("manga", getManga(AUTHORITY_FAVOURITES, jo.getLong("manga_id")))
|
||||
jsonArray.put(jo)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
data.put(TABLE_FAVOURITES, jsonArray)
|
||||
}
|
||||
data.put("timestamp", System.currentTimeMillis())
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
||||
.post(data.toRequestBody())
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute().parseJson()
|
||||
val operations = ArrayList<ContentProviderOperation>()
|
||||
val timestamp = response.getLong("timestamp")
|
||||
operations += ContentProviderOperation.newDelete(uri)
|
||||
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
|
||||
.build()
|
||||
val ja = response.getJSONArray(TABLE_FAVOURITES)
|
||||
ja.mapJSONTo(operations) { jo ->
|
||||
ContentProviderOperation.newInsert(uri)
|
||||
.withValues(jo.toContentValues())
|
||||
.build()
|
||||
}
|
||||
|
||||
val result = provider.applyBatch(operations)
|
||||
syncResult.stats.numDeletes = result.first().count?.toLong() ?: 0L
|
||||
syncResult.stats.numInserts = result.drop(1).sumOf { it.count?.toLong() ?: 0L }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun syncHistory(syncResult: SyncResult) {
|
||||
val uri = uri(AUTHORITY_HISTORY, TABLE_HISTORY)
|
||||
val data = JSONObject()
|
||||
provider.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val jsonArray = JSONArray()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val jo = cursor.toJson()
|
||||
jo.put("manga", getManga(AUTHORITY_HISTORY, jo.getLong("manga_id")))
|
||||
jsonArray.put(jo)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
data.put(TABLE_HISTORY, jsonArray)
|
||||
}
|
||||
data.put("timestamp", System.currentTimeMillis())
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/resource/$TABLE_HISTORY")
|
||||
.post(data.toRequestBody())
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute().parseJson()
|
||||
val operations = ArrayList<ContentProviderOperation>()
|
||||
val timestamp = response.getLong("timestamp")
|
||||
operations += ContentProviderOperation.newDelete(uri)
|
||||
.withSelection("updated_at < ?", arrayOf(timestamp.toString()))
|
||||
.build()
|
||||
val ja = response.getJSONArray(TABLE_HISTORY)
|
||||
ja.mapJSONTo(operations) { jo ->
|
||||
ContentProviderOperation.newInsert(uri)
|
||||
.withValues(jo.toContentValues())
|
||||
.build()
|
||||
}
|
||||
|
||||
val result = provider.applyBatch(operations)
|
||||
syncResult.stats.numDeletes = result.first().count?.toLong() ?: 0L
|
||||
syncResult.stats.numInserts = result.drop(1).sumOf { it.count?.toLong() ?: 0L }
|
||||
}
|
||||
|
||||
private fun getManga(authority: String, id: Long): JSONObject {
|
||||
val manga = provider.query(
|
||||
uri(authority, TABLE_MANGA),
|
||||
null,
|
||||
"manga_id = ?",
|
||||
arrayOf(id.toString()),
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.toJson()
|
||||
}
|
||||
requireNotNull(manga)
|
||||
val tags = provider.query(
|
||||
uri(authority, TABLE_MANGA_TAGS),
|
||||
arrayOf("tag_id"),
|
||||
"manga_id = ?",
|
||||
arrayOf(id.toString()),
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
val json = JSONArray()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val tagId = cursor.getLong(0)
|
||||
json.put(getTag(authority, tagId))
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
json
|
||||
}
|
||||
manga.put("tags", requireNotNull(tags))
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun getTag(authority: String, tagId: Long): JSONObject {
|
||||
val tag = provider.query(
|
||||
uri(authority, TABLE_TAGS),
|
||||
null,
|
||||
"tag_id = ?",
|
||||
arrayOf(tagId.toString()),
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.toJson()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
return requireNotNull(tag)
|
||||
}
|
||||
|
||||
private fun uri(authority: String, table: String) = Uri.parse("content://$authority/$table")
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener {
|
||||
|
||||
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
|
||||
private var resultBundle: Bundle? = null
|
||||
|
||||
private val viewModel by viewModel<SyncAuthViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySyncAuthBinding.inflate(layoutInflater))
|
||||
accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
|
||||
accountAuthenticatorResponse?.onRequestContinued()
|
||||
binding.buttonCancel.setOnClickListener(this)
|
||||
binding.buttonNext.setOnClickListener(this)
|
||||
binding.buttonBack.setOnClickListener(this)
|
||||
binding.buttonDone.setOnClickListener(this)
|
||||
binding.editEmail.addTextChangedListener(EmailTextWatcher(binding.buttonNext))
|
||||
binding.editPassword.addTextChangedListener(PasswordTextWatcher(binding.buttonDone))
|
||||
|
||||
viewModel.onTokenObtained.observe(this, ::onTokenReceived)
|
||||
viewModel.onError.observe(this, ::onError)
|
||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
|
||||
binding.root.setPadding(
|
||||
basePadding + insets.left,
|
||||
basePadding + insets.top,
|
||||
basePadding + insets.right,
|
||||
basePadding + insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.switcher.isVisible && binding.switcher.displayedChild > 0) {
|
||||
binding.switcher.showPrevious()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_cancel -> {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
R.id.button_next -> {
|
||||
binding.switcher.showNext()
|
||||
}
|
||||
R.id.button_back -> {
|
||||
binding.switcher.showPrevious()
|
||||
}
|
||||
R.id.button_done -> {
|
||||
viewModel.obtainToken(
|
||||
email = binding.editEmail.text.toString(),
|
||||
password = binding.editPassword.text.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
accountAuthenticatorResponse?.let { response ->
|
||||
resultBundle?.also {
|
||||
response.onResult(it)
|
||||
} ?: response.onError(AccountManager.ERROR_CODE_CANCELED, getString(R.string.canceled))
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
if (isLoading == binding.layoutProgress.isVisible) {
|
||||
return
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, Fade())
|
||||
binding.switcher.isGone = isLoading
|
||||
binding.layoutProgress.isVisible = isLoading
|
||||
}
|
||||
|
||||
private fun onError(error: Throwable) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(error.getDisplayMessage(resources))
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onTokenReceived(authResult: SyncAuthResult) {
|
||||
val am = AccountManager.get(this)
|
||||
val account = Account(authResult.email, getString(R.string.account_type_sync))
|
||||
val result = Bundle()
|
||||
if (am.addAccountExplicitly(account, authResult.password, Bundle())) {
|
||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
|
||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
|
||||
result.putString(AccountManager.KEY_AUTHTOKEN, authResult.token)
|
||||
am.setAuthToken(account, account.type, authResult.token)
|
||||
} else {
|
||||
result.putString(AccountManager.KEY_ERROR_MESSAGE, getString(R.string.account_already_exists))
|
||||
}
|
||||
resultBundle = result
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
private class EmailTextWatcher(
|
||||
private val button: Button,
|
||||
) : TextWatcher {
|
||||
|
||||
private val regexEmail = Regex("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", RegexOption.IGNORE_CASE)
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = s?.toString()
|
||||
button.isEnabled = !text.isNullOrEmpty() && regexEmail.matches(text)
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordTextWatcher(
|
||||
private val button: Button,
|
||||
) : TextWatcher {
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = s?.toString()
|
||||
button.isEnabled = text != null && text.length >= 4
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||
import java.util.*
|
||||
|
||||
class SyncAuthViewModel(
|
||||
context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val baseUrl = context.getString(R.string.url_sync_server)
|
||||
val onTokenObtained = SingleLiveEvent<SyncAuthResult>()
|
||||
|
||||
fun obtainToken(email: String, password: String) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
authenticate(email, password)
|
||||
val token = UUID.randomUUID().toString()
|
||||
val result = SyncAuthResult(email, password, token)
|
||||
onTokenObtained.postCall(result)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun authenticate(email: String, password: String) {
|
||||
val body = JSONObject(
|
||||
mapOf("email" to email, "password" to password)
|
||||
).toRequestBody()
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/register")
|
||||
.post(body)
|
||||
.build()
|
||||
val response = okHttpClient.newCall(request).await().parseJson()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
|
||||
class SyncAuthenticator(private val context: Context) : AbstractAccountAuthenticator(context) {
|
||||
|
||||
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?): Bundle? = null
|
||||
|
||||
override fun addAccount(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
accountType: String?,
|
||||
authTokenType: String?,
|
||||
requiredFeatures: Array<out String>?,
|
||||
options: Bundle?,
|
||||
): Bundle {
|
||||
val intent = Intent(context, SyncAuthActivity::class.java)
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||
val bundle = Bundle()
|
||||
if (options != null) {
|
||||
bundle.putAll(options)
|
||||
}
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
|
||||
return bundle
|
||||
}
|
||||
|
||||
override fun confirmCredentials(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account?,
|
||||
options: Bundle?,
|
||||
): Bundle? = null
|
||||
|
||||
override fun getAuthToken(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account,
|
||||
authTokenType: String?,
|
||||
options: Bundle?,
|
||||
): Bundle {
|
||||
val result = Bundle()
|
||||
val am = AccountManager.get(context.applicationContext)
|
||||
val authToken = am.peekAuthToken(account, authTokenType)
|
||||
if (!TextUtils.isEmpty(authToken)) {
|
||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
|
||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
|
||||
result.putString(AccountManager.KEY_AUTHTOKEN, authToken)
|
||||
} else {
|
||||
val intent = Intent(context, SyncAuthActivity::class.java)
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||
// intent.putExtra(SyncAuthActivity.EXTRA_TOKEN_TYPE, authTokenType)
|
||||
val bundle = Bundle()
|
||||
if (options != null) {
|
||||
bundle.putAll(options)
|
||||
}
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getAuthTokenLabel(authTokenType: String?): String? = null
|
||||
|
||||
override fun updateCredentials(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account?,
|
||||
authTokenType: String?,
|
||||
options: Bundle?,
|
||||
): Bundle? = null
|
||||
|
||||
override fun hasFeatures(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account?,
|
||||
features: Array<out String>?,
|
||||
): Bundle? = null
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
class SyncAuthenticatorService : Service() {
|
||||
|
||||
private lateinit var authenticator: SyncAuthenticator
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
authenticator = SyncAuthenticator(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return authenticator.iBinder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.koitharu.kotatsu.sync.ui
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentProviderResult
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.net.Uri
|
||||
import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
import java.util.concurrent.Callable
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_MANGA_TAGS
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase.Companion.TABLE_TAGS
|
||||
|
||||
abstract class SyncProvider : ContentProvider() {
|
||||
|
||||
private val database by inject<MangaDatabase>()
|
||||
private val supportedTables = setOf(
|
||||
TABLE_FAVOURITES,
|
||||
TABLE_MANGA,
|
||||
TABLE_TAGS,
|
||||
TABLE_FAVOURITE_CATEGORIES,
|
||||
TABLE_HISTORY,
|
||||
TABLE_MANGA_TAGS,
|
||||
)
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?
|
||||
): Cursor? = if (getTableName(uri) != null) {
|
||||
val sqlQuery = SupportSQLiteQueryBuilder.builder(uri.lastPathSegment)
|
||||
.columns(projection)
|
||||
.selection(selection, selectionArgs)
|
||||
.orderBy(sortOrder)
|
||||
.create()
|
||||
database.openHelper.readableDatabase.query(sqlQuery)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
return getTableName(uri)?.let { "vnd.android.cursor.dir/" }
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
val table = getTableName(uri)
|
||||
if (values == null || table == null) {
|
||||
return null
|
||||
}
|
||||
val db = database.openHelper.writableDatabase
|
||||
db.insert(table, SQLiteDatabase.CONFLICT_REPLACE, values)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
val table = getTableName(uri)
|
||||
if (table == null) {
|
||||
return 0
|
||||
}
|
||||
return database.openHelper.writableDatabase.delete(table, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
val table = getTableName(uri)
|
||||
if (values == null || table == null) {
|
||||
return 0
|
||||
}
|
||||
return database.openHelper.writableDatabase
|
||||
.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override fun applyBatch(operations: ArrayList<ContentProviderOperation>): Array<ContentProviderResult> {
|
||||
return database.runInTransaction(Callable { super.applyBatch(operations) })
|
||||
}
|
||||
|
||||
override fun bulkInsert(uri: Uri, values: Array<out ContentValues>): Int {
|
||||
return database.runInTransaction(Callable { super.bulkInsert(uri, values) })
|
||||
}
|
||||
|
||||
private fun getTableName(uri: Uri): String? {
|
||||
return uri.pathSegments.singleOrNull()?.takeIf { it in supportedTables }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.sync.ui.favourites
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.AbstractThreadedSyncAdapter
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.sync.domain.SyncRepository
|
||||
import org.koitharu.kotatsu.utils.ext.onError
|
||||
|
||||
class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
|
||||
|
||||
override fun onPerformSync(
|
||||
account: Account,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult,
|
||||
) {
|
||||
// Debug.waitForDebugger()
|
||||
val repository = SyncRepository(context, account, provider)
|
||||
runCatching {
|
||||
repository.syncFavouriteCategories(syncResult)
|
||||
repository.syncFavourites(syncResult)
|
||||
}.onFailure(syncResult::onError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.sync.ui.favourites
|
||||
|
||||
import org.koitharu.kotatsu.sync.ui.SyncProvider
|
||||
|
||||
class FavouritesSyncProvider : SyncProvider()
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.sync.ui.favourites
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
class FavouritesSyncService : Service() {
|
||||
|
||||
private lateinit var syncAdapter: FavouritesSyncAdapter
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
syncAdapter = FavouritesSyncAdapter(applicationContext)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return syncAdapter.syncAdapterBinder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.sync.ui.history
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.AbstractThreadedSyncAdapter
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.sync.domain.SyncRepository
|
||||
import org.koitharu.kotatsu.utils.ext.onError
|
||||
|
||||
class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
|
||||
|
||||
override fun onPerformSync(
|
||||
account: Account,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult,
|
||||
) {
|
||||
// Debug.waitForDebugger()
|
||||
val repository = SyncRepository(context, account, provider)
|
||||
runCatching {
|
||||
repository.syncHistory(syncResult)
|
||||
}.onFailure(syncResult::onError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.sync.ui.history
|
||||
|
||||
import org.koitharu.kotatsu.sync.ui.SyncProvider
|
||||
|
||||
class HistorySyncProvider : SyncProvider()
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.sync.ui.history
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
class HistorySyncService : Service() {
|
||||
|
||||
private lateinit var syncAdapter: HistorySyncAdapter
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
syncAdapter = HistorySyncAdapter(applicationContext)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return syncAdapter.syncAdapterBinder
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user