Android10.0和11.0唯一识别标识设计

背景介绍

应用开发中不管是埋点统计还是推送通知,都会用到唯一识别标识,在Android中设备唯一码有很多,如:MAC地址、IMEI号(DeviceId)、IMSI号、ANDROID_ID、序列号(SerialNumber)等,但并不是所有设备上都能稳定获取到这些值。

在10.0以前这些值还能获取到,Mac地址6.0之后通过api是获取不到的,通过扫描硬件端口还能获取,但是10.0之后,这些唯一识别标识都被Android官方禁用了,Mac地址会返回一个虚假的值。

谷歌在禁用上文所述的唯一识别码后,还给了一个唯一识别码的最佳做法,原文链接:https://developer.android.com/training/articles/user-data-ids

使用 Android 标识符的最佳做法

在使用 Android 标识符时,请遵循以下最佳做法:

  1. 避免使用硬件标识符。在大多数用例中,您可以避免使用硬件标识符,例如 SSAID (Android ID),而不会限制所需的功能。

    Android 10(API 级别 29)对不可重置的标识符(包括 IMEI 和序列号)添加了限制。您的应用必须是设备或个人资料所有者应用,具有特殊运营商权限或具有 READ_PRIVILEGED_PHONE_STATE 特许权限,才能访问这些标识符。

  2. 只针对用户剖析或广告用例使用广告 ID。在使用广告 ID 时,请始终遵循用户关于广告跟踪的选择。此外,请确保标识符无法关联到个人身份信息 (PII),并避免桥接广告 ID 重置。

  3. 尽一切可能针对防欺诈支付和电话以外的所有其他用例使用实例 ID 或私密存储的 GUID。对于绝大多数非广告用例,使用实例 ID 或 GUID 应已足够。

  4. 使用适合您的用例的 API 以尽量降低隐私权风险。使用 DRM API 保护重要内容,并使用 SafetyNet API 防止滥用行为。SafetyNet API 是能够确定设备真伪而不会招致隐私权风险的最简单方法。

原文中推荐使用

  • 使用广告 ID

  • 使用实例 ID 和 GUID

广告ID暂不考虑,这里其实主要是使用GUID,什么是GUID呢?

其实GUID就是JDK中的UUID,使用代码如下:

String uniqueID = UUID.randomUUID().toString();

唯一识别标识存储设计

既然官方推荐使用JDK的UUID来做唯一识别码,剩下的工作就是如何存储这个UUID了。我的设计是使用SharedPreferences结合本地外部存储文件存储UUID

SharedPreferences的作用是当应用没有被卸载的时候,如果外部存储中的文件被删除了,启动应用的时候会创建新文件,并将SharedPreferences中的UUID写入文件;

外部存储的UUID文件首先设置成隐藏文件,存储在外部存储中,应用卸载后不会被删除,作用是当应用卸载后再次安装的时候,会去找这个隐藏文件,如果存在这个文件,就把UUID读出来,然后赋值给SharedPreferences

最后还要判断文件中的UUID是否被篡改了,若被篡改了就以SharedPreferences为主,并将SharedPreferences的UUID同步更新到文件中。

本来还想使用联系人的系统数据库进行存储,能够兼容7.0一下的设备,这一块如果需要可以自己实现。从SharedPreferences、本地文件、系统数据库三方面考虑,保证UUID不被破坏。

源码如下:

public class GuidUtil {

    private static final String GUID_KEY = "guid";

    private static final String uuidFileName = ".app-guid";
    private static final String filePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + uuidFileName;

    public static String createGUID(Context context) throws Exception {
        // 读取顺序,sp file
        String guid = getFromSP(context);
        if (guid != null) {
            return guid;
        }
        guid = getFromFile();
        if (guid != null) {
            // 如果能够从文件中获取,表示应用卸载了,需要重新给SP赋值
            setToSP(context, guid);
            return guid;
        }
        // 前面三个都没有数据,表示第一次安装,需要生成一个UUID
        guid = UUID.randomUUID().toString().replace("-", "");
        // 将UUID保存到SP、外部存储目录
        setToSP(context, guid);
        setToFile(guid);
        return guid;
    }

    /**
     * 从SharedPreferences中读取UUID
     */
    private static String getFromSP(Context context) throws Exception {
        String guid = SPUtil.getString(context, GUID_KEY, "");
        if (!TextUtils.isEmpty(guid)) {
            // sp中有值,更新一下系统数据库和外置存储文件
            updateFile(guid);
            return guid;
        }
        return null;
    }


    /**
     * 从文件读取UUID
     */
    private static String getFromFile() throws Exception {
        File file = new File(filePath);
        if (!file.exists()) {
            return null;
        }
        BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
        // 读取一行即可
        return bufferedReader.readLine();
    }


    /**
     * 检测外部存储目录是否有该UUID,没有则更新,没有表示文件被用户清除了
     */
    private static void updateFile(String guid) throws Exception {
        File file = new File(filePath);
        // 如果文件不存在,则表示被清理了,
        if (!file.exists()) {
            file.createNewFile();
            writeData(guid);
        } else {
            // 如果文件存在,则需要对比一下sp中和文件中的是否一致
            String fileUUID = getFromFile();
            // 如果不相等,则表示被篡改了,需要更新
            if (!guid.equals(fileUUID)) {
                setToFile(guid);
            }
        }
    }

    /**
     * 保存到sp
     */
    private static void setToSP(Context context, String guid) {
        SPUtil.putString(context, GUID_KEY, guid);
    }

    /**
     * 保存到文件
     */
    private static void setToFile(String guid) throws Exception {
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        writeData(guid);
    }

    /**
     * 将UUID写入文件
     */
    private static void writeData(String guid) throws Exception {
        FileOutputStream fos = new FileOutputStream(filePath);
        fos.write(guid.getBytes());
        fos.close();
    }

}

有个SPUtil就不贴代码了,可以到源码里去找。

源码地址:https://github.com/RenZhongrui/android-learn/tree/master/026-android-uuid

设计优缺点

  • 谷歌推荐使用,10.0以后只能这么设计了,10.0之前的可以使用Mac地址、AndroidId等;

  • 如果设备恢复出厂设置了,那么应用和文件都会被清理,不过一般也没人会恢复出厂设置;

  • UUID也可以做一下加解密,可以更安全一些;

  • 酷狗音乐也是使用的这种方式,有图有真相;

在这里插入图片描述

兼容Android11.0(重要补充)

上面的方案支持10.0和10.0以下是没有问题的,10.0的设备需要添加过渡方案(targetSdkVersion= 29),在AndroidManifest.xml添加android:requestLegacyExternalStorage="true",如下所示:

<application
    android:requestLegacyExternalStorage="true"
    android:usesCleartextTraffic="true">
</application>

但是过渡方案毕竟是过渡,到了Android11.0,如果targetSdkVersion变更为30,谷歌就会强制执行分区存储,什么是分区存储?

官网介绍:https://developer.android.com/about/versions/11/privacy/storage?hl=zh-cn

分区存储主要体现在以下几点:

  • Android Q文件存储机制修改成了沙盒模式;
  • APP只能访问自己目录下的文件(可以使用File Api操作)和公共媒体文件(使用MediaStore操作);
  • 对于AndroidQ以下,还是使用老的文件存储方式(可以使用File Api操作);

所以到了Android11,过渡方案将不可行,GUID存储文件必须存储到应用自己的沙盒中或者公共媒体文件中,其他目录将无权限访问。

为了一劳永逸,干脆撇去过渡方案,直接兼容Android11,修改了一下方案:

  • GUID存储到SharedPreferences中;
  • 10.0以下,GUID存储到外部存储隐藏文件中;
  • 10.0和11.0,GUID存储到媒体图片中,也就是Pictures中,当然文件会以图片的形式存储到该目录下。在这里插入图片描述

代码这里就别贴了,源码地址:https://github.com/RenZhongrui/android-learn/tree/master/026-android-uuid

了解MediaStore

媒体操作示例:https://developer.android.com/training/data-storage/shared/media?hl=zh-cn

公有目录下的文件不会跟随APP卸载而删除,媒体API操作:

在这里插入图片描述

这里主要说一下10.0和11.0使用MediaStore遇到的一些坑:

1、DownloadsPictures权限问题

一开始想使用Downloads来创建文本存储,但是Downloads有个权限问题,Downloads无法读取和修改别的应用创建的非媒体文件,

那自己创建的文件应该可以修改吧,然后把应用卸载之后,再安装,会发现使用query查询出来的Uri为null,最后导致的是每次卸载应用再启动,都会创建一个新文件,如下图所示:
在这里插入图片描述

后来试了一下Pictures媒体,是可以读取别的应用创建的文件。

2、Pictures删除文件

首先,Pictures媒体,是可以读取别的应用创建的文件,但是不能删除别的应用创建的文件,这里别的应用包括同一个应用卸载后安装后会变成新的应用的情况。

然后,存到Pictures媒体中,文件格式只能是图片格式,如果存储文本格式会报错,另外如果文件名称没有添加后缀的话,系统会默认添加.jpg的后缀,所以最好自己添加一个后缀。

最后,如果手动去Pictures目录中删除文件,再次去创建文件的时候会创建不成功;存储到Pictures媒体中,会以图片的形式显示到相册中,如果从相册中删除图片,则再次创建的时候不会出现问题。

