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
|
github: abaker
|
||||||
liberapay: tasks
|
liberapay: tasks
|
||||||
|
open_collective: tasks
|
||||||
patreon: tasks
|
patreon: tasks
|
||||||
custom: tasks.org/donate
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
<manifest>
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application tools:ignore="MissingApplicationIcon">
|
<application/>
|
||||||
<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>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -1,40 +1,19 @@
|
|||||||
package org.tasks.analytics
|
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 timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
class Firebase @Inject constructor(
|
class Firebase @Inject constructor() {
|
||||||
@param:ApplicationContext val context: Context,
|
|
||||||
private val preferences: Preferences
|
|
||||||
) {
|
|
||||||
fun reportException(t: Throwable) = Timber.e(t)
|
fun reportException(t: Throwable) = Timber.e(t)
|
||||||
|
|
||||||
fun updateRemoteConfig() {}
|
fun updateRemoteConfig() {}
|
||||||
|
|
||||||
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {
|
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {}
|
||||||
Timber.d("${context.getString(event)} -> $params")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addTask(source: String) =
|
fun addTask(source: String) {}
|
||||||
logEvent(R.string.event_add_task, R.string.param_type to source)
|
|
||||||
|
|
||||||
fun completeTask(source: String) =
|
val subscribeCooldown = false
|
||||||
logEvent(R.string.event_complete_task, R.string.param_type to source)
|
val moreOptionsBadge = false
|
||||||
|
val moreOptionsSolid = false
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
|
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
|
||||||
<string name="microsoft_oauth_path">/sEe08kX5nGJi4miFX3VkNXICC/Y=</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue