精华内容
下载资源
问答
  • Android 设备标识获取方案
    千次阅读
    2022-03-30 22:22:31

    在APP开发过程中,不可避免的将遇到如何唯一标识一个设备或者用户的问题。此前一直使用TelephonyManager#getDeivceId接口获取,后来又给禁止了,为了解决这个问题,特意查阅了一些资料,想找一个替代方案。

    IMEI

    IMEI(International Mobile Equipment Identity number),国际移动设备识别码,与卡槽绑定,双卡设备会有两个,Android 10.0以后Google禁止APP获取该属性。

    MEID

    MEID(Mobile Equipment Identifier),移动设备识别码,是CDMA手机的身份识别码,也是每台CDMA手机或通讯平板唯一的识别码。通过这个识别码,网络端可以对该手机进行跟踪和监管,Android 10.0以后Google禁止APP获取该属性。

    MAC

    MAC(Media Access Control Address),媒体存取控制位址,用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的MAC地址。Android6.0以后在运营商网络下无法直接获取mac,需要从Wi-Fi这边过一下WifiManager#getConnectionInfo

    AndroidId

    AndroidId是设备的系统首次启动时随机生成的一串字符,由16个16进制数(64位)组成,AndroidId,可以直接获取无需权限,但存在刷机和恢复出厂设置后变更的问题,此外Android 8.0以后AndroidId生成规则发生变化,8.0以后会根据应用签名、用户和设备三者三折的组合来决定唯一性,即同一个设备,两个应用获取的AndroidId并不一样。

    UUID

    UUID(Universally Unique Identifier),通用唯一识别码,由一组32位数的16进制数字所构成。️有5个版本,很难统一标准。这个当年在做5G需求时碰到过,连权威协议部门都没定下来的东西,可以先不碰了。

    综上所述,可以看出IMEI、MEID两个不可能使用了,MAC只能在Wi-Fi环境下使用,使用环境覆盖面不足,UUID在不同的设备和不同的库中可能不一样。比较而言,AndroidId更加能够满足需求,虽然存在变更的风险,但市场上8.0的产品也不是主流了,这个缺陷可以容忍。

    【参考文献】

    1. 百度百科
    2. android获取设备唯一ID方法
    3. 如何生成唯一的Android设备ID?
    更多相关内容
  • Android 设备唯一标识(适配Android版本)

    千次阅读 多人点赞 2020-12-12 10:26:01
    设备唯一标识前言正文1. 唯一标识的含义2. 新建项目3....① 大数据统计,比如采集这个APP的安装量,那么一个唯一标识就代表一个Android设备 ② 放置多设备重复登录,比如QQ、微信,你在A手机登录了,如果又


    前言

      相信在看这篇文章之前你已经看过一些类似的文章了,那么你肯定知道自己想要的是什么。


    正文

      首先要知道设备唯一标识的重要性,它可以做什么?

    ① 大数据统计,比如采集这个APP的安装量,那么一个唯一标识就代表一个Android设备
    ② 放置多设备重复登录,比如QQ、微信,你在A手机登录了,如果又到B手机上登录,这时候A手机就会下线。
    ③ 有一些APP的资源是每天限量免费的,它不需要你登录,但是你只能看几个,而且卸载重装也是一样的,次数不会刷新,这就是因为再后台添加了你的设备唯一标识。
    ④ 网络安全,比如银行类APP,第一次登录会麻烦一些,后面就比较的容易了。

    而在实际开发中用的最多的就是防止重复登录了。

    1. 唯一标识的含义

      唯一标识简单来说就是一串符号(或者数字),映射现实中硬件设备。这些符号和设备是一一对应的,可称之为“唯一设备ID(Unique Device Identifier)”。这就是概念,也就是说你要拿到的唯一标识是独一无二的才行。

      可惜的是Android平台并没有提供稳定的API来让我们获取到唯一设备ID。你可能要说IMEI和Mac地址可以获取到,但是它并不会适配Android的所有版本。在高版本中这个已经被弃用了,比如Android9.0、Android10.0、Android11.0。虽然现在Android11.0还没有正式投产,但是已经有Beta版本可以提供给开发者进行开发了,因此我们的应用如果要适配高版本就要另谋出路。

      由于Android的碎片化很严重,而版本又很多,导致你要在获取设备唯一标识的同时还是兼容Android的各个版本,这一点就比较难受了,而我看网络上的一些文章,好像都是类似的内容,重复的排版,有的甚至是标题都不换,就跟粘贴复制的一样,故此自己写一篇,起码以后我在获取唯一标识的时候可以看看,就当是做个笔记了。

    2. 新建项目

      熟悉我写博客思路的读者会明白,通常我会重新建一个项目来演示文章的内容和细节,而不是简单的丢几行代码随便解释一下就完事,那样是不负责任的。那么下面新建一个项目,命名为OnlyPhoneID。如下图所示
    在这里插入图片描述

    3. 项目配置

      这里需要对Android的以往版本进行适配,可以选取几个有代表性的版本,那就是Android5.0、Android6.0、Android8.0、Android10.0。为了掩饰方便我会下载对应版本的模拟器来测试。

    下面先配置这个项目,在上面我说过IMEI在Android9.0时就被弃用了,说是弃用实际上是禁止第三方应用获取IMEI,这么一说,那它在Android9.0以下就是可以用的,那么在Android的1.0至8.0都是可以通过获取IMEI来作为唯一标识的。

    而IMEI要获取需要在AndroidManifest.xml中注册静态权限。下面进行添加

        <!--获取手机状态-->
        <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
        <!--获取特权手机状态  高版本编译时需要-->
        <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
            tools:ignore="ProtectedPermissions" />
    

    我习惯了图文并茂。
    在这里插入图片描述

    因为我现在的项目编译版本比较高,我当前的目标版本是Android11.0,最低适配到Android5.0。Android的高版本会自动适配低版本。
    在这里插入图片描述

    4. Android 5.0

    那么首先在Android5.0中来尝试获取IMEI。

    修改一下activity_main.xml的布局代码:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/tv_device_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Hello World!"
            android:textColor="#000"
            android:textSize="16sp" />
    
    </RelativeLayout>
    

    很简单的相对布局中放了一个用于显示设备id的文本控件。
    然后进入到MainActivity,修改代码之后如下:

    package com.llw.onlyphoneid;
    
    import androidx.appcompat.app.AppCompatActivity;
    
    import android.os.Bundle;
    import android.telecom.TelecomManager;
    import android.telephony.TelephonyManager;
    import android.widget.TextView;
    
    public class MainActivity extends AppCompatActivity {
    
        
        private TextView tvDeviceId;
        private TelephonyManager telephonyManager;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            tvDeviceId = findViewById(R.id.tv_device_id);
            
            //获取系统电话服务
            telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
            //显示设备Id
            tvDeviceId.setText(telephonyManager.getDeviceId());
    
        }
    }
    
    

    在这里插入图片描述
    看到图中画横线这个方法,你把鼠标放上去,它会说已经过时了,也就是弃用的意思,因为在build.gradle中当前的版本是Android11.0,而我之前说过,在Android9.0时就已经弃用了,使用过时的方法会很容易出问题,当然这个问题,你在可以使用的Android版本设备中运行是不会出现的。

    下面运行一下:
    在这里插入图片描述
    可以看到在Android5.0上是可以正常获取到IMEI的。

    刚才我是通过获取IMEI号,下面来试试获取序列号、设备序列号以及WIFI 模块的MAC地址。

    下面修改一下activity_main.xml。

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <!--获取IMEI-->
        <Button
            android:id="@+id/btn_get_imei"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="获取IMEI" />
    
        <!--获取序列号-->
        <Button
            android:id="@+id/btn_get_sn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/btn_get_imei"
            android:text="获取序列号" />
    
        <!--获取设备序列号-->
        <Button
            android:id="@+id/btn_get_device_sn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/btn_get_sn"
            android:text="获取设备序列号" />
    
        <!--最终获取结果显示-->
        <TextView
            android:id="@+id/tv_device_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Hello World!"
            android:textColor="#000"
            android:textSize="16sp" />
        <!--Android版本-->
        <TextView
            android:id="@+id/tv_android_version"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="20dp"
            android:textColor="#000"
            android:textSize="16sp" />
    
    </RelativeLayout>
    

    MainActivity

    package com.llw.onlyphoneid;
    
    import androidx.appcompat.app.AppCompatActivity;
    
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.net.wifi.WifiInfo;
    import android.net.wifi.WifiManager;
    import android.os.Build;
    import android.os.Bundle;
    import android.telecom.TelecomManager;
    import android.telephony.TelephonyManager;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    /**
     * @author llw
     */
    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        public static final String TAG = "MainActivity";
    
        private TextView tvDeviceId;
        private TextView tvAndroidVersion;
        private TelephonyManager telephonyManager;
    
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            initView();//初始化
        }
    
        /**
         * 初始化
         */
        private void initView() {
            tvDeviceId = findViewById(R.id.tv_device_id);
            tvAndroidVersion = findViewById(R.id.tv_android_version);
            Button btnGetIMEI = findViewById(R.id.btn_get_imei);
            Button btnGetSN = findViewById(R.id.btn_get_sn);
            Button btnGetDeviceSN = findViewById(R.id.btn_get_device_sn);
    
            btnGetIMEI.setOnClickListener(this);
            btnGetSN.setOnClickListener(this);
            btnGetDeviceSN.setOnClickListener(this);
    
            //获取系统电话服务
            telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
    
            Log.d(TAG,"Android " + android.os.Build.VERSION.RELEASE);
            tvAndroidVersion.setText("Android " + android.os.Build.VERSION.RELEASE);
        }
    
        /**
         * 页面控件点击事件
         *
         * @param v
         */
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_get_imei://获取IMEI
                    //显示设备Id
                    Log.d(TAG, "IMEI: " + telephonyManager.getDeviceId());
                    tvDeviceId.setText(telephonyManager.getDeviceId());
                    break;
                case R.id.btn_get_sn://获取序列号
                    Log.d(TAG, "序列号: " + telephonyManager.getSimSerialNumber());
                    tvDeviceId.setText(telephonyManager.getSimSerialNumber());
                    break;
                case R.id.btn_get_device_sn://获取设备序列号
                    Log.d(TAG, "设备序列号: " + Build.SERIAL);
                    tvDeviceId.setText(Build.SERIAL);
                    break;
                default:
                    break;
            }
        }
    }
    
    
    
    

    运行之后,三个按钮分别点击一下。

    在这里插入图片描述
    OK,下面在6.0中运行试一下。

    5. Android 6.0

      Android6.0推出了动态权限,规定危险权限需要动态申请,而用户需要通过才可以使用。

    下面修改一下app的build.gradle。

    android闭包下

    	compileOptions {//指定使用的JDK1.8
            sourceCompatibility = 1.8
            targetCompatibility = 1.8
        }
    

    dependencies闭包下

    	//权限
        implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
        implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
        implementation "io.reactivex.rxjava2:rxjava:2.0.0"
    

    在这里插入图片描述
    然后点击Sync同步一下。

    同步好了之后回到MainActiivty,修改一下代码。

    	/**
         * 初始化
         */
        private void initView() {
            tvDeviceId = findViewById(R.id.tv_device_id);
            tvAndroidVersion = findViewById(R.id.tv_android_version);
            Button btnGetIMEI = findViewById(R.id.btn_get_imei);
            Button btnGetSN = findViewById(R.id.btn_get_sn);
            Button btnGetDeviceSN = findViewById(R.id.btn_get_device_sn);
    
            btnGetIMEI.setOnClickListener(this);
            btnGetSN.setOnClickListener(this);
            btnGetDeviceSN.setOnClickListener(this);
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                //Android6.0以上,请求动态权限
                RxPermissions rxPermissions = new RxPermissions(this);
                rxPermissions.request(Manifest.permission.READ_PHONE_STATE)
                        .subscribe(granted -> {
                            if (granted) {
                                //获取系统电话服务
                                telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
                            } else {
                                Toast.makeText(this,"权限未通过",Toast.LENGTH_SHORT).show();
                            }
                        });
            } else {
                //获取系统电话服务
                telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
            }
            
            Log.d(TAG, "Android " + android.os.Build.VERSION.RELEASE);
            tvAndroidVersion.setText("Android " + android.os.Build.VERSION.RELEASE);
        }
    

    实际上只要修改一下initView中对于Android版本的判断即可。当用户通过权限之后你点击获取IMEI就可以获取到。否则程序ANR。
    下面运行在Android6.0的模拟器上面,
    在这里插入图片描述
    点击ALLOW,然后三个按钮都点一下:
    在这里插入图片描述

    然后你会发现一个问题,那就是Android5.0和6.0打印的内容,除了版本不一样,其他的都一样,这是为什么?这是因为虚拟机是不存在的,所以Google就给你重复的数据,你想要真正获取到不一样的标识,还是要通过真机来操作,如果你不信的话,可以用自己电脑上的虚拟机试试,说不定你得到的数据和我这里也是一模一样的。不过我已经采购了两台低版本的Android手机,分别是5.0和6.0的,到时候我还是要用真机来试试。

    下面用Android8.0来进行运行

    6. Android 8.0

    其实Android8.0的在获取唯一标识这个方面的变化不大,所以你都不需要做什么改动,你可以直接运行刚才的代码到8.0的虚拟机上面。

    在这里插入图片描述
    各个按钮都点一下,你会发现和Android5.0、6.0是一样的。
    在这里插入图片描述
    不过不用担心,这是在虚拟机上面,真机上不会这样的。

    7. Android 10.0

    在上面我就说过在Android9.0及以后版本中第三方应用是无法获取到IMEI的,那么现在你依然不用改代码,直接运行在Android10.0的虚拟机上。
    在这里插入图片描述
    你会发现系统默认的弹窗都变得好看了一些。

    然后你点击第一个按钮获取IMEI,直接闪退到桌面了。
    在这里插入图片描述
    报错的意思就是当前应用不满足访问设备标识符的要求。因为你不是系统级应用,所以你获取不到这个IMEI。那么重新运行一次,点击第二个按钮试试。你会发现依然会闪退,而且报错的内容和上面的图片一模一样。然后再运行一次,点击第三个按钮。
    在这里插入图片描述

    这个倒是没有报错了,但是是一个unknown,也就是未知,说明这三个方式在Android9.0之后全军覆没,而现在的常用手机版本都是Android9.0、10.0了。基本上都会去升级手机的版本。没有升级的,慢慢的用户也就自己淘汰了。看到这里你就会问了,那现在Android9.0之后要怎么获取设备的唯一标识呢?

    8. 解决方案

      可以通过硬件标识来制作唯一设备id。

    通过一个工具类来获取,这个工具类我也是通过视频学到的,挺牛逼的。

    新建一个DeviceIdUtil 类。

    package com.llw.onlyphoneid;
    
    import android.content.Context;
    import android.os.Build;
    import android.provider.Settings;
    import android.telephony.TelephonyManager;
    import android.util.Log;
    
    import java.security.MessageDigest;
    import java.util.Locale;
    import java.util.UUID;
    
    
    /**
     * 获取手机的唯一标识ID
     */
    public class DeviceIdUtil {
    
        public static String getDeviceId(Context context) {
    
            StringBuilder sbDeviceId = new StringBuilder();
    
            String imei = getIMEI(context);
            String androidId = getAndroidId(context);
            String serial = getSerial();
            String uuid = getDeviceUUID();
    
            //附加imei
            if (imei != null && imei.length() > 0) {
                sbDeviceId.append(imei);
                sbDeviceId.append("|");
            }
            //附加androidId
            if (androidId != null && androidId.length() > 0) {
                sbDeviceId.append(androidId);
                sbDeviceId.append("|");
            }
            //附加serial
            if (serial != null && serial.length() > 0) {
                sbDeviceId.append(serial);
                sbDeviceId.append("|");
            }
            //附加uuid
            if (uuid != null && uuid.length() > 0) {
                sbDeviceId.append(uuid);
            }
    
            if (sbDeviceId.length() > 0) {
                try {
                    byte[] hash = getHashByString(sbDeviceId.toString());
                    String sha1 = bytesToHex(hash);
                    if (sha1 != null && sha1.length() > 0) {
                        //返回最终的DeviceId
                        return sha1;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    
        /**
         * 转16进制字符串
         *
         * @param data 数据
         * @return 16进制字符串
         */
        private static String bytesToHex(byte[] data) {
            StringBuilder sb = new StringBuilder();
            String string;
            for (int i = 0; i < data.length; i++) {
                string = (Integer.toHexString(data[i] & 0xFF));
                if (string.length() == 1) {
                    sb.append("0");
                }
                sb.append(string);
            }
            return sb.toString().toUpperCase(Locale.CHINA);
        }
    
        /**
         * 取 SHA1
         *
         * @param data 数据
         * @return 对应的Hash值
         */
        private static byte[] getHashByString(String data) {
            try {
                MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
                messageDigest.reset();
                messageDigest.update(data.getBytes("UTF-8"));
                return messageDigest.digest();
            } catch (Exception e) {
                return "".getBytes();
            }
        }
    
    
        /**
         * 获取硬件的UUID
         *
         * @return
         */
        private static String getDeviceUUID() {
            String deviceId = "9527" + Build.ID +
                    Build.DEVICE +
                    Build.BOARD +
                    Build.BRAND +
                    Build.HARDWARE +
                    Build.PRODUCT +
                    Build.MODEL +
                    Build.SERIAL;
            return new UUID(deviceId.hashCode(), Build.SERIAL.hashCode()).toString().replace("-", "");
        }
    
        private static String getSerial() {
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    return Build.getSerial();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        /**
         * 获取AndroidId
         *
         * @param context 上下文
         * @return AndroidId
         */
        private static String getAndroidId(Context context) {
            try {
                String androidId = Settings.Secure.getString(context.getContentResolver(),
                        Settings.Secure.ANDROID_ID);
                return androidId;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "";
        }
    
        /**
         * 获取IMEI
         *
         * @param context 上下文
         * @return IMEI
         */
        private static String getIMEI(Context context) {
            try {
                TelephonyManager telephonyManager = (TelephonyManager)
                        context.getSystemService(Context.TELEPHONY_SERVICE);
                return telephonyManager.getDeviceId();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return "";
        }
    }
    
    

    然后回到MainActivity,在onCreate中。

     	@Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            initView();//初始化
            
            //唯一标识ID,兼容Android版本
            Toast.makeText(this, DeviceIdUtil.getDeviceId(this), Toast.LENGTH_SHORT).show();
            Log.d(TAG, "Android " + android.os.Build.VERSION.RELEASE);
            Log.d(TAG, "deviceId--> " + DeviceIdUtil.getDeviceId(this));
        }
    

    下面先运行在Android5.0上。
    在这里插入图片描述
    运行在Android6.0上
    在这里插入图片描述
    运行在Android8.0上
    在这里插入图片描述
    运行在Android10.0上
    在这里插入图片描述
    都可以,而且都不一样,当然你也可以把模拟器上的应用卸载再安装,唯一标识码也不会变化。

    而你需要的只是一个工具类而已。


    总结

    其实也没有啥好总结的,设备唯一标识码通过硬件的信息来获取,不会受到Android版本的影响,应用安装的影响,你甚至都不需要给权限。简单粗暴且有用。

    源码就是上面的那个DeviceIdUtil工具类,复制到自己的项目中直接使用即可。

    展开全文
  • 一、ID 体系:你只是一串代码 想要了解 OAID,我们首先需要明白 ID 体系:想要追踪一个用户就必须先找到用户,在这个过程中,标识符(ID)就像我们的另一张身份证...而在智能设备的 ID 体系中存在许许多多不同种类的标

    一、ID 体系:你只是一串代码

    想要了解 OAID,我们首先需要明白 ID 体系:想要追踪一个用户就必须先找到用户,在这个过程中,标识符(ID)就像我们的另一张身份证,它们就代表了数字化之后的你和我。

    不同 App 可能通过某些唯一标识符对你进行强制跟踪,广告平台则会通过这个唯一标识符对你进行用户画像描绘,进而共享给相关 App 及其后台,一旦「你」打开了其中的某个 App,那么你就会被识别到——你点了什么、看过什么、可能需要什么,它们比你自己都清楚。
    在这里插入图片描述
    而在智能设备的 ID 体系中存在许许多多不同种类的标识符,下面是一小部分 Android 设备内的 ID,它们可能会用于不同方面的跟踪或标识:

    1.IMEI

    IMEI 应该是大家最熟悉的一种 ID了,它是手机的身份证,也是运营商识别入网设备信息的代码,是一种不可重置的永久标识符,作用域为设备。

    在广告跟踪方面,由于 iOS 的权限管控,iOS 上的第三方 App 并不能通过 IMEI 跟踪用户,但目前 Android 平台中绝大部分 App(尤其是在国内)都通过 IMEI 来追踪用户,开篇所举的例子在 Android 平台上大多也通过 IMEI 跟踪来实现。

    与 IMEI 类似的还有一个叫做 IMSI 的标识符,但它主要用于 SIM 卡的身份标识,这里不做展开。

    2.Android ID(SSAID)

    顾名思义,Android ID 是 Android 设备里不依赖于硬件的一种「半永久标识符」,在系统生命周期内不会改变,但系统重置或刷机后会发生变化,其作用域为一组有关联的应用。

    Android 开发者文档和谷歌开发者中文博客对 Android 8.0 后的隐私性和 SSAID 变化做出了说明:
    在这里插入图片描述
    从图中不难看出,在 Android 8.0 以后,签名不同的 App 所获取的 Android ID(SSAID)是不一样的,但同一个开发者可以根据自己的数字签名,将所开发的不同 App 进行关联。

    3.Device ID

    在 Android 平台,Device ID 是一种统称,与硬件相关的 ID 都可以称之为 Device ID,一般是一种不可重置的永久标识符,作用域为设备。

    根据设备、厂家或者 App 调用需求的不同,读取 Device ID 时可能会返回 IMEI 或其他硬件编码,但也有可能因为设备中没有相关硬件而无法获取 Device ID 或返回无效值;与之形成对应的,iOS 设备中也有类似的永久标识符叫做 UDID,但在 iOS 6 之后,苹果已经不允许需要获取 UDID 的 App 上架 App Store 以防止这种不可重置的 ID 被用于追踪或滥用,取而代之的是 IDFA 标识符,即 iOS 设备广告标识符。

    另外还有一种叫做 openUDID 的设备唯一标识符,它在 iOS 和 Android 系统内都可以使用,但由于不是系统官方提供的 ID 体系,且依赖于第三方 App 生成,所以应用并不广泛,而随着系统迭代升级,openUDID 也逐渐被边缘化甚至被废弃。

    4.UUID、GUID

    UUID 也叫做实例 ID,这两个 ID 可以说是在计算机体系内的通用标识符(详细了解 UUID 和 GUID 可以阅读 维基百科 相关内容)。

    根据所面向对象的不同,其意义也有微小差别。如果说前面三个 ID 可以用来识别设备,那么这两个 ID 在 Android 系统中的作用主要是识别 App 进程、元素或数据。

    因为它们的作用域仅仅是单个应用内,如果用户卸载了该 App 并重新安装,那么 UUID 也会发生变化。不过 App 开发者可以通过存储 UUID 或与其他 ID、用户信息进行组合、绑定、计算等方式,实现 UUID 标识符的「准永久化」。

    根据 Android 开发者指南:

    标识运行在设备上的应用实例最简单明了的方法就是使用实例
    ID,在大多数非广告用例中,这是建议的解决方案。只有进行了针对性配置的应用实例才能访问该标识符,并且标识符重置起来(相对)容易,因为它只存在于应用的安装期。

    因此,与无法重置的设备级硬件 ID 相比,实例 ID 具有更好的隐私权属性。

    5.AAID

    AAID 与 IDFA 作用相同——IDFA 是 iOS 平台内的广告跟踪 ID,AAID 则用于 Android 平台。

    它们都是一种非永久、可重置的标识符,专门提供给 App 以进行广告行为,用户随时可以重置该类 ID,或通过系统设置关闭个性化广告跟踪。但 AAID 依托于 Google 服务框架,因此如果手机没有内置该框架、或框架不完整、或无法连接到相关服务,这些情况都有可能导致 AAID 不可用。

    除了以上这些 ID 标识符以外,某些硬件 ID(例如 MAC 地址)也可能会被用于追踪。

    二、国内 Android 的广告追踪之道

    这么多 ID 标识符,每一个都各司其职。而理论上来说,只有 AAID 和 IDFA 是真正用于广告行为的。

    但现实状况显然不是这样。

    一方面,Android 平台的不少 App 普遍存在违反 Android 开发规范、绕过 Google Play 审查,通过滥用 ID 来追踪用户,以此达到为广告流量、营销分析等商业利益服务的目的。

    另一方面,由于 AAID 依托于 Google 服务框架,但在国内使用 Google 服务并不太可行,或者大部分国行手机内置的 Google 服务不完整,App 开发者需要寻找另一个方式去标识用户。

    UUID、GUID 作用域太小,不适合广告跟踪;Android ID 可以通过某些方式被改变或因为 bug 导致不可用,第三方 App 无保证可用性;MAC 地址虽然精准,但在Android 6.0(API 23)到 Android 9(API 28)中,系统限制了第三方 API 获取MAC 地址;再加上早些时候,大部分「非玩机用户」对此类功能并没有太多概念,第三方 App 为了能以更加精准持久的方式来跟踪用户,将 IMEI 变成了用于广告跟踪的首选 ID(在 Google Play 帮助中心,获取永久标识符是一种有条件的、退而求其次的广告投放方法,所以在此之前这种方式也不算完全违规)。

    在这里插入图片描述
    这也是我们看到很多 App 提示必须获取「电话」权限才能运行的原因——因为获取 IMEI 必须获得「电话」权限,可是由此也带来了一些隐私问题:允许「电话」权限可能导致 App 读取到很多种其他信息,就像我需要你给我身份证来查询身份证号,但与此同时你的姓名、住址、生日也暴露给我了。

    我们可以在 这个网站 查询到部分 App 所需要的权限、资源以及它可能会发送的隐私信息。例如微信,在网站中收录的 6.7.3 版本中,微信获取了如下权限,但并没有检测到发送以下隐私数据。
    在这里插入图片描述
    在这里插入图片描述
    随着时代发展,用户逐渐认识到手机 App 疯狂获取权限的行为有可能会侵犯隐私,加之近几年 Android 系统的权限和隐私管理逐渐收紧,Android 10(API 29)终于对第三方 App 获取不可重置永久设备标识符(包括 IMEI)的行为做出了 限制。

    具体到用户层面,在 Android 10 之后应用即便能够获取到「电话」权限,系统返回给应用的 IMEI 信息值也为空(你可以通过 My IMEI 这款应用进行测试)。

    在这里插入图片描述

    三、OAID:Android 10 之后的替代方案

    至此,国内 App 和广告跟踪服务急需一种替代方案以避免广告流量的损失,OAID 顺势而生。
    在这里插入图片描述
    Android 开发者文档中对 Android 10 限制设备标识符读取的说明 OAID 的本质其实是一种在国行系统内使用的、应对 Android 10 限制读取 IMEI 的、「拯救」国内移动广告的广告跟踪标识符,其背后是 移动安全联盟(Mobile Security Alliance,简称 MSA)。

    该联盟由中国信息通信研究院担任理事长和秘书长单位,北京大学、vivo、360、华为担任副理事长单位,并有包括苹果、中兴、OPPO、小米等多家理事和会员单位(点击查看联盟成员详情),OAID 所属的标识符体系也是由该联盟牵头发起的(参见「移动智能终端补充设备标识体系」)。

    在这里插入图片描述
    根据联盟官网以及开发文档,这个「本土化」标识符体系除了 OAID,还包含 UDID、VAID 和 AAID 一共四种标识符。

    我知道你在想什么,不过这里的 UDID 和 AAID 与上一节所说的完全不同。你可以通过下图来了解「移动智能终端补充设备标识体系」所规定的四种标识符以及获取它们的接口开发方式。另外,你也可以在 MSA 官网或会员单位的开发者网站下载 SDK 开发说明。

    在这里插入图片描述
    在这里插入图片描述
    从这四种标识符的描述和功能我们大致可以确定,「移动智能终端补充设备标识体系」所规定的 UDID、OAID、VAID、AAID 在 Android 系统中分别对应了 Device ID(例如 IMEI,或对应了 iOS 设备的 UDID)、AAID、SSAID、UUID(或 GUID)。在理想状态下,引入 OAID 即能保证广告平台的正常运作,也能减小对用户带来的影响,因为第三方 App 无需请求权限即可使用 OAID 完成广告行为,而该过程匿名,用户也可以随时重置 OAID。

    从广告服务商 Adjust 于 2019 年12 月 20 日发布的 新闻稿 中我们也可以获知,Adjust 已经接入了 OAID 广告标识符,能够对中国大陆的广告主提供广告相关服务;国内广告数据服务商神策数据在其 帮助文档 中同样提供了 OAID 匹配指南;华为则在其 开发者平台 提供了基于 OAID 的「HUAWEI Ads OAID」广告平台接入指南。
    在这里插入图片描述
    事实上,主流手机厂商都已经在其开发者平台上提供了 Android 10 适配指引,包括 三星中国开发者网站、华为开发者联盟、OPPO 开放平台、vivo 开放平台 都已针对 Android 10 的相关变化做出了说明和解决方案建议,其中就包括 Google 官方标识符适配建议和 OAID 适配方式。
    在这里插入图片描述
    在这里插入图片描述
    另外 OPPO 和 vivo 也分别在其开放平台提供了「移动智能终端补充设备标识体系」相关文档和 SDK 下载。

    不难看出,广告平台已经开始接入 OAID 作为国内广告标识符的建议方案,主流设备厂家也已经开始指导开发者采用「移动智能终端补充设备标识体系」,并且考虑到国内主流的应用预置和分发平台(例如手机厂商内建的应用商店)与 Google Play 一样开始对上架 App 的 API 等级做出强制要求,包括 OAID 在内的标识符体系毫无疑问将成为国内第三方 App 的强制执行标准。

    四、Q&A

    1.AndroidID什么时候会改变?
    • 恢复出厂设置
    • root/恢复root
    • 三清
    • 刷机
    • 系统更新
    • 软件修改(一般是模拟器,xposed,root)
    展开全文
  • 轻松简单实现Android设备重启的两种方式

    千次阅读 多人点赞 2020-10-18 17:11:31
    前言 本文重点讲述如何实现Android设备重启,这里的Android设备可不是我们用的手机哦,毕竟现在的手机越来越高级,好像除了苹果系列的手机没有定时开关机,Android手机都是有这个功能的,有的人喜欢睡前关机,早上...

    " 须知少时凌云志,曾许人间第一流 "
    在《风犬少年的天空》中刘闻钦下线时,嘴里念叨的就是这句,我们何尝不是这样的人呢,年轻时的凌云大志,曾决心要做人间第一流的人物,干出人间第一流的成绩,但岁月蹉跎,依旧名利双无收。

    前言

    本文重点讲述如何实现Android设备重启,这里的Android设备可不是我们用的手机哦,毕竟现在的手机越来越高级,好像除了苹果系列的手机没有定时开关机,Android手机都是有这个功能的,有的人喜欢睡前关机,早上四五点就定时开机,觉得这样手机用起来才不会卡,当然这只是一小部分人的做法,好多人都是直接通宵玩手机的,玩到没电就充电睡觉去。
    好了,再说就跑题了,哈哈 ~~
    进入正题,为什么需要重启Android设备呢?因为现在好多生产安卓板的厂家用的Android系统大部分是阉割版的,自身硬件和服务需要用到什么功能就放进去,没用到的就切掉,但是有些厂家对于这一块处理得不够好,导致系统长时间运行时,会出现不稳定情况,例如Android板长时间运行一款app的,比如广告投放机、自动售卖机等,如果本身配置不够高,系统做的又不好,就会出现卡死,黑屏或者卡屏闪屏问题出现,只要硬件没坏,只需要重启下Android系统就可以解决上述问题的。

    一、利用系统广播ACTION_REBOOT

    这种方式非常简单,只需要发送一个系统广播ACTION_REBOOT,代码如下:

           mButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent reboot = new Intent(Intent.ACTION_REBOOT);
                    reboot.putExtra("nowait", 1);
                    reboot.putExtra("interval", 1);
                    reboot.putExtra("window", 0);
                    sendBroadcast(reboot);
                }
            });
    

    但是这样是不会重启的,程序还会因此闪退,直接报错:

    
        com.xz.android72test E/AndroidRuntime: FATAL EXCEPTION: main
        Process: com.xz.android72test, PID: 3180
        java.lang.SecurityException: Permission Denial: not allowed to send broadcast android.intent.action.REBOOT from pid=3180, uid=10046
            at android.os.Parcel.readException(Parcel.java:1546)
            at android.os.Parcel.readException(Parcel.java:1499)
            at android.app.ActivityManagerProxy.broadcastIntent(ActivityManagerNative.java:2831)
            at android.app.ContextImpl.sendBroadcast(ContextImpl.java:1331)
            at android.content.ContextWrapper.sendBroadcast(ContextWrapper.java:377)
            at com.xz.android72test.MainActivity$2.onClick(MainActivity.java:115)
            at android.view.View.performClick(View.java:4780)
            at android.view.View$PerformClick.run(View.java:19866)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5293)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
    
    

    最主要的错误是SecurityException: Permission Denial,权限不够,默认的SDK并没有提供应用开发者直接的Android系统关机或重启的API接口,一般来讲,实现Android系统的关机或重启,需要较高的权限(系统权限甚至Root权限),在此,我们需要将app的权限提高至系统权限,找到AndroidManifest.xml,添加下面的代码:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.xz.android72test" android:sharedUserId="android.uid.system">
         ..........
    </manifest>
    

    但是做到这里也是不会重启的,程序还会安装失败,直接报错:

    
    Installation did not succeed.
    The application could not be installed: INSTALL_FAILED_SHARED_USER_INCOMPATIBLE
    Installation failed due to: 'null'
    
    

    这是为什么呢? 因为这个apk没有这个系统的签名。如何签名呢?

    有三种方式:

    第一种,这种方式比较经典传统:

    • 联系安卓板厂家技术获取主板系统的签名文件platform.pk8platform.x509.pem,每家安卓板厂家签名文件都不一样,还需要signapk.jar这个文件,一般厂家也会将这个文件一起发送过来,网上也是可以下载的,jar包里面带有签名的程序。
    • 在windows下,启动cmd.exe,进入命令行模式,通过cd C:\Users\Administrator\Desktop\signapkApp进到带有platform.pk8platform.x509.pemsignapk.jar文件的文件夹。
    • 最后输入下面命令进行签名:
    java -jar signapk.jar  platform.x509.pem platform.pk8 C:\Users\Administrator\Desktop\signapkApp\app-release.apk signed_app-release.apk
    

    其中C:\Users\Administrator\Desktop\signapk.jar\app-release.apk为需要签名的apk路径,signed_app-release.apk是签名之后的apk,跟未签名的apk在同一个目录下。

    最后直接通过adb push或者adb install把这个signed_app-release.apk安装完即可实现重启了。

    第二种,这种方式跟第一种比较,更加简单,也可以在debug版本或者release版本进行签名:
    只需要问安卓板厂家技术要一个文件,或者自己根据主板系统的签名文件platform.pk8platform.x509.pem做一个出来,这个文件就是platform.keystore签名文件。

    然后在app\build.gradle文件下添加下面代码:

    android {
    
        ......
        
        signingConfigs {
            debug {
                storeFile file('platform.keystore文件路径')
                storePassword 'keystore的密码'
                keyAlias '密钥别名'
                keyPassword '密钥密码'
            }
            release {
                storeFile file('platform.keystore文件路径')
                storePassword 'keystore的密码'
                keyAlias '密钥别名'
                keyPassword '密钥密码'
            }
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                signingConfig signingConfigs.release
            }
            debug {
                signingConfig signingConfigs.debug
            }
        }
    }
    

    至此,Build出来的apk无论是debug还是release版本都带有系统签名的。

    第三种,这种方式基本算是白嫖了,利用厂家提供的签名网站,只需要将未签名的apk上传到他们服务器,过一会签完名之后会自动下载下来,而这个文件就是已经签完名了。
    在这里插入图片描述
    对比这三种方式,是不是最后这一种最省事呢?但是呐,人还是不可以太懒的,建议用第二种方式啦。

    二、利用Linux-shell发送reboot指令

    这种方式非常简单,利用Runtime这个Java类可以直接调用并执行shell命令,直接贴代码:

    
           mButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    exec("reboot");
                }
            });
            
    

    如果Android设备没有root过,需要在AndroidManifest.xml下添加一下权限,从而使下面代码获取管理员权限:

    <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
    

    exec方法:

        private String exec(String command) {
    
            Process process = null;
            BufferedReader reader = null;
            InputStreamReader is = null;
            DataOutputStream os = null;
    
            try {
                process = Runtime.getRuntime().exec("su");
                is = new InputStreamReader(process.getInputStream());
                reader = new BufferedReader(is);
                os = new DataOutputStream(process.getOutputStream());
                os.writeBytes(command + "\n");
                os.writeBytes("exit\n");
                os.flush();
                int read;
                char[] buffer = new char[4096];
                StringBuilder output = new StringBuilder();
                while ((read = reader.read(buffer)) > 0) {
                    output.append(buffer, 0, read);
                }
                process.waitFor();
                return output.toString();
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    if (os != null) {
                        os.close();
                    }
    
                    if (is != null) {
                        is.close();
                    }
    
                    if (reader != null) {
                        reader.close();
                    }
    
                    if (process != null) {
                        process.destroy();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    

    普通用户是没有权限执行reboot,所以使用的Android设备最好是root过的,不然会在获取su管理员权限时,出现一个弹窗询问你是否允许请求超级用户访问权限,非常影响用户体验。

    在这里插入图片描述

    非常感谢你能看到这里,如果能够帮助到你是我的荣幸!

    展开全文
  • Android设备唯一标识符(适配Android Q) 目录 Android设备唯一标识符(适配Android Q) 一、需求场景 二、Android设备信息 1、DeviceId(IMEI) 2、AndroidId 3、Serial Number 4、Wlan或者蓝牙的MAC地址...
  • 获取android 设备ID (device ID)

    热门讨论 2012-05-17 17:20:27
    运行的环境> 2.3.3 或者是android sdk 10
  • android 设备序列号Your device’s serial number is a unique code that the manufacturer gives the phone. No two serial numbers are the same. Should you need to find your phone’s, there are a few places...
  • Android 设备信息获取详解

    万次阅读 多人点赞 2020-11-16 07:59:19
    和你一起终身学习,这里是程序员Android经典好文推荐,通过阅读本文,您将收获以下知识点:一、获取手机基本信息(厂商、型号等参数)二、设备信息获取实现图三、获取手机设备 宽、高、IME...
  • 1. 将Android设备和电脑连接到同一个局域网内 Android设备连接网络后,可以从网络设置上看到ip地址(例如:172.32.1.150) 2. 创建adb的tcp监听端口 先用线连接Android设备,然后执行下列操作(两个方案选其一) ...
  • 但对于特殊设备,例如:没有安装电池的设备,在突然断电的情况下就有丢失数据的风险。 第一种改法,直接在framework层直接修改: –a frameworks/opt/...
  • android设备uuid、udid

    千次阅读 2020-08-04 16:08:12
    前言 udid: UDID是Unique Device Identifier的缩写,中文意思是设备唯一标识. uuid: UUID是Universally Unique Identifier的缩写,中文...这里面,我们主要目的是想获得一台android设备对一个的一个唯一的标识码,即udid.
  • Android 设备管理器 理解和使用

    千次阅读 2019-06-28 13:25:54
    顾名思义,DevicePolicyManager为Android系统的管理提供了一套策略,有三种方案 Device Administration, 设备管理员 Profile Owner, 配置文件所有者 Device Owner, 设备所有者 Device Admin 过用户授权自己的应用...
  • 一:文件传输在android开发中,有时候需要将文件从pc端传递至android,或者将软件运行的日志,从android设备传递到pc进行分析,我们可以使用windows的cmd窗口,或者android studio的terminal窗口来传递文件。...
  • Android设备adb调试

    千次阅读 2018-04-13 21:19:57
    引言日常开发经常要对Android设备进行调试,adb是Android自带的调试工具供我们使用。参考系统及开发软件:系统win10开发软件androidstudio1.adb的环境配置在android开发工具中会附带adb,为了能方便全局使用adb的...
  • android手机连接电脑前要打开USB调试功能,如果打开USB调试功能连接电脑后,如果检测不到设备可能是数据线的问题。 android studio安装以后,基本上都会带有adb命令,adb命令位置如下: 我安装后默认没有把adb...
  • 数十亿 Android 设备隐私全面升级!

    千次阅读 2021-09-18 12:58:26
    其宣布,Android 11 的自动重置权限功能将适用于较旧的设备。 所谓自动重置权限功能是 Google 在去年推出的一项服务,即: 在 Android 11 中,用户能够针对位置信息、麦克风和摄像头指定更精细的权限。此外,如果...
  • Adb连接android设备的2种方法!!!!

    千次阅读 2020-09-11 12:31:46
    CMD窗口输入adb devices就可以看到连接的设备了 第二种:Wifi连接 安卓机和电脑都在同一局域网内 CMD窗口输入adb connect 安卓机ip地址 如果提示:unable to connect to 安卓机ip地址:5555 解决方法: 3.1.使用usb...
  • ADB连接Android设备的三种方法

    千次阅读 2019-09-17 11:27:15
    1.连接设备:adb connect <设备IP> 2.adb install -r apk所在路径 ADB连接Android设备的三种方法 ADB连接Android设备的三种方法 连接方式有三种方法: 一、WiFi连接(手机...
  • 获取Android设备的序列号(SN号)

    万次阅读 2019-01-08 18:53:39
    方法(一)通过反射获取sn号 public static String getDeviceSN(){ String serial = null; try { Class&...android.os.SystemProperties"); Method get =c.getMethod("ge...
  • 如何获得Android设备名称

    千次阅读 2018-10-20 15:14:19
    Android中通过以下API可以获取设备的信息: String device_model = Build.MODEL; // 设备型号 。 String version_sdk = Build.VERSION.SDK; // 设备SDK版本 。 String version_release = ...
  • Android设备网络ADB的使用

    千次阅读 2020-12-03 12:44:38
    通常在进行android系统或应用开发时会用到 ADB, 这时只要使用一根USB手机充电线就可以连到android设备(如:手机)进行调试。但是如果android设备的USB口被占用时而我们又要用到ADB功能,这时我们可以用网络ADB,...
  • 解决AndroidStudio连不上Android设备真机

    千次阅读 2018-11-14 14:30:26
    解决AndroidStudio连不上Android设备真机 刚好遇到这个问题,查阅了很多资料,看到有人分享了引起该问题的几个原因,我总结了一下: 1.手机设置问题。开USB调试 方法:手机设置-开发人员调试-USB调试 - 勾选。 2.数据...
  • 最近做的一个项目中需要用到Android设备唯一码(UUID)来标识一台设备, Android中设备唯一码有很多,如:MAC地址、IMEI号(DeviceId)、IMSI号、ANDROID_ID、序列号(SerialNumber)等, 但并不是所有设备上都能...
  • 分别安装在两台android设备上。通过wifi连接到同一个局域网,可以用控制点控制服务提供器的视频播放。加了把服务提供器MainActivity.java的init(Environment.getExternalStorageDirectory() + "/testvideo.mp4");...
  • 使用adb logcat命令显示Android设备上的Log日志_wenzhi的博客-CSDN博客_adb logcat
  • 获取Android 设备信息——build.prop

    千次阅读 2018-12-04 21:55:27
    使用Android设备信息作为控制相关变量获取Android设备相关信息build.prop文件adb获取设备信息Android Jni编程C层获取设备信息 获取Android设备相关信息 在Android开发中有时需要获取设备信息,一般可以通过ADB命令来...
  • AndroidStudio连不上Android设备真机

    万次阅读 多人点赞 2017-11-20 16:37:31
    解决AndroidStudio连不上Android设备真机刚好遇到这个问题,查阅了很多资料,看到有人分享了引起该问题的几个原因,我总结了一下:1.手机设置问题。开USB调试 方法:手机设置-开发人员调试-USB调试 - 勾选。2.数据线...
  • 查看Android设备的分辨率

    千次阅读 2018-07-24 14:54:02
    adb shell wm size 其它命令 adb connect 怎么断开 adb kill-server //结束adb服务 adb start-server //启动adb服务 adb devices //获取adb设备列表
  • 因使用定制Android系统的...1.常规获取Android设备信息. 获取IMEI号 Android设备可以通过设置界面、拨号*#06# 进行查看 APP通过TelephonyManager获取对应的DeviceId.即IMEI号. 获取序列号 首先,序列号分为多...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 453,059
精华内容 181,223
关键字:

android设备

友情链接: chp02.rar