许可参考文档

LVL 类和接口

表 1 列出了通过 Android SDK 提供的许可验证库 (LVL) 中的所有源文件。所有文件都是 com.android.vending.licensing 软件包的一部分。

表 1. LVL 库类和接口的摘要。

类别 名称 说明
许可检查和结果 LicenseChecker 为启动许可检查而实例化(或子类化)的类。
LicenseCheckerCallback 为处理许可检查结果而实现的接口。
政策 Policy 为根据许可响应确定是否允许访问应用而实现的接口。
ServerManagedPolicy 默认 Policy 实现。使用许可服务器提供的设置管理许可数据的本地存储、许可有效性、重试。
StrictPolicy 替代 Policy 实现。仅基于来自服务器的直接许可响应强制执行许可。不使用缓存,也不重试请求。
数据混淆(可选)
Obfuscator 使用 Policy(例如 ServerManagedPolicy)在持久存储区中缓存许可响应数据时要实现的接口。采用混淆算法对写入或读取的数据进行编码和解码。
AESObfuscator 默认混淆器实现,使用 AES 加密/解密算法对数据进行混淆/反混淆。
设备限制(可选)
DeviceLimiter 如果您只想允许特定设备使用某个应用,可以实现该接口。从 LicenseValidator 调用。对于大多数应用,除非设计得非常谨慎,否则建议不要实现 DeviceLimiter,因为它需要后端服务器,并且可能导致用户无法访问所许可的应用。
NullDeviceLimiter 默认的 DeviceLimiter 实现,是一种空操作(允许访问所有设备)。
库核心,无需集成 ResponseData 包含许可响应字段的类。
LicenseValidator 该类用于解密和验证从许可服务器收到的响应。
ValidationException 该类指示在验证由混淆器管理的数据的完整性时发生错误。
PreferenceObfuscator 该实用程序类用于向系统的 SharedPreferences 存储区写入/从中读取经过混淆的数据。
ILicensingService 单向 IPC 接口,用于将许可检查请求传递到 Google Play 客户端。
ILicenseResultListener 单向 IPC 回调实现,应用通过它从许可服务器接收异步响应。

服务器响应

表 2 列出了 许可服务器。

表 2. 许可响应字段摘要 由 Google Play 服务器返回

字段 说明
responseCode 许可服务器返回的响应代码。响应代码为 服务器响应代码中列出的步骤。
signedData 保存许可服务器返回的数据的字符串串联,如下所示: responseCode|nonce|packageName|versionCode|userId|timestamp:extras
  • responseCode:许可服务器返回的响应代码。
  • nonce:请求的 Nonce 标识符。
  • packageName:要检查许可的应用包名称。
  • versionCode:要检查许可的应用的版本代码。
  • userId:每个应用的用户唯一 ID,同一用户会获得 另一个应用的不同 ID
  • timestamp:从以下时间跨度的毫秒数: 1970-01-01 00:00:00 世界协调时间 (UTC)。
  • extras:有助于管理许可的其他信息 。这些额外字段的 服务器响应 Extras
signature 使用应用专用密钥的 signedData 签名。

服务器响应代码

表 3 列出了 许可服务器。通常,应用应处理所有这些响应代码。默认情况下,LVL 中的 LicenseValidator 类可为您对这些响应代码进行所有必要的处理。

表 3. 许可响应中由 Google Play 服务器返回的响应代码摘要。

响应代码 整数值表示法 说明 已签署? 额外项 注释
LICENSED 0 已向用户授予应用许可。用户已购买应用,或者已获授权下载并安装 Alpha 版或 Beta 版应用。 VT、 GTGR 根据 Policy 限制条件允许访问。
LICENSED_OLD_KEY 2 已向用户授予应用许可,但是有使用其他密钥签名的更新应用版本可用。 VTGTGRUT 根据 Policy 限制条件,可以选择允许访问。

可能表明已安装的应用版本使用的密钥对无效或已被破解。应用可以根据需要允许访问,或告知用户有升级可用,并在升级之前限制进一步使用应用。

