Part of Github Copilot with Kotlin

Tell copilot to secure the Jetpack compose app

Sandy LaneSandy Lane

Video: Tell copilot to secure the Jetpack compose app by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Tell Copilot to "Secure" a Jetpack Compose App

Vague prompts produce vague answers. Copilot's first move on "secure my app" is WindowManager.LayoutParams.FLAG_SECURE — which only blocks screenshots. Real security is a much bigger conversation.

A meta-episode: what happens when you give Copilot a fuzzy goal like "secure this app." The answer reveals how Copilot reads context — and where its limitations show.

The Copilot prompt

// Make this Compose app secure (prevent screenshots)
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {

Copilot generates:

import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Prevent screenshots and screen recording
    window.setFlags(
      WindowManager.LayoutParams.FLAG_SECURE,
      WindowManager.LayoutParams.FLAG_SECURE
    )

    setContent {
      MaterialTheme {
        Surface {
          Text("Secure App Content")
        }
      }
    }
  }
}

What FLAG_SECURE actually does

FLAG_SECURE is one specific Android security feature: it tells the system "don't include this window in screenshots, screen recordings, or non-secure displays."

Effects:

  • Screenshots blocked. The user pressing power+volume-down gets a "couldn't capture screenshot" toast.
  • Screen recording blocked. Apps trying to record show a black frame for your activity.
  • Casting to non-secure displays blocked. Mirroring to a TV via Chromecast on most devices fails.
  • Recent apps thumbnail blacked out. When the user opens the app switcher, your app's preview is just a black rectangle.

Banking apps and password managers commonly use this. It's not real security — a determined user with another camera can still capture the screen. But it raises the bar.

What "secure" actually means

"Secure" is not a single feature. The threats vary:

  • Network MITM — TLS, certificate pinning, Network Security Config.
  • Data at rest — EncryptedSharedPreferences, KeyStore-backed keys, full-disk encryption.
  • Reverse engineering — ProGuard/R8 obfuscation, Play Integrity, root/Magisk detection.
  • Tampering — APK signing, signature checks at runtime.
  • Authentication — biometric, PIN, OAuth tokens with refresh.
  • Social engineering — overlay attacks, screen recording (FLAG_SECURE), accessibility-service abuse.

Copilot reads "secure" as the most common meaning in the training data, which for Android is often FLAG_SECURE. It's a starting point, not an answer.

Stacking the standard precautions

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Block screenshots and recording
    window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)

    setContent { /* ... */ }
  }
}

Note addFlags(FLAG) is more correct than setFlags(FLAG, FLAG)addFlags ORs into existing flags; setFlags(value, mask) replaces just the masked bits. For a single flag, addFlags is clearer.

EncryptedSharedPreferences

For storing secrets locally:

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

val masterKey = MasterKey.Builder(context)
  .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
  .build()

val sharedPrefs = EncryptedSharedPreferences.create(
  context,
  "secret_prefs",
  masterKey,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

sharedPrefs.edit().putString("token", userToken).apply()
val token = sharedPrefs.getString("token", null)

Encrypts both keys and values, with the master key stored in Android Keystore (hardware-backed on supporting devices).

For data files: EncryptedFile from the same androidx.security.crypto package.

Network Security Config

<!-- res/xml/network_security_config.xml -->
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <pin-set>
      <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

Pin specific certificates so even a compromised CA can't MITM your traffic. Reference from AndroidManifest.xml:

<application
  android:networkSecurityConfig="@xml/network_security_config">

Biometric authentication

import androidx.biometric.BiometricPrompt

val biometricPrompt = BiometricPrompt(this, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
  .setTitle("Authenticate")
  .setSubtitle("Use your fingerprint")
  .setNegativeButtonText("Cancel")
  .build()
biometricPrompt.authenticate(promptInfo)

Use for "unlock the app" or "confirm sensitive action" prompts.

Anti-tampering: signature check

fun isOurSignature(context: Context): Boolean {
  val packageInfo = context.packageManager.getPackageInfo(
    context.packageName,
    PackageManager.GET_SIGNING_CERTIFICATES
  )
  val signatures = packageInfo.signingInfo.apkContentsSigners
  // Compare against known good fingerprint
  return signatures.any { it.toCharsString() == EXPECTED_SIGNATURE }
}

Dies on rooted/repackaged apps. Doesn't stop a determined attacker — but the bar is higher.

What Copilot won't tell you

  • Threat modeling. What are you actually defending against? Casual users? Insider screenshots? State-level adversaries? The answer determines the cost of your security.
  • Trade-offs. FLAG_SECURE breaks legitimate accessibility tools. EncryptedSharedPreferences are slower than plain ones. Pinning breaks if you renew certs and forget to update the app.
  • The cost of false security. Adding FLAG_SECURE and calling the app "secure" might lull stakeholders into skipping the actual hard work (TLS, pinning, key management, server-side checks).

Copilot can scaffold the implementation, not the threat model. Always sketch the model before reaching for the API.

Common mistakes

FLAG_SECURE as security theater. It blocks screenshots, not data exfiltration, network sniffing, or rooted-device attacks.

Storing secrets in plain SharedPreferences. Anyone with adb access reads them. Use EncryptedSharedPreferences or the Android Keystore.

Trusting client-side checks alone. Server must verify everything. Client checks add friction; they don't add security.

Not pinning TLS certs for high-value endpoints. A compromised CA can MITM your traffic without pinning.

Ignoring obfuscation. Reverse-engineering an unobfuscated APK takes minutes. Enable R8 minification and shrinking.

What's next

Episode 21: Person detail Composable with just a username. Back to UI fundamentals — a small Compose screen that takes a string and lays out a card.

Recap

"Secure" is a multi-dimensional goal. FLAG_SECURE is one tool — blocks screenshots and recording. Real security requires threat modeling first: encrypted storage (EncryptedSharedPreferences), TLS pinning (Network Security Config), authentication (BiometricPrompt), anti-tampering (signature checks), obfuscation (R8). Copilot scaffolds the implementation; you write the threat model.

Next episode: person detail Composable.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.