mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] align Android privacy consent prompts with iOS
This commit is contained in:
@@ -148,6 +148,10 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
|
||||
|
||||
采集时间的有效性在接收时检查,本机解码和显示等待计入画面延时;真实延时超过 5 秒也会继续显示,不会当成无效时间戳过滤。显示线程复用解码器的 CPU NV12 缓冲区,只保留一帧待显示画面,切换显示器或 Surface 时清除旧帧,会话结束时先停止显示线程再销毁原生连接。
|
||||
|
||||
## 隐私授权
|
||||
|
||||
未完成隐私授权时,首次打开或从后台返回应用会自动显示离线隐私政策;需要主动勾选阅读并同意后才能联网,与 iOS 一致。暂不同意后,本次停留不会反复弹窗,仍可浏览设置和本地连接记录。设置 → 隐私 → 隐私与授权可离线查看政策或撤回授权;撤回后下次进入应用会再次提示。
|
||||
|
||||
## 远程画面预览
|
||||
|
||||
设置 → 隐私中的“保存远程画面预览”默认关闭,完成联网授权后可开启。与 iOS 一致,开启前说明预览可能包含个人或工作信息;每次连接只在实际显示画面后保存一张预览,居中裁剪为 640×360、JPEG 质量 78。截图来自远程视频的 Surface,不包含悬浮菜单、键盘、本地光标或画面外的黑边;压缩、读写和解码在串行后台线程执行,不复制完整 4K 位图,也不连续截图。
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private boolean foreground;
|
||||
private RemoteVideoView video;
|
||||
private SessionControls controls;
|
||||
private Dialog passwordDialog, progressDialog;
|
||||
private Dialog passwordDialog, progressDialog, privacyDialog;
|
||||
private AlertDialog disconnectDialog;
|
||||
private AlertDialog remoteUpdateDialog;
|
||||
private RemoteVersionCheck remoteVersionCheck;
|
||||
@@ -152,7 +152,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
LinearLayout toolbar=row();toolbar.setPadding(dp(20),0,dp(20),0);root.addView(toolbar,ui.size(-1,56));
|
||||
signalBadge=text("",12,true);signalBadge.setGravity(Gravity.CENTER);signalBadge.setPadding(dp(11),0,dp(11),0);signalBadge.setSingleLine(true);
|
||||
signalBadge.setOnClickListener(v->{
|
||||
if(!preferences.getBoolean("networkConsent",false))privacyPage();
|
||||
if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);
|
||||
else if(signalState==1)toast("已连接 "+serverKey());
|
||||
else {stopSignaling();ensureSignaling();}
|
||||
});renderSignaling();toolbar.addView(signalBadge,ui.size(-2,34));toolbar.addView(new View(this),new LinearLayout.LayoutParams(0,1,1));
|
||||
@@ -218,8 +218,11 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
passwordSheet(id,remember?"已保存的密码未能保留,请重新输入一次":"",remember);
|
||||
}
|
||||
private void consent(Runnable accepted){
|
||||
new AlertDialog.Builder(this).setTitle("隐私与授权").setMessage("连接时会向所选服务器发送设备身份和认证信息,并传输远程画面、声音与操作指令。设备身份加密保存在本机;仅在你开启“保存密码”时加密保存访问密码。剪贴板内容仅在你操作时发送或写入本机。仅连接你有权访问的电脑。")
|
||||
.setNegativeButton("暂不同意",null).setPositiveButton("同意并继续",(d,w)->{preferences.edit().putBoolean("networkConsent",true).apply();ensureSignaling();accepted.run();}).show();
|
||||
if(privacyDialog!=null&&privacyDialog.isShowing())return;
|
||||
Dialog dialog=new PrivacyConsentDialog(this,ui,()->{
|
||||
preferences.edit().putBoolean("networkConsent",true).apply();renderSignaling();ensureSignaling();accepted.run();
|
||||
});
|
||||
privacyDialog=dialog;dialog.setOnDismissListener(d->{if(privacyDialog==dialog)privacyDialog=null;});dialog.show();
|
||||
}
|
||||
private void passwordSheet(String id,String error,boolean rememberDefault){
|
||||
Dialog dialog=new Dialog(this);passwordDialog=dialog;LinearLayout sheet=column();sheet.setPadding(dp(22),dp(10),dp(22),dp(22));sheet.setBackground(ui.background(BACKGROUND,24));
|
||||
@@ -355,10 +358,14 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
}
|
||||
@Override public void announcementFailed(String requestId){announcementInbox.failed(requestId);}
|
||||
private void privacyPage(){
|
||||
LinearLayout content=formPage("privacy","隐私与授权",this::settings);LinearLayout card=section(content,"");boolean consent=preferences.getBoolean("networkConsent",false);link(card,"授权状态",consent?"已授权":"未授权",()->{});ui.divider(card);link(card,"隐私说明","",()->consent(this::privacyPage));
|
||||
LinearLayout content=formPage("privacy","隐私与授权",this::settings);LinearLayout card=section(content,"");boolean consent=preferences.getBoolean("networkConsent",false);link(card,"授权状态",consent?"已授权":"未授权",()->{});ui.divider(card);link(card,"隐私政策","",this::privacyPolicyPage);
|
||||
footer(content,"设备身份及选择保存的访问密码使用 Android Keystore 加密。连接记录仅保存在本机,不纳入设备备份。剪贴板内容在结束会话时清除。离开应用会结束当前会话。");
|
||||
if(consent)content.addView(ui.action("撤回联网授权",RED,()->new AlertDialog.Builder(this).setTitle("撤回联网授权?").setMessage("撤回后将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和密码会保留,重新连接前需再次授权。").setNegativeButton("取消",null).setPositiveButton("撤回",(d,w)->{preferences.edit().putBoolean("networkConsent",false).apply();previews.setEnabled(false);stopSignaling();announcementInbox.reset();privacyPage();}).show()));else content.addView(ui.primary("阅读并授权",()->consent(this::privacyPage)));
|
||||
}
|
||||
private void privacyPolicyPage(){
|
||||
installRoot("privacy-policy",false);navigation("隐私政策",this::privacyPage);
|
||||
root.addView(new PrivacyPolicyDocument(this).view(ui),new LinearLayout.LayoutParams(-1,0,1));
|
||||
}
|
||||
private void about(){
|
||||
if(aboutPages==null)aboutPages=new AboutPages(this,ui,this::settings);aboutPages.show();
|
||||
}
|
||||
@@ -499,7 +506,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
@Override public void onWindowFocusChanged(boolean focus){super.onWindowFocusChanged(focus);if(!focus)releaseKeys();else applySystemBars(ready);}
|
||||
@android.annotation.SuppressLint("GestureBackNavigation") @Override public void onBackPressed(){back();}
|
||||
@Override public void onConfigurationChanged(Configuration config){super.onConfigurationChanged(config);if(controls!=null)controls.post(controls::constrainPanels);}
|
||||
@Override protected void onStart(){super.onStart();foreground=true;ensureSignaling();}
|
||||
@Override protected void onStop(){foreground=false;pageHost.finish();if(session!=null)end(isFinishing()?"":"应用已进入后台,会话已结束");stopSignaling();super.onStop();}
|
||||
@Override protected void onDestroy(){pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();if(session!=null)session.close();stopSignaling();super.onDestroy();}
|
||||
// onStart marks a new visit; focus/resume callbacks must not reopen a refused notice.
|
||||
@Override protected void onStart(){super.onStart();foreground=true;if(!preferences.getBoolean("networkConsent",false))consent(this::renderSignaling);ensureSignaling();}
|
||||
@Override protected void onStop(){foreground=false;if(privacyDialog!=null)privacyDialog.dismiss();pageHost.finish();if(session!=null)end(isFinishing()?"":"应用已进入后台,会话已结束");stopSignaling();super.onStop();}
|
||||
@Override protected void onDestroy(){pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();if(session!=null)session.close();stopSignaling();super.onDestroy();}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.view.Gravity;
|
||||
import android.view.Window;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import static cn.crossdesk.mobile.MobileUi.*;
|
||||
|
||||
/** Explicit agreement is required; refusal dismisses only the current visit. */
|
||||
final class PrivacyConsentDialog extends Dialog {
|
||||
PrivacyConsentDialog(Activity activity,MobileUi ui,Runnable accepted){
|
||||
this(activity,ui,new PrivacyPolicyDocument(activity),accepted);
|
||||
}
|
||||
PrivacyConsentDialog(Activity activity,MobileUi ui,PrivacyPolicyDocument policy,Runnable accepted){
|
||||
super(activity);requestWindowFeature(Window.FEATURE_NO_TITLE);setCancelable(false);setCanceledOnTouchOutside(false);
|
||||
LinearLayout sheet=ui.column();sheet.setBackground(ui.background(android.graphics.Color.WHITE,20));sheet.setClipToOutline(true);
|
||||
TextView title=ui.text("隐私政策",17,true);title.setGravity(Gravity.CENTER);sheet.addView(title,ui.size(-1,56));ui.divider(sheet);
|
||||
sheet.addView(policy.view(ui),new LinearLayout.LayoutParams(-1,0,1));ui.divider(sheet);
|
||||
LinearLayout actions=ui.column();actions.setPadding(ui.dp(20),ui.dp(12),ui.dp(20),ui.dp(12));sheet.addView(actions);
|
||||
CheckBox hasReadPolicy=new CheckBox(activity);hasReadPolicy.setText("我已阅读并同意《隐私政策》");hasReadPolicy.setTextColor(INK);hasReadPolicy.setTextSize(14);hasReadPolicy.setMinHeight(ui.dp(44));actions.addView(hasReadPolicy,ui.size(-1,-2));ui.gap(actions,12);
|
||||
TextView agree=ui.primary("同意并继续",()->{
|
||||
if(!hasReadPolicy.isChecked()||!policy.available)return;
|
||||
dismiss();accepted.run();
|
||||
});agree.setEnabled(false);agree.setAlpha(.45f);actions.addView(agree,ui.size(-1,48));
|
||||
hasReadPolicy.setOnCheckedChangeListener((button,checked)->{boolean enabled=checked&&policy.available;agree.setEnabled(enabled);agree.setAlpha(enabled?1:.45f);});
|
||||
ui.gap(actions,12);actions.addView(ui.action("暂不同意",SECONDARY,this::dismiss),ui.size(-1,44));
|
||||
setContentView(sheet);Window window=getWindow();if(window!=null){window.setBackgroundDrawableResource(android.R.color.transparent);window.setDimAmount(.22f);}
|
||||
}
|
||||
@Override public void show(){
|
||||
super.show();Window window=getWindow();if(window!=null){window.setLayout(-1,Math.round(getContext().getResources().getDisplayMetrics().heightPixels*.9f));window.setGravity(Gravity.BOTTOM);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import java.io.IOException;
|
||||
|
||||
import static cn.crossdesk.mobile.MobileUi.*;
|
||||
|
||||
/** The same bundled policy as iOS, readable before any network consent. */
|
||||
final class PrivacyPolicyDocument {
|
||||
final boolean available;
|
||||
final String text;
|
||||
|
||||
PrivacyPolicyDocument(Context context){this(load(context));}
|
||||
PrivacyPolicyDocument(String document){
|
||||
available=document!=null&&!document.trim().isEmpty();
|
||||
String value=available?document:"隐私政策暂时无法读取,请拒绝联网并重新安装应用。";
|
||||
int start=value.indexOf("## 中文\n"),end=start<0?-1:value.indexOf("## English",start);
|
||||
text=start>=0&&end>start?value.substring(start+"## 中文\n".length(),end).trim():value;
|
||||
}
|
||||
private static String load(Context context){
|
||||
try{return LicenseCatalog.read(context,"PRIVACY.md");}catch(IOException error){return null;}
|
||||
}
|
||||
ScrollView view(MobileUi ui){
|
||||
ScrollView scroll=new ScrollView(ui.context);scroll.setBackgroundColor(android.graphics.Color.WHITE);
|
||||
LinearLayout content=ui.column();content.setPadding(ui.dp(20),ui.dp(20),ui.dp(20),ui.dp(20));scroll.addView(content);
|
||||
for(String paragraph:text.split("\n\n")){
|
||||
paragraph=paragraph.trim();if(paragraph.isEmpty()||paragraph.equals("---"))continue;
|
||||
boolean heading=paragraph.startsWith("#"),date=paragraph.startsWith("更新日期:");
|
||||
String value=heading?paragraph.replaceFirst("^#+\\s*",""):paragraph.replace("**","");
|
||||
TextView label=ui.text(value,date?12:16,heading);label.setTextIsSelectable(true);label.setLineSpacing(ui.dp(4),1);
|
||||
if(date)label.setTextColor(SECONDARY);
|
||||
LinearLayout.LayoutParams spacing=new LinearLayout.LayoutParams(-1,-2);spacing.bottomMargin=ui.dp(14);content.addView(label,spacing);
|
||||
}
|
||||
return scroll;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ def write_pages(output, catalog, additional, revision, minirtc):
|
||||
def main():
|
||||
output = ANDROID / 'app/build/generated/notices'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
(output / 'PRIVACY.md').write_text((ROOT / 'PRIVACY.md').read_text(encoding='utf-8'), encoding='utf-8')
|
||||
catalog = json.loads((ROOT / 'apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json').read_text())
|
||||
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
|
||||
minirtc = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT / 'deps/submodules/minirtc', text=True).strip()
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PrivacyConsentTest {
|
||||
private SharedPreferences preferences;
|
||||
private Map<String,?> saved;
|
||||
|
||||
@Before public void prepare(){
|
||||
Context context=InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
preferences=context.getSharedPreferences("settings",Context.MODE_PRIVATE);saved=preferences.getAll();
|
||||
preferences.edit().remove("networkConsent").putString("host","127.0.0.1").putInt("port",1).commit();
|
||||
}
|
||||
@After public void restore(){
|
||||
SharedPreferences.Editor edit=preferences.edit();
|
||||
for(String key:new String[]{"networkConsent","host","port"}){
|
||||
edit.remove(key);Object value=saved.get(key);
|
||||
if(value instanceof Boolean)edit.putBoolean(key,(Boolean)value);
|
||||
else if(value instanceof String)edit.putString(key,(String)value);
|
||||
else if(value instanceof Integer)edit.putInt(key,(Integer)value);
|
||||
}
|
||||
edit.commit();
|
||||
}
|
||||
private static Object field(MainActivity activity,String name){
|
||||
try{var field=MainActivity.class.getDeclaredField(name);field.setAccessible(true);return field.get(activity);}
|
||||
catch(Exception error){throw new AssertionError(error);}
|
||||
}
|
||||
private static Dialog notice(MainActivity activity){
|
||||
Dialog dialog=(Dialog)field(activity,"privacyDialog");assertNotNull("An unconsented visit must prompt",dialog);assertTrue(dialog.isShowing());return dialog;
|
||||
}
|
||||
private static TextView findText(View view,String text){
|
||||
if(view instanceof TextView&&((TextView)view).getText().toString().equals(text))return (TextView)view;
|
||||
if(view instanceof ViewGroup){ViewGroup parent=(ViewGroup)view;for(int i=0;i<parent.getChildCount();i++){TextView found=findText(parent.getChildAt(i),text);if(found!=null)return found;}}
|
||||
return null;
|
||||
}
|
||||
private static TextView action(Dialog dialog,String text){
|
||||
TextView view=findText(dialog.getWindow().getDecorView(),text);assertNotNull(text,view);return view;
|
||||
}
|
||||
private static void decline(MainActivity activity){action(notice(activity),"暂不同意").performClick();}
|
||||
private static void agree(MainActivity activity){
|
||||
Dialog dialog=notice(activity);action(dialog,"我已阅读并同意《隐私政策》").performClick();action(dialog,"同意并继续").performClick();
|
||||
}
|
||||
|
||||
@Test public void coldLaunchRequiresExplicitAgreementBeforeNetworking(){
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{
|
||||
Dialog dialog=notice(activity);
|
||||
assertFalse(((CheckBox)action(dialog,"我已阅读并同意《隐私政策》")).isChecked());
|
||||
assertFalse(action(dialog,"同意并继续").isEnabled());
|
||||
action(dialog,"同意并继续").performClick();
|
||||
assertTrue(dialog.isShowing());assertFalse(preferences.getBoolean("networkConsent",false));
|
||||
assertNull(field(activity,"signaling"));
|
||||
decline(activity);assertFalse(dialog.isShowing());assertNull(field(activity,"signaling"));
|
||||
assertNotNull(findText(activity.getWindow().getDecorView(),"等待隐私授权"));
|
||||
});
|
||||
scenario.onActivity(activity->assertNull(field(activity,"privacyDialog")));
|
||||
}
|
||||
}
|
||||
@Test public void refusalPromptsOnBackgroundReturnButNotTransientInactivity(){
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(PrivacyConsentTest::decline);
|
||||
scenario.moveToState(Lifecycle.State.STARTED).moveToState(Lifecycle.State.RESUMED);
|
||||
scenario.onActivity(activity->{assertNull(field(activity,"privacyDialog"));assertNull(field(activity,"signaling"));});
|
||||
for(int visit=0;visit<3;visit++){
|
||||
scenario.moveToState(Lifecycle.State.CREATED).moveToState(Lifecycle.State.RESUMED);
|
||||
scenario.onActivity(activity->{notice(activity);assertNull(field(activity,"signaling"));decline(activity);});
|
||||
}
|
||||
}
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{notice(activity);assertNull(field(activity,"signaling"));});
|
||||
}
|
||||
}
|
||||
@Test public void backgroundDismissesOldDialogAndClearsTheCheckbox(){
|
||||
Dialog[] original={null};
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{original[0]=notice(activity);action(original[0],"我已阅读并同意《隐私政策》").performClick();assertTrue(action(original[0],"同意并继续").isEnabled());});
|
||||
scenario.moveToState(Lifecycle.State.CREATED);
|
||||
assertFalse(original[0].isShowing());
|
||||
scenario.moveToState(Lifecycle.State.RESUMED);
|
||||
scenario.onActivity(activity->{Dialog next=notice(activity);assertNotSame(original[0],next);assertFalse(action(next,"同意并继续").isEnabled());assertNull(field(activity,"signaling"));});
|
||||
}
|
||||
}
|
||||
@Test public void agreementPersistsAndStopsAutomaticPrompts(){
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{Dialog dialog=notice(activity);agree(activity);assertTrue(preferences.getBoolean("networkConsent",false));assertFalse(dialog.isShowing());assertNotNull(field(activity,"signaling"));});
|
||||
scenario.onActivity(activity->assertNull(field(activity,"privacyDialog")));
|
||||
scenario.moveToState(Lifecycle.State.CREATED).moveToState(Lifecycle.State.RESUMED);
|
||||
scenario.onActivity(activity->assertNull(field(activity,"privacyDialog")));
|
||||
}
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->assertNull(field(activity,"privacyDialog")));
|
||||
scenario.moveToState(Lifecycle.State.CREATED);
|
||||
preferences.edit().putBoolean("networkConsent",false).commit();
|
||||
scenario.moveToState(Lifecycle.State.RESUMED);
|
||||
scenario.onActivity(activity->{notice(activity);assertNull(field(activity,"signaling"));});
|
||||
}
|
||||
}
|
||||
@Test public void existingLocalRecordsDoNotImplyConsent(){
|
||||
Context context=InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
SharedPreferences identities=context.getSharedPreferences("identities",Context.MODE_PRIVATE);
|
||||
String key="privacy-upgrade.invalid:1";
|
||||
try{
|
||||
identities.edit().putString(key,"existing-device-identity").commit();
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{notice(activity);assertNull(field(activity,"signaling"));});
|
||||
}
|
||||
}finally{identities.edit().remove(key).commit();}
|
||||
}
|
||||
@Test public void bundledChinesePolicyIsOfflineAndMissingPolicyCannotAuthorize(){
|
||||
Context context=InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
PrivacyPolicyDocument policy=new PrivacyPolicyDocument(context);
|
||||
assertTrue(policy.available);assertTrue(policy.text.contains("更新日期:"));assertTrue(policy.text.contains("查询与删除请求"));assertFalse(policy.text.contains("## English"));
|
||||
assertFalse(new PrivacyPolicyDocument(" \n").available);
|
||||
try(ActivityScenario<MainActivity> scenario=ActivityScenario.launch(MainActivity.class)){
|
||||
scenario.onActivity(activity->{
|
||||
decline(activity);int[] accepted={0};
|
||||
Dialog missing=new PrivacyConsentDialog(activity,new MobileUi(activity),new PrivacyPolicyDocument((String)null),()->accepted[0]++);
|
||||
try{
|
||||
missing.show();action(missing,"我已阅读并同意《隐私政策》").performClick();
|
||||
assertFalse(action(missing,"同意并继续").isEnabled());action(missing,"同意并继续").performClick();
|
||||
assertEquals(0,accepted[0]);assertFalse(preferences.getBoolean("networkConsent",false));assertNull(field(activity,"signaling"));
|
||||
action(missing,"暂不同意").performClick();assertFalse(missing.isShowing());
|
||||
}finally{missing.dismiss();}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ def verify(path):
|
||||
raise ValueError('APK native library entry is not 16 KB aligned')
|
||||
if 'assets/THIRD_PARTY_NOTICES.txt' not in archive.namelist():
|
||||
raise ValueError('Missing third-party notices')
|
||||
if archive.read('assets/PRIVACY.md') != (Path(__file__).resolve().parents[3] / 'PRIVACY.md').read_bytes():
|
||||
raise ValueError('Missing or outdated bundled privacy policy')
|
||||
catalog = json.loads(archive.read('assets/ThirdPartyLicenses.json'))
|
||||
source = json.loads(archive.read('assets/SourceMetadata.json'))
|
||||
components = catalog['components']
|
||||
@@ -53,7 +55,7 @@ def verify(path):
|
||||
for document in source['buildDocuments']:
|
||||
if not archive.read('assets/'+document['asset']):
|
||||
raise ValueError('Missing source build instructions')
|
||||
print(f'PASS: {Path(path).name}: arm64, 16 KB ELF/ZIP alignment, notices, About catalog and documents')
|
||||
print(f'PASS: {Path(path).name}: arm64, 16 KB ELF/ZIP alignment, privacy policy, notices, About catalog and documents')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user