3、应用只能删除自己创建的文件,没有权限删除别的应用创建的文件,包括卸载又安装,卸载前和重新安装后的应用不是一个应用。

4、非自己创建的文件,查询时添加参数无效,返回Cursor为null

ContentResolver resolver = context.getContentResolver();
// 设置入参
String[] projection = new String[]{
    MediaStore.Images.Media._ID
};
// where 占位符
String selection = MediaStore.Images.Media.DISPLAY_NAME + "=?";
// 匹配占位符参数
String[] args = new String[]{
    GUID_FILE_NAME
};
// 查询所有数据
Cursor query = resolver.query(external, projection, selection, args, null);

5、媒体中不允许创建隐藏文件,也就是只有后缀没有名称的文件,如.guid,如果要创建的文件没有名称,系统会自动以下划线为名称,如:_.guid

查询文献:

AndroidQ(10)分区存储完美适配方法:https://www.yht7.com/news/13427

这篇文章所说的方法不可靠:https://www.jianshu.com/p/477c7b5d58e3?utm_campaign=hugo

郭霖的文章:https://blog.csdn.net/guolin_blog/article/details/105419420

Android 媒体操作Demo:https://developer.android.com/training/data-storage/shared/media?hl=zh-cn

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
### 回答1: android 10.0系统应用默认授权是指在android 10.0系统中,部分应用在安装后默认会被授予一些权限,而不需要用户在应用启动后再去手动开启这些权限,这样可以提高用户的使用体验。 android 10.0系统应用默认授权的目的是为了加强应用的安全性,提高用户的隐私保护。应用需要访问某些敏感数据或功能时,用户在使用前需要手动开启权限,这样可以减少恶意应用通过获取用户授权的方式获取用户的私人信息。 在android 10.0系统中,应用默认被授权的权限包括日历、相机、联系人、位置、麦克风、电话、短信、存储空间等。这些权限是应用正常运行所必需的,用户可以在应用管理器中查看和管理应用授权的权限。 尽管android 10.0系统应用默认授权提高了用户的使用体验,但也存在一些风险。如果用户使用的是恶意应用,这些应用也可以默认获取一些敏感权限,从而获取用户的私人信息。因此,用户需要保持对应用的警惕,并仔细审核应用的权限请求。 ### 回答2: 在Android 10.0系统中,应用默认授权是一种新的权限模型。它改变了以往Android系统中权限管理的方式,使用户对应用程序的权限管理更加方便和安全。 传统的权限管理模型通常是一次性处理权限请求,无法区分应用程序对某一项权限的使用情况。这种方式缺少细节和灵活性,可能导致应用程序通过某些权限去访问用户的隐私信息。而Android 10.0系统应用默认授权则可以保护用户的隐私信息和数据安全。 应用默认授权模型允许应用程序在不请求用户手动授权的情况下,自动获得某些权限,并在必要的时候再向用户请求授权。这种方式可以减少用户被安装并包含恶意代码的应用程序所利用的风险。 具体地说,当应用程序请求任何运行时权限时,Android 10.0系统将无法立即授予权限。相反,它将显示一个对话框,询问用户是否要授予权限。只有在用户同意授予权限时,应用程序才能获得此权限。 此外,应用默认授权模型还能够自动限制应用程序对某些权限的访问,比如位置信息和网络数据,除非用户主动授予权限或应用程序已经获得了相应的批准。 总之,Android 10.0系统的应用默认授权能够保护用户隐私,增加应用程序的灵活性,并减少用户遭受来自恶意应用程序的风险。 ### 回答3: Android 10.0系统的应用默认授权,是指所有应用程序在安装时系统会默认授予其部分权限,而不是像以前版本的系统一样需要用户在应用使用时手动授权。 Android 10.0系统应用默认授权的目的,是为了提高应用程序的用户便利性和操作流畅性,同时还可以降低用户在使用应用程序时需要授权的次数。但是这也有可能导致一些应用程序获取用户隐私的风险。 在Android 10.0系统中,应用程序默认被授权的权限包括:网络访问、用户日历、联系人和传感器等。对于其他一些权限,如摄像头、麦克风、存储空间和定位等信息,用户在应用程序使用时需要手动授权才能访问。 因此,用户在使用Android 10.0系统时需要非常谨慎地选择哪些应用程序可以获得自己的隐私权限。建议用户在安装应用程序前,仔细检查其权限请求,尽可能减少不必要的隐私授权操作。 同时,为了更好地保护用户的隐私,Android 10.0系统也提供了更加严格的权限管理功能。用户可以通过设置菜单中的 “应用程序和通知”-“应用程序权限” 来查看和修改应用程序的权限,在此基础上更好地控制应用程序的使用和权限访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ruiurrui

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值