Deep links broken by default on Android 12+
Closed this issue · 21 comments
Describe the bug
Android 12 raised the bar on what is required for automatically providing deep links to your application (via intents). TLDR is that getting it to work will require deploying a file with a hash out onto each server referenced by a deep link. (The idea is that Android can use that file to "automatically verify" your deep link.)
On previous versions of Android, it was enough to just register an intent with the configured app_host
value. Once the app was installed on a device and the user clicked a link that included the app_host
domain, they would be prompted to choose if they wanted to open the link in the app or in the browser.
To Reproduce
Now on Android 12+, once you install the app and open a link, you are never prompted to open it in the app (it just goes straight to the browser).
Expected behavior
The only silver lining here is that once everything is configured for the deep links to actually work, the user should not even see the prompt for selecting where to open the link. Instead these auto-verified deep links should just open by default in the app. (Removing a point of confusion/error on the user side....)
Environment
- Android Version: (12+)
Additional context
You can actually manually re-enable the deep-link behavior on each device by manually verifying the links. Open Settings > Apps > YOUR_APP (e.g. Medic (gamma)) > Open by default. Select "Add link" and choose the configured link from the list.
@jkuester A few questions that I was too lazy to investigate myself...
- When is the verification done? Is it at Play Store listing time or runtime? If runtime, does it work offline? If it's runtime is it verified first time only?
- Can we publish this with the tld "medicmobile.org" without any subdomains and then use it for all medic hosted instances? We'll probably need to add a feature to Core to publish this with a configurable package name for non-medic domains anyway.
Yet another reason to favour PWAs...
When is the verification done? Is it at Play Store listing time or runtime?
It is done when the app is installed on the device (does not have anything to do with the Play Store). In the Android docs it says "install the app on your device. Wait at least 20 seconds for the asynchronous verification process to complete."
If runtime, does it work offline? If it's runtime is it verified first time only?
Reading through the above-linked docs, it seems like once the intent is verified (at the time of installation), the result is cached by Android and the intent does not need to be re-verified each time a deep link is used. (The docs describe a process to use adb
when developing to revert the verification...) So, I would presume that once the initial verification is done, deep links should work fine offline. (We would definitely want to test the behavior of what happens when the app is installed offline. It is not clear how the retry logic for the intent verification works...)
Can we publish this with the tld "medicmobile.org" without any subdomains and then use it for all medic hosted instances?
This would only work if we update the intent registered by the cht-android apps to just be *.medicmobile.org
. Otherwise, each subdomain that is registered has to have its own assetlinks.json
file published.
One thing that has not been mentioned yet is that the Android docs do provide a recommended method for prompting the user to manually verify the app links. I assume this process is similar to prompting the user for location access, etc. If the user manually approves the app link, there is no need for automatic verification.
When is the verification done? Is it at Play Store listing time or runtime?
It is done when the app is installed on the device (does not have anything to do with the Play Store). In the Android docs it says "install the app on your device. Wait at least 20 seconds for the asynchronous verification process to complete."
If runtime, does it work offline? If it's runtime is it verified first time only?
Reading through the above-linked docs, it seems like once the intent is verified (at the time of installation), the result is cached by Android and the intent does not need to be re-verified each time a deep link is used. (The docs describe a process to use adb
when developing to revert the verification...) So, I would presume that once the initial verification is done, deep links should work fine offline. (We would definitely want to test the behavior of what happens when the app is installed offline. It is not clear how the retry logic for the intent verification works...)
Can we publish this with the tld "medicmobile.org" without any subdomains and then use it for all medic hosted instances?
This would only work if we update the intent registered by the cht-android apps to just be *.medicmobile.org
. Otherwise, each subdomain that is registered has to have its own assetlinks.json
file published.
One thing that has not been mentioned yet is that the Android docs do provide a recommended method for prompting the user to manually verify the app links. I assume this process is similar to prompting the user for location access, etc. If the user manually approves the app link, there is no need for automatic verification.
Verification at install time makes sense and when installing from play store you will have a connection so is not a problem, but this could be a pain for sideloaded installs.
According to some wise peeps on StackOverflow, for side loading, they can enable deep linking manually from app settings:
app info setting -> set as default ->support web addresses
@dianabarsan - @jkuester and I have verified your SO comment: after you side load, if you go into that app info setting dialog, the deep links do indeed work as advertised! I suspect the same would be true of Play Store installed apps too.
A good work around to know about!
Hey all - after chatting with @craig-landry - I think this is current not a top priority for us to fix. this is for a couple of reasons:
- While very much a bug, we don't need any changes to CHT Android, so the fix can be deployed server side (with a CHT Core upgrade) and not impact CHWs with a CHT Android upgrade
- There are no deployments that actively have a large collection of Android 12+ devices
We'll no doubt have to fix this eventually, but I think not just right now!
Not commenting on the priority of this issue, but just want to clarify that I believe a change to the cht-android app would be necessary to get auto-verification working on Android 12+. The documentation indicates that you have to have android:autoVerify="true"
set on the intent in the app.
thanks @jkuester ! That's a much bigger imposition on an already deployed APK...we'll have to take that into account then.
@mrjones-plip It may be time to revisit this given we're finding Android 14 devices are being deployed (#332).
@garethbowen - yes, agreed, now is the time! While no one is actively using token login links with Android 12+, Allies can spend another cycle or so doing some prep in Android land.
Note the initial work will only support the CHT Android side of the work. We'll have to add support to the CHT Core later.
As we get ready to actually address this issue, here is a more focused list of things that need to happen (based on the discussion/documentation above):
- Add
android:autoVerify="true"
to the intent filter in the AndroidManifest.xml - this should trigger Android 12+ to try doing auto-verification of the links. At this point auto-verification will fail since the CHT is not setup to host anassetlinks.json
file yet.- Finally, we will need to do some manual testing particularly to verify the auto verification works as expected
- Since we do not support hosting the
assetlinks.json
file yet, we will need to hack something up that we can test against. I think @mrjones-plip's PR should provide at least a starting point that we can work from! - We will want to be sure to test how the auto-verification flow behaves both when installing the APK while the device is online vs side-loading the APK when the device is actually offline. (For the offline flow, I am most interested in knowing if the auto-verification is automatically retried when the device comes back online.)
- We also need to make sure auto-verification/deep-links will work for APKs that need to connect to multiple different cht URLs (e.g. moh_kenya_echis.
- Since we do not support hosting the
- Finally, we will need to do some manual testing particularly to verify the auto verification works as expected
- Add support for requesting the user associate the app with the domain. This will provide a workaround until we support hosting
assetlinks.json
in the CHT.- My thought here is that, when opening the cht-android app, (e.g. during
onCreate
of the StartupActivity) we could trigger a new activity similar to the RequestPermissionActivity that would:- Check to see if the links have already been verified and if not:
- Present the user with a "prominent disclosure" detailing why we are asking for link verification
- Then trigger the "Open by default" settings page so the user can manually approve the links
- Include automated e2e tests that validate the basic happy-path flow of this functionality
- My thought here is that, when opening the cht-android app, (e.g. during
(Edit: updated tasks to associate testing work with the actual functionality being added (and to note that we will be pursuing an expansion of the e2e test functionality of cht-android to involve actually spinning up a test instance of the CHT that we can login to and interact with.))
Updates
Add android:autoVerify="true" to the intent filter in the AndroidManifest.xml - this should trigger Android 12+ to try doing auto-verification of the links. At this point auto-verification will fail since the CHT is not setup to host an assetlinks.json file yet.
The automatic verification process(referring to the quote above) has been accomplished. When the assetlinks.json
file is hosted on cht-core
, any new installations of the applications will have their respective domains pre-verified. As a result, any URI associated with that domain will directly open within the corresponding application itself. However, there are a few constraints on this.
- For the domain verification to be successful, the domain must be publicly accessible. In other words, anyone with an internet connection should be able to access the domain without any restrictions.
a. However, tunneling and port forwarding applications likengrok
can also make the web server publicly accessible. - Along with the domain being publicly accessible, the Android device must also be connected to the internet during and some time(1-5 mins) after installation.
- The conditions stated in points 1 and 2 also apply when updating the application with this change. If the users are updating the app and the domain is not publicly accessible, or if the device is offline, then the domain verification process will fail.
a. If an Android device was offline during the process of updating the application with this change, the domain verification process will not automatically retriggered once the device regains internet connectivity.
Testing
This section outlines how this change was tested.
NOTE: These steps assume that the code changes have already been done.
-
Have a webserver running with the
<domain>/.well-known/assetlinks.json
endpoint available with the required content. The content should be somewhere along this.
a. The gotcha here is that the weblink above says that you should usekeytool -list -v -keystore my-release-key.keystore
to get the application signature that goes in theassetlinks.json
file which is true for production but for development purposes, the output ofadb shell pm get-app-links <package_name>
should be used. NOTE: this second command only works if the package is already installed in the Android device or emulator, so the package needs to be installed/run from Android Studio or your favorite IDE at least once before following these test steps.
b. Make sure that the webserver is publicly accessible. I foundngrok
to be much faster to set up than other tools.
If everything went well, when you visit<domain>/.well-known/assetlinks.json
you should see something like this. -
Select your build variant/flavor and click the
run
button in the Android Studio or your IDE or compile the application to generate the APK files. To compile the application run the following commands.export ANDROID_KEYSTORE_PASSWORD_NEW_BRAND=<keystore_pass> export ANDROID_KEY_PASSWORD_NEW_BRAND=<key_pass> export ANDROID_SECRETS_IV_NEW_BRAND=<secret_iv> export ANDROID_SECRETS_KEY_NEW_BRAND=<secret_key>
make org=new_brand flavor=<flavor> assemble
The flavor's APK files should be in the path
.../cht-android/build/outputs/apk/<flavor>/debug/
. -
In the path given at the end of point 2, there should be multiple APKs respective to the architecture of the CPU. Use the appropriate one and install it on your Android device/emulator. Run the application.
-
Go to the
app info
section of the app.Then, scroll down to the
open by default
section. Go into it.
If everything goes well, you should see something like this.For verification, you can open any link related to the domain built into the app and it should open in the application.
You can also check if the domain was verified for the application using Android'sadb
tool.
For verification do the following:adb shell pm get-app-links <package_name>
For more info: link
Testing for applications with multiple domains
If an application needs to handle multiple domains that should all open within the same application, the web servers for each of those related domains must publicly host the assetlinks.json
file. The rest of the process is the same as for applications with a single domain.
When following the above steps as well for this and if everything went well, then for step 4 in app info > open by default
section you should see something similar to this.
Note the 2 verified links
which was 1 verified link
in the previous section.
You can also use adb
to check the status.
All the domains will be listed under Domain verification state
with their status.
Updates:
I've created a video that overviews this issue and its proposed solutions.
Contents:
Introduction about the issue: 00:00 - 00:53
Issue demo: 00:54 - 02:18
Solutions: 02:20 - 04:20
Auto-verification:
Public URL Local setup: 04:21 - 08:43
Compiling the app with public URL: 09:00 - 10:00
Installing and checking the auto verification: 10:01 - 14:15
Manual-verification:
Compiling the app: 14:16 - 15:10
Installing and checking the manual verification: 15:11 - 18:20
android_deep_links_issue_compressed.mp4
Hi, @sugat009. Thank you so much for the detailed explanation of how to test this and the video. You rock!
I have two questions.
- where do I get the values for:
export ANDROID_KEYSTORE_PASSWORD_NEW_BRAND=<keystore_pass>
export ANDROID_KEY_PASSWORD_NEW_BRAND=<key_pass>
export ANDROID_SECRETS_IV_NEW_BRAND=<secret_iv>
export ANDROID_SECRETS_KEY_NEW_BRAND=<secret_key>
- What changes must I make to Nginx to have
assetlinks.json
available?
Hello, @lorerod
- I created a new flavor following this doc.
- Assuming that you will also be running a CHT docker helper instance, the changes from mrjones' PR in the
nginx.conf
file and creating anassetlinks.json
file in/etc/nginx/private/android/
should be enough. P.S. don't forget to restart the nginx docker container.
@sugat009 I tested single and multiple domains, with physical phones running on Android 14, 10, and 8.1. It works as expected in every phone and scenario. I will add more details of these tests in another comment.
Also, I tried to test it on a physical phone with Android 5.1.1, but the app wasn’t installed successfully. I needed to connect the phone to my laptop to debug the problem to give you more details.
I didn't find Android 5 in Android Studio emulators, so I also tried it on an emulator with Android 7.0, and it gave this error when installing the APK:
Error code: 'UNKNOWN', message='Unknown failure: 'Exception occurred while dumping:
java.lang.IllegalArgumentException: Unknown option --full at
com.android.server.pm.PackageManagerShellCommand.makeInstallParams(PackageManagerShellCommand.java:1070) at
com.android.server.pm.PackageManagerShellCommand.runInstallCreate(PackageManagerShellCommand.java:215) at
com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:101) at
android.os.ShellCommand.exec(ShellCommand.java:94) at
com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:18181) at
android.os.Binder.shellCommand(Binder.java:468) at android.os.Binder.onTransact(Binder.java:367) at
android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2387) at
com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3143) at
android.os.Binder.execTransact(Binder.java:565)
@lorerod This seems to be a bug in Android Studio(for Android 7) when installing applications using the drag-and-drop technique. I discovered this when trying to update Google Chrome by doing the drag-and-drop. The alternative way of installing the application is using the run button in Android Studio(in the Screenshot below). The thing to be careful of here is when running through the run button, the application is also opened after installation so, Android might not have gotten the time to verify the links producing negative results.
Ok I tried what you mentioned in the Emulator with Android 7 and I'm getting this error when using the run button to install the application.
The currently selected variant "new_brandDebug" uses split APKs, but none of the 2 split apks are compatible with the current device with ABIs "x86".
Back to the Physical device running Android 5.1.1. When manually installing the APK using only the device, I can install it, and when opening it, a blank screen shows:
Connecting the device to Android Studio, I run the application, the app is installed, the app opens, the same blank screen shows, and this shows in the Logcat:
---------------------------- PROCESS STARTED (16292) for package org.medicmobile.webapp.mobile.new_brand ----------------------------
2024-04-29 10:14:08.883 16292-16292 ResourceType org...obile.webapp.mobile.new_brand W Found multiple library tables, ignoring...
2024-04-29 10:14:09.003 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.StartupActivity :: onCreate()
2024-04-29 10:14:09.015 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.SettingsStore :: Loading settings for context org.medicmobile.webapp.mobile.StartupActivity@8795be6...
2024-04-29 10:14:09.017 16292-16292 Timeline org...obile.webapp.mobile.new_brand I Timeline: Activity_launch_request id:org.medicmobile.webapp.mobile.new_brand time:930561
2024-04-29 10:14:09.054 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.EmbeddedBrowserActivity :: Starting webview...
2024-04-29 10:14:09.066 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.SettingsStore :: Loading settings for context org.medicmobile.webapp.mobile.EmbeddedBrowserActivity@3ac6e617...
2024-04-29 10:14:09.077 16292-16292 linker org...obile.webapp.mobile.new_brand W libwebviewchromium.so: unused DT entry: type 0x6ffffef5 arg 0x7d98
2024-04-29 10:14:09.078 16292-16292 linker org...obile.webapp.mobile.new_brand W libwebviewchromium.so: unused DT entry: type 0x6ffffffe arg 0x7d38
2024-04-29 10:14:09.078 16292-16292 linker org...obile.webapp.mobile.new_brand W libwebviewchromium.so: unused DT entry: type 0x6fffffff arg 0x3
2024-04-29 10:14:09.120 16292-16292 WebViewFactory org...obile.webapp.mobile.new_brand I Loading com.google.android.webview version 95.0.4638.74 (code 463807400)
2024-04-29 10:14:09.124 16292-16292 ResourceType org...obile.webapp.mobile.new_brand W Found multiple library tables, ignoring...
2024-04-29 10:14:09.127 16292-16292 ResourceType org...obile.webapp.mobile.new_brand W Found multiple library tables, ignoring...
2024-04-29 10:14:09.155 16292-16292 cr_WVCFactoryProvider org...obile.webapp.mobile.new_brand I Loaded version=95.0.4638.74 minSdkVersion=21 isBundle=true multiprocess=false packageId=3
2024-04-29 10:14:09.176 16292-16292 ResourceType org...obile.webapp.mobile.new_brand W Found multiple library tables, ignoring...
2024-04-29 10:14:09.181 16292-16292 cr_LibraryLoader org...obile.webapp.mobile.new_brand I Successfully loaded native library
2024-04-29 10:14:09.183 16292-16292 cr_CachingUmaRecorder org...obile.webapp.mobile.new_brand I Flushed 8 samples from 8 histograms.
2024-04-29 10:14:09.299 16292-16292 chromium org...obile.webapp.mobile.new_brand E [ERROR:network_service_instance_impl.cc(179)] Failed to grant sandbox access to network context data for /data/data/org.medicmobile.webapp.mobile.new_brand/app_webview/Default with result 7: No such file or directory (2)
2024-04-29 10:14:09.330 16292-16339 Connectivi...ackHandler org...obile.webapp.mobile.new_brand D CM callback handler got msg 524290
2024-04-29 10:14:09.383 16292-16292 MediaPlayer-JNI org...obile.webapp.mobile.new_brand E QCMediaPlayer mediaplayer NOT present
2024-04-29 10:14:09.389 16292-16292 MediaPlayer org...obile.webapp.mobile.new_brand E Should have subtitle controller already set
2024-04-29 10:14:09.399 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.EmbeddedBrowserActivity :: Pointing browser to: https://79e8-181-91-85-22.ngrok-free.app
2024-04-29 10:14:09.413 16292-16331 cr_media org...obile.webapp.mobile.new_brand W Requires BLUETOOTH permission
2024-04-29 10:14:09.413 16292-16344 libEGL org...obile.webapp.mobile.new_brand E validate_display:255 error 3008 (EGL_BAD_DISPLAY)
2024-04-29 10:14:09.414 16292-16344 Adreno-EGL org...obile.webapp.mobile.new_brand I <qeglDrvAPI_eglInitialize:410>: EGL 1.4 QUALCOMM build: AU_LINUX_ANDROID_LA.BF.1.1.1_RB1.05.01.00.042.030_msm8974_LA.BF.1.1.1_RB1__release_AU ()
OpenGL ES Shader Compiler Version: E031.25.03.06
Build Date: 05/17/15 Sun
Local Branch: mybranch10089422
Remote Branch: quic/LA.BF.1.1.1_rb1.22
Local Patches: NONE
Reconstruct Branch: AU_LINUX_ANDROID_LA.BF.1.1.1_RB1.05.01.00.042.030 + 6151be1 + NOTHING
2024-04-29 10:14:09.424 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.BrandedSettingsStore :: SettingsStore() :: getting last-url: https://79e8-181-91-85-22.ngrok-free.app/
2024-04-29 10:14:09.444 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.EmbeddedBrowserActivity :: onStart() :: Checking Crosswalk migration ...
2024-04-29 10:14:09.445 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.XWalkMigration :: testFileExists() :: '/data/data/org.medicmobile.webapp.mobile.new_brand/app_xwalkcore/Default': false
2024-04-29 10:14:09.446 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.XWalkMigration :: Crosswalk directory not found: /data/data/org.medicmobile.webapp.mobile.new_brand/files
2024-04-29 10:14:09.448 16292-16292 ContextImpl org...obile.webapp.mobile.new_brand W Failed to ensure directory: /storage/sdcard1/Android/data/org.medicmobile.webapp.mobile.new_brand/files
2024-04-29 10:14:09.449 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.XWalkMigration :: testFileExists() :: '/storage/emulated/0/Android/data/org.medicmobile.webapp.mobile.new_brand/app_xwalkcore/Default': false
2024-04-29 10:14:09.449 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.XWalkMigration :: Crosswalk directory not found: /storage/emulated/0/Android/data/org.medicmobile.webapp.mobile.new_brand/files
2024-04-29 10:14:09.449 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.EmbeddedBrowserActivity :: onStart() :: Crosswalk installation not found - skipping migration
2024-04-29 10:14:09.449 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.EmbeddedBrowserActivity :: onStart() :: Checking Crosswalk migration done.
2024-04-29 10:14:09.456 16292-16349 OpenGLRenderer org...obile.webapp.mobile.new_brand D Use EGL_SWAP_BEHAVIOR_PRESERVED: true
2024-04-29 10:14:09.466 16292-16292 Atlas org...obile.webapp.mobile.new_brand D Validating map...
2024-04-29 10:14:09.500 16292-16344 libEGL org...obile.webapp.mobile.new_brand E validate_display:255 error 3008 (EGL_BAD_DISPLAY)
2024-04-29 10:14:09.505 16292-16292 MediaPlayer org...obile.webapp.mobile.new_brand E Should have subtitle controller already set
2024-04-29 10:14:09.528 16292-16349 OpenGLRenderer org...obile.webapp.mobile.new_brand I Initialized EGL, version 1.4
2024-04-29 10:14:09.533 16292-16349 OpenGLRenderer org...obile.webapp.mobile.new_brand D Enabling debug mode 0
2024-04-29 10:14:09.545 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.UrlHandler :: onPageFinished() :: url: https://79e8-181-91-85-22.ngrok-free.app/
2024-04-29 10:14:09.596 16292-16292 Timeline org...obile.webapp.mobile.new_brand I Timeline: Activity_idle id: android.os.BinderProxy@2c605ea6 time:931141
2024-04-29 10:14:09.841 16292-16324 cr_X509Util org...obile.webapp.mobile.new_brand I Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
2024-04-29 10:14:09.865 16292-16324 cr_X509Util org...obile.webapp.mobile.new_brand I Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
2024-04-29 10:14:09.867 16292-16333 chromium org...obile.webapp.mobile.new_brand E [ERROR:ssl_client_socket_impl.cc(976)] handshake failed; returned -1, SSL error code 1, net_error -202
2024-04-29 10:14:09.882 16292-16292 MedicMobile org...obile.webapp.mobile.new_brand D org.medicmobile.webapp.mobile.UrlHandler :: onPageFinished() :: url: https://79e8-181-91-85-22.ngrok-free.app/
The first issue might be because only 2 APK files are being generated on your PC whereas the second one is likely because the browser is old and incompatible with the hosted CHT version. For the browser-CHT compatibility matrix, please check here.
Posting this publicly from a private issue as it will benefit everyone looking into this issue and wondering about the "asset link modal" issue:
Hey all! Please be aware that if you're on CHT Android
1.4.0
with Android12
or higher, you will see an asset link modal. This is only when you quit the app and start it again. You can click "Don't Link" and you'll be dropped back in the app. You can click "Yes, link domain" and approve the link, and be dropped back into the app.To avoid this modal, you need to be on CHT Android
1.4.0
with Android12
AND have CHT4.7.1
or higher with Assetlinks configured.Here's the screen users will see:
Here's a video demonstrating the different scenarios with your fine hosts @mrjones-plip and @jkuester 📽️ :
asset.links.final.mp4