Remediation

This page describes how to handle issues with integrity verdicts.

When an integrity token is requested, you have the option to display a Google Play dialog to the user. You may display the dialog when there is one or more issues with the integrity verdict. The dialog is displayed on top of your app, and prompts users to resolve the cause of the issue. Once the dialog is closed, you can verify that the issue is fixed with another request to the Integrity API.

Integrity dialogs

GET_LICENSED (Type Code 1)

Verdict issue

When appLicensingVerdict == "UNLICENSED". This means the user account is unlicensed. In other words, they didn't install or buy the app from Google Play.

Remediation

You can show the GET_LICENSED dialog to prompt the user to get your app from Google Play. If the user accepts, the user account becomes licensed (appLicensingVerdict == "LICENSED"). The app is added to the user's Google Play library and Google Play can deliver app updates on your behalf.

Example UX

GET_LICENSED Play dialog

CLOSE_UNKNOWN_ACCESS_RISK (Type Code 2)

Verdict issue

When environmentDetails.appAccessRiskVerdict.appsDetected contains "UNKNOWN_CAPTURING" or "UNKNOWN_CONTROLLING", it means there are unknown apps running on the device that could be capturing the screen or controlling the device.

Remediation

You can show the CLOSE_UNKNOWN_ACCESS_RISK dialog to prompt the user to close all unknown apps which could be capturing the screen or controlling the device. If the user taps the Close all button, all such apps are closed.

Example UX

Dialog for close unknown access risk

CLOSE_ALL_ACCESS_RISK (Type Code 3)

Verdict issue

When environmentDetails.appAccessRiskVerdict.appsDetected contains any of "KNOWN_CAPTURING", "KNOWN_CONTROLLING","UNKNOWN_CAPTURING" or "UNKNOWN_CONTROLLING", it means there are apps running on the device that could be capturing the screen or controlling the device.

Remediation

You can show the CLOSE_ALL_ACCESS_RISK dialog to prompt the user to close all the apps which could be capturing the screen or controlling the device. If the user taps the Close all button, all such apps are closed on the device.

Example UX

Dialog for close all access risk

Request an integrity dialog

When the client requests an integrity token, you can use the method offered in the StandardIntegrityToken (Standard API) and IntegrityTokenResponse (Classic API): showDialog(Activity activity, int integrityDialogTypeCode).