NOT_LICENSED 1 未向用户授予应用许可。 不允许访问。
ERROR_CONTACTING_SERVER 257 本地错误 - Google Play 应用无法连接到许可服务器,可能是由于网络可用性问题。 根据 Policy 重试限制重试许可检查。
ERROR_SERVER_FAILURE 4 服务器错误 - 服务器无法加载用于许可的应用密钥对。 根据 Policy 重试限制重试许可检查。
ERROR_INVALID_PACKAGE_NAME 258 本地错误 - 应用请求进行许可检查的软件包并未安装在设备上。 请勿重试许可检查。

通常由开发错误引起。

ERROR_NON_MATCHING_UID 259 本地错误 - 应用请求进行许可检查的软件包的 UID(软件包、用户 ID 对)与发出请求的应用的 UID 不匹配。 请勿重试许可检查。

通常由开发错误引起。

ERROR_NOT_MARKET_MANAGED 3 服务器错误 - Google Play 无法识别该应用(软件包名称)。 请勿重试许可检查。

可能表明应用并非通过 Google Play 发布,或者许可实现中存在开发错误。

注意:如设置测试环境中所述,响应代码可以由应用开发者和任何注册的测试用户通过 Google Play 管理中心手动替换。

注意:过去,您可以通过上传未发布的“草稿”版本来测试应用。但现在系统已不再支持此功能。因此,您必须将应用发布到 Alpha 或 Beta 分发渠道,才能进行测试。如需了解详情,请参阅草稿应用不再受支持

服务器响应额外项

为帮助您的应用在应用退款期间管理对应用的访问并提供其他信息,许可服务器会在许可响应中包含若干信息。具体而言,该服务为应用的许可有效期、重试宽限期、允许的最大重试次数以及其他设置提供了建议值。如果您的应用使用 APK 扩展文件,响应中还会包含文件名、文件大小和网址。服务器将设置作为键值对附加到许可响应的“额外项”字段中。

任何 Policy 实现都可以从许可响应中提取额外项设置,并根据需要使用它们。LVL 默认 Policy 实现 (ServerManagedPolicy) 可用作工作实现,并用于说明如何获取、存储和使用设置。

表 4. 许可响应中由 Google Play 服务器提供的许可管理设置摘要。

额外项说明
VT 许可有效期时间戳。指定当前(缓存的)许可响应过期并且必须在许可服务器上重新进行检查的日期/时间。请参阅下文有关许可有效期的部分。
GT 宽限期时间戳。指定政策允许访问应用的期限何时结束,即使响应状态为 RETRY

该值由服务器管理,但典型值为 5 天或更长时间。请参阅下文有关重试期限和最大重试次数的部分。

GR 最大重试次数。指定在 Policy 允许连续进行多少次 RETRY 许可检查之后,系统会拒绝用户访问应用。

该值由服务器管理,但典型值为“10”或更高。请参阅下文有关重试期限和最大重试次数的部分。

UT 更新时间戳。指定此应用的最新更新的上传和发布日期/时间。

服务器仅针对 LICENSED_OLD_KEYS 响应返回此额外项,以便让 Policy 确定在使用新许可密钥发布更新之后到系统拒绝用户访问应用之前经过了多长时间。

FILE_URL1FILE_URL2 扩展文件的网址(1 表示主文件,2 表示补丁文件)。使用此项可通过 HTTP 下载文件。
FILE_NAME1FILE_NAME2 扩展文件的名称(1 表示主文件,2 表示补丁文件)。在设备上保存文件时,必须使用此名称。
FILE_SIZE1FILE_SIZE2 文件大小,以字节为单位(1 表示主文件,2 表示补丁文件)。使用此项可帮助您下载并确保在下载之前设备的共享存储位置有足够的可用空间。

许可有效期

Google Play 许可服务器会为所有下载的应用设置许可有效期。该期限表示应用许可状态应被视为在应用中保持不变且可由许可 Policy 缓存的时间间隔。许可服务器在对所有许可检查的响应中包括有效期,并在键 VT 之下将有效期结束时间戳作为额外项附加到响应中。Policy可以提取 VT 键值,并使用该值有条件地允许访问应用而无需重新检查许可,直至有效期到期。

