{"id":1258,"date":"2026-05-30T09:00:00","date_gmt":"2026-05-30T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1258"},"modified":"2026-03-07T03:49:02","modified_gmt":"2026-03-07T08:49:02","slug":"android-package-visibility-deep-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/android-package-visibility-deep-links\/","title":{"rendered":"Package Visibility and Deep Links on Android 11+"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Android 11 (API 30) introduced <a href=\"https:\/\/developer.android.com\/training\/package-visibility\" rel=\"nofollow noopener\" target=\"_blank\">package visibility filtering<\/a>, 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 <code>PackageManager<\/code>. After Android 11, apps can only see a filtered set of apps unless they declare specific needs in the manifest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how package visibility affects deep linking and how to configure your app correctly. For the App Links foundation, see the <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">Android App Links complete guide<\/a>. For changes in Android 12+, see <a href=\"https:\/\/tolinku.com\/blog\/android-12-app-links-changes\/\">Android 12+ App Links changes<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Changed in Android 11<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Before Android 11<\/h3>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ This worked: check if any app can handle a URL\nval intent = Intent(Intent.ACTION_VIEW, Uri.parse(&quot;https:\/\/example.com\/deep-link&quot;))\nval activities = packageManager.queryIntentActivities(intent, 0)\n\/\/ Returns all matching apps, regardless of who they are\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Android 11+<\/h3>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ This now returns an EMPTY list unless the target app is &quot;visible&quot;\nval intent = Intent(Intent.ACTION_VIEW, Uri.parse(&quot;https:\/\/example.com\/deep-link&quot;))\nval activities = packageManager.queryIntentActivities(intent, 0)\n\/\/ Returns empty list if the target app isn&#39;t declared in &lt;queries&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The key behavior: <code>queryIntentActivities()<\/code>, <code>resolveActivity()<\/code>, and <code>getInstalledApplications()<\/code> now return filtered results. Apps that aren&#39;t explicitly declared as visible return as if they don&#39;t exist.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What&#39;s Automatically Visible<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some apps are always visible without any configuration:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Your own app<\/li>\n<li>System apps (Settings, Camera, etc.)<\/li>\n<li>Apps that you&#39;ve started an Activity in (via <code>startActivity()<\/code>)<\/li>\n<li>Apps that are installed as part of a known interaction (e.g., the user shared content to your app from that app)<\/li>\n<li>The current default browser<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">What&#39;s NOT Automatically Visible<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Other installed apps that your user might want to deep link into<\/li>\n<li>Apps you want to check for presence (e.g., &quot;Open in Spotify&quot; button)<\/li>\n<li>Apps you target via explicit package name in an intent<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">How This Affects Deep Linking<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 1: Checking If an App Is Installed<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ This BREAKS on Android 11+ without &lt;queries&gt;\nfun openInTargetApp(url: String) {\n    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n    intent.setPackage(&quot;com.target.app&quot;)\n\n    if (intent.resolveActivity(packageManager) != null) {\n        startActivity(intent)\n    } else {\n        \/\/ Fallback: open in browser\n        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On Android 11+, <code>resolveActivity()<\/code> returns <code>null<\/code> even if the target app IS installed, because your app can&#39;t see it. The result: every user hits the fallback, even when the app is installed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 2: Custom App Chooser<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your app builds a custom &quot;Share to&quot; or &quot;Open with&quot; dialog by querying installed apps:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ Returns incomplete results on Android 11+\nval shareIntent = Intent(Intent.ACTION_SEND).apply {\n    type = &quot;text\/plain&quot;\n    putExtra(Intent.EXTRA_TEXT, &quot;https:\/\/links.yourapp.com\/content&quot;)\n}\nval activities = packageManager.queryIntentActivities(shareIntent, 0)\n\/\/ Missing apps that aren&#39;t declared in &lt;queries&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 3: Intent URL Resolution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When your app creates an <code>intent:\/\/<\/code> URL and wants to verify the target exists before presenting it to the user, the verification fails silently.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Fix: Declare <code>&lt;queries&gt;<\/code> in Your Manifest<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Option 1: Declare Specific Packages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you know which apps you want to interact with:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;manifest&gt;\n    &lt;queries&gt;\n        &lt;!-- Apps you want to deep link into --&gt;\n        &lt;package android:name=&quot;com.spotify.music&quot; \/&gt;\n        &lt;package android:name=&quot;com.twitter.android&quot; \/&gt;\n        &lt;package android:name=&quot;com.instagram.android&quot; \/&gt;\n    &lt;\/queries&gt;\n\n    &lt;application&gt;\n        ...\n    &lt;\/application&gt;\n&lt;\/manifest&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is the most privacy-friendly approach. You declare exactly which apps you need to see, and the system grants visibility only to those.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Option 2: Declare Intent Filters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you need to find any app that handles a specific type of intent (not a specific package):<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;manifest&gt;\n    &lt;queries&gt;\n        &lt;!-- Find any app that handles https URLs --&gt;\n        &lt;intent&gt;\n            &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n            &lt;data android:scheme=&quot;https&quot; \/&gt;\n        &lt;\/intent&gt;\n\n        &lt;!-- Find any app that handles sharing --&gt;\n        &lt;intent&gt;\n            &lt;action android:name=&quot;android.intent.action.SEND&quot; \/&gt;\n            &lt;data android:mimeType=&quot;text\/plain&quot; \/&gt;\n        &lt;\/intent&gt;\n    &lt;\/queries&gt;\n&lt;\/manifest&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This grants visibility to any app that declares a matching intent filter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Option 3: QUERY_ALL_PACKAGES (Last Resort)<\/h3>\n\n\n\n<pre><code class=\"language-xml\">&lt;uses-permission android:name=&quot;android.permission.QUERY_ALL_PACKAGES&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This restores the pre-Android 11 behavior, making all installed apps visible. However:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Google Play may reject apps that use this permission without justification.<\/li>\n<li>Only apps that are primarily app managers, security tools, or browsers have valid use cases.<\/li>\n<li><a href=\"https:\/\/support.google.com\/googleplay\/android-developer\/answer\/10158779\" rel=\"nofollow noopener\" target=\"_blank\">Google Play&#39;s policy<\/a> explicitly restricts this permission.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not use this for deep linking.<\/strong> Use <code>&lt;queries&gt;<\/code> instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Link Patterns That Work Without <code>&lt;queries&gt;<\/code><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 1: Just Launch the Intent<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you don&#39;t need to check whether the target app is installed, just launch the intent:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">fun openDeepLink(url: String) {\n    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n    try {\n        startActivity(intent)\n    } catch (e: ActivityNotFoundException) {\n        \/\/ No app handles this URL; open in browser\n        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))\n        startActivity(browserIntent)\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>startActivity()<\/code> doesn&#39;t require the target app to be visible. The system resolves the intent and launches the target app regardless of <code>&lt;queries&gt;<\/code>. The visibility restriction only affects your ability to <strong>query<\/strong> for apps, not your ability to <strong>launch<\/strong> them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the recommended approach for most deep linking use cases.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 2: Use Verified App Links<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verified App Links (via Digital Asset Links) bypass the disambiguation dialog entirely. The system opens the verified app without asking the user. No <code>&lt;queries&gt;<\/code> needed because you&#39;re not querying; you&#39;re just opening a URL.<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ This works on all Android versions, no &lt;queries&gt; needed\nval intent = Intent(Intent.ACTION_VIEW, Uri.parse(&quot;https:\/\/links.targetapp.com\/content&quot;))\nstartActivity(intent)\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Pattern 3: Use Tolinku Smart Links<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku deep links<\/a> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Test on Android 11+ Devices<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Package visibility issues are invisible on Android 10 and below. Always test deep linking on at least one Android 11+ device.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Verify <code>&lt;queries&gt;<\/code> Configuration<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Check your app&#39;s declared queries\nadb shell dumpsys package com.example.yourapp | grep -A 10 &quot;queriesPackages&quot;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Simulate Missing Visibility<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To test what happens when a target app isn&#39;t visible:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Remove the target app&#39;s package from <code>&lt;queries&gt;<\/code>.<\/li>\n<li>Build and install your app.<\/li>\n<li>Try the deep link. It should fail gracefully.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Common Test Cases<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Scenario<\/th>\n<th>Expected Behavior<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Target app installed, declared in <code>&lt;queries&gt;<\/code><\/td>\n<td><code>resolveActivity()<\/code> returns the target<\/td>\n<\/tr>\n<tr>\n<td>Target app installed, NOT in <code>&lt;queries&gt;<\/code><\/td>\n<td><code>resolveActivity()<\/code> returns null<\/td>\n<\/tr>\n<tr>\n<td>Target app installed, launched via <code>startActivity()<\/code><\/td>\n<td>Works regardless of <code>&lt;queries&gt;<\/code><\/td>\n<\/tr>\n<tr>\n<td>Target app NOT installed<\/td>\n<td><code>ActivityNotFoundException<\/code> (catch and fallback)<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Migration Guide<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your app was built before Android 11 and uses intent queries for deep linking:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Audit all <code>queryIntentActivities()<\/code> and <code>resolveActivity()<\/code> calls.<\/strong> Search your codebase for these methods.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For each call, decide the approach:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Can you skip the query and just launch the intent? (Preferred)<\/li>\n<li>Do you need to show a custom chooser? (Add intent-based <code>&lt;queries&gt;<\/code>)<\/li>\n<li>Do you need to check specific app presence? (Add package-based <code>&lt;queries&gt;<\/code>)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Add the appropriate <code>&lt;queries&gt;<\/code> to your manifest.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Test on Android 11+ with <code>targetSdkVersion<\/code> 30+.<\/strong> Package visibility is enforced when targeting API 30+.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Handle the <code>ActivityNotFoundException<\/code> everywhere<\/strong> you call <code>startActivity()<\/code> with an external intent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For configuring your Android App Links with <a href=\"https:\/\/tolinku.com\/docs\/developer\/app-links\/\">Tolinku<\/a>, 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a broader view of intent filter configuration, see the <a href=\"https:\/\/tolinku.com\/blog\/android-intent-filters-deep-links\/\">Android intent filters guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Handle Android 11+ package visibility restrictions for deep linking. Configure queries in your manifest, handle unknown app targets, and avoid silent failures.<\/p>\n","protected":false},"author":2,"featured_media":1257,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Package Visibility and Deep Links on Android 11+","rank_math_description":"Handle Android 11+ package visibility restrictions for deep linking. Configure queries in your manifest and handle unknown app targets.","rank_math_focus_keyword":"package visibility deep links","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-android-package-visibility-deep-links.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-android-package-visibility-deep-links.png","footnotes":""},"categories":[10],"tags":[25,319,23,20,320,69,318],"class_list":["post-1258","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","tag-android","tag-android-11","tag-app-links","tag-deep-linking","tag-manifest","tag-mobile-development","tag-package-visibility"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1258","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1258"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1258\/revisions"}],"predecessor-version":[{"id":2571,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1258\/revisions\/2571"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1257"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1258"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1258"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1258"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}