mirror of https://github.com/tasks/tasks
Compare commits
No commits in common. 'main' and '13.11.2' have entirely different histories.
@ -1,4 +1,5 @@
|
||||
github: abaker
|
||||
liberapay: tasks
|
||||
open_collective: tasks
|
||||
patreon: tasks
|
||||
custom: tasks.org/donate
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
name: Update Dependency Diff
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'gradle/libs.versions.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'gradle/libs.versions.toml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-deps:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Update dependency diffs
|
||||
run: ./update_dependency_diff
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add deps_*.txt
|
||||
git diff --staged --quiet || git commit -m "Update dependency diffs"
|
||||
git push
|
||||
@ -1,32 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FASTLANE: ${{ secrets.FASTLANE }}
|
||||
|
||||
jobs:
|
||||
bundle:
|
||||
uses: ./.github/workflows/bundle.yml
|
||||
secrets: inherit
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ bundle ]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Fastlane key
|
||||
run: |
|
||||
echo "$FASTLANE" > ./fastlane.json
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release
|
||||
path: .
|
||||
- name: Deploy
|
||||
run: bundle exec fastlane deploy
|
||||
@ -1 +1 @@
|
||||
3.4.8
|
||||
3.3.4
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" singleton="true">
|
||||
<module name="tasks.Tasks.wear.main" />
|
||||
<option name="DEPLOY" value="true" />
|
||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
||||
<option name="DEPLOY_AS_INSTANT" value="false" />
|
||||
<option name="ARTIFACT_NAME" value="" />
|
||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
||||
<option name="ALL_USERS" value="false" />
|
||||
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
|
||||
<option name="CLEAR_APP_STORAGE" value="false" />
|
||||
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
||||
<option name="MODE" value="default_activity" />
|
||||
<option name="RESTORE_ENABLED" value="false" />
|
||||
<option name="RESTORE_FILE" value="" />
|
||||
<option name="CLEAR_LOGCAT" value="true" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||
<Auto>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
||||
</Auto>
|
||||
<Hybrid>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
||||
</Hybrid>
|
||||
<Java>
|
||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
||||
</Java>
|
||||
<Native>
|
||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||
<option name="SHOW_STATIC_VARS" value="true" />
|
||||
<option name="WORKING_DIR" value="" />
|
||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
||||
</Native>
|
||||
<Profilers>
|
||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
|
||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||
</Profilers>
|
||||
<option name="DEEP_LINK" value="" />
|
||||
<option name="ACTIVITY_CLASS" value="" />
|
||||
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
|
||||
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
|
||||
<method v="2">
|
||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "org.tasks.ak",
|
||||
"variantName": "genericRelease",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 130804,
|
||||
"versionName": "14.0.6",
|
||||
"outputFile": "app-generic-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
package com.todoroo.astrid.gcal
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.core.net.toUri
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.tasks.TestUtilities.withTZ
|
||||
import org.tasks.data.entity.Task
|
||||
import org.tasks.injection.InjectingTestCase
|
||||
import org.tasks.time.DateTime
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class GCalHelperTest : InjectingTestCase() {
|
||||
|
||||
@get:Rule
|
||||
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
||||
Manifest.permission.READ_CALENDAR,
|
||||
Manifest.permission.WRITE_CALENDAR
|
||||
)
|
||||
|
||||
@Inject lateinit var gcalHelper: GCalHelper
|
||||
|
||||
private var testCalendarId: Long = -1
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
testCalendarId = createTestCalendar()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
if (testCalendarId > 0) {
|
||||
try {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
context.contentResolver.delete(
|
||||
ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendarId),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun allDayEventInNewYork() = assertAllDayEvent("America/New_York") // UTC-5
|
||||
@Test fun allDayEventInBerlin() = assertAllDayEvent("Europe/Berlin") // UTC+1
|
||||
@Test fun allDayEventInAuckland() = assertAllDayEvent("Pacific/Auckland") // UTC+13
|
||||
@Test fun allDayEventInTokyo() = assertAllDayEvent("Asia/Tokyo") // UTC+9
|
||||
@Test fun allDayEventInHonolulu() = assertAllDayEvent("Pacific/Honolulu") // UTC-10
|
||||
@Test fun allDayEventInChatham() = assertAllDayEvent("Pacific/Chatham") // UTC+13:45
|
||||
|
||||
private fun assertAllDayEvent(timezone: String) = withTZ(timezone) {
|
||||
val task = Task(dueDate = DateTime(2024, 12, 20).millis)
|
||||
|
||||
val eventUri = gcalHelper.createTaskEvent(task, testCalendarId.toString())
|
||||
?: throw RuntimeException("Event not created")
|
||||
|
||||
val event = queryEvent(eventUri.toString()) ?: throw RuntimeException("Event not found")
|
||||
|
||||
assertEquals(
|
||||
"DTSTART should be Dec 20 00:00 UTC",
|
||||
DateTime(2024, 12, 20, timeZone = DateTime.UTC).millis,
|
||||
event.dtStart
|
||||
)
|
||||
assertEquals(
|
||||
"DTEND should be Dec 21 00:00 UTC",
|
||||
DateTime(2024, 12, 21, timeZone = DateTime.UTC).millis,
|
||||
event.dtEnd
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestCalendar(): Long {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val values = ContentValues().apply {
|
||||
put(Calendars.ACCOUNT_NAME, "test@test.com")
|
||||
put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
|
||||
put(Calendars.NAME, "Test Calendar")
|
||||
put(Calendars.CALENDAR_DISPLAY_NAME, "Test Calendar")
|
||||
put(Calendars.CALENDAR_COLOR, 0xFF0000)
|
||||
put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
put(Calendars.OWNER_ACCOUNT, "test@test.com")
|
||||
put(Calendars.VISIBLE, 1)
|
||||
put(Calendars.SYNC_EVENTS, 1)
|
||||
}
|
||||
val uri = Calendars.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, "test@test.com")
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
|
||||
.build()
|
||||
val calendarUri = context.contentResolver.insert(uri, values)
|
||||
return ContentUris.parseId(calendarUri!!)
|
||||
}
|
||||
|
||||
private fun queryEvent(eventUri: String): CalendarEvent? {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val cursor = context.contentResolver.query(
|
||||
eventUri.toUri(),
|
||||
arrayOf(
|
||||
Events.DTSTART,
|
||||
Events.DTEND,
|
||||
Events.ALL_DAY,
|
||||
Events.EVENT_TIMEZONE
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
CalendarEvent(
|
||||
dtStart = it.getLong(0),
|
||||
dtEnd = it.getLong(1),
|
||||
allDay = it.getInt(2) == 1,
|
||||
timezone = it.getString(3)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
private data class CalendarEvent(
|
||||
val dtStart: Long,
|
||||
val dtEnd: Long,
|
||||
val allDay: Boolean,
|
||||
val timezone: String?
|
||||
)
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package org.tasks.data
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.tasks.data.dao.CaldavDao
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.injection.InjectingTestCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class CaldavDaoExtensionsTest : InjectingTestCase() {
|
||||
@Inject lateinit var caldavDao: CaldavDao
|
||||
|
||||
@Test
|
||||
fun getLocalListCreatesAccountIfNeeded() = runBlocking {
|
||||
withTimeout(5000L) {
|
||||
assertTrue(caldavDao.getAccounts().isEmpty())
|
||||
caldavDao.getLocalList()
|
||||
assertTrue(caldavDao.getAccounts(CaldavAccount.TYPE_LOCAL).isNotEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package org.tasks
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.google.api.client.http.HttpRequest
|
||||
import com.google.api.client.http.HttpResponse
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class DebugNetworkInterceptor @Inject constructor(@param:ApplicationContext private val context: Context) {
|
||||
fun apply(builder: OkHttpClient.Builder?) {
|
||||
builder?.addNetworkInterceptor(FlipperOkhttpInterceptor(getNetworkPlugin(context)))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun <T> execute(request: HttpRequest, responseClass: Class<T>): T? {
|
||||
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
|
||||
request
|
||||
.setInterceptor(interceptor)
|
||||
.setResponseInterceptor(interceptor)
|
||||
.execute()
|
||||
return interceptor.response
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun <T> report(httpResponse: HttpResponse, responseClass: Class<T>, start: Long, finish: Long): T? {
|
||||
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
|
||||
interceptor.report(httpResponse, start, finish)
|
||||
return interceptor.response
|
||||
}
|
||||
|
||||
private fun getNetworkPlugin(context: Context): NetworkFlipperPlugin {
|
||||
return AndroidFlipperClient.getInstance(context).getPlugin(NetworkFlipperPlugin.ID)!!
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package org.tasks
|
||||
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.NetworkReporter
|
||||
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo
|
||||
import com.google.api.client.http.*
|
||||
import com.google.api.client.json.GenericJson
|
||||
import org.tasks.data.UUIDHelper
|
||||
import org.tasks.time.DateTimeUtils2.currentTimeMillis
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
internal class FlipperHttpInterceptor<T>(private val plugin: NetworkFlipperPlugin, private val responseClass: Class<T>) : HttpExecuteInterceptor, HttpResponseInterceptor {
|
||||
private val requestId = UUIDHelper.newUUID()
|
||||
|
||||
var response: T? = null
|
||||
private set
|
||||
|
||||
override fun intercept(request: HttpRequest) {
|
||||
plugin.reportRequest(toRequestInfo(request, currentTimeMillis()))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun interceptResponse(response: HttpResponse) {
|
||||
plugin.reportResponse(toResponseInfo(response, currentTimeMillis()))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun report(response: HttpResponse, start: Long, end: Long) {
|
||||
plugin.reportRequest(toRequestInfo(response.request, start))
|
||||
plugin.reportResponse(toResponseInfo(response, end))
|
||||
}
|
||||
|
||||
private fun toRequestInfo(request: HttpRequest, timestamp: Long): NetworkReporter.RequestInfo {
|
||||
val requestInfo = NetworkReporter.RequestInfo()
|
||||
requestInfo.method = request.requestMethod
|
||||
requestInfo.body = bodyToByteArray(request.content)
|
||||
requestInfo.headers = getHeaders(request.headers)
|
||||
requestInfo.requestId = requestId
|
||||
requestInfo.timeStamp = timestamp
|
||||
requestInfo.uri = request.url.toString()
|
||||
return requestInfo
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun toResponseInfo(response: HttpResponse, timestamp: Long): ResponseInfo {
|
||||
val responseInfo = ResponseInfo()
|
||||
responseInfo.timeStamp = timestamp
|
||||
responseInfo.headers = getHeaders(response.headers)
|
||||
responseInfo.requestId = requestId
|
||||
responseInfo.statusCode = response.statusCode
|
||||
responseInfo.statusReason = response.statusMessage
|
||||
this.response = response.parseAs(responseClass)
|
||||
if (this.response is GenericJson) {
|
||||
try {
|
||||
responseInfo.body = (this.response as GenericJson).toPrettyString().toByteArray()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
return responseInfo
|
||||
}
|
||||
|
||||
private fun getHeaders(headers: HttpHeaders): List<NetworkReporter.Header> {
|
||||
return headers.map { (name, value) -> NetworkReporter.Header(name, value.toString()) }
|
||||
}
|
||||
|
||||
private fun bodyToByteArray(content: HttpContent?): ByteArray? {
|
||||
if (content == null) {
|
||||
return null
|
||||
}
|
||||
val output = ByteArrayOutputStream()
|
||||
try {
|
||||
content.writeTo(output)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return null
|
||||
}
|
||||
return output.toByteArray()
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"redirect_uri" : "msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D",
|
||||
"account_mode" : "MULTIPLE",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "verbose",
|
||||
"logcat_enabled": true,
|
||||
"pii_enabled": true
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest>
|
||||
|
||||
<application tools:ignore="MissingApplicationIcon">
|
||||
<activity
|
||||
android:name=".auth.MicrosoftAuthenticationActivity"
|
||||
android:theme="@style/TranslucentDialog"/>
|
||||
<activity
|
||||
android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:host="${applicationId}"
|
||||
android:scheme="msauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<application/>
|
||||
|
||||
</manifest>
|
||||
|
||||
@ -1,40 +1,19 @@
|
||||
package org.tasks.analytics
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.tasks.R
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.time.DateTimeUtils2.currentTimeMillis
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class Firebase @Inject constructor(
|
||||
@param:ApplicationContext val context: Context,
|
||||
private val preferences: Preferences
|
||||
) {
|
||||
class Firebase @Inject constructor() {
|
||||
fun reportException(t: Throwable) = Timber.e(t)
|
||||
|
||||
fun updateRemoteConfig() {}
|
||||
|
||||
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {
|
||||
Timber.d("${context.getString(event)} -> $params")
|
||||
}
|
||||
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {}
|
||||
|
||||
fun addTask(source: String) =
|
||||
logEvent(R.string.event_add_task, R.string.param_type to source)
|
||||
fun addTask(source: String) {}
|
||||
|
||||
fun completeTask(source: String) =
|
||||
logEvent(R.string.event_complete_task, R.string.param_type to source)
|
||||
|
||||
val subscribeCooldown: Boolean
|
||||
get() = installCooldown
|
||||
|| preferences.lastSubscribeRequest + days(28L) > currentTimeMillis()
|
||||
|
||||
private val installCooldown: Boolean
|
||||
get() = preferences.installDate + days(7L) > currentTimeMillis()
|
||||
|
||||
private fun days(default: Long): Long =
|
||||
TimeUnit.DAYS.toMillis(default)
|
||||
}
|
||||
val subscribeCooldown = false
|
||||
val moreOptionsBadge = false
|
||||
val moreOptionsSolid = false
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
package org.tasks.auth
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class IdentityProvider(
|
||||
val name: String,
|
||||
val discoveryEndpoint: Uri,
|
||||
val clientId: String,
|
||||
val redirectUri: Uri,
|
||||
val scope: String
|
||||
) {
|
||||
suspend fun retrieveConfig(): AuthorizationServiceConfiguration {
|
||||
return suspendCoroutine { cont ->
|
||||
AuthorizationServiceConfiguration.fetchFromUrl(discoveryEndpoint) { serviceConfiguration, ex ->
|
||||
cont.resumeWith(
|
||||
when {
|
||||
ex != null -> Result.failure(ex)
|
||||
serviceConfiguration != null -> Result.success(serviceConfiguration)
|
||||
else -> Result.failure(IllegalStateException())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val MICROSOFT = IdentityProvider(
|
||||
"Microsoft",
|
||||
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
|
||||
"9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
|
||||
"user.read Tasks.ReadWrite openid offline_access email"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import net.openid.appauth.AuthState
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.security.KeyStoreEncryption
|
||||
import javax.inject.Inject
|
||||
|
||||
class MicrosoftTokenProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val encryption: KeyStoreEncryption,
|
||||
) {
|
||||
suspend fun getToken(account: CaldavAccount): String {
|
||||
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")
|
||||
}
|
||||
return authState.accessToken!!
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.microsoft.identity.client.AcquireTokenParameters
|
||||
import com.microsoft.identity.client.AuthenticationCallback
|
||||
import com.microsoft.identity.client.IAuthenticationResult
|
||||
import com.microsoft.identity.client.Prompt
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import com.microsoft.identity.client.exception.MsalException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Constants
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.data.UUIDHelper
|
||||
import org.tasks.data.dao.CaldavDao
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
|
||||
import org.tasks.extensions.Context.toast
|
||||
import org.tasks.jobs.WorkManager
|
||||
import org.tasks.sync.SyncAdapters
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MicrosoftSignInViewModel @Inject constructor(
|
||||
private val caldavDao: CaldavDao,
|
||||
private val firebase: Firebase,
|
||||
private val syncAdapters: SyncAdapters,
|
||||
private val workManager: WorkManager,
|
||||
) : ViewModel() {
|
||||
fun signIn(activity: Activity) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
|
||||
activity,
|
||||
R.raw.microsoft_config
|
||||
)
|
||||
|
||||
val parameters = AcquireTokenParameters.Builder()
|
||||
.startAuthorizationFromActivity(activity)
|
||||
.withScopes(scopes)
|
||||
.withPrompt(Prompt.SELECT_ACCOUNT)
|
||||
.withCallback(object : AuthenticationCallback {
|
||||
override fun onSuccess(authenticationResult: IAuthenticationResult) {
|
||||
val email = authenticationResult.account.claims?.get("preferred_username") as? String
|
||||
if (email == null) {
|
||||
Timber.e("No email found")
|
||||
return
|
||||
}
|
||||
Timber.d("Successfully signed in")
|
||||
viewModelScope.launch {
|
||||
caldavDao
|
||||
.getAccount(TYPE_MICROSOFT, email)
|
||||
?.let {
|
||||
caldavDao.update(
|
||||
it.copy(error = null)
|
||||
)
|
||||
}
|
||||
?: caldavDao
|
||||
.insert(
|
||||
CaldavAccount(
|
||||
uuid = UUIDHelper.newUUID(),
|
||||
name = email,
|
||||
username = email,
|
||||
accountType = TYPE_MICROSOFT,
|
||||
)
|
||||
)
|
||||
.also {
|
||||
firebase.logEvent(
|
||||
R.string.event_sync_add_account,
|
||||
R.string.param_type to Constants.SYNC_TYPE_MICROSOFT
|
||||
)
|
||||
}
|
||||
syncAdapters.sync(true)
|
||||
workManager.updateBackgroundSync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(exception: MsalException?) {
|
||||
Timber.e(exception)
|
||||
activity.toast(exception?.message ?: exception?.javaClass?.simpleName ?: "Sign in failed")
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
Timber.d("onCancel")
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
app.acquireToken(parameters)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val scopes = listOf("https://graph.microsoft.com/.default")
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.content.Context
|
||||
import com.microsoft.identity.client.AcquireTokenSilentParameters
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.tasks.R
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MicrosoftTokenProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun getToken(account: CaldavAccount): String {
|
||||
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
|
||||
context,
|
||||
R.raw.microsoft_config
|
||||
)
|
||||
|
||||
val result = try {
|
||||
val msalAccount = app.accounts.firstOrNull { it.username == account.username }
|
||||
?: throw RuntimeException("No matching account found")
|
||||
|
||||
val parameters = AcquireTokenSilentParameters.Builder()
|
||||
.withScopes(MicrosoftSignInViewModel.scopes)
|
||||
.forAccount(msalAccount)
|
||||
.fromAuthority(msalAccount.authority)
|
||||
.forceRefresh(true)
|
||||
.build()
|
||||
|
||||
app.acquireTokenSilent(parameters)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw RuntimeException("Authentication failed: ${e.message}")
|
||||
}
|
||||
return result.accessToken
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package org.tasks.wear
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
||||
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
|
||||
import com.google.android.horologist.data.WearDataLayerRegistry
|
||||
import com.google.android.horologist.datalayer.grpc.server.BaseGrpcDataService
|
||||
import com.todoroo.astrid.dao.TaskDao
|
||||
import com.todoroo.astrid.service.TaskCompleter
|
||||
import com.todoroo.astrid.service.TaskCreator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.tasks.GrpcProto.Settings
|
||||
import org.tasks.WearServiceGrpcKt
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.extensions.wearDataLayerRegistry
|
||||
import org.tasks.filters.FilterProvider
|
||||
import org.tasks.preferences.DefaultFilterProvider
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.tasklist.HeaderFormatter
|
||||
import org.tasks.themes.ColorProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
@AndroidEntryPoint
|
||||
class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCoroutineImplBase>() {
|
||||
|
||||
@Inject lateinit var taskDao: TaskDao
|
||||
@Inject lateinit var preferences: Preferences
|
||||
@Inject lateinit var taskCompleter: TaskCompleter
|
||||
@Inject lateinit var headerFormatter: HeaderFormatter
|
||||
@Inject lateinit var firebase: Firebase
|
||||
@Inject lateinit var filterProvider: FilterProvider
|
||||
@Inject lateinit var inventory: Inventory
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
|
||||
@Inject lateinit var taskCreator: TaskCreator
|
||||
|
||||
override val registry: WearDataLayerRegistry by lazy {
|
||||
applicationContext.wearDataLayerRegistry(lifecycleScope)
|
||||
}
|
||||
|
||||
private val settings: DataStore<Settings> by lazy {
|
||||
registry.protoDataStore(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun buildService(): WearServiceGrpcKt.WearServiceCoroutineImplBase {
|
||||
return WearService(
|
||||
taskDao = taskDao,
|
||||
appPreferences = preferences,
|
||||
taskCompleter = taskCompleter,
|
||||
headerFormatter = headerFormatter,
|
||||
settings = settings,
|
||||
firebase = firebase,
|
||||
filterProvider = filterProvider,
|
||||
inventory = inventory,
|
||||
colorProvider = colorProvider,
|
||||
defaultFilterProvider = defaultFilterProvider,
|
||||
taskCreator = taskCreator,
|
||||
is24HourTime = DateFormat.is24HourFormat(applicationContext),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package org.tasks.wear
|
||||
|
||||
import org.tasks.GrpcProto.Settings
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.preferences.QueryPreferences
|
||||
|
||||
class WearPreferences(
|
||||
preferences: Preferences,
|
||||
private val settings: Settings,
|
||||
): QueryPreferences by preferences {
|
||||
override val showHidden: Boolean
|
||||
get() = settings.showHidden
|
||||
|
||||
override val showCompleted: Boolean
|
||||
get() = settings.showCompleted
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package org.tasks.wear
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.google.android.horologist.annotations.ExperimentalHorologistApi
|
||||
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
|
||||
import com.google.android.horologist.data.WearDataLayerRegistry
|
||||
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.tasks.GrpcProto.LastUpdate
|
||||
import org.tasks.copy
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalHorologistApi::class)
|
||||
class WearRefresherImpl(
|
||||
phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
|
||||
private val registry: WearDataLayerRegistry,
|
||||
private val scope: CoroutineScope,
|
||||
) : WearRefresher {
|
||||
|
||||
private var watchConnected = false
|
||||
|
||||
init {
|
||||
phoneDataLayerAppHelper
|
||||
.connectedAndInstalledNodes
|
||||
.catch { Timber.e("${it.message}") }
|
||||
.onEach { nodes ->
|
||||
Timber.d("Connected nodes: ${nodes.joinToString()}")
|
||||
watchConnected = nodes.isNotEmpty()
|
||||
lastUpdate.update()
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private val lastUpdate: DataStore<LastUpdate> by lazy {
|
||||
registry.protoDataStore<LastUpdate>(scope)
|
||||
}
|
||||
|
||||
override suspend fun refresh() {
|
||||
if (watchConnected) {
|
||||
lastUpdate.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataStore<LastUpdate>.update() {
|
||||
updateData { it.copy { now = System.currentTimeMillis() } }
|
||||
}
|
||||
@ -1,264 +0,0 @@
|
||||
package org.tasks.wear
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.todoroo.astrid.core.SortHelper.SORT_DUE
|
||||
import com.todoroo.astrid.dao.TaskDao
|
||||
import com.todoroo.astrid.service.TaskCompleter
|
||||
import com.todoroo.astrid.service.TaskCreator
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import org.tasks.GrpcProto
|
||||
import org.tasks.GrpcProto.CompleteTaskRequest
|
||||
import org.tasks.GrpcProto.CompleteTaskResponse
|
||||
import org.tasks.GrpcProto.GetListsResponse
|
||||
import org.tasks.GrpcProto.GetTaskResponse
|
||||
import org.tasks.GrpcProto.GetTasksRequest
|
||||
import org.tasks.GrpcProto.ListItem
|
||||
import org.tasks.GrpcProto.ListItemType
|
||||
import org.tasks.GrpcProto.SaveTaskResponse
|
||||
import org.tasks.GrpcProto.Tasks
|
||||
import org.tasks.GrpcProto.ToggleGroupRequest
|
||||
import org.tasks.GrpcProto.ToggleGroupResponse
|
||||
import org.tasks.WearServiceGrpcKt
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.copy
|
||||
import org.tasks.data.NO_COUNT
|
||||
import org.tasks.data.isHidden
|
||||
import org.tasks.filters.AstridOrderingFilter
|
||||
import org.tasks.filters.Filter
|
||||
import org.tasks.filters.FilterProvider
|
||||
import org.tasks.filters.MyTasksFilter
|
||||
import org.tasks.filters.NavigationDrawerSubheader
|
||||
import org.tasks.filters.getIcon
|
||||
import org.tasks.kmp.org.tasks.time.DateStyle
|
||||
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
|
||||
import org.tasks.kmp.org.tasks.time.getTimeString
|
||||
import org.tasks.preferences.DefaultFilterProvider
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.tasklist.HeaderFormatter
|
||||
import org.tasks.tasklist.SectionedDataSource
|
||||
import org.tasks.tasklist.UiItem
|
||||
import org.tasks.themes.ColorProvider
|
||||
import org.tasks.time.DateTimeUtils2.currentTimeMillis
|
||||
import org.tasks.time.startOfDay
|
||||
import timber.log.Timber
|
||||
|
||||
class WearService(
|
||||
private val taskDao: TaskDao,
|
||||
private val appPreferences: Preferences,
|
||||
private val taskCompleter: TaskCompleter,
|
||||
private val headerFormatter: HeaderFormatter,
|
||||
private val settings: DataStore<GrpcProto.Settings>,
|
||||
private val firebase: Firebase,
|
||||
private val filterProvider: FilterProvider,
|
||||
private val inventory: Inventory,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val defaultFilterProvider: DefaultFilterProvider,
|
||||
private val taskCreator: TaskCreator,
|
||||
private val is24HourTime: Boolean,
|
||||
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
|
||||
override suspend fun getTasks(request: GetTasksRequest): Tasks {
|
||||
val position = request.position
|
||||
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
|
||||
val settingsData = settings.data.firstOrNull() ?: GrpcProto.Settings.getDefaultInstance()
|
||||
val filter =
|
||||
defaultFilterProvider.getFilterFromPreference(settingsData.filter.takeIf { it.isNotBlank() })
|
||||
val preferences = WearPreferences(appPreferences, settingsData)
|
||||
val collapsed = settingsData?.collapsedList?.toSet() ?: emptySet()
|
||||
val payload = SectionedDataSource(
|
||||
tasks = taskDao.fetchTasks(preferences, filter),
|
||||
disableHeaders = filter.disableHeaders()
|
||||
|| (filter.supportsManualSort() && preferences.isManualSort)
|
||||
|| (filter is AstridOrderingFilter && preferences.isAstridSort),
|
||||
groupMode = preferences.groupMode,
|
||||
subtaskMode = preferences.subtaskMode,
|
||||
completedAtBottom = preferences.completedTasksAtBottom,
|
||||
collapsed = collapsed,
|
||||
)
|
||||
return Tasks.newBuilder()
|
||||
.setTotalItems(payload.size)
|
||||
.addAllItems(
|
||||
payload
|
||||
.subList(position, position + limit)
|
||||
.map { item ->
|
||||
when (item) {
|
||||
is UiItem.Header ->
|
||||
GrpcProto.UiItem.newBuilder()
|
||||
.setId(item.value)
|
||||
.setType(ListItemType.Header)
|
||||
.setTitle(headerFormatter.headerString(item.value, style = DateStyle.MEDIUM))
|
||||
.setCollapsed(item.collapsed)
|
||||
.build()
|
||||
|
||||
is UiItem.Task -> {
|
||||
val timestamp = if (preferences.groupMode == SORT_DUE &&
|
||||
(item.task.sortGroup
|
||||
?: 0) >= currentTimeMillis().startOfDay()
|
||||
) {
|
||||
item.task.takeIf { it.hasDueTime() }?.let {
|
||||
getTimeString(item.task.dueDate, is24HourTime)
|
||||
}
|
||||
} else if (item.task.hasDueDate()) {
|
||||
getRelativeDateTime(
|
||||
item.task.dueDate,
|
||||
is24HourTime,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
GrpcProto.UiItem.newBuilder()
|
||||
.setType(ListItemType.Item)
|
||||
.setId(item.task.id)
|
||||
.setPriority(item.task.priority)
|
||||
.setCompleted(item.task.isCompleted)
|
||||
.setHidden(item.task.task.isHidden)
|
||||
.setIndent(item.task.indent)
|
||||
.setCollapsed(item.task.isCollapsed)
|
||||
.setNumSubtasks(item.task.children)
|
||||
.apply {
|
||||
if (item.task.title != null) {
|
||||
setTitle(item.task.title)
|
||||
}
|
||||
if (timestamp != null) {
|
||||
setTimestamp(timestamp)
|
||||
}
|
||||
}
|
||||
.setRepeating(item.task.task.isRecurring)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override suspend fun completeTask(request: CompleteTaskRequest): CompleteTaskResponse {
|
||||
taskCompleter.setComplete(request.id, request.completed)
|
||||
firebase.completeTask("wearable")
|
||||
return CompleteTaskResponse.newBuilder().setSuccess(true).build()
|
||||
}
|
||||
|
||||
override suspend fun toggleGroup(request: ToggleGroupRequest): ToggleGroupResponse {
|
||||
settings.updateData {
|
||||
it.copy {
|
||||
if (request.collapsed) {
|
||||
if (!collapsed.contains(request.value)) {
|
||||
collapsed.add(request.value)
|
||||
}
|
||||
} else {
|
||||
if (collapsed.contains(request.value)) {
|
||||
collapsed.clear()
|
||||
collapsed.addAll(
|
||||
it.collapsedList.toMutableList().apply { remove(request.value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ToggleGroupResponse.getDefaultInstance()
|
||||
}
|
||||
|
||||
override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings {
|
||||
return settings.updateData { request.settings }
|
||||
}
|
||||
|
||||
override suspend fun toggleSubtasks(request: ToggleGroupRequest): ToggleGroupResponse {
|
||||
taskDao.setCollapsed(request.value, request.collapsed)
|
||||
return ToggleGroupResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
override suspend fun getLists(request: GrpcProto.GetListsRequest): GetListsResponse {
|
||||
val position = request.position
|
||||
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
|
||||
val selected = settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
|
||||
?: defaultFilterProvider.getFilterPreferenceValue(MyTasksFilter.create())
|
||||
val filters = filterProvider.wearableFilters()
|
||||
return GetListsResponse.newBuilder()
|
||||
.setTotalItems(filters.size)
|
||||
.addAllItems(
|
||||
filters
|
||||
.subList(position, (position + limit).coerceAtMost(filters.size))
|
||||
.map { item ->
|
||||
when (item) {
|
||||
is Filter -> {
|
||||
ListItem.newBuilder()
|
||||
.setId(defaultFilterProvider.getFilterPreferenceValue(item))
|
||||
.setType(ListItemType.Item)
|
||||
.setTitle(item.title ?: "")
|
||||
.setIcon(item.getIcon(inventory))
|
||||
.setColor(getColor(item))
|
||||
.setTaskCount(item.count.takeIf { it != NO_COUNT } ?: try {
|
||||
taskDao.count(item)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
0
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
is NavigationDrawerSubheader ->
|
||||
ListItem.newBuilder()
|
||||
.setType(ListItemType.Header)
|
||||
.setTitle(item.title ?: "")
|
||||
.setId("${item.subheaderType}_${item.id}")
|
||||
.build()
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override suspend fun getTask(request: GrpcProto.GetTaskRequest): GetTaskResponse {
|
||||
Timber.d("getTask($request)")
|
||||
val task = taskDao.fetch(request.taskId)
|
||||
?: throw IllegalArgumentException()
|
||||
return GetTaskResponse.newBuilder()
|
||||
.setTitle(task.title ?: "")
|
||||
.setCompleted(task.isCompleted)
|
||||
.setPriority(task.priority)
|
||||
.setRepeating(task.isRecurring)
|
||||
.build()
|
||||
}
|
||||
|
||||
override suspend fun saveTask(request: GrpcProto.SaveTaskRequest): SaveTaskResponse {
|
||||
Timber.d("saveTask($request)")
|
||||
if (request.taskId == 0L) {
|
||||
val filter = defaultFilterProvider.getFilterFromPreference(
|
||||
settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
|
||||
)
|
||||
val task = taskCreator.basicQuickAddTask(
|
||||
title = request.title,
|
||||
filter = filter,
|
||||
)
|
||||
firebase.addTask("wearable")
|
||||
return SaveTaskResponse.newBuilder().setTaskId(task.id).build()
|
||||
} else {
|
||||
taskDao.fetch(request.taskId)?.let { task ->
|
||||
taskDao.save(
|
||||
task.copy(
|
||||
title = request.title,
|
||||
completionDate = when {
|
||||
!request.completed -> 0
|
||||
task.isCompleted -> task.completionDate
|
||||
else -> System.currentTimeMillis()
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
return SaveTaskResponse.newBuilder().setTaskId(request.taskId).build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColor(filter: Filter): Int {
|
||||
if (filter.tint != 0) {
|
||||
val color = colorProvider.getThemeColor(filter.tint, true)
|
||||
if (color.isFree || inventory.purchasedThemes()) {
|
||||
return color.primaryColor
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string-array name="android_wear_capabilities" tools:ignore="UnusedResources">
|
||||
<item>data_layer_app_helper_device_phone</item>
|
||||
<item>horologist_phone</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"redirect_uri" : "msauth://org.tasks/sEe08kX5nGJi4miFX3VkNXICC%2FY%3D",
|
||||
"account_mode" : "MULTIPLE",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"logcat_enabled": true,
|
||||
"pii_enabled": false
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
|
||||
<string name="microsoft_oauth_path">/sEe08kX5nGJi4miFX3VkNXICC/Y=</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue