前言
React-Native框架开发Android App第一次启动弹出隐私政策时,Android Studio日志抓包提示有获取Android ID、手机设备等用户隐私信息,导致上架应用市场审核不通过,审核不通过原因:用户在未同意隐私政策之前App不能获取手机设备、用户相关的敏感信息。
React Native App启动页违规收集用户信息
问题
应用在不同意隐私政策之前违规获取了收集设备信息,初步分析com.facebook.react.bridge有可能会引起这个获取ANDROID ID的问题
论证
由于com.facebook.react.bridge是react-native框架层面自带,为了排除其他因素的影响,我新建了一个React Native(版本0.64.3)的空工程,运行App发现还是会有获取ANDROID ID的问题:
结论
得到的结论是由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>