Skip to content
Tolinku
Tolinku
Sign In Start Free
Android Development · · 6 min read

Package Visibility and Deep Links on Android 11+

By Tolinku Staff
|
Tolinku app links dashboard screenshot for android blog posts

Android 11 (API 30) introduced package visibility filtering, a privacy feature that limits which installed apps your app can see and interact with. Before Android 11, any app could query the full list of installed apps using PackageManager. After Android 11, apps can only see a filtered set of apps unless they declare specific needs in the manifest.

This directly affects deep linking. If your app tries to resolve an intent to another app (checking if a specific app is installed before launching a deep link), and that app isn't visible due to the new restrictions, the intent resolution fails silently. The user taps a link, nothing happens, and your app has no idea why.

This guide covers how package visibility affects deep linking and how to configure your app correctly. For the App Links foundation, see the Android App Links complete guide. For changes in Android 12+, see Android 12+ App Links changes.

What Changed in Android 11

Before Android 11

// This worked: check if any app can handle a URL
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/deep-link"))
val activities = packageManager.queryIntentActivities(intent, 0)
// Returns all matching apps, regardless of who they are

Android 11+

// This now returns an EMPTY list unless the target app is "visible"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/deep-link"))
val activities = packageManager.queryIntentActivities(intent, 0)
// Returns empty list if the target app isn't declared in <queries>

The key behavior: queryIntentActivities(), resolveActivity(), and getInstalledApplications() now return filtered results. Apps that aren't explicitly declared as visible return as if they don't exist.

What's Automatically Visible

Some apps are always visible without any configuration:

  • Your own app
  • System apps (Settings, Camera, etc.)
  • Apps that you've started an Activity in (via startActivity())
  • Apps that are installed as part of a known interaction (e.g., the user shared content to your app from that app)
  • The current default browser

What's NOT Automatically Visible

  • Other installed apps that your user might want to deep link into
  • Apps you want to check for presence (e.g., "Open in Spotify" button)
  • Apps you target via explicit package name in an intent

How This Affects Deep Linking

Scenario 1: Checking If an App Is Installed

A common pattern: before launching a deep link, check if the target app is installed. If not, redirect to the app store or web fallback.

// This BREAKS on Android 11+ without <queries>
fun openInTargetApp(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    intent.setPackage("com.target.app")

    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    } else {
        // Fallback: open in browser
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
    }
}

On Android 11+, resolveActivity() returns null even if the target app IS installed, because your app can't see it. The result: every user hits the fallback, even when the app is installed.

Scenario 2: Custom App Chooser

If your app builds a custom "Share to" or "Open with" dialog by querying installed apps:

// Returns incomplete results on Android 11+
val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "https://links.yourapp.com/content")
}
val activities = packageManager.queryIntentActivities(shareIntent, 0)
// Missing apps that aren't declared in <queries>

Scenario 3: Intent URL Resolution

When your app creates an intent:// URL and wants to verify the target exists before presenting it to the user, the verification fails silently.

The Fix: Declare <queries> in Your Manifest

Option 1: Declare Specific Packages

If you know which apps you want to interact with:

<manifest>
    <queries>
        <!-- Apps you want to deep link into -->
        <package android:name="com.spotify.music" />
        <package android:name="com.twitter.android" />
        <package android:name="com.instagram.android" />
    </queries>

    <application>
        ...
    </application>
</manifest>

This is the most privacy-friendly approach. You declare exactly which apps you need to see, and the system grants visibility only to those.

Option 2: Declare Intent Filters

If you need to find any app that handles a specific type of intent (not a specific package):

<manifest>
    <queries>
        <!-- Find any app that handles https URLs -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>

        <!-- Find any app that handles sharing -->
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>
</manifest>

This grants visibility to any app that declares a matching intent filter.

Option 3: QUERY_ALL_PACKAGES (Last Resort)

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

This restores the pre-Android 11 behavior, making all installed apps visible. However:

  • Google Play may reject apps that use this permission without justification.
  • Only apps that are primarily app managers, security tools, or browsers have valid use cases.
  • Google Play's policy explicitly restricts this permission.

Do not use this for deep linking. Use <queries> instead.

Pattern 1: Just Launch the Intent

If you don't need to check whether the target app is installed, just launch the intent:

fun openDeepLink(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    try {
        startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        // No app handles this URL; open in browser
        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        startActivity(browserIntent)
    }
}

startActivity() doesn't require the target app to be visible. The system resolves the intent and launches the target app regardless of <queries>. The visibility restriction only affects your ability to query for apps, not your ability to launch them.

This is the recommended approach for most deep linking use cases.

Verified App Links (via Digital Asset Links) bypass the disambiguation dialog entirely. The system opens the verified app without asking the user. No <queries> needed because you're not querying; you're just opening a URL.

// This works on all Android versions, no <queries> needed
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://links.targetapp.com/content"))
startActivity(intent)

Tolinku deep links handle the routing externally. Your app opens a Tolinku URL, and the redirect chain handles app detection and routing. No need to query installed packages because the link itself determines the behavior.

Testing

Test on Android 11+ Devices

Package visibility issues are invisible on Android 10 and below. Always test deep linking on at least one Android 11+ device.

Verify <queries> Configuration

# Check your app's declared queries
adb shell dumpsys package com.example.yourapp | grep -A 10 "queriesPackages"

Simulate Missing Visibility

To test what happens when a target app isn't visible:

  1. Remove the target app's package from <queries>.
  2. Build and install your app.
  3. Try the deep link. It should fail gracefully.

Common Test Cases

Scenario Expected Behavior
Target app installed, declared in <queries> resolveActivity() returns the target
Target app installed, NOT in <queries> resolveActivity() returns null
Target app installed, launched via startActivity() Works regardless of <queries>
Target app NOT installed ActivityNotFoundException (catch and fallback)

Migration Guide

If your app was built before Android 11 and uses intent queries for deep linking:

  1. Audit all queryIntentActivities() and resolveActivity() calls. Search your codebase for these methods.

    For each call, decide the approach:

    • Can you skip the query and just launch the intent? (Preferred)
    • Do you need to show a custom chooser? (Add intent-based <queries>)
    • Do you need to check specific app presence? (Add package-based <queries>)

    Add the appropriate <queries> to your manifest.

    Test on Android 11+ with targetSdkVersion 30+. Package visibility is enforced when targeting API 30+.

    Handle the ActivityNotFoundException everywhere you call startActivity() with an external intent.

    For configuring your Android App Links with Tolinku, the platform manages the Digital Asset Links file. Configure your package name and SHA-256 fingerprint in the Tolinku dashboard and verification is handled automatically.

    For a broader view of intent filter configuration, see the Android intent filters guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.