Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.example.util.simpletimetracker

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.util.simpletimetracker.core.utils.ACTION_EXTERNAL_QUERY_ACTIVITIES
import com.example.util.simpletimetracker.core.utils.ACTION_EXTERNAL_QUERY_RUNNING
import com.example.util.simpletimetracker.core.utils.ACTION_EXTERNAL_RESPONSE_ACTIVITIES
import com.example.util.simpletimetracker.core.utils.ACTION_EXTERNAL_RESPONSE_RUNNING
import com.example.util.simpletimetracker.core.utils.EXTRA_ANSWER_TYPE
import com.example.util.simpletimetracker.core.utils.EXTRA_DATA
import com.example.util.simpletimetracker.utils.BaseUiTest
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

/**
* Covers the ACTION_EXTERNAL_QUERY_ACTIVITIES / ACTION_EXTERNAL_QUERY_RUNNING broadcast API
* end to end: sends the query broadcast and asserts on the actual response broadcast content,
* instead of only checking the settings toggle.
*/
@RunWith(AndroidJUnit4::class)
class ExternalQueryBroadcastTest : BaseUiTest() {

private val context: Context
get() = ApplicationProvider.getApplicationContext()

@Test
fun queryIgnoredWhenSettingDisabled() {
testUtils.addActivity("work")

val data = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_ACTIVITIES,
responseAction = ACTION_EXTERNAL_RESPONSE_ACTIVITIES,
timeoutSeconds = 2,
)

assertNull(data)
}

@Test
fun queryActivitiesSimple() {
enableReceiveQueries()
testUtils.addActivity("work")

val data = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_ACTIVITIES,
responseAction = ACTION_EXTERNAL_RESPONSE_ACTIVITIES,
).orEmpty()

assertTrue(data.startsWith("work:"))
val hex = data.substringAfter("work:")
assertTrue(hex.matches(Regex("[0-9a-f]{6}")))
}

@Test
fun queryActivitiesHiddenExcluded() {
enableReceiveQueries()
testUtils.addActivity("work")
testUtils.addActivity("archived", archived = true)

val data = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_ACTIVITIES,
responseAction = ACTION_EXTERNAL_RESPONSE_ACTIVITIES,
).orEmpty()

assertTrue(data.contains("work:"))
assertTrue(!data.contains("archived:"))
}

@Test
fun queryActivitiesJsonMatchesSimpleColor() {
enableReceiveQueries()
testUtils.addActivity("work")

val simpleData = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_ACTIVITIES,
responseAction = ACTION_EXTERNAL_RESPONSE_ACTIVITIES,
).orEmpty()
val jsonData = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_ACTIVITIES,
responseAction = ACTION_EXTERNAL_RESPONSE_ACTIVITIES,
answerType = "json",
).orEmpty()

val simpleHex = simpleData.substringAfter("work:")
val jsonColor = Regex("\"color\":(-?\\d+)").find(jsonData)?.groupValues?.get(1)?.toLong()

assertTrue(jsonData.contains("\"name\":\"work\""))
assertEquals(simpleHex, jsonColor?.let { String.format("%06x", it and 0xFFFFFF) })
}

@Test
fun queryRunningSimple() {
enableReceiveQueries()
testUtils.addActivity("work")
val timeStarted = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5)
testUtils.addRunningRecord("work", timeStarted = timeStarted)

val data = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_RUNNING,
responseAction = ACTION_EXTERNAL_RESPONSE_RUNNING,
)

assertEquals("work:${timeStarted / 1000}", data)
}

@Test
fun queryRunningEmptyWhenNothingRunning() {
enableReceiveQueries()

val data = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_RUNNING,
responseAction = ACTION_EXTERNAL_RESPONSE_RUNNING,
)

assertEquals("", data)
}

@Test
fun queryRunningJsonContainsId() {
enableReceiveQueries()
testUtils.addActivity("work")
testUtils.addRunningRecord("work")

val jsonData = sendAndAwaitResponse(
queryAction = ACTION_EXTERNAL_QUERY_RUNNING,
responseAction = ACTION_EXTERNAL_RESPONSE_RUNNING,
answerType = "json",
).orEmpty()

assertTrue(jsonData.contains("\"startedAt\""))
assertTrue(jsonData.contains("\"id\""))
}

private fun enableReceiveQueries() = runBlocking {
prefsInteractor.setAutomatedTrackingReceiveQueries(true)
}