The following steps outline how you can use the Play Integrity API to show the GET_LICENSED dialog:

  1. Request an integrity token from your app, and send the token to your server. You can use the Standard or Classic request.

    Kotlin

    // Request an integrity token
    val tokenResponse: StandardIntegrityToken = requestIntegrityToken()
    // Send token to app server and get response on what to do next
    val yourServerResponse: YourServerResponse = sendToServer(tokenResponse.token())  

    Java

    // Request an integrity token
    StandardIntegrityToken tokenResponse = requestIntegrityToken();
    // Send token to app server and get response on what to do next
    YourServerResponse yourServerResponse = sendToServer(tokenResponse.token());  

    Unity

    // Request an integrity token
    StandardIntegrityToken tokenResponse = RequestIntegrityToken();
    // Send token to app server and get response on what to do next
    YourServerResponse yourServerResponse = sendToServer(tokenResponse.Token); 

    Native

    /// Request an integrity token
    StandardIntegrityToken* response = requestIntegrityToken();
    /// Send token to app server and get response on what to do next
    YourServerResponse yourServerResponse = sendToServer(StandardIntegrityToken_getToken(response));
    
  2. On your server, decrypt the integrity token and check the appLicensingVerdict field. It could look something like this:

    // Licensing issue
    {
      ...
      accountDetails: {
          appLicensingVerdict: "UNLICENSED"
      }
    }
    
  3. If the token contains appLicensingVerdict: "UNLICENSED", reply to your app client, requesting it show the licensing dialog:

    Kotlin

    private fun getDialogTypeCode(integrityToken: String): Int{
      // Get licensing verdict from decrypted and verified integritytoken
      val licensingVerdict: String = getLicensingVerdictFromDecryptedToken(integrityToken)
    
      return if (licensingVerdict == "UNLICENSED") {
              1 // GET_LICENSED
          } else 0
    }
    

    Java

    private int getDialogTypeCode(String integrityToken) {
      // Get licensing verdict from decrypted and verified integrityToken
      String licensingVerdict = getLicensingVerdictFromDecryptedToken(integrityToken);
    
      if (licensingVerdict.equals("UNLICENSED")) {
        return 1; // GET_LICENSED
      }
      return 0;
    }
    

    Unity

    private int GetDialogTypeCode(string IntegrityToken) {
      // Get licensing verdict from decrypted and verified integrityToken
      string licensingVerdict = GetLicensingVerdictFromDecryptedToken(IntegrityToken);
    
      if (licensingVerdict == "UNLICENSED") {
        return 1; // GET_LICENSED
      }
      return 0;
    } 

    Native

    private int getDialogTypeCode(string integrity_token) {
      /// Get licensing verdict from decrypted and verified integrityToken
      string licensing_verdict = getLicensingVerdictFromDecryptedToken(integrity_token);
    
      if (licensing_verdict == "UNLICENSED") {
        return 1; // GET_LICENSED
      }
      return 0;
    }
    
  4. On your app, call showDialog with the requested code retrieved from your server:

    Kotlin

    // Show dialog as indicated by the server
    val showDialogType: Int? = yourServerResponse.integrityDialogTypeCode()
    if (showDialogType != null) {
      // Call showDialog with type code, the dialog will be shown on top of the
      // provided activity and complete when the dialog is closed.
      val integrityDialogResponseCode: Task<Int> =
      tokenResponse.showDialog(activity, showDialogType)
      // Handle response code, call the Integrity API again to confirm that
      // verdicts have been resolved.
    } 

    Java

    // Show dialog as indicated by the server
    @Nullable Integer showDialogType = yourServerResponse.integrityDialogTypeCode();
    if (showDialogType != null) {
      // Call showDialog with type code, the dialog will be shown on top of the
      // provided activity and complete when the dialog is closed.
      Task<Integer> integrityDialogResponseCode =
          tokenResponse.showDialog(activity, showDialogType);
      // Handle response code, call the Integrity API again to confirm that
      // verdicts have been resolved.
    }

    Unity

    IEnumerator ShowDialogCoroutine() {
      int showDialogType = yourServerResponse.IntegrityDialogTypeCode();
    
      // Call showDialog with type code, the dialog will be shown on top of the
      // provided activity and complete when the dialog is closed.
      var showDialogTask = tokenResponse.ShowDialog(showDialogType);
    
      // Wait for PlayAsyncOperation to complete.
      yield return showDialogTask;
    
      // Handle response code, call the Integrity API again to confirm that
      // verdicts have been resolved.
    } 

    Native

    // Show dialog as indicated by the server
    int show_dialog_type = yourServerResponse.integrityDialogTypeCode();
    if (show_dialog_type != 0) {
      /// Call showDialog with type code, the dialog will be shown on top of the
      /// provided activity and complete when the dialog is closed.
      StandardIntegrityErrorCode error_code =
          IntegrityTokenResponse_showDialog(response, activity, show_dialog_type);
    
      /// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
      if (error_code != STANDARD_INTEGRITY_NO_ERROR)
      {
          /// Remember to call the *_destroy() functions.
          return;
      }
    
      /// Use polling to wait for the async operation to complete.
      /// Note, the polling shouldn't block the thread where the IntegrityManager
      /// is running.
    
      IntegrityDialogResponseCode* response_code;
      error_code = StandardIntegrityToken_getDialogResponseCode(response, response_code);
    
      if (error_code != STANDARD_INTEGRITY_NO_ERROR)
      {
          /// Remember to call the *_destroy() functions.
          return;
      }
    
      /// Handle response code, call the Integrity API again to confirm that
      /// verdicts have been resolved.
    }
    
  5. The dialog is displayed on top of the provided activity. When the user has closed the dialog, the Task completes with a response code.

  6. (Optional) Request another token to display any further dialogs. If you make standard requests, you need to warm up the token provider again to obtain a fresh verdict.