Saltar al contenido
Todas las entradas

Android App Links en 2026: por qué tus enlaces https:// se siguen abriendo en el navegador

La verificación de Digital Asset Links, las trampas de assetlinks.json y los comandos adb que explican por qué Android no le entrega el enlace a tu app.

MFKAPPS 5 min de lectura

Un esquema de URI personalizado como myapp://item/42 es fácil de configurar y fácil de estropear de otra manera: cualquier app puede registrar el mismo esquema, así que el sistema no puede garantizar que la tuya sea la que lo abre. Un Android App Link — una URL real https://tudominio.com/... que abre la app directamente en lugar de una pestaña del navegador — resuelve esto, pero solo si el sistema puede verificar que tu app realmente es dueña del dominio. La mayoría de las veces un enlace así cae silenciosamente en Chrome, y el manifest se ve perfectamente correcto. La pieza que falta casi siempre es la verificación de Digital Asset Links, no el intent filter.

Las dos cosas que tienen que coincidir

Un App Link solo se autoverifica cuando dos piezas independientes coinciden: el intent filter de la app y un archivo JSON que el propio dominio sirve. Que ninguno de los dos lados sepa del otro es precisamente el punto — es lo que evita que una app cualquiera reclame tu dominio.

El intent filter va en el manifest:

<activity android:name=".MainActivity" android:exported="true">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https"
              android:host="mfkapps.com"
              android:pathPrefix="/apps" />
    </intent-filter>
</activity>

android:autoVerify="true" es lo que dispara la verificación real de propiedad por parte de Android en el momento de la instalación, en lugar de simplemente hacer coincidir el patrón de la URL. Omítelo y el filtro seguirá emparejando enlaces, pero solo después de que el usuario haya elegido tu app explícitamente una vez desde la hoja de desambiguación — nunca se asciende al nivel de “abrir siempre automáticamente”.

El lado del dominio es un archivo estático en una ruta fija, no negociable:

https://mfkapps.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.mfk.granyn",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A3:88:1E:22:0B:99:19:33:4B:C0:5B:0B:64:0C:1D"
    ]
  }
}]

package_name y sha256_cert_fingerprints tienen que coincidir con el build exacto que está instalado. Ese detalle del fingerprint es donde casi cualquier configuración rota realmente se rompe.

El desajuste de fingerprint que rompe todo en silencio

sha256_cert_fingerprints necesita el SHA-256 del certificado de firma del APK que el usuario tiene instalado — y si la app se distribuye mediante Play App Signing, esa es la clave de firma de Google, no la upload key que firmas localmente. Pon el fingerprint equivocado en assetlinks.json y la verificación falla sin ningún error visible para el usuario; el enlace se abre en un navegador, para siempre, y nada en Logcat señala hacia ese archivo.

Consigue el fingerprint correcto desde Play Console, no desde tu keystore local:

Play Console → Setup → App integrity → App signing key certificate → SHA-256

Para un build de debug durante el desarrollo, usa el fingerprint del keystore de debug, y lista ambos en el array si quieres que debug y release verifiquen contra el mismo assetlinks.json:

keytool -list -v -keystore ~/.android/debug.keystore \
  -alias androiddebugkey -storepass android -keypass android

sha256_cert_fingerprints acepta un array, así que ambos fingerprints pueden convivir — la verificación comprueba si cualquier entrada coincide con la firma de la app instalada.

Verificar que realmente funcionó

Android ejecuta la verificación en el momento de la instalación y guarda el resultado en caché, así que la forma más rápida de ver el estado real es preguntarle directamente al sistema en lugar de adivinar por el comportamiento:

adb shell pm get-app-links com.mfk.granyn

Una configuración que funciona reporta el dominio como verified:

com.mfk.granyn:
  ID: d3a1f0b2-...
  Signatures: [14:6D:E9:...]
  Domain verification state:
    mfkapps.com: verified

Si dice legacy_failure o none, el fingerprint o el JSON están mal — corrige assetlinks.json, y luego fuerza una re-verificación sin reinstalar:

adb shell pm verify-app-links --re-verify com.mfk.granyn
adb shell pm get-app-links com.mfk.granyn

Para probar el comportamiento real del tap una vez que la verificación pasa, dispara un intent tal como lo haría un navegador u otra app:

adb shell am start -a android.intent.action.VIEW \
  -d "https://mfkapps.com/apps/granyn"

Si la app se abre sin ninguna hoja de selección, la verificación está funcionando. Si aparece un diálogo de desambiguación, la verificación falló y el sistema lo está tratando como un enlace ordinario, sin verificar.

Tres cosas que lo rompen en silencio después del lanzamiento

El JSON tiene que servirse con el tipo de contenido correcto y sin redirección. Un CDN o proxy inverso que redirige con un 301 /.well-known/assetlinks.json a una URL canónica, o lo sirve como text/html, hace fallar la verificación aunque un navegador lo muestre perfectamente. Compruébalo directamente con curl:

curl -sI https://mfkapps.com/.well-known/assetlinks.json | grep -i content-type

Volver a firmar la app cambia el fingerprint. Rotar una clave de firma, pasar de un keystore local a Play App Signing, u onboardear un nuevo pipeline de build rompen en silencio cada enlace previamente verificado hasta que assetlinks.json se actualiza con el nuevo fingerprint. Merece una línea en un checklist de release, porque el modo de fallo — enlaces que caen silenciosamente al navegador — no produce ni un crash ni un reporte de error.

Varias apps que comparten un dominio necesitan varias declaraciones. Si más de una app del mismo equipo reclama rutas bajo el mismo host, assetlinks.json es un array JSON — un objeto por app, cada uno con su propio package_name y su lista de fingerprints. Un archivo que solo contiene la declaración de una app hace fallar en silencio la verificación de todas las demás apps de ese dominio.

Lo que esto te da más allá de un tap más agradable

Una vez que un dominio está verificado, el mismo intent filter también permite que la app actúe como manejador por defecto de ese dominio en cualquier lugar donde el sistema entregue una URL — una acción de notificación, un código QR, un enlace compartido desde otra app, un flujo de credenciales de Smart Lock. Nada de eso necesita cableado aparte; es el mismo filtro autoVerify y el mismo assetlinks.json, haciendo más una vez que el sistema realmente confía en lo que hay detrás.

Si un enlace hacia la app todavía se abre en un navegador después de todo esto, revisa pm get-app-links antes de volver a tocar el manifest — en la práctica, el intent filter casi nunca es la mitad rota.