Add BaseInvoker and InvokerFactory

pull/1216/head
Alex Baker 4 years ago
parent 0664e23076
commit 727ad6e7a4

@ -1,27 +1,16 @@
package com.todoroo.astrid.gtasks.api
import android.content.Context
import com.google.api.client.http.HttpResponseException
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.GenericJson
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.services.tasks.Tasks
import com.google.api.services.tasks.TasksRequest
import com.google.api.services.tasks.TasksScopes
import com.google.api.services.tasks.model.Task
import com.google.api.services.tasks.model.TaskList
import com.google.api.services.tasks.model.TaskLists
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.DebugNetworkInterceptor
import org.tasks.gtasks.GoogleAccountManager
import org.tasks.googleapis.BaseInvoker
import org.tasks.preferences.Preferences
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
/**
* Wrapper around the official Google Tasks API to simplify common operations. In the case of an
@ -29,50 +18,15 @@ import javax.inject.Inject
*
* @author Sam Bosley
*/
class GtasksInvoker {
private val context: Context
private val googleAccountManager: GoogleAccountManager
private val preferences: Preferences
private val interceptor: DebugNetworkInterceptor
private val account: String?
private val service: Tasks?
private val credentialsAdapter: HttpCredentialsAdapter?
@Inject
constructor(
@ApplicationContext context: Context,
googleAccountManager: GoogleAccountManager,
preferences: Preferences,
interceptor: DebugNetworkInterceptor) {
this.context = context
this.googleAccountManager = googleAccountManager
this.preferences = preferences
this.interceptor = interceptor
account = null
service = null
credentialsAdapter = null
}
private constructor(
context: Context,
googleAccountManager: GoogleAccountManager,
preferences: Preferences,
interceptor: DebugNetworkInterceptor,
account: String) {
this.context = context
this.googleAccountManager = googleAccountManager
this.preferences = preferences
this.interceptor = interceptor
this.account = account
credentialsAdapter = HttpCredentialsAdapter(googleAccountManager)
service = Tasks.Builder(NetHttpTransport(), JacksonFactory(), credentialsAdapter)
.setApplicationName(String.format("Tasks/%s", BuildConfig.VERSION_NAME))
.build()
}
fun forAccount(account: String): GtasksInvoker {
return GtasksInvoker(context, googleAccountManager, preferences, interceptor, account)
}
class GtasksInvoker(
credentials: HttpCredentialsAdapter,
preferences: Preferences,
interceptor: DebugNetworkInterceptor
) : BaseInvoker(credentials, preferences, interceptor) {
private val service =
Tasks.Builder(NetHttpTransport(), JacksonFactory(), credentials)
.setApplicationName(APP_NAME)
.build()
@Throws(IOException::class)
suspend fun allGtaskLists(pageToken: String?): TaskLists? =
@ -82,28 +36,28 @@ class GtasksInvoker {
suspend fun getAllGtasksFromListId(
listId: String?, lastSyncDate: Long, pageToken: String?): com.google.api.services.tasks.model.Tasks? =
execute(
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(true)
.setShowHidden(true)
.setPageToken(pageToken)
.setUpdatedMin(
GtasksApiUtilities.unixTimeToGtasksCompletionTime(lastSyncDate).toStringRfc3339()))
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(true)
.setShowHidden(true)
.setPageToken(pageToken)
.setUpdatedMin(
GtasksApiUtilities.unixTimeToGtasksCompletionTime(lastSyncDate).toStringRfc3339()))
@Throws(IOException::class)
suspend fun getAllPositions(
listId: String?, pageToken: String?): com.google.api.services.tasks.model.Tasks? =
execute(
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(false)
.setShowHidden(false)
.setPageToken(pageToken)
.setFields("items(id,parent,position),nextPageToken"))
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(false)
.setShowHidden(false)
.setPageToken(pageToken)
.setFields("items(id,parent,position),nextPageToken"))
@Throws(IOException::class)
suspend fun createGtask(
@ -149,57 +103,4 @@ class GtasksInvoker {
} catch (ignored: HttpNotFoundException) {
}
}
@Synchronized
@Throws(IOException::class)
private suspend fun <T> execute(request: TasksRequest<T>): T? = execute(request, false)
@Synchronized
@Throws(IOException::class)
private suspend fun <T> execute(request: TasksRequest<T>, retry: Boolean): T? =
withContext(Dispatchers.IO) {
credentialsAdapter!!.checkToken(account, TasksScopes.TASKS)
val response: T?
response = try {
val httpRequest = request.buildHttpRequest()
Timber.d("%s", httpRequest.url)
if (preferences.isFlipperEnabled) {
interceptor.execute(httpRequest, request.responseClass)
} else {
httpRequest.execute().parseAs(request.responseClass)
}
} catch (e: HttpResponseException) {
return@withContext if (e.statusCode == 401 && !retry) {
credentialsAdapter.invalidateToken()
execute(request, true)
} else if (e.statusCode == 404) {
throw HttpNotFoundException(e)
} else {
throw e
}
}
Timber.d("%s response: %s", getCaller(retry), prettyPrint(response))
response
}
@Throws(IOException::class)
private fun <T> prettyPrint(`object`: T?): Any? {
if (BuildConfig.DEBUG) {
if (`object` is GenericJson) {
return (`object` as GenericJson).toPrettyString()
}
}
return `object`
}
private fun getCaller(retry: Boolean): String {
if (BuildConfig.DEBUG) {
try {
return Thread.currentThread().stackTrace[if (retry) 6 else 5].methodName
} catch (e: Exception) {
Timber.e(e)
}
}
return ""
}
}

@ -40,7 +40,11 @@ import java.net.URI
import java.util.*
import javax.inject.Inject
class HttpCredentialsAdapter @Inject constructor(private val googleAccountManager: GoogleAccountManager) : HttpRequestInitializer {
class HttpCredentialsAdapter @Inject constructor(
private val googleAccountManager: GoogleAccountManager,
private val account: String,
private val scope: String
) : HttpRequestInitializer {
private var credentials: GoogleCredentials? = null
@ -62,7 +66,7 @@ class HttpCredentialsAdapter @Inject constructor(private val googleAccountManage
}
}
suspend fun checkToken(account: String?, scope: String) {
suspend fun checkToken() {
if (credentials == null) {
val token = googleAccountManager.getAccessToken(account, scope)
credentials = GoogleCredentials(AccessToken(token, null))

@ -5,7 +5,7 @@ import java.io.IOException;
public class HttpNotFoundException extends IOException {
HttpNotFoundException(HttpResponseException e) {
public HttpNotFoundException(HttpResponseException e) {
super(e.getMessage());
}
}

@ -2,12 +2,13 @@ package org.tasks.activities
import androidx.hilt.lifecycle.ViewModelInject
import com.google.api.services.tasks.model.TaskList
import com.todoroo.astrid.gtasks.api.GtasksInvoker
import org.tasks.googleapis.InvokerFactory
import org.tasks.ui.CompletableViewModel
class CreateListViewModel @ViewModelInject constructor(
private val invoker: GtasksInvoker) : CompletableViewModel<TaskList>() {
private val invoker: InvokerFactory
) : CompletableViewModel<TaskList>() {
suspend fun createList(account: String, name: String) {
run { invoker.forAccount(account).createGtaskList(name)!! }
run { invoker.getGtasksInvoker(account).createGtaskList(name)!! }
}
}

@ -1,13 +1,13 @@
package org.tasks.activities
import androidx.hilt.lifecycle.ViewModelInject
import com.todoroo.astrid.gtasks.api.GtasksInvoker
import org.tasks.data.GoogleTaskList
import org.tasks.googleapis.InvokerFactory
import org.tasks.ui.ActionViewModel
class DeleteListViewModel @ViewModelInject constructor(
private val invoker: GtasksInvoker) : ActionViewModel() {
private val invoker: InvokerFactory) : ActionViewModel() {
suspend fun deleteList(list: GoogleTaskList) {
run { invoker.forAccount(list.account!!).deleteGtaskList(list.remoteId) }
run { invoker.getGtasksInvoker(list.account!!).deleteGtaskList(list.remoteId) }
}
}

@ -15,7 +15,6 @@ import com.google.api.services.tasks.model.TaskList
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.gtasks.api.GtasksInvoker
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
@ -32,7 +31,6 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
@Inject @ApplicationContext lateinit var context: Context
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var gtasksInvoker: GtasksInvoker
@BindView(R.id.name)
lateinit var name: TextInputEditText

@ -2,13 +2,13 @@ package org.tasks.activities
import androidx.hilt.lifecycle.ViewModelInject
import com.google.api.services.tasks.model.TaskList
import com.todoroo.astrid.gtasks.api.GtasksInvoker
import org.tasks.data.GoogleTaskList
import org.tasks.googleapis.InvokerFactory
import org.tasks.ui.CompletableViewModel
class RenameListViewModel @ViewModelInject constructor(
private val invoker: GtasksInvoker) : CompletableViewModel<TaskList>() {
private val invoker: InvokerFactory) : CompletableViewModel<TaskList>() {
suspend fun renameList(list: GoogleTaskList, name: String) {
run { invoker.forAccount(list.account!!).renameGtaskList(list.remoteId, name)!! }
run { invoker.getGtasksInvoker(list.account!!).renameGtaskList(list.remoteId, name)!! }
}
}

@ -2,39 +2,29 @@ package org.tasks.drive
import android.content.Context
import android.net.Uri
import com.google.api.client.http.HttpResponseException
import com.google.api.client.http.InputStreamContent
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.GenericJson
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveRequest
import com.google.api.services.drive.DriveScopes
import com.google.api.services.drive.model.File
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.backup.BackupConstants
import com.todoroo.astrid.gtasks.api.HttpCredentialsAdapter
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.DebugNetworkInterceptor
import org.tasks.R
import org.tasks.files.FileHelper
import org.tasks.googleapis.BaseInvoker
import org.tasks.preferences.Preferences
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
class DriveInvoker @Inject constructor(
class DriveInvoker(
@param:ApplicationContext private val context: Context,
private val preferences: Preferences,
private val credentialsAdapter: HttpCredentialsAdapter,
private val interceptor: DebugNetworkInterceptor) {
preferences: Preferences,
credentialsAdapter: HttpCredentialsAdapter,
interceptor: DebugNetworkInterceptor
) : BaseInvoker(credentialsAdapter, preferences, interceptor) {
private val service =
Drive
.Builder(NetHttpTransport(), JacksonFactory(), credentialsAdapter)
.setApplicationName(String.format("Tasks/%s", BuildConfig.VERSION_NAME))
Drive.Builder(NetHttpTransport(), JacksonFactory(), credentialsAdapter)
.setApplicationName(APP_NAME)
.build()
@Throws(IOException::class)
@ -83,64 +73,6 @@ class DriveInvoker @Inject constructor(
return execute(service.files().create(metadata, content))
}
@Synchronized
@Throws(IOException::class)
private suspend fun <T> execute(request: DriveRequest<T>): T? {
return execute(request, false)
}
@Synchronized
@Throws(IOException::class)
private suspend fun <T> execute(
request: DriveRequest<T>,
retry: Boolean
): T? = withContext(Dispatchers.IO) {
val account = preferences.getStringValue(R.string.p_google_drive_backup_account)
credentialsAdapter.checkToken(account, DriveScopes.DRIVE_FILE)
Timber.d("%s request: %s", caller, request)
val response: T?
response = try {
if (preferences.isFlipperEnabled) {
val start = DateUtilities.now()
val httpResponse = request.executeUnparsed()
interceptor.report(httpResponse, request.responseClass, start, DateUtilities.now())
} else {
request.execute()
}
} catch (e: HttpResponseException) {
return@withContext if (e.statusCode == 401 && !retry) {
credentialsAdapter.invalidateToken()
execute(request, true)
} else {
throw e
}
}
Timber.d("%s response: %s", caller, prettyPrint(response))
return@withContext response
}
@Throws(IOException::class)
private fun <T> prettyPrint(`object`: T?): Any? {
if (BuildConfig.DEBUG) {
if (`object` is GenericJson) {
return (`object` as GenericJson).toPrettyString()
}
}
return `object`
}
private val caller: String
get() {
if (BuildConfig.DEBUG) {
try {
return Thread.currentThread().stackTrace[4].methodName
} catch (e: Exception) {
Timber.e(e)
}
}
return ""
}
companion object {
private const val MIME_FOLDER = "application/vnd.google-apps.folder"
private val DRIVE_FILE_COMPARATOR = Comparator<File> { f1, f2 ->

@ -0,0 +1,80 @@
package org.tasks.googleapis
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest
import com.google.api.client.http.HttpResponseException
import com.google.api.client.json.GenericJson
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.gtasks.api.HttpCredentialsAdapter
import com.todoroo.astrid.gtasks.api.HttpNotFoundException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.DebugNetworkInterceptor
import org.tasks.preferences.Preferences
import timber.log.Timber
import java.io.IOException
abstract class BaseInvoker(
private val credentialsAdapter: HttpCredentialsAdapter,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor
) {
@Synchronized
@Throws(IOException::class)
protected suspend fun <T> execute(request: AbstractGoogleJsonClientRequest<T>): T? = execute(request, false)
@Synchronized
@Throws(IOException::class)
private suspend fun <T> execute(request: AbstractGoogleJsonClientRequest<T>, retry: Boolean): T? =
withContext(Dispatchers.IO) {
credentialsAdapter.checkToken()
Timber.d("%s request: %s", caller, request)
val response: T?
response = try {
if (preferences.isFlipperEnabled) {
val start = DateUtilities.now()
val httpResponse = request.executeUnparsed()
interceptor.report(httpResponse, request.responseClass, start, DateUtilities.now())
} else {
request.execute()
}
} catch (e: HttpResponseException) {
return@withContext if (e.statusCode == 401 && !retry) {
credentialsAdapter.invalidateToken()
execute(request, true)
} else if (e.statusCode == 404) {
throw HttpNotFoundException(e)
} else {
throw e
}
}
Timber.d("%s response: %s", caller, prettyPrint(response))
response
}
@Throws(IOException::class)
private fun <T> prettyPrint(`object`: T?): Any? {
if (BuildConfig.DEBUG) {
if (`object` is GenericJson) {
return (`object` as GenericJson).toPrettyString()
}
}
return `object`
}
private val caller: String
get() {
if (BuildConfig.DEBUG) {
try {
return Thread.currentThread().stackTrace[4].methodName
} catch (e: Exception) {
Timber.e(e)
}
}
return ""
}
companion object {
const val APP_NAME = "Tasks/${BuildConfig.VERSION_NAME}"
}
}

@ -0,0 +1,39 @@
package org.tasks.googleapis
import android.content.Context
import com.google.api.services.drive.DriveScopes
import com.google.api.services.tasks.TasksScopes
import com.todoroo.astrid.gtasks.api.GtasksInvoker
import com.todoroo.astrid.gtasks.api.HttpCredentialsAdapter
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.DebugNetworkInterceptor
import org.tasks.R
import org.tasks.drive.DriveInvoker
import org.tasks.gtasks.GoogleAccountManager
import org.tasks.preferences.Preferences
import javax.inject.Inject
class InvokerFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val googleAccountManager: GoogleAccountManager,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor
) {
fun getDriveInvoker() = DriveInvoker(
context,
preferences,
HttpCredentialsAdapter(
googleAccountManager,
preferences.getStringValue(R.string.p_google_drive_backup_account) ?: "",
DriveScopes.DRIVE_FILE
),
interceptor
)
fun getGtasksInvoker(account: String) = GtasksInvoker(
HttpCredentialsAdapter(googleAccountManager, account, TasksScopes.TASKS),
preferences,
interceptor
)
}

@ -25,6 +25,7 @@ import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.data.*
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.googleapis.InvokerFactory
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
@ -55,7 +56,7 @@ class GoogleTaskSynchronizer @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager,
private val inventory: Inventory,
private val taskDeleter: TaskDeleter,
private val gtasksInvoker: GtasksInvoker) {
private val invokers: InvokerFactory) {
suspend fun sync(account: GoogleTaskAccount, i: Int) {
Timber.d("%s: start sync", account)
@ -106,7 +107,7 @@ class GoogleTaskSynchronizer @Inject constructor(
account.error = context.getString(R.string.cannot_access_account)
return
}
val gtasksInvoker = gtasksInvoker.forAccount(account.account!!)
val gtasksInvoker = invokers.getGtasksInvoker(account.account!!)
pushLocalChanges(account, gtasksInvoker)
val gtaskLists: MutableList<TaskList> = ArrayList()
var nextPageToken: String? = null

@ -13,7 +13,7 @@ import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.drive.DriveInvoker
import org.tasks.googleapis.InvokerFactory
import org.tasks.injection.BaseWorker
import org.tasks.preferences.Preferences
import timber.log.Timber
@ -28,10 +28,11 @@ class DriveUploader @WorkerInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
firebase: Firebase,
private val drive: DriveInvoker,
invokers: InvokerFactory,
private val preferences: Preferences,
private val localBroadcastManager: LocalBroadcastManager
) : BaseWorker(context, workerParams, firebase) {
private val drive = invokers.getDriveInvoker()
override suspend fun run(): Result {
val inputData = inputData

@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.drive.DriveInvoker
import org.tasks.googleapis.InvokerFactory
import org.tasks.gtasks.GoogleAccountManager
import timber.log.Timber
import java.io.File
@ -23,9 +23,10 @@ import java.io.File
class PreferencesViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context,
private val preferences: Preferences,
private val driveInvoker: DriveInvoker,
invokers: InvokerFactory,
private val googleAccountManager: GoogleAccountManager,
) : ViewModel() {
private val driveInvoker = invokers.getDriveInvoker()
val lastBackup = MutableLiveData<Long?>()
val lastDriveBackup = MutableLiveData<Long?>()
val lastAndroidBackup = MutableLiveData<Long>()

Loading…
Cancel
Save