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 full-size slide

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

    View full-size slide


  3. Gotta Catch ‘Em All

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide





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



    View full-size slide





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




    View full-size slide





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





    View full-size slide





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



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

    View full-size slide





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





    View full-size slide





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



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

    View full-size slide





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



    Nope…

    View full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide
























  27. android:autoVerify="true"

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. Digital Asset Links JSON
    Declaring website & app association

    View full-size slide

  36. [
    {
    "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 full-size slide

  37. [
    {
    "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 full-size slide

  38. [
    {
    "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 full-size slide

  39. [
    {
    "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 full-size slide

  40. [
    {
    "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 full-size slide

  41. [
    {
    "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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  49. Your website must be served as
    HTTPS

    View full-size slide

  50. {

    "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 full-size slide

  51. {

    "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 full-size slide

  52. {

    "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 full-size slide

  53. {

    "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 full-size slide

  54. {

    "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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  58. 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 full-size slide

  59. 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 full-size slide

  60. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  76. 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 full-size slide

  77. [
    {
    "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 full-size slide

  78. [
    {
    "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 full-size slide

  79. [
    {
    "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 full-size slide

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

    View full-size slide

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

    View full-size slide

  82. 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 full-size slide


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

    View full-size slide




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


    View full-size slide

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

    View full-size slide

  86. Miscellaneous
    Go even further
    5

    View full-size slide

  87. App Indexing
    Google crawler for native apps

    View full-size slide

  88. Firebase Dynamic Links
    Post install deferred links

    View full-size slide

  89. Firebase Dynamic Links
    Post install deferred links

    View full-size slide

  90. App Preview Messaging
    Pushing messages & your messaging app

    View full-size slide

  91. App Preview Messaging
    Pushing messages & your messaging app

    View full-size slide

  92. App Preview Messaging
    Pushing messages & your messaging app

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. Thank you
    @cyrilmottier

    View full-size slide