Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Cyril Mottier

November 10, 2016
Tweet

More Decks by Cyril Mottier

Other Decks in Programming

Transcript

  1. Seamless
    linking to your app
    @cyrilmottier

    View Slide

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

    View Slide


  3. View Slide


  4. Gotta Catch ‘Em All

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. https://www.trainline.eu/search/nantes/paris

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide





  12. android:scheme="https"
    android:host="www.trainline.eu" />



    View Slide





  13. android:scheme="https"
    android:host="www.trainline.eu" />




    View Slide





  14. android:scheme="https"
    android:host="www.trainline.eu" />





    View Slide





  15. android:scheme="https"
    android:host="www.trainline.eu" />



    android:scheme="https"
    android:host="www.trainline.eu" />

    View Slide





  16. android:scheme="https"
    android:host="www.trainline.eu" />





    View Slide





  17. android:scheme="https"
    android:host="www.trainline.eu" />



    Are you f***ing serious?
    The real world is that simple?

    View Slide





  18. android:scheme="https"
    android:host="www.trainline.eu" />



    Nope…

    View Slide






















  19. View Slide




























  20. View Slide






























  21. View Slide

  22. public final class AppConfig {
    private AppConfig() {
    }
    public static final List SCHEMES = Collections.unmodifiableList(Arrays.asList(
    "http",
    "https"));
    public static final List 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";
    }

    View Slide

  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 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);
    }
    }

    View Slide

  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 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())) {

    View Slide

  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 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 segments = uri.getPathSegments();
    if (segments != null &&
    segments.size() == 3 &&
    AppConfig.PATH_SEARCH.equals(segments.get(0))) {
    arrival = segments.get(1);
    departure = segments.get(2);
    }

    View Slide

  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 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);

    View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. App Links
    Associate your app with your domain
    2
    2

    View Slide

  31. View Slide

  32. View Slide

  33. View Slide

  34. View Slide

  35. View Slide

  36. View Slide

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

    View Slide

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

    View Slide

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

    View Slide
























  40. View Slide

























  41. View Slide

























  42. View Slide


























  43. View Slide
























  44. android:autoVerify="true"

    View Slide

  45. android:autoVerify="true"
    Link verification is done at app install time
    Verified only if all hosts are verified

    View Slide

  46. https:///.well-known/assetlinks.json

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. https:///.well-known/assetlinks.json
    Must be served as Content-Type: application/json
    Responses other than HTTP 200 are treated as errors

    View Slide

  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

    View Slide

  52. Digital Asset Links JSON
    Declaring website & app association

    View Slide

  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”
    ]
    }
    }
    ]

    View Slide

  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"
    ],

    View Slide

  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",

    View Slide

  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",

    View Slide

  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”
    ]

    View Slide

  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”
    ]
    }
    }
    ]

    View Slide

  59. keytool -list -v -keystore release-key.keystore

    View Slide

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

    View Slide

  61. View Slide

  62. View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. Related Apps Banner
    Promote your app from your website
    3

    View Slide

  67. View Slide

  68. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. Your website must be served as
    HTTPS

    View Slide






  74. View Slide







  75. View Slide

  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"

    }


    View Slide

  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"

    }

    ],

    View Slide

  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"

    View Slide

  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,

    View Slide

  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"

    }

    ],

    View Slide

  81. w3c.github.io/manifest/
    Full specifications

    View Slide

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

    View Slide

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

    View Slide

  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…

    View Slide

  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…

    View Slide

  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…

    View Slide

  87. View Slide

  88. View Slide

  89. View Slide

  90. View Slide

  91. Smart Lock for Passwords
    Retrieve credentials from the web
    2
    4

    View Slide

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

    View Slide

  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 mRequestCallback =
    new ResultCallback() {
    @Override
    public void onResult(CredentialRequestResult result) {
    if (result.getStatus().isSuccess()) {
    onCredentialRetrieved(result.getCredential());
    } else {
    resolveResult(result.getStatus());
    }
    }
    };

    View Slide

  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 mRequestCallback =
    new ResultCallback() {
    @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);
    }

    View Slide

  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 mRequestCallback =
    new ResultCallback() {
    @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());
    }

    View Slide

  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.getParcelableExtra(Credential.EXTRA_KEY));
    }
    break;
    }
    }

    View Slide

  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.getParcelableExtra(Credential.EXTRA_KEY));
    }
    break;
    }
    }
    private void onCredentialRetrieved(Credential credential) {
    Auth.CredentialsApi.disableAutoSignIn(mCredentialsClient);
    signIn(credential.getId(), credential.getPassword());
    }

    View Slide

  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.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
    }
    }
    }

    View Slide

  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.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.getParcelableExtra(Credential.EXTRA_KEY));
    }
    break;
    }
    }

    View Slide

  100. View Slide

  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 mSavedCallback =
    new ResultCallback() {
    @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
    }
    }
    }
    }
    };

    View Slide

  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 mSavedCallback =
    new ResultCallback() {
    @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);
    }

    View Slide

  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 mSavedCallback =
    new ResultCallback() {
    @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();
    }

    View Slide

  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 mSavedCallback =
    new ResultCallback() {
    @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
    }
    }

    View Slide

  105. View Slide

  106. private void deleteCredential(String email, String password) {
    final Credential credential = new Credential.Builder(email).
    setPassword(password).
    build();
    Auth.CredentialsApi.delete(mCredentialsClient, credential);
    }

    View Slide

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

    View Slide

  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/

    View Slide

  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”
    ]
    }
    }
    ]

    View Slide

  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”
    ]
    }
    }
    ]

    View Slide

  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"

    View Slide

  112. View Slide

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

    View Slide

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

    View Slide

  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

    View Slide


  116. android:name="asset_statements"
    android:resource="@string/smartlock_asset_statements" />

    View Slide




  117. [{
    \"include\": \"https://www.trainline.eu/.well-known/assetlinks.json\"
    }]


    View Slide

  118. [
    {
    "relation": [
    "delegate_permission/common.get_login_creds"
    ],
    "target": {
    "namespace": "web",
    "site": "https://www.trainline.eu"
    }
    }
    ]

    View Slide

  119. View Slide

  120. View Slide

  121. View Slide

  122. Miscellaneous
    Go even further
    5

    View Slide

  123. App Indexing
    Google crawler for native apps

    View Slide

  124. Firebase Dynamic Links
    Post install deferred links

    View Slide

  125. Firebase Dynamic Links
    Post install deferred links

    View Slide

  126. App Preview Messaging
    Pushing messages & your messaging app

    View Slide

  127. App Preview Messaging
    Pushing messages & your messaging app

    View Slide

  128. App Preview Messaging
    Pushing messages & your messaging app

    View Slide

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

    View Slide

  130. Links are not web-only
    Links act as a glue between your different
    product apps.

    View Slide

  131. Don’t be afraid or lazy
    Tear down the walls between your web site
    and your native mobile apps.

    View Slide

  132. Thank you
    @cyrilmottier

    View Slide