private fun sendAndAwaitResponse(
queryAction: String,
responseAction: String,
answerType: String? = null,
timeoutSeconds: Long = 5,
): String? {
val latch = CountDownLatch(1)
var data: String? = null
val receiver = object : BroadcastReceiver() {
override fun onReceive(receiverContext: Context?, intent: Intent?) {
data = intent?.getStringExtra(EXTRA_DATA)
latch.countDown()
}
}
ContextCompat.registerReceiver(
context,
receiver,
IntentFilter(responseAction),
ContextCompat.RECEIVER_NOT_EXPORTED,
)
try {
Intent(queryAction).apply {
setPackage(context.packageName)
if (answerType != null) putExtra(EXTRA_ANSWER_TYPE, answerType)
}.let(context::sendBroadcast)
latch.await(timeoutSeconds, TimeUnit.SECONDS)
} finally {
context.unregisterReceiver(receiver)
}
return data
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2888,6 +2888,24 @@ class SettingsTest : BaseUiTest() {
tryAction { clickOnView(allOf(isDescendantOfA(withId(baseR.id.viewRunningRecordItem)), withText(name))) }
}

@Test
fun automatedTrackingReceiveQueries() {
// Change setting
NavUtils.openSettingsScreen()
NavUtils.openSettingsAdditional()
scrollSettingsRecyclerToText(coreR.string.settings_automated_tracking_receive_queries)
checkCheckboxIsNotChecked(settingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries))
clickOnSettingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries)
checkCheckboxIsChecked(settingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries))

// Change setting back
NavUtils.openSettingsScreen()
scrollSettingsRecyclerToText(coreR.string.settings_automated_tracking_receive_queries)
checkCheckboxIsChecked(settingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries))
clickOnSettingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries)
checkCheckboxIsNotChecked(settingsCheckboxBesideText(coreR.string.settings_automated_tracking_receive_queries))
}

