安卓应用安全指南 5.5.1 处理隐私数据 示例代码
5.5.1 处理隐私数据 示例代码
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
在准备应用的隐私政策时,你可以使用“协助创建应用隐私政策的工具” [29]。 这些工具以 HTML 格式和 XML 格式输出两个文件 - 应用隐私策略的摘要版本和详细版本。 这些文件的 HTML 和 XML 内容符合 MIC SPI 的建议,包括搜索标签等特性。 在下面的示例代码中,我们将演示此工具的用法,并使用由这个工具产生的 HTML 文件来展示程序隐私策略。
更具体地说,你可以使用以下流程图来确定使用哪个示例代码。
这里,“广泛同意”一词,指代广泛许可,由用户在应用的首次加载时,通过展示和查看程序隐私策略授予应用,用于应用将用户数据传输到服务器。 相反,短语“特定同意”指代在传输特定用户数据之前,立即获得的预先同意。
5.5.1.1 授予广泛同意和特定同意:包含应用隐私政策的应用
要点:
- 首次加载(或应用更新)时,获得广泛同意,来传输将由应用处理的用户数据。
- 如果用户未授予广泛同意,请勿传输用户数据。
- 在传输需要特别细致的处理的用户数据之前获得特定同意。
- 如果用户未授予特定同意,请勿传输相应的数据。
- 向用户提供可以查看应用隐私策略的方法。
- 提供通过用户操作删除传输的数据的方法。
- 提供通过用户操作停止数据传输的方法。
- 使用 UUID 或 cookie 来跟踪用户数据。
- 将应用隐私策略的摘要版本放置在素材文件夹中。
MainActivity.java
package org.jssec.android.privacypolicy;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.jssec.android.privacypolicy.ConfirmFragment.DialogListener;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.LocationClient;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Bundle;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends FragmentActivity
implements GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener, DialogListener {
private static final String BASE_URL = "https://www.example.com/pp";
private static final String GET_ID_URI = BASE_URL + "/get_id.php";
private static final String SEND_DATA_URI = BASE_URL + "/send_data.php";
private static final String DEL_ID_URI = BASE_URL + "/del_id.php";
private static final String ID_KEY = "id";
private static final String LOCATION_KEY = "location";
private static final String NICK_NAME_KEY = "nickname";
private static final String PRIVACY_POLICY_COMPREHENSIVE_AGREED_KEY = "privacyPolicyComprehensiveAgreed";
private static final String PRIVACY_POLICY_DISCRETE_TYPE1_AGREED_KEY = "privacyPolicyDiscreteType1Agreed";
private static final String PRIVACY_POLICY_PREF_NAME = "privacypolicy_preference";
private static final int CONNECTION_FAILURE_RESOLUTION_REQUEST = 257;
private String UserId = "";
private LocationClient mLocationClient = null;
private final int DIALOG_TYPE_COMPREHENSIVE_AGREEMENT = 1;
private final int DIALOG_TYPE_PRE_CONFIRMATION = 2;
private static final int VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW = 1;
private TextWatcher watchHandler = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean buttonEnable = (s.length() > 0);
MainActivity.this.findViewById(R.id.buttonStart).setEnabled(buttonEnable);
}
@Override
public void afterTextChanged(Editable s) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Fetch user ID from serverFetch user ID from server
new GetDataAsyncTask().execute();
findViewById(R.id.buttonStart).setEnabled(false);
((TextView) findViewById(R.id.editTextNickname)).addTextChangedListener(watchHandler);
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resultCode == ConnectionResult.SUCCESS) {
mLocationClient = new LocationClient(this, this, this);
}
}
@Override
protected void onStart() {
super.onStart();
SharedPreferences pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
int privacyPolicyAgreed = pref.getInt(PRIVACY_POLICY_COMPREHENSIVE_AGREED_KEY, -1);
if (privacyPolicyAgreed <= VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application.
// When the application is updated, it is only necessary to renew the user's grant of broad c
onsent if the updated application will handle new types of user data.
ConfirmFragment dialog = ConfirmFragment.newInstance(R.string.privacyPolicy, R.string.agreeP
rivacyPolicy, DIALOG_TYPE_COMPREHENSIVE_AGREEMENT);
dialog.setDialogListener(this);
FragmentManager fragmentManager = getSupportFragmentManager();
dialog.show(fragmentManager, "dialog");
}
// Used to obtain location data
if (mLocationClient != null) {
mLocationClient.connect();
}
}
@Override
protected void onStop() {
if (mLocationClient != null) {
mLocationClient.disconnect();
}
super.onStop();
}
public void onSendToServer(View view) {
// Check the status of user consent.
// Actually, it is necessary to obtain consent for each user data type.
SharedPreferences pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
int privacyPolicyAgreed = pref.getInt(PRIVACY_POLICY_DISCRETE_TYPE1_AGREED_KEY, -1);
if (privacyPolicyAgreed <= VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW) {
// *** POINT 3 *** Obtain specific consent before transmitting user data that requires particularly delicate handling.
ConfirmFragment dialog = ConfirmFragment.newInstance(R.string.sendLocation, R.string.cofirmS
endLocation, DIALOG_TYPE_PRE_CONFIRMATION);
dialog.setDialogListener(this);
FragmentManager fragmentManager = getSupportFragmentManager();
dialog.show(fragmentManager, "dialog");
} else {
// Start transmission, since it has the user consent.
onPositiveButtonClick(DIALOG_TYPE_PRE_CONFIRMATION);
}
}
public void onPositiveButtonClick(int type) {
if (type == DIALOG_TYPE_COMPREHENSIVE_AGREEMENT) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application.
SharedPreferences.Editor pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE).edit();
pref.putInt(PRIVACY_POLICY_COMPREHENSIVE_AGREED_KEY, getVersionCode());
pref.apply();
} else if (type == DIALOG_TYPE_PRE_CONFIRMATION) {
// *** POINT 3 *** Obtain specific consent before transmitting user data that requires particularly delicate handling.
if (mLocationClient != null && mLocationClient.isConnected()) {
Location currentLocation = mLocationClient.getLastLocation();
if (currentLocation != null) {
String locationData = "Latitude:" + currentLocation.getLatitude() + ", Longitude:" +
currentLocation.getLongitude();
String nickname = ((TextView) findViewById(R.id.editTextNickname)).getText().toString();
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + "¥n - nickname :
" + nickname + "¥n - location : " + locationData, Toast.LENGTH_SHORT).show();
new SendDataAsyncTack().execute(SEND_DATA_URI, UserId, locationData, nickname);
}
}
// Store the status of user consent.
// Actually, it is necessary to obtain consent for each user data type.
SharedPreferences.Editor pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE).edit();
pref.putInt(PRIVACY_POLICY_DISCRETE_TYPE1_AGREED_KEY, getVersionCode());
pref.apply();
}
}
public void onNegativeButtonClick(int type) {
if (type == DIALOG_TYPE_COMPREHENSIVE_AGREEMENT) {
// *** POINT 2 *** If the user does not grant general consent, do not transmit user data.
// In this sample application we terminate the application in this case.
finish();
} else if (type == DIALOG_TYPE_PRE_CONFIRMATION) {
// *** POINT 4 *** If the user does not grant specific consent, do not transmit the correspon
ding data.
// The user did not grant consent, so we do nothing.
}
}
private int getVersionCode() {
int versionCode = -1;
PackageManager packageManager = this.getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(this.getPackageName(), PackageManager.GET_ACTIVITIES);
versionCode = packageInfo.versionCode;
} catch (NameNotFoundException e) {
// This is sample, so omit the exception process
}
return versionCode;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_show_pp:
// *** POINT 5 *** Provide methods by which the user can review the application privacy policy.
Intent intent = new Intent();
intent.setClass(this, WebViewAssetsActivity.class);
startActivity(intent);
return true;
case R.id.action_del_id:
// *** POINT 6 *** Provide methods by which transmitted data can be deleted by user operations.
new SendDataAsyncTack().execute(DEL_ID_URI, UserId);
return true;
case R.id.action_donot_send_id:
// *** POINT 7 *** Provide methods by which transmitting data can be stopped by user operations.
// If the user stop sending data, user consent is deemed to have been revoked.
SharedPreferences.Editor pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE).edit();
pref.putInt(PRIVACY_POLICY_COMPREHENSIVE_AGREED_KEY, 0);
pref.apply();
// In this sample application if the user data cannot be sent by user operations,
// finish the application because we do nothing.
String message = getString(R.string.stopSendUserData);
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + message, Toast.LENGTH_SHORT).show();
finish();
return true;
}
return false;
}
@Override
public void onConnected(Bundle connectionHint) {
if (mLocationClient != null && mLocationClient.isConnected()) {
Location currentLocation = mLocationClient.getLastLocation();
if (currentLocation != null) {
String locationData = "Latitude ¥t: " + currentLocation.getLatitude() + "¥n¥tLongitude ¥t: " + currentLocation.getLongitude();
String text = "¥n" + getString(R.string.your_location_title) + "¥n¥t" + locationData;
TextView appText = (TextView) findViewById(R.id.appText);
appText.setText(text);
}
}
}
@Override
public void onConnectionFailed(ConnectionResult result) {
if (result.hasResolution()) {
try {
result.startResolutionForResult(this, CONNECTION_FAILURE_RESOLUTION_REQUEST);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
}
@Override
public void onDisconnected() {
mLocationClient = null;
}
private class GetDataAsyncTask extends AsyncTask<String, Void, String> {
private String extMessage = "";
@Override
protected String doInBackground(String... params) {
// *** POINT 8 *** Use UUIDs or cookies to keep track of user data
// In this sample we use an ID generated on the server side
SharedPreferences sp = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
UserId = sp.getString(ID_KEY, null);
if (UserId == null) {
// No token in SharedPreferences; fetch ID from server
try {
UserId = NetworkUtil.getCookie(GET_ID_URI, "", "id");
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
}
// Store the fetched ID in SharedPreferences
sp.edit().putString(ID_KEY, UserId).commit();
}
return UserId;
}
@Override
protected void onPostExecute(final String data) {
String status = (data != null) ? "success" : "error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
private class SendDataAsyncTack extends AsyncTask<String, Void, Boolean> {
private String extMessage = "";
@Override
protected Boolean doInBackground(String... params) {
String url = params[0];
String id = params[1];
String location = params.length > 2 ? params[2] : null;
String nickname = params.length > 3 ? params[3] : null;
Boolean result = false;
try {
JSONObject jsonData = new JSONObject();
jsonData.put(ID_KEY, id);
if (location != null)
jsonData.put(LOCATION_KEY, location);
if (nickname != null)
jsonData.put(NICK_NAME_KEY, nickname);
NetworkUtil.sendJSON(url, "", jsonData.toString());
result = true;
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
} catch (JSONException e) {
extMessage = e.toString();
}
return result;
}
@Override
protected void onPostExecute(Boolean result) {
String status = result ? "Success" : "Error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
}
ConfirmFragment.java
package org.jssec.android.privacypolicy;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class ConfirmFragment extends DialogFragment {
private DialogListener mListener = null;
public static interface DialogListener {
public void onPositiveButtonClick(int type);
public void onNegativeButtonClick(int type);
}
public static ConfirmFragment newInstance(int title, int sentence, int type) {
ConfirmFragment fragment = new ConfirmFragment();
Bundle args = new Bundle();
args.putInt("title", title);
args.putInt("sentence", sentence);
args.putInt("type", type);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle args) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application.
// *** POINT 3 *** Obtain specific consent before transmitting user data that requires particularly delicate handling.
final int title = getArguments().getInt("title");
final int sentence = getArguments().getInt("sentence");
final int type = getArguments().getInt("type");
LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View content = inflater.inflate(R.layout.fragment_comfirm, null);
TextView linkPP = (TextView) content.findViewById(R.id.tx_link_pp);
linkPP.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// *** POINT 5 *** Provide methods by which the user can review the application privacy policy.
Intent intent = new Intent();
intent.setClass(getActivity(), WebViewAssetsActivity.class);
startActivity(intent);
}
});
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(R.drawable.ic_launcher);
builder.setTitle(title);
builder.setMessage(sentence);
builder.setView(content);
builder.setPositiveButton(R.string.buttonConsent, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (mListener != null) {
mListener.onPositiveButtonClick(type);
}
}
});
builder.setNegativeButton(R.string.buttonDonotConsent, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (mListener != null) {
mListener.onNegativeButtonClick(type);
}
}
});
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof DialogListener)) {
throw new ClassCastException(activity.toString() + " must implement DialogListener.");
}
mListener = (DialogListener) activity;
}
public void setDialogListener(DialogListener listener) {
mListener = listener;
}
}
WebViewAssetsActivity.java
package org.jssec.android.privacypolicy;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class WebViewAssetsActivity extends Activity {
// *** POINT 9 *** Place a summary version of the application privacy policy in the assets folder
private static final String ABST_PP_URL = "file:///android_asset/PrivacyPolicy/app-policy-abst-privacypolicy-1.0.html";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
WebView webView = (WebView) findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setAllowFileAccess(false);
webView.loadUrl(ABST_PP_URL);
}
}
5.5.1.2 授予广泛同意:包含应用隐私政策的应用
要点:
- 首次加载(或应用更新)时,获得广泛同意,来传输将由应用处理的用户数据。
- 如果用户未授予广泛同意,请勿传输用户数据。
- 向用户提供可以查看应用隐私策略的方法。
- 提供通过用户操作删除传输的数据的方法。
- 提供通过用户操作停止数据传输的方法。
- 使用 UUID 或 cookie 来跟踪用户数据。
- 将应用隐私策略的摘要版本放置在素材文件夹中。
MainActivity.java
package org.jssec.android.privacypolicynopreconfirm;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.jssec.android.privacypolicynopreconfirm.MainActivity;
import org.jssec.android.privacypolicynopreconfirm.R;
import org.jssec.android.privacypolicynopreconfirm.ConfirmFragment.DialogListener;
import android.os.AsyncTask;
import android.os.Bundle;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.telephony.TelephonyManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends FragmentActivity implements DialogListener {
private final String BASE_URL = "https://www.example.com/pp";
private final String GET_ID_URI = BASE_URL + "/get_id.php";
private final String SEND_DATA_URI = BASE_URL + "/send_data.php";
private final String DEL_ID_URI = BASE_URL + "/del_id.php";
private final String ID_KEY = "id";
private final String NICK_NAME_KEY = "nickname";
private final String IMEI_KEY = "imei";
private final String PRIVACY_POLICY_AGREED_KEY = "privacyPolicyAgreed";
private final String PRIVACY_POLICY_PREF_NAME = "privacypolicy_preference";
private String UserId = "";
private final int DIALOG_TYPE_COMPREHENSIVE_AGREEMENT = 1;
private final int VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW = 1;
private TextWatcher watchHandler = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean buttonEnable = (s.length() > 0);
MainActivity.this.findViewById(R.id.buttonStart).setEnabled(buttonEnable);
}
@Override
public void afterTextChanged(Editable s) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Fetch user ID from serverFetch user ID from server
new GetDataAsyncTask().execute();
findViewById(R.id.buttonStart).setEnabled(false);
((TextView) findViewById(R.id.editTextNickname)).addTextChangedListener(watchHandler);
}
@Override
protected void onStart() {
super.onStart();
SharedPreferences pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
int privacyPolicyAgreed = pref.getInt(PRIVACY_POLICY_AGREED_KEY, -1);
if (privacyPolicyAgreed <= VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application.
// When the application is updated, it is only necessary to renew the user's grant of broad consent if the updated application will handle new types of user data.
ConfirmFragment dialog = ConfirmFragment.newInstance(R.string.privacyPolicy, R.string.agreePr
ivacyPolicy, DIALOG_TYPE_COMPREHENSIVE_AGREEMENT);
dialog.setDialogListener(this);
FragmentManager fragmentManager = getSupportFragmentManager();
dialog.show(fragmentManager, "dialog");
}
}
public void onSendToServer(View view) {
String nickname = ((TextView) findViewById(R.id.editTextNickname)).getText().toString();
TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
String imei = tm.getDeviceId();
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + "¥n - nickname : " + nickname + ", imei = " + imei, Toast.LENGTH_SHORT).show();
new SendDataAsyncTack().execute(SEND_DATA_URI, UserId, nickname, imei);
}
public void onPositiveButtonClick(int type) {
if (type == DIALOG_TYPE_COMPREHENSIVE_AGREEMENT) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit
user data that will be handled by the application.
SharedPreferences.Editor pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE).edit();
pref.putInt(PRIVACY_POLICY_AGREED_KEY, getVersionCode());
pref.apply();
}
}
public void onNegativeButtonClick(int type) {
if (type == DIALOG_TYPE_COMPREHENSIVE_AGREEMENT) {
// *** POINT 2 *** If the user does not grant general consent, do not transmit user data.
// In this sample application we terminate the application in this case.
finish();
}
}
private int getVersionCode() {
int versionCode = -1;
PackageManager packageManager = this.getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(this.getPackageName(), PackageManager.GET_ACTIVITIES);
versionCode = packageInfo.versionCode;
} catch (NameNotFoundException e) {
// This is sample, so omit the exception process
}
return versionCode;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_show_pp:
// *** POINT 3 *** Provide methods by which the user can review the application privacy policy.
Intent intent = new Intent();
intent.setClass(this, WebViewAssetsActivity.class);
startActivity(intent);
return true;
case R.id.action_del_id:
// *** POINT 4 *** Provide methods by which transmitted data can be deleted by user operation
s.
new SendDataAsyncTack().execute(DEL_ID_URI, UserId);
return true;
case R.id.action_donot_send_id:
// *** POINT 5 *** Provide methods by which transmitting data can be stopped by user operations.
// If the user stop sending data, user consent is deemed to have been revoked.
SharedPreferences.Editor pref = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE).edit();
pref.putInt(PRIVACY_POLICY_AGREED_KEY, 0);
pref.apply();
// In this sample application if the user data cannot be sent by user operations,
// finish the application because we do nothing.
String message = getString(R.string.stopSendUserData);
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + message, Toast.L
ENGTH_SHORT).show();
finish();
return true;
}
return false;
}
private class GetDataAsyncTask extends AsyncTask<String, Void, String> {
private String extMessage = "";
@Override
protected String doInBackground(String... params) {
// *** POINT 6 *** Use UUIDs or cookies to keep track of user data
// In this sample we use an ID generated on the server side
SharedPreferences sp = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
UserId = sp.getString(ID_KEY, null);
if (UserId == null) {
// No token in SharedPreferences; fetch ID from server
try {
UserId = NetworkUtil.getCookie(GET_ID_URI, "", "id");
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
}
// Store the fetched ID in SharedPreferences
sp.edit().putString(ID_KEY, UserId).commit();
}
return UserId;
}
@Override
protected void onPostExecute(final String data) {
String status = (data != null) ? "success" : "error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
private class SendDataAsyncTack extends AsyncTask<String, Void, Boolean> {
private String extMessage = "";
@Override
protected Boolean doInBackground(String... params) {
String url = params[0];
String id = params[1];
String nickname = params.length > 2 ? params[2] : null;
String imei = params.length > 3 ? params[3] : null;
Boolean result = false;
try {
JSONObject jsonData = new JSONObject();
jsonData.put(ID_KEY, id);
if (nickname != null)
jsonData.put(NICK_NAME_KEY, nickname);
if (imei != null)
jsonData.put(IMEI_KEY, imei);
NetworkUtil.sendJSON(url, "", jsonData.toString());
result = true;
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
} catch (JSONException e) {
extMessage = e.toString();
}
return result;
}
@Override
protected void onPostExecute(Boolean result) {
String status = result ? "Success" : "Error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
}
ConfirmFragment.java
package org.jssec.android.privacypolicynopreconfirm;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class ConfirmFragment extends DialogFragment {
private DialogListener mListener = null;
public static interface DialogListener {
public void onPositiveButtonClick(int type);
public void onNegativeButtonClick(int type);
}
public static ConfirmFragment newInstance(int title, int sentence, int type) {
ConfirmFragment fragment = new ConfirmFragment();
Bundle args = new Bundle();
args.putInt("title", title);
args.putInt("sentence", sentence);
args.putInt("type", type);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle args) {
// *** POINT 1 *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application.
final int title = getArguments().getInt("title");
final int sentence = getArguments().getInt("sentence");
final int type = getArguments().getInt("type");
LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View content = inflater.inflate(R.layout.fragment_comfirm, null);
TextView linkPP = (TextView) content.findViewById(R.id.tx_link_pp);
linkPP.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// *** POINT 3 *** Provide methods by which the user can review the application privacy policy.
Intent intent = new Intent();
intent.setClass(getActivity(), WebViewAssetsActivity.class);
startActivity(intent);
}
});
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(R.drawable.ic_launcher);
builder.setTitle(title);
builder.setMessage(sentence);
builder.setView(content);
builder.setPositiveButton(R.string.buttonConsent, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (mListener != null) {
mListener.onPositiveButtonClick(type);
}
}
});
builder.setNegativeButton(R.string.buttonDonotConsent, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if (mListener != null) {
mListener.onNegativeButtonClick(type);
}
}
});
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof DialogListener)) {
throw new ClassCastException(activity.toString() + " must implement DialogListener.");
}
mListener = (DialogListener) activity;
}
public void setDialogListener(DialogListener listener) {
mListener = listener;
}
}
WebViewAssetsActivity.java
package org.jssec.android.privacypolicynopreconfirm;
import org.jssec.android.privacypolicynopreconfirm.R;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class WebViewAssetsActivity extends Activity {
// *** POINT 7 *** Place a summary version of the application privacy policy in the assets folder
private final String ABST_PP_URL = "file:///android_asset/PrivacyPolicy/app-policy-abst-privacypolicy-1.0.html";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
WebView webView = (WebView) findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setAllowFileAccess(false);
webView.loadUrl(ABST_PP_URL);
}
}
5.5.1.3 不需要广泛同意:包含应用隐私策略的应用
要点:
- 向用户提供查看应用隐私策略的方法。
- 提供通过用户操作删除传输的数据的方法。
- 提供通过用户操作停止数据传输的方法
- 使用 UUID 或 cookie 来跟踪用户数据。
- 将应用隐私策略的摘要版本放置在素材文件夹中。
MainActivity.java
package org.jssec.android.privacypolicynocomprehensive;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import android.os.AsyncTask;
import android.os.Bundle;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.v4.app.FragmentActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends FragmentActivity {
private static final String BASE_URL = "https://www.example.com/pp";
private static final String GET_ID_URI = BASE_URL + "/get_id.php";
private static final String SEND_DATA_URI = BASE_URL + "/send_data.php";
private static final String DEL_ID_URI = BASE_URL + "/del_id.php";
private static final String ID_KEY = "id";
private static final String NICK_NAME_KEY = "nickname";
private static final String PRIVACY_POLICY_PREF_NAME = "privacypolicy_preference";
private String UserId = "";
private TextWatcher watchHandler = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean buttonEnable = (s.length() > 0);
MainActivity.this.findViewById(R.id.buttonStart).setEnabled(buttonEnable);
}
@Override
public void afterTextChanged(Editable s) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Fetch user ID from serverFetch user ID from server
new GetDataAsyncTask().execute();
findViewById(R.id.buttonStart).setEnabled(false);
((TextView) findViewById(R.id.editTextNickname)).addTextChangedListener(watchHandler);
}
public void onSendToServer(View view) {
String nickname = ((TextView) findViewById(R.id.editTextNickname)).getText().toString();
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + "¥n - nickname : " + nickname, Toast.LENGTH_SHORT).show();
new sendDataAsyncTack().execute(SEND_DATA_URI, UserId, nickname);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_show_pp:
// *** POINT 1 *** Provide methods by which the user can review the application privacy policy.
Intent intent = new Intent();
intent.setClass(this, WebViewAssetsActivity.class);
startActivity(intent);
return true;
case R.id.action_del_id:
// *** POINT 2 *** Provide methods by which transmitted data can be deleted by user operations.
new sendDataAsyncTack().execute(DEL_ID_URI, UserId);
return true;
case R.id.action_donot_send_id:
// *** POINT 3 *** Provide methods by which transmitting data can be stopped by user operations.
// In this sample application if the user data cannot be sent by user operations,
// finish the application because we do nothing.
String message = getString(R.string.stopSendUserData);
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + message, Toast.LENGTH_SHORT).show();
finish();
return true;
}
return false;
}
private class GetDataAsyncTask extends AsyncTask<String, Void, String> {
private String extMessage = "";
@Override
protected String doInBackground(String... params) {
// *** POINT 4 *** Use UUIDs or cookies to keep track of user data
// In this sample we use an ID generated on the server side
SharedPreferences sp = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE);
UserId = sp.getString(ID_KEY, null);
if (UserId == null) {
// No token in SharedPreferences; fetch ID from server
try {
UserId = NetworkUtil.getCookie(GET_ID_URI, "", "id");
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
}
// Store the fetched ID in SharedPreferences
sp.edit().putString(ID_KEY, UserId).commit();
}
return UserId;
}
@Override
protected void onPostExecute(final String data) {
String status = (data != null) ? "success" : "error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
private class sendDataAsyncTack extends AsyncTask<String, Void, Boolean> {
private String extMessage = "";
@Override
protected Boolean doInBackground(String... params) {
String url = params[0];
String id = params[1];
String nickname = params.length > 2 ? params[2] : null;
Boolean result = false;
try {
JSONObject jsonData = new JSONObject();
jsonData.put(ID_KEY, id);
if (nickname != null)
jsonData.put(NICK_NAME_KEY, nickname);
NetworkUtil.sendJSON(url, "", jsonData.toString());
result = true;
} catch (IOException e) {
// Catch exceptions such as certification errors
extMessage = e.toString();
} catch (JSONException e) {
extMessage = e.toString();
}
return result;
}
@Override
protected void onPostExecute(Boolean result) {
String status = result ? "Success" : "Error";
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + " - " + status + " : " +
extMessage, Toast.LENGTH_SHORT).show();
}
}
}
WebViewAssetsActivity.java
package org.jssec.android.privacypolicynocomprehensive;
import org.jssec.android.privacypolicynocomprehensive.R;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class WebViewAssetsActivity extends Activity {
// *** POINT 5 *** Place a summary version of the application privacy policy in the assets folder
private static final String ABST_PP_URL = "file:///android_asset/PrivacyPolicy/app-policy-abst-privacypolicy-1.0.html";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
WebView webView = (WebView) findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setAllowFileAccess(false);
webView.loadUrl(ABST_PP_URL);
}
}
5.5.1.4 不包含应用隐私策略的应用
要点:
- 如果你的应用只使用它在设备中获取的信息,则不需要显示应用隐私策略。
- 在市场应用或类似应用的文档中,请注意应用不会将其获取的信息传输到外部。
MainActivity.java
package org.jssec.android.privacypolicynoinfosent;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.location.LocationClient;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.content.Intent;
import android.content.IntentSender;
import android.support.v4.app.FragmentActivity;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends FragmentActivity implements GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener {
private LocationClient mLocationClient = null;
private final int CONNECTION_FAILURE_RESOLUTION_REQUEST = 257;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLocationClient = new LocationClient(this, this, this);
}
@Override
protected void onStart() {
super.onStart();
// Used to obtain location data
if (mLocationClient != null) {
mLocationClient.connect();
}
}
@Override
protected void onStop() {
if (mLocationClient != null) {
mLocationClient.disconnect();
}
super.onStop();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
public void onStartMap(View view) {
// *** POINT 1 *** You do not need to display an application privacy policy if your application w
ill only use the information it obtains within the device.
if (mLocationClient != null && mLocationClient.isConnected()) {
Location currentLocation = mLocationClient.getLastLocation();
if (currentLocation != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("geo:" + currentLocation.getLatitude() + "," + currentLocation.getLongitude()));
startActivity(intent);
}
}
}
@Override
public void onConnected(Bundle connectionHint) {
if (mLocationClient != null && mLocationClient.isConnected()) {
Location currentLocation = mLocationClient.getLastLocation();
if (currentLocation != null) {
String locationData = "Latitude ¥t: " + currentLocation.getLatitude() + "¥n¥tLongitude ¥t: " + currentLocation.getLongitude();
String text = "¥n" + getString(R.string.your_location_title) + "¥n¥t" + locationData;
Toast.makeText(MainActivity.this, this.getClass().getSimpleName() + text, Toast.LENGTH_SHORT).show();
TextView appText = (TextView) findViewById(R.id.appText);
appText.setText(text);
}
}
}
@Override
public void onConnectionFailed(ConnectionResult result) {
if (result.hasResolution()) {
try {
result.startResolutionForResult(this, CONNECTION_FAILURE_RESOLUTION_REQUEST);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
}
@Override
public void onDisconnected() {
mLocationClient = null;
Toast.makeText(this, "Disconnected. Please re-connect.", Toast.LENGTH_SHORT).show();
}
}
市场上的示例如下。