Seamless Linking to Your App

Seamless Linking to Your App

An Android app is usually only a single part of a larger product. Indeed, a product is usually made of several independent entities such as a website, one or several mobile apps, etc.

In this talk, we will learn how to increase app engagement and tear down the walls between your website and your apps. You will also discover how you can give your users the most integrated mobile experience possible with features such as Related Apps Banner, Smart Lock for Passwords and more… In a nutshell, this talk is all about driving users to your mobile app and making your product successful.

E9bf8f6d5480ea2a2623df7dccfd1f70?s=128

Cyril Mottier

November 10, 2016
Tweet

Transcript

  1. Seamless linking to your app @cyrilmottier

  2. Linking to your App Tell your app to handle some

    links 1
  3. <intent-filter>

  4. <intent-filter> Gotta Catch ‘Em All

  5. Available since Android 1.0 Declared statically General intent filtering mechanism

    <intent-filter>
  6. Available since Android 1.0 Declared statically General intent filtering mechanism

    <intent-filter>
  7. Available since Android 1.0 Declared statically General intent filtering mechanism

    <intent-filter>
  8. https://www.trainline.eu/search/nantes/paris

  9. https://www.trainline.eu/search/nantes/paris Scheme

  10. https://www.trainline.eu/search/nantes/paris Host

  11. https://www.trainline.eu/search/nantes/paris Path

  12. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter>
  13. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <action android:name="android.intent.action.VIEW" />
  14. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" />
  15. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <data android:scheme="https" android:host="www.trainline.eu" />
  16. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <data android:path="/search" /> <data android:pathPrefix="/search/" />
  17. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> Are you f***ing serious? The real world is that simple?
  18. <intent-filter> <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="www.trainline.eu" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> Nope…
  19. <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter>
  20. <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:scheme="https" />
  21. <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> <data android:scheme="http" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" />
  22. public final class AppConfig { private AppConfig() { } public

    static final List<String> SCHEMES = Collections.unmodifiableList(Arrays.asList( "http", "https")); public static final List<String> AUTHORITIES = Collections.unmodifiableList(Arrays.asList( "captaintrain.com", "trainline.de", "trainline.es", "trainline.eu", "trainline.fr", "trainline.it", "www.captaintrain.com", "www.trainline.de", "www.trainline.es", "www.trainline.eu", "www.trainline.fr", "www.trainline.it")); public static final String PATH_SEARCH = "search"; }
  23. public final class SearchActivity extends Activity { @Override protected void

    onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String departure = null; String arrival = null; final Uri uri = getIntent().getData(); if (uri != null) { if (AppConfig.AUTHORITIES.contains(uri.getAuthority()) && AppConfig.SCHEMES.contains(uri.getScheme())) { final List<String> segments = uri.getPathSegments(); if (segments != null && segments.size() == 3 && AppConfig.PATH_SEARCH.equals(segments.get(0))) { arrival = segments.get(1); departure = segments.get(2); } } } startSearch(departure, arrival); } }
  24. public final class SearchActivity extends Activity { @Override protected void

    onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String departure = null; String arrival = null; final Uri uri = getIntent().getData(); if (uri != null) { if (AppConfig.AUTHORITIES.contains(uri.getAuthority()) && AppConfig.SCHEMES.contains(uri.getScheme())) { final List<String> segments = uri.getPathSegments(); if (segments != null && segments.size() == 3 && AppConfig.PATH_SEARCH.equals(segments.get(0))) { arrival = segments.get(1); departure = segments.get(2); } } } startSearch(departure, arrival); } } if (AppConfig.AUTHORITIES.contains(uri.getAuthority()) && AppConfig.SCHEMES.contains(uri.getScheme())) {
  25. public final class SearchActivity extends Activity { @Override protected void

    onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String departure = null; String arrival = null; final Uri uri = getIntent().getData(); if (uri != null) { if (AppConfig.AUTHORITIES.contains(uri.getAuthority()) && AppConfig.SCHEMES.contains(uri.getScheme())) { final List<String> segments = uri.getPathSegments(); if (segments != null && segments.size() == 3 && AppConfig.PATH_SEARCH.equals(segments.get(0))) { arrival = segments.get(1); departure = segments.get(2); } } } startSearch(departure, arrival); } } final List<String> segments = uri.getPathSegments(); if (segments != null && segments.size() == 3 && AppConfig.PATH_SEARCH.equals(segments.get(0))) { arrival = segments.get(1); departure = segments.get(2); }
  26. public final class SearchActivity extends Activity { @Override protected void

    onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String departure = null; String arrival = null; final Uri uri = getIntent().getData(); if (uri != null) { if (AppConfig.AUTHORITIES.contains(uri.getAuthority()) && AppConfig.SCHEMES.contains(uri.getScheme())) { final List<String> segments = uri.getPathSegments(); if (segments != null && segments.size() == 3 && AppConfig.PATH_SEARCH.equals(segments.get(0))) { arrival = segments.get(1); departure = segments.get(2); } } } startSearch(departure, arrival); } } startSearch(departure, arrival);
  27. None
  28. None
  29. None
  30. App Links Associate your app with your domain 2 2

  31. None
  32. None
  33. None
  34. None
  35. None
  36. None
  37. Available on Marshmallow+ (API 23) Consider app & website as

    a single entity Prevent “Open with” dialog
  38. Available on Marshmallow+ (API 23) Consider app & website as

    a single entity Prevent “Open with” dialog
  39. Available on Marshmallow+ (API 23) Consider app & website as

    a single entity Prevent “Open with” dialog
  40. <activity ...> <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="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> </activity>
  41. <activity ...> <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="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> </activity> <action android:name="android.intent.action.VIEW" />
  42. <activity ...> <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="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> </activity> <category android:name="android.intent.category.BROWSABLE" />
  43. <activity ...> <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="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> </activity> <data android:scheme="http" /> <data android:scheme="https" />
  44. <activity ...> <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="http" /> <data android:scheme="https" /> <data android:host="captaintrain.com" /> <data android:host="trainline.de" /> <data android:host="trainline.es" /> <data android:host="trainline.eu" /> <data android:host="trainline.fr" /> <data android:host="trainline.it" /> <data android:host="www.captaintrain.com" /> <data android:host="www.trainline.de" /> <data android:host="www.trainline.es" /> <data android:host="www.trainline.eu" /> <data android:host="www.trainline.fr" /> <data android:host="www.trainline.it" /> <data android:path="/search" /> <data android:pathPrefix="/search/" /> </intent-filter> </activity> android:autoVerify="true"
  45. android:autoVerify="true" Link verification is done at app install time Verified

    only if all hosts are verified
  46. https://<host>/.well-known/assetlinks.json

  47. https://<host>/.well-known/assetlinks.json Android always uses HTTPS to retrieve statements

  48. https://<host>/.well-known/assetlinks.json The host to verify links against

  49. https://<host>/.well-known/assetlinks.json A fixed “well-known” location as defined in Digital Assets

    Links specs
  50. https://<host>/.well-known/assetlinks.json Must be served as Content-Type: application/json Responses other than

    HTTP 200 are treated as errors
  51. https://captaintrain.com/.well-known/assetlinks.json https://trainline.de/.well-known/assetlinks.json https://trainline.es/.well-known/assetlinks.json https://trainline.eu/.well-known/assetlinks.json https://trainline.fr/.well-known/assetlinks.json https://trainline.it/.well-known/assetlinks.json https://www.captaintrain.com/.well-known/assetlinks.json https://www.trainline.de/.well-known/assetlinks.json https://www.trainline.es/.well-known/assetlinks.json https://www.trainline.eu/.well-known/assetlinks.json

    https://www.trainline.fr/.well-known/assetlinks.json https://www.trainline.it/.well-known/assetlinks.json
  52. Digital Asset Links JSON Declaring website & app association

  53. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ]
  54. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] "relation": [ "delegate_permission/common.handle_all_urls" ],
  55. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] "namespace": "android_app",
  56. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] "package_name": "com.capitainetrain.android",
  57. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ]
  58. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ]
  59. keytool -list -v -keystore release-key.keystore

  60. https://digitalassetlinks.googleapis.com/v1/statements:list? source.web.site=https://<host>& relation=delegate_permission/common.handle_all_urls

  61. None
  62. None
  63. None
  64. None
  65. None
  66. Related Apps Banner Promote your app from your website 3

  67. None
  68. None
  69. Available on Chrome 44+ Knows whether an app is installed

    Easy to add to your website Displayed when appropriate
  70. Available on Chrome 44+ Knows whether an app is installed

    Easy to add to your website Displayed when appropriate
  71. Available on Chrome 44+ Knows whether an app is installed

    Easy to add to your website Displayed when appropriate
  72. Available on Chrome 44+ Knows whether an app is installed

    Easy to add to your website Displayed when appropriate
  73. Your website must be served as HTTPS

  74. <html> <head> <link rel="manifest" href="manifest.json"> </head> </html>

  75. <html> <head> <link rel="manifest" href="manifest.json"> </head> </html> <link rel="manifest" href="manifest.json">

  76. {
 "display": "browser",
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",


    "type": "image/png"
 }
 ],
 "name": "Trainline EU",
 "prefer_related_applications": true,
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
 "short_name": "Trainline EU"
 }

  77. {
 "display": "browser",
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",


    "type": "image/png"
 }
 ],
 "name": "Trainline EU",
 "prefer_related_applications": true,
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
 "short_name": "Trainline EU"
 }
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",
 "type": "image/png"
 }
 ],
  78. {
 "display": "browser",
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",


    "type": "image/png"
 }
 ],
 "name": "Trainline EU",
 "prefer_related_applications": true,
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
 "short_name": "Trainline EU"
 }
 "short_name": "Trainline EU"
  79. {
 "display": "browser",
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",


    "type": "image/png"
 }
 ],
 "name": "Trainline EU",
 "prefer_related_applications": true,
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
 "short_name": "Trainline EU"
 }
 "prefer_related_applications": true,
  80. {
 "display": "browser",
 "icons": [
 {
 "src": "../favicons/android-icon-3x.png",
 "sizes": "144x144",


    "type": "image/png"
 }
 ],
 "name": "Trainline EU",
 "prefer_related_applications": true,
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
 "short_name": "Trainline EU"
 }
 "related_applications": [
 {
 "platform": "play",
 "id": "com.capitainetrain.android"
 }
 ],
  81. w3c.github.io/manifest/ Full specifications

  82. Why is nothing happening? aka “WTF moment”…

  83. The user has visited your site twice over two separate

    days during the course of two weeks
  84. The user has visited your site twice over two separate

    days during the course of two weeks There is a dev option for that…
  85. The user has visited your site twice over two separate

    days during the course of two weeks There is a dev option for everything…
  86. The user has visited your site twice over two separate

    days during the course of two weeks There is a dev option chrome://flags/#bypass-app-banner-engagement-checks for everything…
  87. None
  88. None
  89. None
  90. None
  91. Smart Lock for Passwords Retrieve credentials from the web 2

    4
  92. mCredentialsClient = new GoogleApiClient.Builder(this). addApi(Auth.CREDENTIALS_API). addConnectionCallbacks(mConnectionCallbacks). addOnConnectionFailedListener(mOnConnectionFailedListener). build();

  93. private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public

    void onConnected(Bundle connectionHint) { final CredentialRequest request = new CredentialRequest.Builder(). setPasswordLoginSupported(true). build(); Auth.CredentialsApi.request(mCredentialsClient, request). setResultCallback(mRequestCallback); } @Override public void onConnectionSuspended(int cause) { // ... } }; private ResultCallback<CredentialRequestResult> mRequestCallback = new ResultCallback<CredentialRequestResult>() { @Override public void onResult(CredentialRequestResult result) { if (result.getStatus().isSuccess()) { onCredentialRetrieved(result.getCredential()); } else { resolveResult(result.getStatus()); } } };
  94. private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public

    void onConnected(Bundle connectionHint) { final CredentialRequest request = new CredentialRequest.Builder(). setPasswordLoginSupported(true). build(); Auth.CredentialsApi.request(mCredentialsClient, request). setResultCallback(mRequestCallback); } @Override public void onConnectionSuspended(int cause) { // ... } }; private ResultCallback<CredentialRequestResult> mRequestCallback = new ResultCallback<CredentialRequestResult>() { @Override public void onResult(CredentialRequestResult result) { if (result.getStatus().isSuccess()) { onCredentialRetrieved(result.getCredential()); } else { resolveResult(result.getStatus()); } } }; public void onConnected(Bundle connectionHint) { final CredentialRequest request = new CredentialRequest.Builder(). setPasswordLoginSupported(true). build(); Auth.CredentialsApi.request(mCredentialsClient, request). setResultCallback(mRequestCallback); }
  95. private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public

    void onConnected(Bundle connectionHint) { final CredentialRequest request = new CredentialRequest.Builder(). setPasswordLoginSupported(true). build(); Auth.CredentialsApi.request(mCredentialsClient, request). setResultCallback(mRequestCallback); } @Override public void onConnectionSuspended(int cause) { // ... } }; private ResultCallback<CredentialRequestResult> mRequestCallback = new ResultCallback<CredentialRequestResult>() { @Override public void onResult(CredentialRequestResult result) { if (result.getStatus().isSuccess()) { onCredentialRetrieved(result.getCredential()); } else { resolveResult(result.getStatus()); } } }; if (result.getStatus().isSuccess()) { onCredentialRetrieved(result.getCredential()); } else { resolveResult(result.getStatus()); }
  96. private void onCredentialRetrieved(Credential credential) { Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient); signIn(credential.getId(), credential.getPassword()); } private

    void resolveResult(Status status) { if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { try { status.startResolutionForResult(SmartLockActivity.this, RC_SLP_READ); } catch (IntentSender.SendIntentException e) { // Fail silently } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RC_SLP_READ: if (resultCode == RESULT_OK) { onCredentialRetrieved(data.<Credential>getParcelableExtra(Credential.EXTRA_KEY)); } break; } }
  97. private void onCredentialRetrieved(Credential credential) { Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient); signIn(credential.getId(), credential.getPassword()); } private

    void resolveResult(Status status) { if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { try { status.startResolutionForResult(SmartLockActivity.this, RC_SLP_READ); } catch (IntentSender.SendIntentException e) { // Fail silently } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RC_SLP_READ: if (resultCode == RESULT_OK) { onCredentialRetrieved(data.<Credential>getParcelableExtra(Credential.EXTRA_KEY)); } break; } } private void onCredentialRetrieved(Credential credential) { Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient); signIn(credential.getId(), credential.getPassword()); }
  98. private void onCredentialRetrieved(Credential credential) { Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient); signIn(credential.getId(), credential.getPassword()); } private

    void resolveResult(Status status) { if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { try { status.startResolutionForResult(SmartLockActivity.this, RC_SLP_READ); } catch (IntentSender.SendIntentException e) { // Fail silently } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RC_SLP_READ: if (resultCode == RESULT_OK) { onCredentialRetrieved(data.<Credential>getParcelableExtra(Credential.EXTRA_KEY)); } break; } } private void resolveResult(Status status) { if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { try { status.startResolutionForResult(SmartLockActivity.this, RC_SLP_READ); } catch (IntentSender.SendIntentException e) { // Fail silently } } }
  99. private void onCredentialRetrieved(Credential credential) { Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient); signIn(credential.getId(), credential.getPassword()); } private

    void resolveResult(Status status) { if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) { try { status.startResolutionForResult(SmartLockActivity.this, RC_SLP_READ); } catch (IntentSender.SendIntentException e) { // Fail silently } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RC_SLP_READ: if (resultCode == RESULT_OK) { onCredentialRetrieved(data.<Credential>getParcelableExtra(Credential.EXTRA_KEY)); } break; } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case RC_SLP_READ: if (resultCode == RESULT_OK) { onCredentialRetrieved(data.<Credential>getParcelableExtra(Credential.EXTRA_KEY)); } break; } }
  100. None
  101. private void saveCredential(String email, String password) { final Credential credential

    = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.save(mCredentialsClient, credential). setResultCallback(mSavedCallback); } private final ResultCallback<Status> mSavedCallback = new ResultCallback<Status>() { @Override public void onResult(final Status status) { final Activity ziss = SmartLockActivity.this; if (status.isSuccess()) { Toast.makeText(ziss, "Your login credentials have been saved", Toast.LENGTH_LONG).show(); } else { if (status.hasResolution()) { try { status.startResolutionForResult(ziss, RC_SLP_WRITE); } catch (IntentSender.SendIntentException e) { // Fail silently } } } } };
  102. private void saveCredential(String email, String password) { final Credential credential

    = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.save(mCredentialsClient, credential). setResultCallback(mSavedCallback); } private final ResultCallback<Status> mSavedCallback = new ResultCallback<Status>() { @Override public void onResult(final Status status) { final Activity ziss = SmartLockActivity.this; if (status.isSuccess()) { Toast.makeText(ziss, "Your login credentials have been saved", Toast.LENGTH_LONG).show(); } else { if (status.hasResolution()) { try { status.startResolutionForResult(ziss, RC_SLP_WRITE); } catch (IntentSender.SendIntentException e) { // Fail silently } } } } }; private void saveCredential(String email, String password) { final Credential credential = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.save(mCredentialsClient, credential). setResultCallback(mSavedCallback); }
  103. private void saveCredential(String email, String password) { final Credential credential

    = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.save(mCredentialsClient, credential). setResultCallback(mSavedCallback); } private final ResultCallback<Status> mSavedCallback = new ResultCallback<Status>() { @Override public void onResult(final Status status) { final Activity ziss = SmartLockActivity.this; if (status.isSuccess()) { Toast.makeText(ziss, "Your login credentials have been saved", Toast.LENGTH_LONG).show(); } else { if (status.hasResolution()) { try { status.startResolutionForResult(ziss, RC_SLP_WRITE); } catch (IntentSender.SendIntentException e) { // Fail silently } } } } }; if (status.isSuccess()) { Toast.makeText(ziss, "Your login credentials have been saved", Toast.LENGTH_LONG).show(); }
  104. private void saveCredential(String email, String password) { final Credential credential

    = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.save(mCredentialsClient, credential). setResultCallback(mSavedCallback); } private final ResultCallback<Status> mSavedCallback = new ResultCallback<Status>() { @Override public void onResult(final Status status) { final Activity ziss = SmartLockActivity.this; if (status.isSuccess()) { Toast.makeText(ziss, "Your login credentials have been saved", Toast.LENGTH_LONG).show(); } else { if (status.hasResolution()) { try { status.startResolutionForResult(ziss, RC_SLP_WRITE); } catch (IntentSender.SendIntentException e) { // Fail silently } } } } }; if (status.hasResolution()) { try { status.startResolutionForResult(ziss, RC_SLP_WRITE); } catch (IntentSender.SendIntentException e) { // Fail silently } }
  105. None
  106. private void deleteCredential(String email, String password) { final Credential credential

    = new Credential.Builder(email). setPassword(password). build(); Auth.CredentialsApi.delete(mCredentialsClient, credential); }
  107. It would be so magical to get credentials already saved

    on my website. Doing so would streamline the sign-in experience.
  108. It would be so magical to get credentials already saved

    on my website. Doing so would streamline the sign-in experience. Digital Assets Links to the rescue \o/
  109. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ]
  110. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] " ", "delegate_permission/common.get_login_creds" ], "target": { "namespace": "android_app", "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ]
  111. [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app",

    "package_name": "com.capitainetrain.android", "sha256_cert_fingerprints": [ “5A:BF:2C:43:1A:2E:54:A3:60:31:58:A7:62:AA:4D:E0:AA:BE: 50:F7:00:36:3C:CB:41:CD:83:FE:F5:B9:58:2C” ] } } ] , “delegate_permission/common.get_login_creds" "delegate_permission/common.get_login_creds"
  112. None
  113. It would be so magical to get credentials already saved

    on my website. Doing so would streamline the sign-in experience.
  114. Android app It would be so magical to get credentials

    already saved on my . Doing so would streamline the sign-in experience.
  115. Android app It would be so magical to get credentials

    already saved on my . Doing so would streamline the sign-in experience. Digital Assets Links to the rescue \o/ … again
  116. <application> <meta-data android:name="asset_statements" android:resource="@string/smartlock_asset_statements" /> </application>

  117. <?xml version="1.0" encoding="utf-8"?> <resources> <string name="smartlock_asset_statements" translatable="false"> [{ \"include\": \"https://www.trainline.eu/.well-known/assetlinks.json\"

    }] </string> </resources>
  118. [ { "relation": [ "delegate_permission/common.get_login_creds" ], "target": { "namespace": "web",

    "site": "https://www.trainline.eu" } } ]
  119. None
  120. None
  121. None
  122. Miscellaneous Go even further 5

  123. App Indexing Google crawler for native apps

  124. Firebase Dynamic Links Post install deferred links

  125. Firebase Dynamic Links Post install deferred links

  126. App Preview Messaging Pushing messages & your messaging app

  127. App Preview Messaging Pushing messages & your messaging app

  128. App Preview Messaging Pushing messages & your messaging app

  129. Android Instant Apps Blow up the walls between web &

    native apps
  130. Links are not web-only Links act as a glue between

    your different product apps.
  131. Don’t be afraid or lazy Tear down the walls between

    your web site and your native mobile apps.
  132. Thank you @cyrilmottier