@Test
fun showFiltersOnMain() {
val name = "ActivityFilter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const val ACTION_EXTERNAL_CHANGE_RECORD = "com.razeeman.util.simpletimetracker.A
const val ACTION_EXTERNAL_CREATE_RECORD_TAG = "com.razeeman.util.simpletimetracker.ACTION_CREATE_TAG"
const val ACTION_EXTERNAL_AUTOMATIC_BACKUP = "com.razeeman.util.simpletimetracker.ACTION_EXTERNAL_AUTOMATIC_BACKUP"
const val ACTION_EXTERNAL_AUTOMATIC_EXPORT = "com.razeeman.util.simpletimetracker.ACTION_EXTERNAL_AUTOMATIC_EXPORT"
const val ACTION_EXTERNAL_QUERY_ACTIVITIES = "com.razeeman.util.simpletimetracker.ACTION_QUERY_ACTIVITIES"
const val ACTION_EXTERNAL_QUERY_RUNNING = "com.razeeman.util.simpletimetracker.ACTION_QUERY_RUNNING"

// Sent by the app in response to ACTION_EXTERNAL_QUERY_ACTIVITIES / ACTION_EXTERNAL_QUERY_RUNNING.
const val ACTION_EXTERNAL_RESPONSE_ACTIVITIES = "com.razeeman.util.simpletimetracker.ACTION_RESPONSE_ACTIVITIES"
const val ACTION_EXTERNAL_RESPONSE_RUNNING = "com.razeeman.util.simpletimetracker.ACTION_RESPONSE_RUNNING"

const val EVENT_STARTED_ACTIVITY = "com.razeeman.util.simpletimetracker.EVENT_STARTED_ACTIVITY"
const val EVENT_STOPPED_ACTIVITY = "com.razeeman.util.simpletimetracker.EVENT_STOPPED_ACTIVITY"
Expand All @@ -36,5 +42,7 @@ const val EXTRA_FIND_RECORD_MODE = "extra_find_record_mode" // current_or_last,
const val EXTRA_FIND_RECORD_WITH_ACTIVITY_NAME = "extra_find_record_with_activity_name"
const val EXTRA_GOAL_TYPE = "extra_goal_type" // duration, count
const val EXTRA_GOAL_VALUE = "extra_goal_value"
const val EXTRA_ANSWER_TYPE = "extra_answer_type" // simple, json
const val EXTRA_DATA = "data"

const val DELAY_DATA_LOAD_MS = 300L // Same as @integer/screen_animation_time
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Compani
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_ACTIVITY_REMINDER_RECURRENT
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_ALLOW_MULTIPLE_ACTIVITY_FILTERS
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_ALLOW_MULTITASKING
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_AUTOMATED_TRACKING_RECEIVE_QUERIES
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_AUTOMATED_TRACKING_SEND_EVENTS
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_AUTOSTART_POMODORO_ACTIVITIES
import com.example.util.simpletimetracker.data_local.prefs.PrefsRepoImpl.Companion.KEY_CARD_ORDER
Expand Down Expand Up @@ -248,6 +249,7 @@ class BackupPrefsRepo @Inject constructor(
PrefsProcessor(KEY_SHOW_COMMENT_INPUT_EXCLUDE_ACTIVITIES, ::commentInputExcludeActivities),
PrefsProcessor(KEY_AUTOSTART_POMODORO_ACTIVITIES, ::autostartPomodoroActivities),
PrefsProcessor(KEY_AUTOMATED_TRACKING_SEND_EVENTS, ::automatedTrackingSendEvents),
PrefsProcessor(KEY_AUTOMATED_TRACKING_RECEIVE_QUERIES, ::automatedTrackingReceiveQueries),
PrefsProcessor(KEY_REPEAT_BUTTON_TYPE, ::repeatButtonType),
PrefsProcessor(KEY_WIDGET_TRANSPARENCY_PERCENT, ::widgetBackgroundTransparencyPercent),
PrefsProcessor(KEY_DEFAULT_TYPES_HIDDEN, ::defaultTypesHidden),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@ class PrefsRepoImpl @Inject constructor(
KEY_AUTOMATED_TRACKING_SEND_EVENTS, false,
)

override var automatedTrackingReceiveQueries: Boolean by prefs.delegate(
KEY_AUTOMATED_TRACKING_RECEIVE_QUERIES, false,
)

override var automaticBackupUri: String by prefs.delegate(
KEY_AUTOMATIC_BACKUP_URI, "",
)
Expand Down Expand Up @@ -824,6 +828,7 @@ class PrefsRepoImpl @Inject constructor(
const val KEY_AUTOSTART_POMODORO_ACTIVITIES = "autostartPomodoroActivities"
const val KEY_RECORD_TAG_SELECTION_CLOSE_AFTER_ONE = "recordTagSelectionCloseAfterOne"
const val KEY_AUTOMATED_TRACKING_SEND_EVENTS = "automatedTrackingSendEvents"
const val KEY_AUTOMATED_TRACKING_RECEIVE_QUERIES = "automatedTrackingReceiveQueries"
const val KEY_REPEAT_BUTTON_TYPE = "repeatButtonType"
const val KEY_WIDGET_TRANSPARENCY_PERCENT = "widgetTransparencyPercent"
const val KEY_DEFAULT_TYPES_HIDDEN = "defaultTypesHidden"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.util.simpletimetracker.domain.notifications.model

enum class ExternalAnswerType(val dataValue: String) {
SIMPLE("simple"),
JSON("json"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,14 @@ class PrefsInteractor @Inject constructor(
prefsRepo.automatedTrackingSendEvents = value
}

suspend fun getAutomatedTrackingReceiveQueries(): Boolean = withContext(Dispatchers.IO) {
prefsRepo.automatedTrackingReceiveQueries
}

suspend fun setAutomatedTrackingReceiveQueries(value: Boolean) = withContext(Dispatchers.IO) {
prefsRepo.automatedTrackingReceiveQueries = value
}

suspend fun setWidget(widgetId: Int, recordType: Long) = withContext(Dispatchers.IO) {
prefsRepo.setWidget(widgetId, recordType)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ interface PrefsRepo {

var automatedTrackingSendEvents: Boolean

var automatedTrackingReceiveQueries: Boolean

var automaticBackupUri: String

var automaticBackupError: Boolean
Expand Down
2 changes: 2 additions & 0 deletions features/feature_notification/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ android {

dependencies {
implementation(project(":core"))
implementation(project(":wear_api"))
implementation(libs.google.gson)
implementation(libs.google.dagger)
ksp(libs.kapt.dagger)
}
2 changes: 2 additions & 0 deletions features/feature_notification/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<action android:name="com.razeeman.util.simpletimetracker.ACTION_CREATE_TAG" />
<action android:name="com.razeeman.util.simpletimetracker.ACTION_EXTERNAL_AUTOMATIC_BACKUP" />
<action android:name="com.razeeman.util.simpletimetracker.ACTION_EXTERNAL_AUTOMATIC_EXPORT" />
<action android:name="com.razeeman.util.simpletimetracker.ACTION_QUERY_ACTIVITIES" />
<action android:name="com.razeeman.util.simpletimetracker.ACTION_QUERY_RUNNING" />
</intent-filter>
</receiver>

Expand Down
Loading