Android原生开发第一次启动隐私政策弹窗功能(附全部代码)

1,906 阅读8分钟

前言

React-Native框架开发Android App第一次启动弹出隐私政策时,Android Studio日志抓包提示有获取Android ID、手机设备等用户隐私信息,导致上架应用市场审核不通过,审核不通过原因:用户在未同意隐私政策之前App不能获取手机设备、用户相关的敏感信息。

React Native App启动页违规收集用户信息

问题

获取用户的“ANDROID ID”敏感信息行为.png

android_id检测不通过.png

应用在不同意隐私政策之前违规获取了收集设备信息,初步分析com.facebook.react.bridge有可能会引起这个获取ANDROID ID的问题

论证

由于com.facebook.react.bridge是react-native框架层面自带,为了排除其他因素的影响,我新建了一个React Native(版本0.64.3)的空工程,运行App发现还是会有获取ANDROID ID的问题:

rn空工程检测android_id.png

结论

得到的结论是由React Native框架自带的库引起,改造框架层面的东西不现实,于是我考虑从原生方式实现启动页的加载逻辑

解决方案

原生端实现

在路径android/app/src/main/java/包名路径,目录下新建文件CommonDialog.java

package com.caih.gxsinglewindow;

import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;

/**
 * 创建自定义的Dialog
 */

public class CommonDialog extends Dialog {
    private Button yes;//确定按钮
    private Button no;//取消按钮
    private TextView titleTV;//消息标题文本
    private TextView message;//消息提示文本
    private String titleStr;//从外界设置的title文本
    private CharSequence messageStr;//从外界设置的消息文本
    //确定文本和取消文本的显示的内容
    private String yesStr, noStr;
    private noButtonOnclickListener noButtonOnclickListener;//取消按钮被点击了的监听器
    private yesButtonOnclickListener yesButtonOnclickListener;//确定按钮被点击了的监听器
    private messageOnclickListener messageOnclickListener;//确定按钮被点击了的监听器

    public CommonDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);
    }
    public CommonDialog(@NonNull Context context) {
        super(context);
    }
    /**
     * 设置取消按钮的显示内容和监听
     *
     * @param str
     * @param noButtonOnclickListener
     */
    public void setNoOnclickListener(String str, noButtonOnclickListener noButtonOnclickListener) {
        if (str != null) {
            noStr = str;
        }
        this.noButtonOnclickListener = noButtonOnclickListener;
    }

    /**
     * 设置确定按钮的显示内容和监听
     *
     * @param str
     * @param yesButtonOnclickListener
     */
    public void setYesOnclickListener(String str, yesButtonOnclickListener yesButtonOnclickListener) {
        if (str != null) {
            yesStr = str;
        }
        this.yesButtonOnclickListener = yesButtonOnclickListener;
    }
    public void setMessageOnclickListener(messageOnclickListener messageOnclickListener) {
        this.messageOnclickListener = messageOnclickListener;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.common_dialog);
        //空白处不能取消动画
        setCanceledOnTouchOutside(false);

        //初始化界面控件
        initView();

        //初始化界面数据
        initData();
        //初始化界面控件的事件
        initEvent();
    }

    /**
     * 初始化界面控件
     */
    private void initView() {
        yes = findViewById(R.id.yes);
        no = findViewById(R.id.no);
        titleTV = (TextView) findViewById(R.id.title);
        message = (TextView) findViewById(R.id.message);
    }

    /**
     * 初始化界面控件的显示数据
     */
    private void initData() {
        //如果用户自定了title和message
        if (titleStr != null) {
            titleTV.setText(titleStr);
        }
        message.setMovementMethod(LinkMovementMethod.getInstance());
        if (messageStr != null) {
            message.setText(messageStr);
        }
        //如果设置按钮文字
        if (yesStr != null) {
            yes.setText(yesStr);
        }
        if (noStr != null) {
            no.setText(noStr);
        }
    }

    /**
     * 初始化界面的确定和取消监听
     */
    private void initEvent() {
        //设置确定按钮被点击后,向外界提供监听
        yes.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (yesButtonOnclickListener != null) {
                    yesButtonOnclickListener.onYesClick();
                }
            }
        });
        //设置取消按钮被点击后,向外界提供监听
        no.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (noButtonOnclickListener != null) {
                    noButtonOnclickListener.onNoClick();
                }
            }
        });
        /*message.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(messageOnclickListener != null){
                    String url = "https://www.baidu.com/";
                    messageOnclickListener.messageClick(url);
                }
            }
        });*/
    }

    /**
     * 从外界Activity为Dialog设置标题
     *
     * @param title
     */
    public void setTitle(String title) {
        titleStr = title;
    }

    /**
     * 从外界Activity为Dialog设置message
     *
     * @param message
     */
    public void setMessage(CharSequence message) {
        messageStr = message;
    }

    public interface noButtonOnclickListener {
        public void onNoClick();
    }

    public interface yesButtonOnclickListener {
        public void onYesClick();
    }

    public interface messageOnclickListener{
        public void messageClick(String url);
    }
}

