Skip to content

Commit 5caf9d2

Browse files
authored
PIR: Add support for multiple profile queries in PirScan and PirOptOut (#6413)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1210594645151737/task/1210750227652612?focus=true ### Description Adds support for running the initial PIR scan and PIR opt-out scan on multiple stored profiles instead of the first stored profile. See the task for more details and a full tech spec. ### Steps to test this PR _1 profile (current behavior)_ - [x] Enter a new profile and trigger scan - scan runs for 1 profile - [x] Go to opt-out scan and trigger it for any broker - scan runs for 1 profile _Multiple profiles (new behavior)_ - [x] Clear data and trigger scan without any input - scan runs for 3 hardcoded profiles - [x] Go to opt-out scan and trigger it for any broker - scan runs for all 3 profiles (assuming that the profiles were found for that broker) ### UI changes No UI Changes
1 parent ba302c8 commit 5caf9d2

File tree

8 files changed

+293
-156
lines changed

8 files changed

+293
-156
lines changed

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/common/BrokerStepsParser.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ interface BrokerStepsParser {
4040
*
4141
* @param brokerName - name of the broker to which these steps belong to
4242
* @param stepsJson - string in JSONObject format obtained from the broker's json representing a step (scan / opt-out).
43+
* @param profileQueryId - profile query id associated with the step (used for the opt-out step)
4344
* @return list of broker steps resulting from the passed params. If the step is of type OptOut, it will return a list of
4445
* OptOutSteps where an OptOut step is mapped to each of the profile for the broker.
4546
*/
4647
suspend fun parseStep(
4748
brokerName: String,
4849
stepsJson: String,
50+
profileQueryId: Long? = null,
4951
): List<BrokerStep>
5052

5153
sealed class BrokerStep(
@@ -101,16 +103,22 @@ class RealBrokerStepsParser @Inject constructor(
101103
override suspend fun parseStep(
102104
brokerName: String,
103105
stepsJson: String,
106+
profileQueryId: Long?,
104107
): List<BrokerStep> = withContext(dispatcherProvider.io()) {
105108
return@withContext runCatching {
106109
adapter.fromJson(stepsJson)?.run {
107110
if (this is OptOutStep) {
108-
repository.getExtractProfileResultForBroker(brokerName)?.extractResults?.map {
109-
this.copy(
110-
brokerName = brokerName,
111-
profileToOptOut = it,
112-
)
113-
}
111+
repository.getExtractProfileResultsForBroker(brokerName)
112+
.filter { it.profileQuery != null && it.profileQuery.id == profileQueryId }
113+
.map {
114+
it.extractResults.map { extractedProfile ->
115+
this.copy(
116+
brokerName = brokerName,
117+
profileToOptOut = extractedProfile,
118+
)
119+
}
120+
}
121+
.flatten()
114122
} else {
115123
listOf((this as ScanStep).copy(brokerName = brokerName))
116124
}

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/common/PirActionsRunner.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,12 @@ class RealPirActionsRunner @AssistedInject constructor(
145145
}
146146

147147
withContext(dispatcherProvider.main()) {
148-
logcat { "PIR-RUNNER (${this@RealPirActionsRunner}): ${Thread.currentThread().name} Brokers to execute $brokerSteps" }
149-
logcat { "PIR-RUNNER (${this@RealPirActionsRunner}): ${Thread.currentThread().name} Brokers size: ${brokerSteps.size}" }
148+
logcat {
149+
"PIR-RUNNER (${this@RealPirActionsRunner}): ${Thread.currentThread().name} " +
150+
"Brokers size: ${brokerSteps.size} " +
151+
"profile=$profileQuery " +
152+
"Brokers to execute $brokerSteps"
153+
}
150154
detachedWebView = pirDetachedWebViewProvider.createInstance(
151155
context,
152156
pirScriptToLoad,

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/common/PirUtils.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,23 @@
1717
package com.duckduckgo.pir.internal.common
1818

1919
internal fun <T> List<T>.splitIntoParts(parts: Int): List<List<T>> {
20-
return if (this.isEmpty()) {
21-
emptyList()
22-
} else {
23-
val chunkSize = (this.size + parts - 1) / parts // Ensure rounding up
24-
this.chunked(chunkSize)
20+
if (this.isEmpty()) {
21+
return emptyList()
2522
}
23+
24+
val partSize = this.size / parts
25+
val remainder = this.size % parts
26+
27+
val result = mutableListOf<List<T>>()
28+
var startIndex = 0
29+
30+
for (i in 0 until parts) {
31+
val currentPartSize = partSize + if (i < remainder) 1 else 0
32+
val endIndex = startIndex + currentPartSize
33+
34+
result.add(this.subList(startIndex, endIndex))
35+
startIndex = endIndex
36+
}
37+
38+
return result
2639
}

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/optout/PirOptOut.kt

Lines changed: 121 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,58 @@ class RealPirOptOut @Inject constructor(
101101
private val dispatcherProvider: DispatcherProvider,
102102
callbacks: PluginPoint<PirCallbacks>,
103103
) : PirOptOut, PirJob(callbacks) {
104-
private var profileQuery: ProfileQuery = ProfileQuery(
105-
firstName = "William",
106-
lastName = "Smith",
107-
city = "Chicago",
108-
state = "IL",
109-
addresses = listOf(
110-
Address(
111-
city = "Chicago",
112-
state = "IL",
104+
private var profileQueries: List<ProfileQuery> = listOf(
105+
ProfileQuery(
106+
id = -1,
107+
firstName = "William",
108+
lastName = "Smith",
109+
city = "Chicago",
110+
state = "IL",
111+
addresses = listOf(
112+
Address(
113+
city = "Chicago",
114+
state = "IL",
115+
),
116+
),
117+
birthYear = 1993,
118+
fullName = "William Smith",
119+
age = 32,
120+
deprecated = false,
121+
),
122+
ProfileQuery(
123+
id = -2,
124+
firstName = "Jane",
125+
lastName = "Doe",
126+
city = "New York",
127+
state = "NY",
128+
addresses = listOf(
129+
Address(
130+
city = "New York",
131+
state = "NY",
132+
),
133+
),
134+
birthYear = 1990,
135+
fullName = "Jane Doe",
136+
age = 35,
137+
deprecated = false,
138+
),
139+
ProfileQuery(
140+
id = -3,
141+
firstName = "Alicia",
142+
lastName = "West",
143+
city = "Los Angeles",
144+
state = "CA",
145+
addresses = listOf(
146+
Address(
147+
city = "Los Angeles",
148+
state = "CA",
149+
),
113150
),
151+
birthYear = 1985,
152+
fullName = "Alicia West",
153+
age = 40,
154+
deprecated = false,
114155
),
115-
birthYear = 1993,
116-
fullName = "William Smith",
117-
age = 32,
118-
deprecated = false,
119156
)
120157

121158
private val runners: MutableList<PirActionsRunner> = mutableListOf()
@@ -131,9 +168,9 @@ class RealPirOptOut @Inject constructor(
131168
cleanRunners()
132169
runners.clear()
133170
}
134-
obtainProfile()
171+
obtainProfiles()
135172

136-
logcat { "PIR-OPT-OUT: Running opt-out on profile: $profileQuery on ${Thread.currentThread().name}" }
173+
logcat { "PIR-OPT-OUT: Running debug opt-out for $brokers on profiles: $profileQueries on ${Thread.currentThread().name}" }
137174

138175
runners.add(
139176
pirActionsRunnerFactory.create(
@@ -143,21 +180,28 @@ class RealPirOptOut @Inject constructor(
143180
),
144181
)
145182

146-
// Start each runner on a subset of the broker steps
183+
// Load opt-out steps jsons for each broker
184+
val brokerOptOutStepsJsons = brokers.mapNotNull { broker ->
185+
repository.getBrokerOptOutSteps(broker)?.let { broker to it }
186+
}
147187

148-
brokers.mapNotNull { broker ->
149-
repository.getBrokerOptOutSteps(broker)?.run {
150-
brokerStepsParser.parseStep(broker, this)
151-
}
152-
}.filter {
153-
it.isNotEmpty()
188+
// Map broker steps with their associated profile queries
189+
val allSteps = profileQueries.map { profileQuery ->
190+
brokerOptOutStepsJsons.map { (broker, stepsJson) ->
191+
brokerStepsParser.parseStep(broker, stepsJson, profileQuery.id)
192+
}.flatten().map { step -> profileQuery to step }
154193
}.flatten()
155-
.also { list ->
156-
runners[0].startOn(webView, profileQuery, list)
157-
runners[0].stop()
158-
}
159194

160-
logcat { "PIR-OPT-OUT: Optout completed for all runners" }
195+
// Execute each steps sequentially on the single runner
196+
allSteps.forEach { (profileQuery, step) ->
197+
logcat { "PIR-OPT-OUT: Start thread=${Thread.currentThread().name}, profile=$profileQuery and step=$step" }
198+
runners[0].startOn(webView, profileQuery, listOf(step))
199+
runners[0].stop()
200+
logcat { "PIR-OPT-OUT: Finish thread=${Thread.currentThread().name}, profile=$profileQuery and step=$step" }
201+
}
202+
203+
logcat { "PIR-OPT-OUT: Opt-out completed for all runners and profiles" }
204+
161205
emitCompletedPixel()
162206
onJobCompleted()
163207
return@withContext Result.success(Unit)
@@ -173,25 +217,28 @@ class RealPirOptOut @Inject constructor(
173217
cleanRunners()
174218
runners.clear()
175219
}
176-
obtainProfile()
220+
obtainProfiles()
177221

178-
logcat { "PIR-OPT-OUT: Running opt-out on profile: $profileQuery on ${Thread.currentThread().name}" }
222+
logcat { "PIR-OPT-OUT: Running opt-out on profiles: $profileQueries on ${Thread.currentThread().name}" }
179223

180224
val script = pirCssScriptLoader.getScript()
181225

182-
val brokerSteps = brokers.mapNotNull { broker ->
183-
repository.getBrokerOptOutSteps(broker)?.run {
184-
brokerStepsParser.parseStep(broker, this)
185-
}
186-
}.filter {
187-
it.isNotEmpty()
226+
// Load opt-out steps jsons for each broker
227+
val brokerOptOutStepsJsons = brokers.mapNotNull { broker ->
228+
repository.getBrokerOptOutSteps(broker)?.let { broker to it }
229+
}
230+
231+
// Map broker steps with their associated profile queries
232+
val allSteps = profileQueries.map { profileQuery ->
233+
brokerOptOutStepsJsons.map { (broker, stepsJson) ->
234+
brokerStepsParser.parseStep(broker, stepsJson, profileQuery.id)
235+
}.flatten().map { step -> profileQuery to step }
188236
}.flatten()
189237

190-
maxWebViewCount = if (brokerSteps.size <= MAX_DETACHED_WEBVIEW_COUNT) {
191-
brokerSteps.size
192-
} else {
193-
MAX_DETACHED_WEBVIEW_COUNT
194-
}
238+
maxWebViewCount = minOf(allSteps.size, MAX_DETACHED_WEBVIEW_COUNT)
239+
240+
// Assign steps to runners based on the maximum number of WebViews we can use
241+
val stepsPerRunner = allSteps.splitIntoParts(maxWebViewCount)
195242

196243
logcat { "PIR-OPT-OUT: Attempting to create $maxWebViewCount parallel runners on ${Thread.currentThread().name}" }
197244

@@ -208,45 +255,49 @@ class RealPirOptOut @Inject constructor(
208255
createCount++
209256
}
210257

211-
// Start each runner on a subset of the broker steps
212-
brokerSteps.splitIntoParts(maxWebViewCount)
213-
.mapIndexed { index, part ->
214-
async {
215-
runners[index].start(profileQuery, part)
258+
// Execute the steps on all runners in parallel
259+
stepsPerRunner.mapIndexed { index, partSteps ->
260+
async {
261+
partSteps.map { (profileQuery, step) ->
262+
logcat { "PIR-OPT-OUT: Start opt-out on runner=$index, profile=$profileQuery and step=$step" }
263+
runners[index].start(profileQuery, listOf(step))
216264
runners[index].stop()
265+
logcat { "PIR-OPT-OUT: Finish opt-out on runner=$index, profile=$profileQuery and step=$step" }
217266
}
218-
}.awaitAll()
267+
}
268+
}.awaitAll()
219269

220-
logcat { "PIR-OPT-OUT: Optout completed for all runners" }
270+
logcat { "PIR-OPT-OUT: Opt-out completed for all runners and profiles" }
221271
emitCompletedPixel()
222272
onJobCompleted()
223273
return@withContext Result.success(Unit)
224274
}
225275

226-
private suspend fun obtainProfile() {
227-
repository.getUserProfiles().also {
228-
if (it.isNotEmpty()) {
229-
// Temporarily taking the first profile only for the PoC. In the reality, more than 1 should be allowed.
230-
val storedProfile = it[0]
231-
profileQuery = ProfileQuery(
232-
firstName = storedProfile.userName.firstName,
233-
lastName = storedProfile.userName.lastName,
234-
city = storedProfile.addresses.city,
235-
state = storedProfile.addresses.state,
236-
addresses = listOf(
237-
Address(
238-
city = storedProfile.addresses.city,
239-
state = storedProfile.addresses.state,
276+
private suspend fun obtainProfiles() {
277+
repository.getUserProfiles().also { profiles ->
278+
if (profiles.isNotEmpty()) {
279+
profileQueries = profiles.map { storedProfile ->
280+
ProfileQuery(
281+
id = storedProfile.id,
282+
firstName = storedProfile.userName.firstName,
283+
lastName = storedProfile.userName.lastName,
284+
city = storedProfile.addresses.city,
285+
state = storedProfile.addresses.state,
286+
addresses = listOf(
287+
Address(
288+
city = storedProfile.addresses.city,
289+
state = storedProfile.addresses.state,
290+
),
240291
),
241-
),
242-
birthYear = storedProfile.birthYear,
243-
fullName = storedProfile.userName.middleName?.run {
244-
"${storedProfile.userName.firstName} $this ${storedProfile.userName.lastName}"
245-
}
246-
?: "${storedProfile.userName.firstName} ${storedProfile.userName.lastName}",
247-
age = LocalDate.now().year - storedProfile.birthYear,
248-
deprecated = false,
249-
)
292+
birthYear = storedProfile.birthYear,
293+
fullName = storedProfile.userName.middleName?.run {
294+
"${storedProfile.userName.firstName} $this ${storedProfile.userName.lastName}"
295+
}
296+
?: "${storedProfile.userName.firstName} ${storedProfile.userName.lastName}",
297+
age = LocalDate.now().year - storedProfile.birthYear,
298+
deprecated = false,
299+
)
300+
}
250301
}
251302
}
252303
}

0 commit comments

Comments
 (0)