Skip to content

Implement internal pir opt out #5845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 8, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "4fda73a442b6411886706c42bedc64bc",
"identityHash": "a82b3c3fcdfd42d3736c96fd4b979451",
"entities": [
{
"tableName": "pir_broker_json_etag",
Expand Down Expand Up @@ -364,7 +364,7 @@
},
{
"tableName": "pir_user_profile",
"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)",
"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)",
"fields": [
{
"fieldPath": "id",
Expand All @@ -384,12 +384,6 @@
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "age",
"columnName": "age",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userName.firstName",
"columnName": "user_firstName",
Expand Down Expand Up @@ -449,7 +443,7 @@
"foreignKeys": []
},
{
"tableName": "pir_scan_log",
"tableName": "pir_events_log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventTimeInMillis` INTEGER NOT NULL, `eventType` TEXT NOT NULL, PRIMARY KEY(`eventTimeInMillis`))",
"fields": [
{
Expand Down Expand Up @@ -505,12 +499,150 @@
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pir_scan_complete_brokers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`brokerName` TEXT NOT NULL, `startTimeInMillis` INTEGER NOT NULL, `endTimeInMillis` INTEGER NOT NULL, PRIMARY KEY(`brokerName`))",
"fields": [
{
"fieldPath": "brokerName",
"columnName": "brokerName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "startTimeInMillis",
"columnName": "startTimeInMillis",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTimeInMillis",
"columnName": "endTimeInMillis",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"brokerName"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pir_opt_out_complete_brokers",
"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)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "brokerName",
"columnName": "brokerName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extractedProfile",
"columnName": "extractedProfile",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "startTimeInMillis",
"columnName": "startTimeInMillis",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endTimeInMillis",
"columnName": "endTimeInMillis",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSubmitSuccess",
"columnName": "isSubmitSuccess",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pir_opt_out_action_log",
"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)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "brokerName",
"columnName": "brokerName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extractedProfile",
"columnName": "extractedProfile",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "completionTimeInMillis",
"columnName": "completionTimeInMillis",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "actionType",
"columnName": "actionType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isError",
"columnName": "isError",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "result",
"columnName": "result",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4fda73a442b6411886706c42bedc64bc')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a82b3c3fcdfd42d3736c96fd4b979451')"
]
}
}
26 changes: 22 additions & 4 deletions pir/pir-internal/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<activity
android:name=".settings.PirDevSettingsActivity"
android:label="@string/pirDevSettings" />

<activity
android:name=".settings.PirResultsActivity"
android:label="@string/pirDevSimpleScanHeader" />

<activity
android:name=".settings.PirDevScanActivity"
android:label="@string/pirDevScanTitle" />

<activity
android:name=".settings.PirDevOptOutActivity"
android:label="@string/pirDevOptOutTitle" />

<activity
android:name=".settings.PirScanResultsActivity"
android:label="@string/pirDevViewScanResults" />
android:name=".settings.PirWebViewActivity"
android:label="@string/pirDevDebugOptOutTitle" />

<service
android:name=".scan.PirForegroundScanService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"
android:process=":pir" />

<service
android:name=".service.PirForegroundScanService"
android:name=".optout.PirForegroundOptOutService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"
android:process=":pir" />

<service
android:name=".service.PirRemoteWorkerService"
android:name=".scan.PirRemoteWorkerService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"
android:process=":pir" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.pir.internal.PirRemoteFeatures
import com.duckduckgo.pir.internal.store.PitTestingStore
import com.duckduckgo.subscriptions.api.Subscriptions
import com.squareup.anvil.annotations.ContributesMultibinding
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
Expand All @@ -39,10 +41,14 @@ class PirDataUpdateObserver @Inject constructor(
private val brokerJsonUpdater: BrokerJsonUpdater,
private val subscriptions: Subscriptions,
private val pirRemoteFeatures: PirRemoteFeatures,
private val testingStore: PitTestingStore,
) : MainProcessLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
coroutineScope.launch(dispatcherProvider.io()) {
if (pirRemoteFeatures.allowPirRun().isEnabled() && subscriptions.getAccessToken() != null) {
if (testingStore.testerId == null) {
testingStore.testerId = UUID.randomUUID().toString()
}
logcat { "PIR-update: Attempting to update all broker data" }
if (brokerJsonUpdater.update()) {
logcat { "PIR-update: Update successfully completed." }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.pir.internal.callbacks

import android.os.SystemClock
import android.system.Os
import android.system.OsConstants
import androidx.annotation.WorkerThread
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.util.InvalidPropertiesFormatException
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import logcat.logcat

interface CPUUsageReader {
fun readCPUUsage(): Double
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealCPUUsageReader @Inject constructor() : CPUUsageReader {

@WorkerThread
override fun readCPUUsage(): Double {
val pid = android.os.Process.myPid()
logcat { "PIR-MONITOR: Reading CPU load for process with pid=$pid" }
val procFile = File("/proc/$pid/stat")
val statsText = (FileReader(procFile)).buffered().use(BufferedReader::readText)

val procArray = statsText.split(WHITE_SPACE)
if (procArray.size < PROC_SIZE) {
throw InvalidPropertiesFormatException("Unexpected /proc file size: " + procArray.size)
}

val procCPUTimeSec = (procArray[UTIME_IDX].toLong() + procArray[STIME_IDX].toLong()) / CLOCK_SPEED_HZ
val systemUptimeSec = SystemClock.elapsedRealtime() / 1.seconds.inWholeMilliseconds.toDouble()
val procTimeSec = systemUptimeSec - (procArray[STARTTIME_IDX].toLong() / CLOCK_SPEED_HZ)

return (100 * (procCPUTimeSec / procTimeSec)) / NUM_CORES
}

companion object {
private val CLOCK_SPEED_HZ = Os.sysconf(OsConstants._SC_CLK_TCK).toDouble()
private val NUM_CORES = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF)

private val WHITE_SPACE = "\\s".toRegex()

// Indices in /proc/[pid]/stat (https://linux.die.net/man/5/proc)
private val UTIME_IDX = 13
private val STIME_IDX = 14
private val STARTTIME_IDX = 21
private val PROC_SIZE = 44
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.pir.internal.callbacks

import com.duckduckgo.anvil.annotations.ContributesPluginPoint
import com.duckduckgo.di.scopes.AppScope
import kotlinx.coroutines.CoroutineScope

interface PirCallbacks {
fun onPirJobStarted(coroutineScope: CoroutineScope)
fun onPirJobCompleted()
fun onPirJobStopped()
}

@ContributesPluginPoint(
scope = AppScope::class,
boundType = PirCallbacks::class,
)
@Suppress("unused")
private interface PirCallbacksPluginPoint
Loading
Loading