同级目录下新建文件tempactivity.java,编写弹窗内容和二次弹窗

package com.caih.gxsinglewindow;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.NonNull;

public class TempActivity extends Activity {
    //
    private SharedPreferences.Editor editor;
    // 隐私条款对话框
    private CommonDialog privacyDialog;
    // 隐私条款二次确认对话框
    private CommonDialog secondDialog;

    private String userRegisterPolicyUrl = "https://www.baidu.com/";
    private String privacyPolicyUrl = "https://www.baidu.com/";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Set the theme to AppTheme BEFORE onCreate to support
        // coloring the background, status bar, and navigation bar.
        // This is required for expo-splash-screen.
        setTheme(R.style.AppTheme);
        super.onCreate(null);
        checkIfAgreePrivacy();
    }

    // 判断是否同意隐私条款
    private void checkIfAgreePrivacy() {
        SharedPreferences preferences = getPreferences(MODE_PRIVATE);
        boolean ifAgree = preferences.getBoolean("ifAgreePrivacy", false);
        // 如果没同意过隐私条款,初始化隐私条款对话框,弹出隐私条款
        if (!ifAgree) {
            initPrivacyDialog();
            initSecondDialog();
            privacyDialog.show();
        } else {
            // 已同意,跳转到主页面
            // 传值
            Intent intent = new Intent(TempActivity.this, MainActivity.class);
            // 跳转页面
            startActivity(intent);
            finish();
        }
    }

    /**
     * 隐私协议
     */
    private void initPrivacyDialog() {
        privacyDialog = new CommonDialog(TempActivity.this);
        privacyDialog.setContentView(R.layout.common_dialog);
        // 隐私条款同意按钮事件
        privacyDialog.setYesOnclickListener("同意", new CommonDialog.yesButtonOnclickListener() {
            @Override
            public void onYesClick() {
                SharedPreferences preferences = getPreferences(MODE_PRIVATE);
                editor = preferences.edit();
                editor.putBoolean("ifAgreePrivacy", true);
                editor.commit();
                privacyDialog.dismiss();
                // 跳转到真正首页
                Intent intent = new Intent(TempActivity.this, MainActivity.class);
                // 跳转页面
                startActivity(intent);
                finish();
            }
        });
        // 隐私条款不同意按钮事件
        privacyDialog.setNoOnclickListener("不同意", new CommonDialog.noButtonOnclickListener() {
            @Override
            public void onNoClick() {
                privacyDialog.dismiss();
                // 弹出二次确认框
                secondDialog.show();
            }
        });
        // 隐私条款协议按钮事件
        privacyDialog.setMessageOnclickListener(new CommonDialog.messageOnclickListener() {
            @Override
            public void messageClick(String url) {
                if (url == null || url.isEmpty()) {
                    return;
                }
                // 传值
                Intent intent = new Intent(TempActivity.this, WebViewActivity.class);
                intent.putExtra("url", url);
                // 跳转页面
                startActivity(intent);
            }
        });
        privacyDialog.setTitle("温馨提示");
        String messageStr = "亲爱的用户,感谢您使用app!\n" +
                "为了更好地保护您的权益,同时遵守相关监管要求,在您使用服务前,请仔细阅读《用户注册协议》和《app平台隐私政策》,\n" +
                "以便您能更好地行驶个人权利和保护个人信息,特向您说明如下:\n" +
                "1、为向您提供基本服务,我们会遵循正当、合法、必要的原则收集和使用必要的信息;\n" +
                "2、为实现消息推送,我们需要获取本机识别码权限;为xx协同功能,我们需要获取相机权限和录音权限;" +
                "为办理xx业务,我们需要获取本地存储读写权限、地理位置和相册权限;" +
                "所有获取权限描述和用途在隐私协议中有详细说明;\n" +
                "3、未经您的授权同意,我们不会将您的信息提供给第三方;\n" +
                "4、您可以查询、更正、删除您的个人信息,我们也提供账号注销的渠道。\n" +
                "如您同意此协议,请点击“同意”并开始使用我们的服务,如您无法认同此协议,很遗憾,我们将无法为您提供服务。";
        SpannableStringBuilder style = new SpannableStringBuilder();
        style.append(messageStr);
        // 用户注册协议点击区域
        ClickableSpan span1 = new ClickableSpan() {
            @Override
            public void onClick(@NonNull View view) {
                startWebView(userRegisterPolicyUrl);
            }

            @Override
            public void updateDrawState(@NonNull TextPaint ds) {
                super.updateDrawState(ds);
                ds.setColor(Color.parseColor("#26B3A3"));
            }
        };
        style.setSpan(span1, 53, 60, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        // 隐私政策点击区域
        ClickableSpan span2 = new ClickableSpan() {
            @Override
            public void onClick(@NonNull View view) {
                startWebView(privacyPolicyUrl);
            }

            @Override
            public void updateDrawState(@NonNull TextPaint ds) {
                super.updateDrawState(ds);
                ds.setColor(Color.parseColor("#26B3A3"));
            }
        };
        style.setSpan(span2, 61, 72, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

        CharacterStyle span = new CharacterStyle() {
            @Override
            public void updateDrawState(TextPaint textPaint) {

            }
        };
        // 第2点获取授权部分部分加粗
        style.setSpan(new StyleSpan(Typeface.BOLD), 161, 172, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        style.setSpan(new StyleSpan(Typeface.BOLD), 185, 196, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        style.setSpan(new StyleSpan(Typeface.BOLD), 211, 224, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        style.setSpan(new StyleSpan(Typeface.BOLD), 239, 249, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

        // 最后一段样式
        style.setSpan(new ForegroundColorSpan(Color.parseColor("#999999")), 336, messageStr.length(),
                Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        privacyDialog.setMessage(style);
        privacyDialog.getWindow().setBackgroundDrawableResource(R.drawable.dialog_shape);
    }

    /**
     * 二次确认框
     */
    private void initSecondDialog() {
        secondDialog = new CommonDialog(TempActivity.this);
        secondDialog.setContentView(R.layout.common_dialog);
        // 查看协议按钮事件
        secondDialog.setYesOnclickListener("查看协议", new CommonDialog.yesButtonOnclickListener() {
            @Override
            public void onYesClick() {
                startWebView(privacyPolicyUrl);
                secondDialog.dismiss();
                privacyDialog.show();
            }
        });
        // 退出应用按钮事件
        secondDialog.setNoOnclickListener("退出应用", new CommonDialog.noButtonOnclickListener() {
            @Override
            public void onNoClick() {
                secondDialog.dismiss();
                finish();
            }
        });
        secondDialog.setTitle("您需要同意本隐私政策才能继续使用APP");
        SpannableStringBuilder style = new SpannableStringBuilder();
        String messageStr = "如你不同意本隐私协议,很遗憾我们将无法为您提供服务。";
        style.append(messageStr);
        style.setSpan(new ForegroundColorSpan(Color.parseColor("#999999")), 0, messageStr.length(),
                Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        secondDialog.setMessage(style);
        secondDialog.getWindow().setBackgroundDrawableResource(R.drawable.dialog_shape);
    }

    private void startWebView(String url) {
        // 传值
        Intent intent = new Intent(TempActivity.this, WebViewActivity.class);
        intent.putExtra("url", url);
        // 跳转页面
        startActivity(intent);
    }
}

同级目录下,新建webview承载网页WebViewActivity.java

package com.caih.gxsinglewindow;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;

import androidx.annotation.Nullable;

public class WebViewActivity extends Activity {

    private WebView webView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载webView布局
        setContentView(R.layout.web_view);
        Intent intent = this.getIntent();
        String url = intent.getStringExtra("url");
        //获取webView对象
        webView = findViewById(R.id.webView);
        //webView初始化属性
        webView.getSettings().setJavaScriptEnabled(true);//设置WebView属性,能够执行Javascript脚本
        webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
        webView.getSettings().setLayoutAlgorithm( WebSettings.LayoutAlgorithm.NORMAL);
        webView.getSettings().setAllowFileAccess(true); //设置可以访问文件
        webView.getSettings().setBuiltInZoomControls(false); //设置支持缩放
        webView.getSettings().setSupportZoom(true);
        webView.getSettings().setUseWideViewPort(true);
        webView.getSettings().setLoadWithOverviewMode(true);
        webView.getSettings().setAppCacheEnabled(true);
        webView.getSettings().setDomStorageEnabled(true);
        webView.getSettings().setDatabaseEnabled(true);
        //加载页面
        webView.loadUrl(url);
    }
}

在路径android/app/src/main/res/drawable-xxhdpi,目录下新建文件dialog_shape.xml

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/white" />
    <corners android:radius="20dp"/>
</shape>

在路径android/app/src/main/res/layout,目录下新建文件common_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/dialog_shape">
    <LinearLayout
        android:layout_width="280dp"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@color/white"
        android:orientation="vertical">
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp"
            android:textAlignment="center"
            android:text="温馨提示"
            android:textColor="@color/black"
            android:textSize="16sp"
            android:textStyle="bold"/>
        <TextView
            android:id="@+id/message"
            android:clickable="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:layout_gravity="center"
            android:textColor="#111111"
            android:text=""
            android:layout_marginTop="3dp"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="bottom"
            android:orientation="horizontal">
            <Button
                android:id="@+id/no"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_marginLeft="10dp"
                android:background="@color/white"
                android:gravity="center"
                android:lines="1"
                android:text="不同意"
                android:textColor="#999999"
                android:textSize="16sp"
                style="@style/Widget.AppCompat.Button.Borderless"/>
            <View
                android:layout_width="1px"
                android:layout_height="match_parent"
                android:background="#E4E4E4"/>
            <Button
                android:id="@+id/yes"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:layout_marginRight="10dp"
                android:background="@color/white"
                android:gravity="center"
                android:lines="1"
                android:text="同意"
                android:textColor="#26B3A3"
                android:textSize="16sp"
                style="@style/Widget.AppCompat.Button.Borderless"/>
        </LinearLayout>
    </LinearLayout>
</ScrollView>

在路径android/app/src/main/res/layout,目录下新建文件web_view.xml

<?xml version="2.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></WebView>
</LinearLayout>

在路径android/app/src/main/res/layout,目录下新建文件colors.xml

<resources>
  <color name="splashscreen_background">#ffffff</color>
  <color name="iconBackground">#ffffff</color>
  <color name="colorPrimary">#023c69</color>
  <color name="colorPrimaryDark">#ffffff</color>
  <color name="purple_200">#FFBB86FC</color>
  <color name="purple_500">#FF6200EE</color>
  <color name="purple_700">#FF3700B3</color>
  <color name="teal_200">#FF03DAC5</color>
  <color name="teal_700">#FF018786</color>
  <color name="black">#FF000000</color>
  <color name="white">#FFFFFFFF</color>
</resources>

最后修改AndroidManifest.xml配置文件

<activity android:name=".TempActivity" android:exported="true" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
        <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
      </intent-filter>
</activity>
    <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="myapp"/>
        <data android:scheme="com.caih.gxsinglewindow"/>
      </intent-filter>
</activity>
<!--  增加隐私协议的activity  -->
<activity android:name=".WebViewActivity"></activity>