b2d03ef8b2
Android UI Tests / ui-tests (pull_request) Has been cancelled
The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.
Changes:
- TaskCompletionViewModel.createTaskCompletionWithImages now does the
presign→POST→collect-ids dance internally. The signature stays the
same so the three Android UI call sites (TasksScreen, AllTasksScreen,
ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
no changes.
- APILayer.createTaskCompletionWithImages removed (dead).
- TaskCompletionApi.createCompletionWithImages removed (the multipart
HTTP helper that posted to the legacy POST /api/task-completions/
multipart endpoint).
- TaskCompletionCreateRequest.imageUrls field removed.
- Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
PushNotificationManager) updated to drop the imageUrls argument.
- Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.
Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
171 lines
5.9 KiB
Kotlin
171 lines
5.9 KiB
Kotlin
package com.tt.honeyDue
|
|
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.util.Log
|
|
import androidx.core.app.NotificationManagerCompat
|
|
import com.tt.honeyDue.data.DataManager
|
|
import com.tt.honeyDue.models.TaskCompletionCreateRequest
|
|
import com.tt.honeyDue.network.APILayer
|
|
import com.tt.honeyDue.network.ApiResult
|
|
import com.tt.honeyDue.storage.TokenStorage
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
|
|
/**
|
|
* BroadcastReceiver for handling notification action button clicks.
|
|
* Performs task actions (complete, cancel, etc.) directly from notifications.
|
|
*/
|
|
class NotificationActionReceiver : BroadcastReceiver() {
|
|
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
val action = intent.action ?: return
|
|
val taskId = intent.getIntExtra(EXTRA_TASK_ID, -1)
|
|
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0)
|
|
|
|
Log.d(TAG, "Action received: $action for task $taskId")
|
|
|
|
if (taskId == -1) {
|
|
Log.e(TAG, "No task ID provided")
|
|
return
|
|
}
|
|
|
|
// Dismiss the notification
|
|
NotificationManagerCompat.from(context).cancel(notificationId)
|
|
|
|
// Check subscription status
|
|
val isPremium = isPremiumUser()
|
|
if (!isPremium) {
|
|
Log.d(TAG, "Non-premium user, ignoring action")
|
|
launchMainActivity(context, null)
|
|
return
|
|
}
|
|
|
|
// Handle action
|
|
when (action) {
|
|
ACTION_VIEW_TASK -> {
|
|
launchMainActivity(context, taskId)
|
|
}
|
|
ACTION_COMPLETE_TASK -> {
|
|
performCompleteTask(context, taskId)
|
|
}
|
|
ACTION_MARK_IN_PROGRESS -> {
|
|
performMarkInProgress(context, taskId)
|
|
}
|
|
ACTION_CANCEL_TASK -> {
|
|
performCancelTask(context, taskId)
|
|
}
|
|
ACTION_UNCANCEL_TASK -> {
|
|
performUncancelTask(context, taskId)
|
|
}
|
|
else -> {
|
|
Log.w(TAG, "Unknown action: $action")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun isPremiumUser(): Boolean {
|
|
val subscription = DataManager.subscription.value
|
|
// User is premium if limitations are disabled
|
|
return subscription?.limitationsEnabled == false
|
|
}
|
|
|
|
private fun launchMainActivity(context: Context, taskId: Int?) {
|
|
val intent = Intent(context, MainActivity::class.java).apply {
|
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
if (taskId != null) {
|
|
putExtra(EXTRA_NAVIGATE_TO_TASK, taskId)
|
|
}
|
|
}
|
|
context.startActivity(intent)
|
|
}
|
|
|
|
private fun performCompleteTask(context: Context, taskId: Int) {
|
|
Log.d(TAG, "Completing task $taskId")
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
val request = TaskCompletionCreateRequest(
|
|
taskId = taskId,
|
|
completedAt = null,
|
|
notes = null,
|
|
actualCost = null,
|
|
rating = null,
|
|
)
|
|
|
|
when (val result = APILayer.createTaskCompletion(request)) {
|
|
is ApiResult.Success -> {
|
|
Log.d(TAG, "Task $taskId completed successfully")
|
|
// Launch app to show result
|
|
launchMainActivity(context, taskId)
|
|
}
|
|
is ApiResult.Error -> {
|
|
Log.e(TAG, "Failed to complete task: ${result.message}")
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun performMarkInProgress(context: Context, taskId: Int) {
|
|
Log.d(TAG, "Marking task $taskId as in progress")
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
when (val result = APILayer.markInProgress(taskId)) {
|
|
is ApiResult.Success -> {
|
|
Log.d(TAG, "Task $taskId marked as in progress")
|
|
}
|
|
is ApiResult.Error -> {
|
|
Log.e(TAG, "Failed to mark task in progress: ${result.message}")
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun performCancelTask(context: Context, taskId: Int) {
|
|
Log.d(TAG, "Cancelling task $taskId")
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
when (val result = APILayer.cancelTask(taskId)) {
|
|
is ApiResult.Success -> {
|
|
Log.d(TAG, "Task $taskId cancelled")
|
|
}
|
|
is ApiResult.Error -> {
|
|
Log.e(TAG, "Failed to cancel task: ${result.message}")
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun performUncancelTask(context: Context, taskId: Int) {
|
|
Log.d(TAG, "Uncancelling task $taskId")
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
when (val result = APILayer.uncancelTask(taskId)) {
|
|
is ApiResult.Success -> {
|
|
Log.d(TAG, "Task $taskId uncancelled")
|
|
}
|
|
is ApiResult.Error -> {
|
|
Log.e(TAG, "Failed to uncancel task: ${result.message}")
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "NotificationAction"
|
|
|
|
// Action constants
|
|
const val ACTION_VIEW_TASK = "com.tt.honeyDue.ACTION_VIEW_TASK"
|
|
const val ACTION_COMPLETE_TASK = "com.tt.honeyDue.ACTION_COMPLETE_TASK"
|
|
const val ACTION_MARK_IN_PROGRESS = "com.tt.honeyDue.ACTION_MARK_IN_PROGRESS"
|
|
const val ACTION_CANCEL_TASK = "com.tt.honeyDue.ACTION_CANCEL_TASK"
|
|
const val ACTION_UNCANCEL_TASK = "com.tt.honeyDue.ACTION_UNCANCEL_TASK"
|
|
|
|
// Extra constants
|
|
const val EXTRA_TASK_ID = "task_id"
|
|
const val EXTRA_NOTIFICATION_ID = "notification_id"
|
|
const val EXTRA_NAVIGATE_TO_TASK = "navigate_to_task"
|
|
}
|
|
}
|