Create, rename, and delete microsoft lists

pull/2003/head
Alex Baker 2 years ago
parent 1793d95d4d
commit d589a89506

@ -673,6 +673,7 @@
<activity
android:name=".preferences.ManageSpaceActivity"
android:theme="@style/Tasks" />
<activity android:name="org.tasks.sync.microsoft.MicrosoftListSettingsActivity" />
<!-- launcher icons -->

@ -461,7 +461,8 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
lifecycleScope.launch {
val account = caldavDao.getAccountByUuid(calendar.account!!)
val caldavSettings = Intent(activity, account!!.listSettingsClass())
caldavSettings.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, calendar)
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT, account)
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, calendar)
startActivityForResult(caldavSettings, REQUEST_LIST_SETTINGS)
}
true

@ -132,6 +132,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
hideProgressIndicator()
when (t) {
is HttpException -> showSnackbar(t.message)
is retrofit2.HttpException -> showSnackbar(t.message() ?: "HTTP ${t.code()}")
is DisplayableException -> showSnackbar(t.resId)
is ConnectException -> showSnackbar(R.string.network_error)
else -> showSnackbar(R.string.error_adding_account, t.message!!)

@ -1,5 +1,6 @@
package org.tasks.data
import android.app.Activity
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
@ -11,7 +12,6 @@ import androidx.room.PrimaryKey
import com.todoroo.andlib.data.Table
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.activities.BaseListSettingsActivity
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.caldav.CaldavCalendarSettingsActivity
@ -25,6 +25,7 @@ import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.opentasks.OpenTaskAccountSettingsActivity
import org.tasks.opentasks.OpenTasksListSettingsActivity
import org.tasks.security.KeyStoreEncryption
import org.tasks.sync.microsoft.MicrosoftListSettingsActivity
import java.net.HttpURLConnection
@Entity(tableName = "caldav_accounts")
@ -102,10 +103,11 @@ class CaldavAccount : Parcelable {
val isMicrosoft: Boolean
get() = accountType == TYPE_MICROSOFT
fun listSettingsClass(): Class<out BaseListSettingsActivity> = when(accountType) {
fun listSettingsClass(): Class<out Activity> = when(accountType) {
TYPE_LOCAL -> LocalListSettingsActivity::class.java
TYPE_ETESYNC, TYPE_OPENTASKS -> OpenTasksListSettingsActivity::class.java
TYPE_ETEBASE -> EtebaseCalendarSettingsActivity::class.java
TYPE_MICROSOFT -> MicrosoftListSettingsActivity::class.java
else -> CaldavCalendarSettingsActivity::class.java
}

@ -7,12 +7,19 @@ import com.franmontiel.persistentcookiejar.persistence.CookiePersistor
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.openid.appauth.AuthState
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.TasksCookieJar
import org.tasks.data.CaldavAccount
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import org.tasks.sync.microsoft.MicrosoftService
import org.tasks.sync.microsoft.requestTokenRefresh
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject
import javax.net.ssl.SSLContext
@ -65,4 +72,39 @@ class HttpClientFactory @Inject constructor(
}
return builder.build()
}
suspend fun getMicrosoftService(account: CaldavAccount): MicrosoftService {
val authState = encryption.decrypt(account.password)?.let { AuthState.jsonDeserialize(it) }
?: throw RuntimeException("Missing credentials")
if (authState.needsTokenRefresh) {
val (token, ex) = context.requestTokenRefresh(authState)
authState.update(token, ex)
if (authState.isAuthorized) {
account.password = encryption.encrypt(authState.jsonSerializeString())
}
}
if (!authState.isAuthorized) {
throw RuntimeException("Needs authentication")
}
val client = newClient {
it.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("Authorization", "Bearer ${authState.accessToken}")
.build()
)
}
}
val retrofit = Retrofit.Builder()
.baseUrl(URL_MICROSOFT)
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
return retrofit.create(MicrosoftService::class.java)
}
companion object {
const val URL_MICROSOFT = "https://graph.microsoft.com"
val MEDIA_TYPE_JSON = "application/json".toMediaType()
}
}

@ -19,6 +19,9 @@ suspend fun IdentityProvider.retrieveConfig(): AuthorizationServiceConfiguration
}
}
suspend fun Context.requestTokenRefresh(state: AuthState) =
requestToken(state.createTokenRefreshRequest())
suspend fun Context.requestTokenExchange(response: AuthorizationResponse) =
requestToken(response.createTokenExchangeRequest())

@ -0,0 +1,18 @@
package org.tasks.sync.microsoft
import com.google.gson.Gson
import retrofit2.Response
data class Error(
val error: ErrorBody,
) {
data class ErrorBody(
val code: String,
val message: String,
)
companion object {
fun <T> Response<T>.toMicrosoftError()
= errorBody()?.string()?.let { Gson().fromJson(it, Error::class.java) }
}
}

@ -0,0 +1,63 @@
package org.tasks.sync.microsoft
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.api.CaldavFilter
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
@AndroidEntryPoint
class MicrosoftListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
private val viewModel: MicrosoftListSettingsActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenResumed {
viewModel
.viewState
.collect { state ->
state.error?.let { throwable ->
requestFailed(throwable)
viewModel.clearError()
}
if (state.deleted) {
setResult(Activity.RESULT_OK, Intent(TaskListFragment.ACTION_DELETED))
finish()
}
state.result?.let {
setResult(
Activity.RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD).putExtra(
MainActivity.OPEN_FILTER,
CaldavFilter(it)
)
)
finish()
}
}
}
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) =
viewModel.createList(name)
override suspend fun updateNameAndColor(
account: CaldavAccount,
calendar: CaldavCalendar,
name: String,
color: Int
) = viewModel.updateList(name)
override suspend fun deleteCalendar(
caldavAccount: CaldavAccount,
caldavCalendar: CaldavCalendar
) = viewModel.deleteList()
}

@ -0,0 +1,102 @@
package org.tasks.sync.microsoft
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.google.gson.Gson
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.http.HttpClientFactory
import org.tasks.http.HttpClientFactory.Companion.MEDIA_TYPE_JSON
import retrofit2.HttpException
import retrofit2.Response
import javax.inject.Inject
@HiltViewModel
class MicrosoftListSettingsActivityViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val httpClientFactory: HttpClientFactory,
private val caldavDao: CaldavDao,
private val taskDeleter: TaskDeleter,
) : ViewModel() {
data class ViewState(
val requestInFlight: Boolean = false,
val result: CaldavCalendar? = null,
val error: Throwable? = null,
val deleted: Boolean = false,
)
private val _viewState = MutableStateFlow(ViewState())
val viewState = _viewState.asStateFlow()
private val account: CaldavAccount =
savedStateHandle[BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT]!!
val list: CaldavCalendar? =
savedStateHandle[BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR]
suspend fun createList(displayName: String) {
_viewState.update { it.copy(requestInFlight = true) }
val microsoftService = httpClientFactory.getMicrosoftService(account)
val taskList = TaskLists.TaskList(displayName = displayName)
val body = Gson().toJson(taskList).toRequestBody(MEDIA_TYPE_JSON)
val result = microsoftService.createList(body)
if (result.isSuccessful) {
val list = CaldavCalendar().apply {
account = this@MicrosoftListSettingsActivityViewModel.account.uuid
result.body()?.applyTo(this)
}
caldavDao.insert(list)
_viewState.update { it.copy(result = list) }
} else {
requestFailed(result)
}
}
suspend fun deleteList() {
_viewState.update { it.copy(requestInFlight = true) }
val microsoftService = httpClientFactory.getMicrosoftService(account)
val result = microsoftService.deleteList(list?.uuid!!)
if (result.isSuccessful) {
taskDeleter.delete(list)
_viewState.update { it.copy(deleted = true) }
} else {
requestFailed(result)
}
}
suspend fun updateList(displayName: String) {
_viewState.update { it.copy(requestInFlight = true) }
val microsoftService = httpClientFactory.getMicrosoftService(account)
val taskList = TaskLists.TaskList(displayName = displayName)
val body = Gson().toJson(taskList).toRequestBody(MEDIA_TYPE_JSON)
val result = microsoftService.updateList(list?.uuid!!, body)
if (result.isSuccessful) {
result.body()?.applyTo(list)
caldavDao.update(list)
_viewState.update { it.copy(result = list) }
} else {
requestFailed(result)
}
}
fun clearError() {
_viewState.update { it.copy(error = null) }
}
private fun <T> requestFailed(result: Response<T>) {
_viewState.update {
it.copy(
requestInFlight = false,
error = HttpException(result),
)
}
}
}

@ -22,7 +22,7 @@ interface MicrosoftService {
): Response<TaskLists.TaskList>
@DELETE("/v1.0/me/todo/lists/{listId}")
suspend fun deleteList(@Path("listId") listId: String)
suspend fun deleteList(@Path("listId") listId: String): Response<ResponseBody>
@GET("/v1.0/me/todo/lists/{listId}/tasks/delta")
suspend fun getTasks(@Path("listId") listId: String): Response<Tasks>

@ -1,6 +1,7 @@
package org.tasks.sync.microsoft
import com.squareup.moshi.Json
import org.tasks.data.CaldavCalendar
data class TaskLists(
@field:Json(name = "@odata.context") val context: String,
@ -8,11 +9,24 @@ data class TaskLists(
@field:Json(name = "@odata.nextLink") val nextPage: String?,
) {
data class TaskList(
@Json(name = "@odata.etag") val etag: String,
val displayName: String,
val isOwner: Boolean,
val isShared: Boolean,
val wellknownListName: String,
val id: String,
)
@Json(name = "@odata.etag") val etag: String? = null,
val displayName: String? = null,
val isOwner: Boolean? = null,
val isShared: Boolean? = null,
val wellknownListName: String? = null,
val id: String? = null,
) {
fun applyTo(list: CaldavCalendar) {
with (list) {
name = displayName
url = this@TaskList.id
uuid = this@TaskList.id
access = when {
isOwner == true -> CaldavCalendar.ACCESS_OWNER
isShared == true -> CaldavCalendar.ACCESS_READ_WRITE
else -> CaldavCalendar.ACCESS_UNKNOWN
}
}
}
}
}
Loading…
Cancel
Save