diff --git a/README.md b/README.md
index 3dfae22c..53778b0d 100644
--- a/README.md
+++ b/README.md
@@ -34,8 +34,8 @@ Mobile, on the emulator harness against the live API:
Choices survive player recreation; unavailable audio tracks fall back to automatic selection
- Account settings: subtitles, history, Trash, resume playback, proxy route,
and default sort order, each saved to the account and shared across devices
-- Privacy controls for the support chat widget, shared across put.io apps,
- with a strictly-necessary storage disclosure
+- Privacy controls for diagnostics (on by default) and product analytics
+ (opt-in), shared across put.io apps, with a strictly-necessary storage disclosure
- About section with copyable app, Android, device, and player info
- Downloads: video saves the same HLS rendition it streams (without subtitles
for accounts that hide them), audio saves the original. A Downloads screen
diff --git a/design/phosphor-icons.lock.json b/design/phosphor-icons.lock.json
index e47f66bc..b0b39761 100644
--- a/design/phosphor-icons.lock.json
+++ b/design/phosphor-icons.lock.json
@@ -196,13 +196,6 @@
"sourceSha256": "8a432fac53ae887b1726a96462aa8ea285e714b8e480261751361e4d6b2b226f",
"outputSha256": "ef55ee652c4ff22f0c61ecb65991dfe2be4486b5441f9f8774f64520af9f7d32"
},
- {
- "resDir": "mobile/src/main/res",
- "resource": "ic_ph_lifebuoy",
- "asset": "assets/regular/lifebuoy.svg",
- "sourceSha256": "450ce5b7ec604f37c2d3dfc0f2972d35557121a050e83854135429376c3b95f0",
- "outputSha256": "8cabc249ef22af0239f8b36bf22faa2216f4394e62618299e516b5a7d8fc6333"
- },
{
"resDir": "mobile/src/main/res",
"resource": "ic_ph_shield_check",
diff --git a/docs/behavior.md b/docs/behavior.md
index 823d4463..73677ee8 100644
--- a/docs/behavior.md
+++ b/docs/behavior.md
@@ -979,6 +979,16 @@ with what is used either way.
Tests: `PutioAuthSessionGatewayTest`, `PutioTvSessionGatewayTest`,
`MobileAccountScreenTest`, `TvAccountScreenTest`.
+## Privacy controls
+
+Mobile Settings shows two account-wide privacy controls, shared with every
+put.io app: Diagnostics (`diagnostics_enabled`, on by default) and Product
+analytics (`product_analytics_enabled`, opt-in and off by default). Strictly
+necessary processing is disclosed beside them and has no toggle. The app reads
+and writes no support-widget setting.
+
+Tests: `SdkAccountSettingsRepositoryTest`, `MobileAccountScreenTest`.
+
## Recent searches
Recent searches are the account's `/config` `searchHistory` list, newest first
diff --git a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsPreferences.kt b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsPreferences.kt
index b24aae8b..ba521e4b 100644
--- a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsPreferences.kt
+++ b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsPreferences.kt
@@ -23,7 +23,6 @@ private fun AccountSettingsPreferences.toggleValue(key: AccountSettingsKey): Boo
AccountSettingsKey.ResumePlayback -> resumePlayback
AccountSettingsKey.Diagnostics -> diagnosticsEnabled
AccountSettingsKey.ProductAnalytics -> productAnalyticsEnabled
- AccountSettingsKey.SupportWidget -> supportWidgetEnabled
AccountSettingsKey.TunnelRoute,
AccountSettingsKey.DefaultSort,
-> error("$key is not a toggle")
@@ -40,7 +39,6 @@ private fun AccountSettingsPreferences.applyingToggle(
AccountSettingsKey.ResumePlayback -> copy(resumePlayback = change.enabled)
AccountSettingsKey.Diagnostics -> copy(diagnosticsEnabled = change.enabled)
AccountSettingsKey.ProductAnalytics -> copy(productAnalyticsEnabled = change.enabled)
- AccountSettingsKey.SupportWidget -> copy(supportWidgetEnabled = change.enabled)
AccountSettingsKey.TunnelRoute,
AccountSettingsKey.DefaultSort,
-> error("${change.key} is not a toggle")
diff --git a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsRepository.kt b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsRepository.kt
index a345745f..b1a50cbc 100644
--- a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsRepository.kt
+++ b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsRepository.kt
@@ -144,7 +144,6 @@ private fun AccountSettings.toPreferences(): AccountSettingsPreferences =
defaultSort = FilesSort.fromApiValue(sortBy),
diagnosticsEnabled = diagnosticsEnabled,
productAnalyticsEnabled = productAnalyticsEnabled,
- supportWidgetEnabled = supportWidgetEnabled,
)
private fun AccountSettingsChange.toPatch(): AccountSettingsPatch =
@@ -159,7 +158,6 @@ private fun AccountSettingsChange.toPatch(): AccountSettingsPatch =
AccountSettingsKey.ResumePlayback -> AccountSettingsPatch(useStartFrom = enabled)
AccountSettingsKey.Diagnostics -> AccountSettingsPatch(diagnosticsEnabled = enabled)
AccountSettingsKey.ProductAnalytics -> AccountSettingsPatch(productAnalyticsEnabled = enabled)
- AccountSettingsKey.SupportWidget -> AccountSettingsPatch(supportWidgetEnabled = enabled)
AccountSettingsKey.TunnelRoute,
AccountSettingsKey.DefaultSort,
-> error("$key is not a toggle")
diff --git a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsState.kt b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsState.kt
index 891cb01c..03730f11 100644
--- a/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsState.kt
+++ b/domain/account/src/main/kotlin/io/putdotio/android/settings/AccountSettingsState.kt
@@ -20,10 +20,10 @@ public data class AccountSettingsPreferences(
// Account-wide `sort_by`: folders without their own sort use it. Null means the
// server reported a value this app does not know.
val defaultSort: FilesSort? = null,
- // Privacy controls per the frontend analytics contract; the server defaults all three to true.
+ // Privacy controls per the frontend analytics contract: diagnostics are on by
+ // default, product analytics is opt-in.
val diagnosticsEnabled: Boolean = true,
- val productAnalyticsEnabled: Boolean = true,
- val supportWidgetEnabled: Boolean = true,
+ val productAnalyticsEnabled: Boolean = false,
)
/** Server route identifier. `default` is the direct Amsterdam route and what a null setting means. */
@@ -56,7 +56,6 @@ public enum class AccountSettingsKey {
DefaultSort,
Diagnostics,
ProductAnalytics,
- SupportWidget,
}
public sealed interface AccountSettingsChange {
diff --git a/domain/account/src/test/kotlin/io/putdotio/android/settings/SdkAccountSettingsRepositoryTest.kt b/domain/account/src/test/kotlin/io/putdotio/android/settings/SdkAccountSettingsRepositoryTest.kt
index f9497db2..bac8d846 100644
--- a/domain/account/src/test/kotlin/io/putdotio/android/settings/SdkAccountSettingsRepositoryTest.kt
+++ b/domain/account/src/test/kotlin/io/putdotio/android/settings/SdkAccountSettingsRepositoryTest.kt
@@ -53,8 +53,7 @@ class SdkAccountSettingsRepositoryTest {
repository.save(AccountSettingsChange(AccountSettingsKey.AutoSelectSubtitles, true))
repository.save(AccountSettingsChange(AccountSettingsKey.ResumePlayback, false))
repository.save(AccountSettingsChange(AccountSettingsKey.Diagnostics, false))
- repository.save(AccountSettingsChange(AccountSettingsKey.ProductAnalytics, false))
- repository.save(AccountSettingsChange(AccountSettingsKey.SupportWidget, false))
+ repository.save(AccountSettingsChange(AccountSettingsKey.ProductAnalytics, true))
assertEquals(AccountSettingsPatch(historyEnabled = false), patches[0])
assertEquals(AccountSettingsPatch(trashEnabled = true), patches[1])
@@ -62,8 +61,7 @@ class SdkAccountSettingsRepositoryTest {
assertEquals(AccountSettingsPatch(dontAutoselectSubtitles = false), patches[3])
assertEquals(AccountSettingsPatch(useStartFrom = false), patches[4])
assertEquals(AccountSettingsPatch(diagnosticsEnabled = false), patches[5])
- assertEquals(AccountSettingsPatch(productAnalyticsEnabled = false), patches[6])
- assertEquals(AccountSettingsPatch(supportWidgetEnabled = false), patches[7])
+ assertEquals(AccountSettingsPatch(productAnalyticsEnabled = true), patches[6])
}
@Test
@@ -244,25 +242,33 @@ class SdkAccountSettingsRepositoryTest {
}
@Test
- fun mapsPrivacyControlsFromServerAndDefaultsThemOn() =
+ fun mapsPrivacyControlsFromServer() =
runBlocking {
- val loaded = repository(settings = Settings).load() as AccountSettingsRepositoryResult.Success
- assertTrue(loaded.value.diagnosticsEnabled)
- assertTrue(loaded.value.productAnalyticsEnabled)
- assertTrue(loaded.value.supportWidgetEnabled)
+ val optedIn = repository(
+ settings = Settings.copy(diagnosticsEnabled = true, productAnalyticsEnabled = true),
+ ).load() as AccountSettingsRepositoryResult.Success
+ assertTrue(optedIn.value.diagnosticsEnabled)
+ assertTrue(optedIn.value.productAnalyticsEnabled)
val optedOut = repository(
- settings = Settings.copy(
- diagnosticsEnabled = false,
- productAnalyticsEnabled = false,
- supportWidgetEnabled = false,
- ),
+ settings = Settings.copy(diagnosticsEnabled = false, productAnalyticsEnabled = false),
).load() as AccountSettingsRepositoryResult.Success
assertFalse(optedOut.value.diagnosticsEnabled)
assertFalse(optedOut.value.productAnalyticsEnabled)
- assertFalse(optedOut.value.supportWidgetEnabled)
}
+ @Test
+ fun productAnalyticsIsOptInWhileDiagnosticsStayOnByDefault() {
+ val defaults = AccountSettingsPreferences(
+ historyEnabled = true,
+ trashEnabled = true,
+ showSubtitles = true,
+ autoSelectSubtitles = true,
+ )
+ assertTrue(defaults.diagnosticsEnabled)
+ assertFalse(defaults.productAnalyticsEnabled)
+ }
+
@Test
fun mapsDefaultSortFromServerAndPatchesTheExactApiValue() =
runBlocking {
@@ -381,6 +387,8 @@ class SdkAccountSettingsRepositoryTest {
hideSubtitles = true,
dontAutoselectSubtitles = true,
useStartFrom = false,
+ diagnosticsEnabled = true,
+ productAnalyticsEnabled = false,
)
}
}
diff --git a/mobile/src/main/kotlin/io/putdotio/android/account/MobileAccountScreen.kt b/mobile/src/main/kotlin/io/putdotio/android/account/MobileAccountScreen.kt
index 07052d18..69d96a8d 100644
--- a/mobile/src/main/kotlin/io/putdotio/android/account/MobileAccountScreen.kt
+++ b/mobile/src/main/kotlin/io/putdotio/android/account/MobileAccountScreen.kt
@@ -432,7 +432,7 @@ private fun LazyListScope.accountSettingsItems(
}
// Purpose-based opt-outs per the frontend analytics contract. Strictly necessary
-// processing is disclosed, never toggled; the three account keys are cross-client.
+// processing is disclosed, never toggled; both account keys are cross-client.
private fun LazyListScope.privacyControlsItems(
preferences: AccountSettingsPreferences,
mutation: AccountSettingsMutation,
@@ -469,18 +469,6 @@ private fun LazyListScope.privacyControlsItems(
onChange = onChange,
)
}
- item(key = AccountSettingsKey.SupportWidget) {
- MobileAccountSettingRow(
- title = R.string.mobile_settings_support_widget,
- description = R.string.mobile_settings_support_widget_description,
- icon = R.drawable.ic_ph_lifebuoy,
- checked = preferences.supportWidgetEnabled,
- key = AccountSettingsKey.SupportWidget,
- mutation = mutation,
- onRetry = onRetryChange,
- onChange = onChange,
- )
- }
item(key = PRIVACY_CONTROLS_NOTE_KEY) {
Text(
text = stringResource(R.string.mobile_settings_privacy_controls_note),
diff --git a/mobile/src/main/res/drawable/ic_ph_lifebuoy.xml b/mobile/src/main/res/drawable/ic_ph_lifebuoy.xml
deleted file mode 100644
index ea58adf7..00000000
--- a/mobile/src/main/res/drawable/ic_ph_lifebuoy.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index d3cd7bb0..f593fdbc 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -170,8 +170,6 @@
Send crash and playback error reports so we can fix problems. They never include file names or content.
Product analytics
Share which features you use so we can improve the app.
- Support chat
- Load in-app support chat and share account context with the support team when you open it.
These choices apply to every put.io app you sign in to.
Playback
About
diff --git a/mobile/src/test/kotlin/io/putdotio/android/MobileAccountScreenTest.kt b/mobile/src/test/kotlin/io/putdotio/android/MobileAccountScreenTest.kt
index a80bb00b..4a395be4 100644
--- a/mobile/src/test/kotlin/io/putdotio/android/MobileAccountScreenTest.kt
+++ b/mobile/src/test/kotlin/io/putdotio/android/MobileAccountScreenTest.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.text.format.Formatter
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
@@ -242,21 +243,30 @@ class MobileAccountScreenTest {
SemanticsMatcher.keyNotDefined(SemanticsProperties.ToggleableState),
)
- for ((label, key) in listOf(
- "Diagnostics" to AccountSettingsKey.Diagnostics,
- "Product analytics" to AccountSettingsKey.ProductAnalytics,
- "Support chat" to AccountSettingsKey.SupportWidget,
- )) {
- compose.onNodeWithTag(MOBILE_ACCOUNT_LIST_TAG).performScrollToNode(hasText(label))
- compose.onNodeWithText(label).assertIsOn().performClick()
- compose.runOnIdle {
- assertEquals(
- AccountSettingsEvent.ChangeRequested(AccountSettingsChange(key, enabled = false)),
- events.last(),
- )
- }
+ compose.onNodeWithTag(MOBILE_ACCOUNT_LIST_TAG).performScrollToNode(hasText("Diagnostics"))
+ compose.onNodeWithText("Diagnostics").assertIsOn().performClick()
+ compose.runOnIdle {
+ assertEquals(
+ AccountSettingsEvent.ChangeRequested(
+ AccountSettingsChange(AccountSettingsKey.Diagnostics, enabled = false),
+ ),
+ events.last(),
+ )
}
- compose.runOnIdle { assertEquals(3, events.size) }
+ compose.onNodeWithTag(MOBILE_ACCOUNT_LIST_TAG).performScrollToNode(hasText("Product analytics"))
+ compose.onNodeWithText("Product analytics").assertIsOff().performClick()
+ compose.runOnIdle {
+ assertEquals(
+ AccountSettingsEvent.ChangeRequested(
+ AccountSettingsChange(AccountSettingsKey.ProductAnalytics, enabled = true),
+ ),
+ events.last(),
+ )
+ }
+ compose.onNodeWithTag(MOBILE_ACCOUNT_LIST_TAG)
+ .performScrollToNode(hasText("These choices apply to every put.io app you sign in to."))
+ compose.onAllNodesWithText("Support chat").assertCountEquals(0)
+ compose.runOnIdle { assertEquals(2, events.size) }
}
@Test