当必须通过许可服务器重新检查许可状态时,许可有效期会向许可 Policy 发出信号。目的不是为了暗示应用是否已获得使用许可。也就是说,当应用的许可有效期到期时,并不意味着该应用不再具有使用许可,而只是表示 Policy 必须通过服务器重新检查许可状态。因此,只要许可有效期未过期,Policy 就可以在本地缓存初始许可状态并返回缓存的许可状态,而不是向服务器发送新的许可检查。

许可服务器管理有效期,以帮助应用在 Google Play 为付费应用提供的退款期间正确执行许可。它根据应用是否已购买(如果已购买,是多久前购买的)来设置有效期。具体而言,服务器会如下设置有效期:

  • 对于付费应用,服务器设置初始许可有效期,以便许可响应在应用可退款期间保持有效。应用中的许可 Policy 可以缓存初始许可检查的结果,并且在有效期到期之前不需要重新检查许可。
  • 当应用不再可退款时,服务器会设置较长的有效期 - 通常为若干天。
  • 对于免费应用,服务器会将有效期设置为非常高的值 (long.MAX_VALUE)。这可以确保,如果 Policy 在本地缓存了有效期时间戳,则无需在将来重新检查应用的许可状态。

ServerManagedPolicy 实现使用提取的时间戳 (mValidityTimestamp) 作为主要条件来决定是否通过服务器重新检查许可状态后再允许用户访问应用。

重试期限和最大重试次数

在某些情况下,系统或网络条件会阻止应用的许可检查到达许可服务器,或阻止服务器响应到达 Google Play 客户端应用。例如,用户可能在没有可用的蜂窝网络或数据连接时(例如在飞机上),或在网络连接不稳定或手机信号较弱时启动应用。

当网络问题阻止或中断许可检查时,Google Play 客户端会通过向 PolicyprocessServerResponse() 方法返回 RETRY 响应代码来通知应用。如果系统出现问题,例如当应用无法与 Google Play 的 ILicensingService 实现绑定时,LicenseChecker 库本身会使用 RETRY 响应代码调用政策 processServerResponse() 方法。

通常,RETRY 响应代码是向应用发出的信号,表示已发生错误,许可检查无法完成。

Google Play 服务器通过设置重试“宽限期”和建议的最大重试次数,帮助应用管理发生错误情况下的许可。服务器会在所有许可检查响应中包含这些值,并将其作为额外项附加到键 GTGR 之下。

应用 Policy 可以提取 GTGR 额外项,并使用它们有条件地允许访问应用,如下所示:

  • 对于导致 RETRY 响应的许可检查,Policy 应该缓存 RETRY 响应代码并增加一次 RETRY 响应的计数。
  • Policy 应允许用户访问应用,前提是重试宽限期仍然有效或未达到最大重试次数。

ServerManagedPolicy 使用服务器提供的上述 GTGR 值。以下示例显示了 allow() 方法中重试响应的有条件处理。RETRY 响应的计数在 processServerResponse() 方法中保持不变,未显示。

Kotlin

fun allowAccess(): Boolean {
    val ts = System.currentTimeMillis()
    return when(lastResponse) {
        LICENSED -> {
            // Check if the LICENSED response occurred within the validity timeout.
            ts <= validityTimestamp  // Cached LICENSED response is still valid.
        }
        RETRY -> {
            ts < lastResponseTime + MILLIS_PER_MINUTE &&
                    // Only allow access if we are within the retry period
                    // or we haven't used up our max retries.
                    (ts <= retryUntil || retryCount <= maxRetries)
        }
        else -> false
    }
}

Java

public boolean allowAccess() {
    long ts = System.currentTimeMillis();
    if (lastResponse == LicenseResponse.LICENSED) {
        // Check if the LICENSED response occurred within the validity timeout.
        if (ts <= validityTimestamp) {
            // Cached LICENSED response is still valid.
            return true;
        }
    } else if (lastResponse == LicenseResponse.RETRY &&
                ts < lastResponseTime + MILLIS_PER_MINUTE) {
        // Only allow access if we are within the retry period
        // or we haven't used up our max retries.
        return (ts <= retryUntil || retryCount <= maxRetries);
    }
    return false;
}