Skip to content

Commit e13ae94

Browse files
authored
Implement internal pir opt out (#5845)
Task/Issue URL: https://app.asana.com/0/72649045549333/1209242689024725/f ### Description This PR covers all the logic for opt-out. More details in the attached task. ### Steps to test this PR No need to do a specific test for this PR. The stacked PR’2 test case should cover all - since formatting is much better.
1 parent 9aa97fb commit e13ae94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4648
-1130
lines changed

pir/pir-internal/schemas/com.duckduckgo.pir.internal.store.PirDatabase/3.json

+142-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"formatVersion": 1,
33
"database": {
44
"version": 3,
5-
"identityHash": "4fda73a442b6411886706c42bedc64bc",
5+
"identityHash": "a82b3c3fcdfd42d3736c96fd4b979451",
66
"entities": [
77
{
88
"tableName": "pir_broker_json_etag",
@@ -364,7 +364,7 @@
364364
},
365365
{
366366
"tableName": "pir_user_profile",
367-
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `birthYear` INTEGER NOT NULL, `phone` TEXT, `age` INTEGER NOT NULL, `user_firstName` TEXT NOT NULL, `user_lastName` TEXT NOT NULL, `user_middleName` TEXT, `user_suffix` TEXT, `address_city` TEXT NOT NULL, `address_state` TEXT NOT NULL, `address_street` TEXT, `address_zip` TEXT)",
367+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `birthYear` INTEGER NOT NULL, `phone` TEXT, `user_firstName` TEXT NOT NULL, `user_lastName` TEXT NOT NULL, `user_middleName` TEXT, `user_suffix` TEXT, `address_city` TEXT NOT NULL, `address_state` TEXT NOT NULL, `address_street` TEXT, `address_zip` TEXT)",
368368
"fields": [
369369
{
370370
"fieldPath": "id",
@@ -384,12 +384,6 @@
384384
"affinity": "TEXT",
385385
"notNull": false
386386
},
387-
{
388-
"fieldPath": "age",
389-
"columnName": "age",
390-
"affinity": "INTEGER",
391-
"notNull": true
392-
},
393387
{
394388
"fieldPath": "userName.firstName",
395389
"columnName": "user_firstName",
@@ -449,7 +443,7 @@
449443
"foreignKeys": []
450444
},
451445
{
452-
"tableName": "pir_scan_log",
446+
"tableName": "pir_events_log",
453447
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventTimeInMillis` INTEGER NOT NULL, `eventType` TEXT NOT NULL, PRIMARY KEY(`eventTimeInMillis`))",
454448
"fields": [
455449
{
@@ -505,12 +499,150 @@
505499
},
506500
"indices": [],
507501
"foreignKeys": []
502+
},
503+
{
504+
"tableName": "pir_scan_complete_brokers",
505+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`brokerName` TEXT NOT NULL, `startTimeInMillis` INTEGER NOT NULL, `endTimeInMillis` INTEGER NOT NULL, PRIMARY KEY(`brokerName`))",
506+
"fields": [
507+
{
508+
"fieldPath": "brokerName",
509+
"columnName": "brokerName",
510+
"affinity": "TEXT",
511+
"notNull": true
512+
},
513+
{
514+
"fieldPath": "startTimeInMillis",
515+
"columnName": "startTimeInMillis",
516+
"affinity": "INTEGER",
517+
"notNull": true
518+
},
519+
{
520+
"fieldPath": "endTimeInMillis",
521+
"columnName": "endTimeInMillis",
522+
"affinity": "INTEGER",
523+
"notNull": true
524+
}
525+
],
526+
"primaryKey": {
527+
"autoGenerate": false,
528+
"columnNames": [
529+
"brokerName"
530+
]
531+
},
532+
"indices": [],
533+
"foreignKeys": []
534+
},
535+
{
536+
"tableName": "pir_opt_out_complete_brokers",
537+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `brokerName` TEXT NOT NULL, `extractedProfile` TEXT NOT NULL, `startTimeInMillis` INTEGER NOT NULL, `endTimeInMillis` INTEGER NOT NULL, `isSubmitSuccess` INTEGER NOT NULL)",
538+
"fields": [
539+
{
540+
"fieldPath": "id",
541+
"columnName": "id",
542+
"affinity": "INTEGER",
543+
"notNull": true
544+
},
545+
{
546+
"fieldPath": "brokerName",
547+
"columnName": "brokerName",
548+
"affinity": "TEXT",
549+
"notNull": true
550+
},
551+
{
552+
"fieldPath": "extractedProfile",
553+
"columnName": "extractedProfile",
554+
"affinity": "TEXT",
555+
"notNull": true
556+
},
557+
{
558+
"fieldPath": "startTimeInMillis",
559+
"columnName": "startTimeInMillis",
560+
"affinity": "INTEGER",
561+
"notNull": true
562+
},
563+
{
564+
"fieldPath": "endTimeInMillis",
565+
"columnName": "endTimeInMillis",
566+
"affinity": "INTEGER",
567+
"notNull": true
568+
},
569+
{
570+
"fieldPath": "isSubmitSuccess",
571+
"columnName": "isSubmitSuccess",
572+
"affinity": "INTEGER",
573+
"notNull": true
574+
}
575+
],
576+
"primaryKey": {
577+
"autoGenerate": true,
578+
"columnNames": [
579+
"id"
580+
]
581+
},
582+
"indices": [],
583+
"foreignKeys": []
584+
},
585+
{
586+
"tableName": "pir_opt_out_action_log",
587+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `brokerName` TEXT NOT NULL, `extractedProfile` TEXT NOT NULL, `completionTimeInMillis` INTEGER NOT NULL, `actionType` TEXT NOT NULL, `isError` INTEGER NOT NULL, `result` TEXT NOT NULL)",
588+
"fields": [
589+
{
590+
"fieldPath": "id",
591+
"columnName": "id",
592+
"affinity": "INTEGER",
593+
"notNull": true
594+
},
595+
{
596+
"fieldPath": "brokerName",
597+
"columnName": "brokerName",
598+
"affinity": "TEXT",
599+
"notNull": true
600+
},
601+
{
602+
"fieldPath": "extractedProfile",
603+
"columnName": "extractedProfile",
604+
"affinity": "TEXT",
605+
"notNull": true
606+
},
607+
{
608+
"fieldPath": "completionTimeInMillis",
609+
"columnName": "completionTimeInMillis",
610+
"affinity": "INTEGER",
611+
"notNull": true
612+
},
613+
{
614+
"fieldPath": "actionType",
615+
"columnName": "actionType",
616+
"affinity": "TEXT",
617+
"notNull": true
618+
},
619+
{
620+
"fieldPath": "isError",
621+
"columnName": "isError",
622+
"affinity": "INTEGER",
623+
"notNull": true
624+
},
625+
{
626+
"fieldPath": "result",
627+
"columnName": "result",
628+
"affinity": "TEXT",
629+
"notNull": true
630+
}
631+
],
632+
"primaryKey": {
633+
"autoGenerate": true,
634+
"columnNames": [
635+
"id"
636+
]
637+
},
638+
"indices": [],
639+
"foreignKeys": []
508640
}
509641
],
510642
"views": [],
511643
"setupQueries": [
512644
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
513-
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4fda73a442b6411886706c42bedc64bc')"
645+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a82b3c3fcdfd42d3736c96fd4b979451')"
514646
]
515647
}
516648
}

pir/pir-internal/src/main/AndroidManifest.xml

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,44 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
45
<application>
56
<activity
67
android:name=".settings.PirDevSettingsActivity"
78
android:label="@string/pirDevSettings" />
9+
810
<activity
911
android:name=".settings.PirResultsActivity"
1012
android:label="@string/pirDevSimpleScanHeader" />
13+
14+
<activity
15+
android:name=".settings.PirDevScanActivity"
16+
android:label="@string/pirDevScanTitle" />
17+
18+
<activity
19+
android:name=".settings.PirDevOptOutActivity"
20+
android:label="@string/pirDevOptOutTitle" />
21+
1122
<activity
12-
android:name=".settings.PirScanResultsActivity"
13-
android:label="@string/pirDevViewScanResults" />
23+
android:name=".settings.PirWebViewActivity"
24+
android:label="@string/pirDevDebugOptOutTitle" />
25+
26+
<service
27+
android:name=".scan.PirForegroundScanService"
28+
android:enabled="true"
29+
android:exported="false"
30+
android:foregroundServiceType="specialUse"
31+
android:process=":pir" />
1432

1533
<service
16-
android:name=".service.PirForegroundScanService"
34+
android:name=".optout.PirForegroundOptOutService"
1735
android:enabled="true"
1836
android:exported="false"
1937
android:foregroundServiceType="specialUse"
2038
android:process=":pir" />
2139

2240
<service
23-
android:name=".service.PirRemoteWorkerService"
41+
android:name=".scan.PirRemoteWorkerService"
2442
android:exported="false"
2543
android:permission="android.permission.BIND_JOB_SERVICE"
2644
android:process=":pir" />

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/brokers/PirDataUpdateObserver.kt

+6
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
2222
import com.duckduckgo.common.utils.DispatcherProvider
2323
import com.duckduckgo.di.scopes.AppScope
2424
import com.duckduckgo.pir.internal.PirRemoteFeatures
25+
import com.duckduckgo.pir.internal.store.PitTestingStore
2526
import com.duckduckgo.subscriptions.api.Subscriptions
2627
import com.squareup.anvil.annotations.ContributesMultibinding
28+
import java.util.UUID
2729
import javax.inject.Inject
2830
import kotlinx.coroutines.CoroutineScope
2931
import kotlinx.coroutines.launch
@@ -39,10 +41,14 @@ class PirDataUpdateObserver @Inject constructor(
3941
private val brokerJsonUpdater: BrokerJsonUpdater,
4042
private val subscriptions: Subscriptions,
4143
private val pirRemoteFeatures: PirRemoteFeatures,
44+
private val testingStore: PitTestingStore,
4245
) : MainProcessLifecycleObserver {
4346
override fun onCreate(owner: LifecycleOwner) {
4447
coroutineScope.launch(dispatcherProvider.io()) {
4548
if (pirRemoteFeatures.allowPirRun().isEnabled() && subscriptions.getAccessToken() != null) {
49+
if (testingStore.testerId == null) {
50+
testingStore.testerId = UUID.randomUUID().toString()
51+
}
4652
logcat { "PIR-update: Attempting to update all broker data" }
4753
if (brokerJsonUpdater.update()) {
4854
logcat { "PIR-update: Update successfully completed." }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.internal.callbacks
18+
19+
import android.os.SystemClock
20+
import android.system.Os
21+
import android.system.OsConstants
22+
import androidx.annotation.WorkerThread
23+
import com.duckduckgo.di.scopes.AppScope
24+
import com.squareup.anvil.annotations.ContributesBinding
25+
import dagger.SingleInstanceIn
26+
import java.io.BufferedReader
27+
import java.io.File
28+
import java.io.FileReader
29+
import java.util.InvalidPropertiesFormatException
30+
import javax.inject.Inject
31+
import kotlin.time.Duration.Companion.seconds
32+
import logcat.logcat
33+
34+
interface CPUUsageReader {
35+
fun readCPUUsage(): Double
36+
}
37+
38+
@SingleInstanceIn(AppScope::class)
39+
@ContributesBinding(AppScope::class)
40+
class RealCPUUsageReader @Inject constructor() : CPUUsageReader {
41+
42+
@WorkerThread
43+
override fun readCPUUsage(): Double {
44+
val pid = android.os.Process.myPid()
45+
logcat { "PIR-MONITOR: Reading CPU load for process with pid=$pid" }
46+
val procFile = File("/proc/$pid/stat")
47+
val statsText = (FileReader(procFile)).buffered().use(BufferedReader::readText)
48+
49+
val procArray = statsText.split(WHITE_SPACE)
50+
if (procArray.size < PROC_SIZE) {
51+
throw InvalidPropertiesFormatException("Unexpected /proc file size: " + procArray.size)
52+
}
53+
54+
val procCPUTimeSec = (procArray[UTIME_IDX].toLong() + procArray[STIME_IDX].toLong()) / CLOCK_SPEED_HZ
55+
val systemUptimeSec = SystemClock.elapsedRealtime() / 1.seconds.inWholeMilliseconds.toDouble()
56+
val procTimeSec = systemUptimeSec - (procArray[STARTTIME_IDX].toLong() / CLOCK_SPEED_HZ)
57+
58+
return (100 * (procCPUTimeSec / procTimeSec)) / NUM_CORES
59+
}
60+
61+
companion object {
62+
private val CLOCK_SPEED_HZ = Os.sysconf(OsConstants._SC_CLK_TCK).toDouble()
63+
private val NUM_CORES = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF)
64+
65+
private val WHITE_SPACE = "\\s".toRegex()
66+
67+
// Indices in /proc/[pid]/stat (https://linux.die.net/man/5/proc)
68+
private val UTIME_IDX = 13
69+
private val STIME_IDX = 14
70+
private val STARTTIME_IDX = 21
71+
private val PROC_SIZE = 44
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.internal.callbacks
18+
19+
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
20+
import com.duckduckgo.di.scopes.AppScope
21+
import kotlinx.coroutines.CoroutineScope
22+
23+
interface PirCallbacks {
24+
fun onPirJobStarted(coroutineScope: CoroutineScope)
25+
fun onPirJobCompleted()
26+
fun onPirJobStopped()
27+
}
28+
29+
@ContributesPluginPoint(
30+
scope = AppScope::class,
31+
boundType = PirCallbacks::class,
32+
)
33+
@Suppress("unused")
34+
private interface PirCallbacksPluginPoint

0 commit comments

Comments
 (0)