Enable privacy protection in Android Compose apps

When capturing user behavior data, you must take measures to exclude personally identifiable information (PII) such as credit card numbers or home addresses. In the Connect library, you can create a rule for certain text fields or exclude a whole screen from capturing.

The current version of the library supports Text, TextField and OutlinedTextField.

Instructions

Step A: tag text fields with PII

  1. Open the UI file that has the text field you want to exclude from capturing.
  2. Add an import statement for the text field depending on its type. See the cheat sheet below for quick reference.
  3. Rename the text field (Text -> LoggedText, TextField -> LoggedTextField, OutlinedTextField -> LoggedOutlinedTextField).
  4. Add the maskLabel parameter to the text field you have renamed.
import com.acoustic.connect.android.connectmod.composeui.customcomposable.LoggedText

LoggedText(
    text = "This is some text",
    maskLabel = "pii"  // Required for masking
)
import com.acoustic.connect.android.connectmod.composeui.customcomposable.LoggedTextField

LoggedTextField(
    value = text,
    maskLabel = "pii"  // Required for masking
)
import com.acoustic.connect.android.connectmod.composeui.customcomposable.LoggedOutlinedTextField

LoggedOutlinedTextField(
    value = "Outlined Text",
    maskLabel = "pii" // Required for masking
)
  1. If needed, repeat the steps for more text fields. You can reuse the same label for all of them.

Here is an example from Google's open-source Reply app. We have added the pii tag to the screen status message. This tag will only be used by the Connect library.

Sample masking

Step B: create privacy rules

  1. Create the ConnectLayoutConfig.json file in the Assets Folder.
  2. Populate it with the content from our template file.
  3. In the masking object, set HasMasking to true.
  4. In the MaskAccessibilityLabelList property, enter the maskLabel tags you have assigned to the text fields.
  5. (optional) To disable the recording of a whole screen containing PII, add the AutoLayout.ScreenName object where ScreenName is the name of the screen. Set the ScreenChange property for that screen to false.
{
  "AutoLayout": {
    "GlobalScreenSettings": {
      "ScreenChange": true,
      "Masking": {
        "HasMasking": true,
        "HasCustomMask": false,
        "MaskAccessibilityLabelList": [
          "pii",
          "password"
        ]
      }
    },
    "com.example.reply.ui.navigation.Route.Articles": {
      "ScreenChange": false
    }
  }
}
  1. (recommended) Run the file through a JSON validator.

Step C: test the settings (Connect Ultimate)

If your company has an Ultimate subscription for Connect, you can check how your new rule is working.

  1. Run the app and view the text field you have masked - for example, the screen status in Google's Reply app. If the field is interactive, enter some text into it.
  1. In your Connect account, navigate to Insights > Sessions > Session search.
  2. Play back the session and make sure the text field hasn't been captured.
Masked text field

The protected text field in session replay

  1. If you excluded a screen from capturing, make sure its name isn't present in the session.

How it works

The masking object in ConnectLayoutConfig.json contains all properties related to PII protection.

PropertyValuesDescription
HasMaskingBoolean. Default value - false.Set the value to true to allow masking.
HasCustomMaskBoolean. Valid value - false.Protected text is replaced with a grey background.

In the current version, the default mask isn't customizable.
MaskAccessibilityLabelListArray of stringThe maskLabel properties of the elements that require privacy protection

Auto instrumentation

If there are many text fields that require privacy protection, you can use our auto instrumentation script that will wrap them for you. Their names will change in the following way: Text -> LoggedText, TextField -> LoggedTextField, OutlinedTextField -> LoggedOutlinedTextField.

  1. In the root project directory, create a new file (connect-composables.gradle) with the following content.
/*
 * Copyright 2025 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * Gradle task: replaceWithConnectComposeWrapper
 *
 * Scans Kotlin source files (excluding tests) and replaces:
 *   Text(...)              → LoggedText(...)
 *   TextField(...)         → LoggedTextField(...)
 *   OutlinedTextField(...) → LoggedOutlinedTextField(...)
 *
 * Skip nested calls if preceded by '{ ' (e.g., inside lambda blocks).
 * Ensure import statements for any used Logged composables.
 *
 * Regex pattern summary:
 *   (?<!\{ )   Negative lookbehind: not preceded by "{ ".
 *   \bKEY\b    Word boundary: match whole composable name.
 *   \s*\(      Optional whitespace + '('.
 *   ([^)]*)      Capture all args (simple, no nested parentheses).
 *   \)          Closing parenthesis.
 *
 * Usage:
 *   Create/Copy connect-composables.gradle file under project's root folder
 *   apply from: '../connect-composables.gradle'
 *   ./gradlew replaceWithConnectComposeWrapper
 */
tasks.register("replaceWithConnectComposeWrapper") {
    group = 'custom'
    description = 'Replace Compose calls with Connect SDK wrappers, skip nested, and add imports.'

    // Opt‑out of config‑cache (task mutates sources in‑place)
    notCompatibleWithConfigurationCache("Mutates Kotlin sources; simpler than implementing a cacheable task type.")

    def composables = [
            Text             : 'LoggedText',
            TextField        : 'LoggedTextField',
            OutlinedTextField: 'LoggedOutlinedTextField'
    ]

    println "\n----- Starting replaceWithConnectComposeWrapper -----\n"
    println "Mapping: $composables"

    def ktFiles = fileTree(dir: project.projectDir,
            includes: ['**/*.kt'],
            excludes: ['**/{test,androidTest}/**']
    )
    println "Scanning ${ktFiles.files.size()} Kotlin files..."

    int count = 0
    ktFiles.each { File file ->
        String original = file.getText('UTF-8')
        String result = original

        // Perform replacements
        composables.each { key, wrapper ->
            def pattern = java.util.regex.Pattern.compile("(?<!\\{ )\\b${key}\\b\\s*\\(([^)]*)\\)")
            def matcher = pattern.matcher(result)
            result = matcher.replaceAll(wrapper + '($1)')
        }

        // Compute which wrappers are present in result
        def neededImports = composables.values().findAll { wrapper ->
            result =~ java.util.regex.Pattern.compile("\\b${wrapper}\\b\\s*\\(")
        }.toSet()

        // Insert missing imports
        if (neededImports) {
            def lines = result.readLines()
            int idx = lines.findLastIndexOf { it.startsWith('import ') }
            if (idx < 0) idx = lines.findIndexOf { it.startsWith('package ') }
            neededImports.each { wrapper ->
                def importLine = "import com.acoustic.connect.android.connectmod.composeui.customcomposable.${wrapper}"
                if (!result.contains(importLine)) {
                    lines.add(idx + 1, importLine)
                    idx++
                }
            }
            result = lines.join('\n')
        }

        if (original != result) {
            file.write(result, 'UTF-8')
            println "✓ Updated: ${file.path}"
            count++
        }
    }

    println "\nDone. $count file(s) updated."
    println "----- Completed replaceWithConnectComposeWrapper -----\n"
}
  1. Add the script to the app-level build configuration file.
apply from: '../connect-composables.gradle'
apply(from = "../connect-composables.gradle")
  1. Open a Terminal window in Android Studio and run the following task from the root project directory.
./gradlew replaceWithConnectComposeWrapper
  1. Synchronize the project.

Now you can start assigning the maskLabel parameter to the UI elements that require privacy protection. See Step A: tag text fields with PII, point 4.