06 Anykey用户数据的存储和加载SecuritySharedPreference

编程入门 行业动态 更新时间:2024-10-08 13:36:44

06 Anykey用户数据的存储和<a href=https://www.elefans.com/category/jswz/34/1771433.html style=加载SecuritySharedPreference"/>

06 Anykey用户数据的存储和加载SecuritySharedPreference

目录

  • 预先准备
    • 1 先看一下最终效果图
    • 2 确认默认启动界面仍是登录界面
    • 3 创建LogUtiljava用来记录日志
    • 4 添加以下依赖库
  • 使用SecuritySharedPreference完成登录的加解密过程
    • 1 配置SecuritySharedPreference
    • 2 LoginActivity重制
  • 填表数据的绑定和加载
    • 1 右分页布局
    • 2 保存用户数据
      • 21 点击保存按钮时的业务
      • 22 保存卡片数据到本地SavedToMySharedPrefs
  • 运行测试
  • 更新修复
    • 1 一些小的更新修复
    • 2 OOM内存溢出问题的临时解决方案
  • 相关参考
  • 日志

1. 预先准备

1.1. 先看一下最终效果图

1.2. 确认默认启动界面仍是登录界面

下面的章节会先从登录逻辑的完整实现开始讲起。
有些同学在调试的过程中为了方便,在AndroidManifest中把默认启动项改成了MainActivity。所以请预先确认将启动项设置为LoginActivity(如图所示):

(PS:换用了新的主题,具体可见: )

1.3. 创建LogUtil.java用来记录日志

由于在项目测试期间会留下大量的日志信息,会给将来项目的正式发布带来麻烦。所以现在开始编写一个LogUtil用来管理日志的输出与关闭,主要功能如下:

  1. 使用方便,只传一个参量(String msg)即可
  2. 可以通过调节阀值来控制打印等级,甚至关闭所有打印

代码如下(参考:LogUtil:郭霖《第一行代码 第二版》p464):

LogUtil.java

import android.util.Log;/*** Created by Likianta_DoDoRa on 2017/11/30 0030.*/public class LogUtil {/*** 自制Log管理工具,通过设定level的等级,来显示自己想看的Log,而且只需要传入msg字段即可,tag统一为默认的“likianta_ddr”。* level设的越高,显示的信息就越危险。不过level设为NOTHING(=6)的时候,该Log工具将会关闭。因此在apk正式发布前,应把level调到NOTHING,从而把LogUtil关闭掉。*/public static final int VERBOSE = 1;public static final int DEBUG = 2;public static final int INFO = 3;public static final int WARN = 4;public static final int ERROR = 5;public static final int NOTHING = 6;public static final String TAG = "likianta_ddr"; //你也可以更改为自己想要的TAGpublic static int level = VERBOSE;public static void v(String msg){if (level <= VERBOSE){Log.d(TAG,msg);}}public static void d(String msg){if (level <= DEBUG){Log.d(TAG,msg);}}public static void i(String msg){if (level <= INFO){Log.d(TAG,msg);}}public static void w(String msg){if (level <= WARN){Log.d(TAG,msg);}}public static void e(String msg){if (level <= ERROR){Log.d(TAG,msg);}}}

使用方法:

  1. LogUtil.d("输入一些文字");
  2. 打开Logcat,在过滤器中输入自定义的TAG名称以查看日志信息

1.4. 添加以下依赖库

本章中新增的有:Glide(用于加载图片)、Glide-transformation(Glide增强工具)、Gson(用于处理Json数据):

app:build.gradle

dependencies {..pile 'me.relex:circleindicator:1.2.2'compile 'de.hdodenhof:circleimageview:2.2.0'compile 'com.github.bumptech.glide:glide:3.7.0'compile 'jp.wasabeef:glide-transformations:2.0.0'compile 'com.google.code.gson:gson:2.8.0'
}

配置完成注意点击“Sync Now”(后面的文章不再提醒此操作)。

下面开始正文。


2. 使用SecuritySharedPreference完成登录的加/解密过程

2.1. 配置SecuritySharedPreference

这里本来想用Github上的SecuredPreferenceStore项目,但是发现初始化函数会出错,并且EncrtyManager也无法正常使用,所以就放弃了。

后来使用@随便想个名字 的代码,功能和之前想用的SecuredPreferenceStore相同,并且支持旧数据的迁移,所以就完全照搬了:

作者的博客地址:【Android安全】自带加密光环的SharedPreference - CSDN博客

1. 新建SecuritySharedPreference.java(以下代码直接复制即可):

import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;/*** 自动加密SharedPreference* Created by Max on 2016/11/23.*/public class SecuritySharedPreference implements SharedPreferences {private SharedPreferences mSharedPreferences;private static final String TAG = SecuritySharedPreference.class.getName();private Context mContext;/*** constructor* @param context should be ApplicationContext not activity* @param name file name* @param mode context mode*/public SecuritySharedPreference(Context context, String name, int mode){mContext = context;if (TextUtils.isEmpty(name)){mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);} else {mSharedPreferences =  context.getSharedPreferences(name, mode);}}@Overridepublic Map<String, String> getAll() {final Map<String, ?> encryptMap = mSharedPreferences.getAll();final Map<String, String> decryptMap = new HashMap<>();for (Map.Entry<String, ?> entry : encryptMap.entrySet()){Object cipherText = entry.getValue();if (cipherText != null){decryptMap.put(entry.getKey(), entry.getValue().toString());}}return decryptMap;}/*** encrypt function* @return cipherText base64*/private String encryptPreference(String plainText){return EncryptUtil.getInstance(mContext).encrypt(plainText);}/*** decrypt function* @return plainText*/private String decryptPreference(String cipherText){return EncryptUtil.getInstance(mContext).decrypt(cipherText);}@Nullable@Overridepublic String getString(String key, String defValue) {final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);return encryptValue == null ? defValue : decryptPreference(encryptValue);}@Nullable@Overridepublic Set<String> getStringSet(String key, Set<String> defValues) {final Set<String> encryptSet = mSharedPreferences.getStringSet(encryptPreference(key), null);if (encryptSet == null){return defValues;}final Set<String> decryptSet = new HashSet<>();for (String encryptValue : encryptSet){decryptSet.add(decryptPreference(encryptValue));}return decryptSet;}@Overridepublic int getInt(String key, int defValue) {final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);if (encryptValue == null) {return defValue;}return Integer.parseInt(decryptPreference(encryptValue));}@Overridepublic long getLong(String key, long defValue) {final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);if (encryptValue == null) {return defValue;}return Long.parseLong(decryptPreference(encryptValue));}@Overridepublic float getFloat(String key, float defValue) {final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);if (encryptValue == null) {return defValue;}return Float.parseFloat(decryptPreference(encryptValue));}@Overridepublic boolean getBoolean(String key, boolean defValue) {final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);if (encryptValue == null) {return defValue;}return Boolean.parseBoolean(decryptPreference(encryptValue));}@Overridepublic boolean contains(String key) {return mSharedPreferences.contains(encryptPreference(key));}@Overridepublic SecurityEditor edit() {return new SecurityEditor();}@Overridepublic void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);}@Overridepublic void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);}/*** 处理加密过渡*/public void handleTransition(){Map<String, ?> oldMap = mSharedPreferences.getAll();Map<String, String> newMap = new HashMap<>();for (Map.Entry<String, ?> entry : oldMap.entrySet()){Log.i(TAG, "key:"+entry.getKey()+", value:"+ entry.getValue());newMap.put(encryptPreference(entry.getKey()), encryptPreference(entry.getValue().toString()));}Editor editor = mSharedPreferences.edit();editor.clear()mit();for (Map.Entry<String, String> entry : newMap.entrySet()){editor.putString(entry.getKey(), entry.getValue());}editormit();}/*** 自动加密Editor*/final class SecurityEditor implements Editor {private Editor mEditor;/*** constructor*/private SecurityEditor(){mEditor = mSharedPreferences.edit();}@Overridepublic Editor putString(String key, String value) {mEditor.putString(encryptPreference(key), encryptPreference(value));return this;}@Overridepublic Editor putStringSet(String key, Set<String> values) {final Set<String> encryptSet = new HashSet<>();for (String value : values){encryptSet.add(encryptPreference(value));}mEditor.putStringSet(encryptPreference(key), encryptSet);return this;}@Overridepublic Editor putInt(String key, int value) {mEditor.putString(encryptPreference(key), encryptPreference(Integer.toString(value)));return this;}@Overridepublic Editor putLong(String key, long value) {mEditor.putString(encryptPreference(key), encryptPreference(Long.toString(value)));return this;}@Overridepublic Editor putFloat(String key, float value) {mEditor.putString(encryptPreference(key), encryptPreference(Float.toString(value)));return this;}@Overridepublic Editor putBoolean(String key, boolean value) {mEditor.putString(encryptPreference(key), encryptPreference(Boolean.toString(value)));return this;}@Overridepublic Editor remove(String key) {mEditor.remove(encryptPreference(key));return this;}/*** Mark in the editor to remove all values from the preferences.* @return this*/@Overridepublic Editor clear() {mEditor.clear();return this;}/*** 提交数据到本地* @return Boolean 判断是否提交成功*/@Overridepublic boolean commit() {return mEditormit();}/*** Unlike commit(), which writes its preferences out to persistent storage synchronously,* apply() commits its changes to the in-memory SharedPreferences immediately but starts* an asynchronous commit to disk and you won't be notified of any failures.*/@Override@TargetApi(Build.VERSION_CODES.GINGERBREAD)public void apply() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {mEditor.apply();} else {commit();}}}
}

2. 新建EncryptUtil.java

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;/*** AES加密解密工具* @author Max* 2016年11月25日15:25:17*/
public class EncryptUtil {private String key;private static EncryptUtil instance;private static final String TAG = EncryptUtil.class.getSimpleName();private EncryptUtil(Context context){String serialNo = getDeviceSerialNumber(context);//加密随机字符串生成AES keykey = SHA(serialNo + "#$ERDTS$D%F^Gojikbh").substring(0, 16);Log.e(TAG, key);}/*** 单例模式* @param context context* @return*/public static EncryptUtil getInstance(Context context){if (instance == null){synchronized (EncryptUtil.class){if (instance == null){instance = new EncryptUtil(context);}}}return instance;}/*** Gets the hardware serial number of this device.** @return serial number or Settings.Secure.ANDROID_ID if not available.*/@SuppressLint("HardwareIds")private String getDeviceSerialNumber(Context context) {// We're using the Reflection API because Build.SERIAL is only available// since API Level 9 (Gingerbread, Android 2.3).try {String deviceSerial = (String) Build.class.getField("SERIAL").get(null);if (TextUtils.isEmpty(deviceSerial)) {return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);} else {return deviceSerial;}} catch (Exception ignored) {// Fall back  to Android_IDreturn Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);}}/*** SHA加密* @param strText 明文* @return*/private String SHA(final String strText){// 返回值String strResult = null;// 是否是有效字符串if (strText != null && strText.length() > 0){try{// SHA 加密开始MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");// 传入要加密的字符串messageDigest.update(strText.getBytes());byte byteBuffer[] = messageDigest.digest();StringBuffer strHexString = new StringBuffer();for (int i = 0; i < byteBuffer.length; i++){String hex = Integer.toHexString(0xff & byteBuffer[i]);if (hex.length() == 1){strHexString.append('0');}strHexString.append(hex);}strResult = strHexString.toString();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}}return strResult;}/*** AES128加密* @param plainText 明文* @return*/public String encrypt(String plainText) {try {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");cipher.init(Cipher.ENCRYPT_MODE, keyspec);byte[] encrypted = cipher.doFinal(plainText.getBytes());return Base64.encodeToString(encrypted, Base64.NO_WRAP);} catch (Exception e) {e.printStackTrace();return null;}}/*** AES128解密* @param cipherText 密文* @return*/public String decrypt(String cipherText) {try {byte[] encrypted1 = Base64.decode(cipherText, Base64.NO_WRAP);Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");cipher.init(Cipher.DECRYPT_MODE, keyspec);byte[] original = cipher.doFinal(encrypted1);String originalString = new String(original);return originalString;} catch (Exception e) {e.printStackTrace();return null;}}
}

作者采用的加密逻辑为:

我们实现了SharedPreference接口和SharedPreference.Editor接口,重写了保存数据和取出数据的方法,我们在保存数据和取出数据的时候加了加解密层,这样可以保证我们在操作自定义的SharedPreference时候像调用原生的一样简单。

我采用的是AES128的加密方式,首先获取当前设备的序列号,然后拼接一个随机字符串,生成hash值,作为AES加密的key

现在还不用研究这些代码,只要记住SecuritySharedPreference的使用方法和普通的SharedPreferences相同就行了。(EncrypUtil本章中暂时用不到)
在以后的章节中我们会逐渐学习使用自定义主密码+签名的方式来生成AES256加密级别的key,这将会帮助我们实现密码被盗后通过更改签名来阻止数据被破解。

下面我们开始把第一章中使用到的SharedPreferences存取全部替换为SecuritySharedPreference:

2.2. LoginActivity重制

**LoginActivity.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)**

package com.likianta.anykey;import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.method.PasswordTransformationMethod;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;/*** Created by Likianta_DoDoRa on 2017/11/9 0009.*/public class LoginActivity extends AppCompatActivity {final String DEFAULT = "44d5fz533393";private EditText etPassword;private TextView loginWelcome;private Button loginOn;private String passwordValue;private SharedPreferences sharedPreferences;private SecuritySharedPreference securitySharedPreference;private EncryptUtil encryptUtil;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);//隐藏标题栏if (getSupportActionBar() != null) {getSupportActionBar().hide();}//绑定按钮etPassword = (EditText) findViewById(R.id.et_password);loginWelcome = (TextView) findViewById(R.id.login_welcome);loginOn = (Button) findViewById(R.id.login_on);//以密文形式显示你的输入etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());/*//这些代码可以删除了sharedPreferences = this.getSharedPreferences("UserInfo", Context.MODE_PRIVATE); final String saved_userPassword = sharedPreferences.getString("UserPassword", "N/A");final SharedPreferences.Editor editor = sharedPreferences.edit(); //SharedPreferences是抽象类,不能直接编辑,因此创造一个editor来负责sp的读写操作*///SecuritySharedPreferencesecuritySharedPreference = new SecuritySharedPreference(getApplicationContext(), "user_login", MODE_PRIVATE); //PRIVATE模式表示只有本应用可以读写该实例final String saved_masterPassword = securitySharedPreference.getString("MasterPassword", DEFAULT); //第一个参数是键,第二个参数是get不到该键时的返回值//我们在第一次安装该应用时,sharedPreferences肯定啥都get不到,所以就会返回第二个参数DEFAULT,这是我们判断用户是不是初次使用的重要依据final SecuritySharedPreference.Editor editor = securitySharedPreference.edit();LogUtil.d("Your masterPassword is: " + saved_masterPassword);//判断该用哪条欢迎语changeLoginWelcome(saved_masterPassword);//首次登录判断的业务逻辑为:判断密码是不是DEFAULT,是的话就是首次登录,此时文本框的欢迎词就变了;不是的话就什么事情也不做//触发点击事件,监听登录按钮loginOn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {passwordValue = etPassword.getText().toString().trim(); //trim()函数可以消除字符串前后的空白符,避免用户手误多按一个空格if (saved_masterPassword.equals(DEFAULT)) {//首次使用的业务逻辑editor.putString("MasterPassword", passwordValue).apply();Toast.makeText(LoginActivity.this, "创建成功", Toast.LENGTH_SHORT).show();//跳转至主界面Intent intent = new Intent(LoginActivity.this, MainActivity.class);LoginActivity.this.startActivity(intent);finish();} else {//非首次登录时的常规业务if (passwordValue.equals(saved_masterPassword)) {Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();//跳转至主界面Intent intent = new Intent(LoginActivity.this, MainActivity.class);LoginActivity.this.startActivity(intent);finish();} else {//密码错误时的业务changeLoginWelcome("WRONG_CODE");etPassword.setText(""); //重置密码框为空//让输入框重新获取焦点etPassword.setFocusable(true);etPassword.setFocusableInTouchMode(true);etPassword.requestFocus();//通过调用输入管理器来弹出软键盘InputMethodManager inputMethodManager = (InputMethodManager) etPassword.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);inputMethodManager.showSoftInput(etPassword, 0);}}}});etPassword.setOnKeyListener(new View.OnKeyListener() {@Overridepublic boolean onKey(View view, int i, KeyEvent keyEvent) {if (i == keyEvent.KEYCODE_ENTER) {loginOn.callOnClick(); //callOnClick是performClick的简化版,适合简单触发点击监听事件return true;}return false;/** 另外再设置一个监听软键盘输入,当输入Enter的时候,也能触发上面的登录按钮* 关于返回值的说明* 返回值为true,表示事件已完全处理,系统无需再处理此键* 返回值为false,表示事件处理过后,还要交给系统继续处理* 参考此回答:.html*/}});}// Select welcome words.public void changeLoginWelcome(String userCode) {switch (userCode) {case DEFAULT:// 首次使用时的欢迎语loginWelcome.setText("首次使用\n请创建你的主密码");break;case "WRONG_CODE":// 密码输入错误时的欢迎语loginWelcome.setText("密码错误\n请重新登录"); // 在“欢迎词”中显示出错提示loginWelcome.setTextColor(0xFFF4297E);final AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.6f); // 不透明度的参数取值是小数,范围在0-1之间,0表示完全透明,1表示完全显示alphaAnimation.setDuration(500); // 动画持续时间alphaAnimation.setFillAfter(true); // 表示动画停留在最终完成的状态上loginWelcome.setAnimation(alphaAnimation);alphaAnimation.startNow();break;}}//监听按下的按键,如果按下了返回键,则退出登录界面@Overridepublic boolean onKeyDown(int keyCode, KeyEvent keyEvent) {if (keyCode == KeyEvent.KEYCODE_BACK) {LoginActivity.this.finish();return true;}return super.onKeyDown(keyCode, keyEvent);}
}

至此登录流程已完整实现。下面开始本章的重头戏——表单数据的绑定和加载。

3. 填表数据的绑定和加载

注:这里给一些文件改名了,比如“page_item_all”和“page_item_new”改成了“page_card_list”和“page_card_new”(使用shift+f6可以全局改名)。
另外删掉了一些没有用到的类,具体变更可参照下图:

3.1. 右分页布局

在04章中已经制作好了右分页的布局,这里重新贴出整体的代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android=""xmlns:app=""xmlns:tools=""android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/mycolorBackground"android:padding="5dp"><!-- 最外层是一个RelativeLayout,内边距5dp,背景色与左分页背景同色 --><ScrollView
        android:layout_width="match_parent"android:layout_height="match_parent"android:layout_above="@id/userInfoSave"android:layout_marginBottom="5dp"><RelativeLayout
            android:layout_width="match_parent"android:layout_height="wrap_content"><!-- 这里同样使用的是RelativeLayout。因为考虑到以后可能还需要在上面贴一些控件等……所以用相对布局更好一些--><!-- 头像的背景,根据头像主色调来生成(方法在函数中实现) --><ImageView
                android:id="@+id/userHeadBackground"android:layout_width="match_parent"android:layout_height="108dp"android:background="#8e8e8e" /><!-- 使用开源控件CircleImageView制作圆形头像 --><de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/userHead"android:layout_width="96dp"android:layout_height="96dp"android:layout_centerHorizontal="true"android:layout_marginTop="60dp"android:padding="5dp" /><!-- 常规表单填写 --><TableLayout
                android:id="@+id/table1"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_below="@id/userHead"android:padding="30dp"android:shrinkColumns="1"android:stretchColumns="1"><!-- 第一行,标题栏 --><TableRow><TextView
                        android:layout_margin="5dp"android:text="标题"android:textSize="16sp" /><EditText
                        android:id="@+id/userTitle"android:layout_height="30dp"android:layout_margin="5dp"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textColorHint="@color/colorAccent"android:textSize="16sp" /><!-- 分组按钮 --><ImageView
                        android:id="@+id/userTitleGroup"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="5dp"android:padding="2dp"android:src="@drawable/icon_folder" /></TableRow><!-- 第二行,帐号栏 --><TableRow><TextView
                        android:layout_margin="5dp"android:text="帐号"android:textSize="16sp" /><EditText
                        android:id="@+id/userName"android:layout_height="30dp"android:layout_margin="5dp"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 这个按钮是用来关联同标题的小号的,点击后会让你设置哪个是大号,哪个是隐私号,哪些是小号……小号还可以手动排序 --><ImageView
                        android:id="@+id/userNameLink"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="5dp"android:padding="2dp"android:src="@drawable/icon_attach" /></TableRow><!-- 第三行,密码栏 --><TableRow><TextView
                        android:layout_margin="5dp"android:text="密码"android:textSize="16sp" /><EditText
                        android:id="@+id/userPassword"android:layout_height="30dp"android:layout_margin="5dp"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 随机密码生成按钮 --><ImageView
                        android:id="@+id/userPasswordRandom"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="5dp"android:padding="4dp"android:src="@drawable/icon_random" /></TableRow><!-- 第四行,比较特殊,是一排绑定按钮 --><TableRow><TextView
                        android:layout_gravity="center_vertical"android:layout_margin="5dp"android:text="绑定"android:textSize="20sp" /><!-- 这些绑定按钮可自定义还可横向滚动 --><HorizontalScrollView
                        android:id="@+id/userBoundBar"android:layout_gravity="center"android:layout_span="5"><LinearLayout
                            android:layout_width="wrap_content"android:layout_height="match_parent"><ImageView
                                android:id="@+id/userBoundQq"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_qq" /><ImageView
                                android:id="@+id/userBoundWeixin"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_weixin" /><ImageView
                                android:id="@+id/userBoundWeibo"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_weibo" /><ImageView
                                android:id="@+id/userBoundPhone"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_phone" /><ImageView
                                android:id="@+id/userBoundEmail"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_email" /><ImageView
                                android:id="@+id/userBoundWangyi"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="15dp"android:src="@drawable/icon_bound_wangyi" /></LinearLayout></HorizontalScrollView><!-- 对绑定按钮进行详细设置,比如默认值、同类型小号(红色显示)、新增自定义字段等 --><ImageView
                        android:id="@+id/userBoundMore"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="5dp"android:src="@drawable/icon_more" /></TableRow><!-- 第五行,URL --><TableRow><TextView
                        android:layout_margin="5dp"android:text="URL"android:textSize="16sp" /><!-- 默认是单行输入的 --><EditText
                        android:id="@+id/userUrl"android:layout_height="30dp"android:layout_margin="5dp"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 通过此按钮可以切换单/多行输入模式 --><ImageView
                        android:id="@+id/userUrlMultilines"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:layout_margin="5dp"android:src="@drawable/icon_add2" /></TableRow><!-- 第六行,备注栏 --><TableRow><!-- 备注栏是多行输入的 --><EditText
                        android:id="@+id/userNote"android:layout_marginTop="15dp"android:layout_span="7"android:background="@drawable/bg_edittext"android:hint="请在此输入备注"android:padding="16dp"android:textSize="14sp" /></TableRow></TableLayout><!-- 这个小按钮比较特殊,是专为备注栏服务的,功能是点击一下生成一条分割线默认状态是隐藏的,只有当备注栏获取焦点时才会出现 --><ImageView
                android:id="@+id/userNoteSlash"android:layout_width="30dp"android:layout_height="30dp"android:layout_alignBottom="@id/table1"android:layout_alignParentEnd="true"android:layout_marginBottom="40dp"android:layout_marginEnd="10dp"android:background="#0000"android:padding="6dp"android:src="@drawable/icon_slash" /></RelativeLayout></ScrollView><!-- 保存按钮 --><Button
        android:id="@+id/userInfoSave"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="@drawable/bg_button"android:text="保存"android:textSize="16sp"android:visibility="visible" /></RelativeLayout>

布局中用到的drawable资源可以在此下载:链接: 密码: ivce

3.2. 保存用户数据

这里分为三部分来考虑:

  1. 点击保存按钮,产生新的卡片数据
  2. 在cardList中添加此卡片并出现在左分页中
  3. 在SecuritySharedPreference中保存卡片数据(保存到/data/data//my_prefs/包名/card_data.xml文件中)

那么首先我们来看点击保存按钮是的业务:

3.2.1. 点击“保存”按钮时的业务

1. MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {//以下变量是与分页相关的变量private TextView titleAll; //标题文字“全部”private TextView titleNew; //标题文字“新增”private CircleIndicator indicator; //滚动指示器private View page1; //左分页private View page2; //右分页private ViewPager viewPager; //控制分页逻辑的容器private ArrayList<View> pageList; //装载分页元素的容器//以下变量是与左分页相关的控件private RecyclerView recyclerView1; //卡片列表,注意改名啦(原先名字是“recyclerView”)public CardAdapter cardAdapter;public List<Card> cardList = new ArrayList<Card>(); //卡片数据//以下变量是与右分页相关的控件private Button saveUserButton; //右分页“保存”按钮@Overrideprotected void onCreate(Bundle savedInstanceState) {...//用LayoutInflater来绑定布局LayoutInflater inflater = getLayoutInflater();page1 = inflater.inflate(R.layout.page_card_list, null); //预加载左分页page2 = inflater.inflate(R.layout.page_card_new, null); //预加载右分页pageList = new ArrayList<View>(); //pageList被实例化为装载View元素的数组pageList.add(page1);pageList.add(page2);//add的先后顺序不要搞错,先add的就是array[0]位置的元素了...//绑定右分页的按钮saveUserButton = (Button) page2.findViewById(R.id.userInfoSave);//监听按钮点击...saveUserButton.setOnClickListener(this);}@Overridepublic void onClick(View view) {switch (view.getId()) {...case R.id.userInfoSave://点击右分页的保存按钮LogUtil.d("You clicked userInfoSave button.");//首先判断标题是不是空的,空的话必须填写标题if (new SaveUserInfo(page2).isTitleEmpty()) {//SaveUserInfo.java是重点,会新建一个类来写Toast.makeText(MainActivity.this, "标题不能为空!", Toast.LENGTH_SHORT).show();break;}Card card = new SaveUserInfo(page2).getNewCard();cardList.add(0, card); //在零号位(也就是第一位)添加这张新卡片cardAdapter.notifyItemInserted(0); //添加后要给RecyclerView释放一个更新信号。具体方法写在CardAdapter.java里viewPager.setCurrentItem(0); //自动跳转到左分页recyclerView1.scrollToPosition(0); //自动跳转到列表首部,方便用户看到自己新增的卡片break;}}}

2. 新建SaveUserInfo.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)

package com.likianta.anykey;import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;import com.google.gson.Gson;import de.hdodenhof.circleimageview.CircleImageView;/*** Created by Likianta_DoDoRa on 2017/11/27 0027.*/public class SaveUserInfo extends MainActivity {private EditText userTitle;private EditText userName;private EditText userPassword;private EditText userUrl;private EditText userNote;private CircleImageView userHead;private String uTitle;private String uName;private String uPassword;private String uUrl;private String uNote;private Card card;public SaveUserInfo(View pageview) {LogUtil.d("向SaveUserInfo()传入右分页布局");userTitle = (EditText) pageview.findViewById(R.id.userTitle);userName = (EditText) pageview.findViewById(R.id.userName);userPassword = (EditText) pageview.findViewById(R.id.userPassword);userUrl = (EditText) pageview.findViewById(R.id.userUrl);userNote = (EditText) pageview.findViewById(R.id.userNote);userHead = (CircleImageView) pageview.findViewById(R.id.userHead);uTitle = userTitle.getText().toString();uName = userName.getText().toString();uPassword = userPassword.getText().toString();uUrl = userUrl.getText().toString();uNote = userNote.getText().toString();LogUtil.d("User note is " + uNote);getNewCard(); //生成新卡片的操作}//检测保存时的标题栏的文字是否为空,空的话禁止保存并提醒填写public boolean isTitleEmpty() {if (uTitle.equals("")) {userTitle.setHint("请输入标题");userTitle.setFocusable(true);userTitle.setFocusableInTouchMode(true);userTitle.requestFocus();//通过调用输入管理器来弹出软键盘InputMethodManager inputMethodManager = (InputMethodManager) userTitle.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);inputMethodManager.showSoftInput(userTitle, 0);return true;}return false;}public Card getNewCard() {String uSummary = uName + "\n" + uPassword + "\n" + uNote; //卡片的摘要=用户名+密码+备注card = new Card(uTitle, uSummary); //目前我们用到的卡片属性只有这两个,后面会逐渐补充完整return card;}}

还记得刚才在MainActivity中点击“保存”按钮时的业务代码吗?

然后CardAdapter中还需要添加相关的代码才行:

3. CardAdapter.java

public class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> {...public void add(int position, Card card) {cardList.add(position, card);notifyItemInserted(position);}}

当然对于一个CardAdapter来说,光有添加卡片的业务是不行的,还要有删除卡片的业务:

import android.widget.Toast;public class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> {...public void add(int position, Card card) {...}public void remove(Context context, int position) {if (position < 0) {Toast.makeText(context, "列表中并没有结果!", Toast.LENGTH_SHORT).show();} else {cardList.remove(position);notifyItemRemoved(position);}}}

其实还有更多的操作(比如删除全部卡片)没写,不过那些目前都用不上,等到以后有需求再添加。

至此,我们已经完成了最初构想的两大部分了:

**1. 点击保存按钮,产生新的卡片数据
2. 在cardList中添加此卡片并出现在左分页中**

接下来要做的是完成最后一部分——在SecuritySharedPreference中保存卡片数据。

3.2.2. 保存卡片数据到本地(SavedToMySharedPrefs)

新建SavedToMySharedPrefs.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)

package com.likianta.anykey;import android.content.Context;
import android.content.SharedPreferences;
import android.widget.Toast;import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;import java.util.ArrayList;
import java.util.List;import static android.content.Context.MODE_PRIVATE;/*** Created by Likianta_DoDoRa on 2017/12/2 0002.*/public class SavedToMySharedPrefs {//建议在退出程序时调用,以将数据保存到文件中private SecuritySharedPreference securitySharedPreference;private SharedPreferences.Editor editor;public SavedToMySharedPrefs(Context context, String preferenceName) {securitySharedPreference = new SecuritySharedPreference(context, preferenceName, MODE_PRIVATE);editor = securitySharedPreference.edit();}/*** 获取cardList*/public List<Card> getCardData() {List<Card> cardList = new ArrayList<>();String strJson = securitySharedPreference.getString("Card", null);LogUtil.d("从已保存的card_data文件中获取列表!strJson="+strJson);if (null == strJson) {LogUtil.d("获取到的json为空!");return cardList;}Gson gson = new Gson();cardList = gson.fromJson(strJson, new TypeToken<List<Card>>() {}.getType());for (Card card:cardList){LogUtil.d("Card info 1 "+card.getCardTitle());LogUtil.d("Card info 2 "+card.getCardSummary());}LogUtil.d("获取到的cardList是"+cardList);return cardList;}/*** 保存cardList*/public void setCardData(List<Card> cardList) {if (null == cardList || cardList.size() <= 0) {return;}Gson gson = new Gson();//转换成json数据,再保存String strJson = gson.toJson(cardList);editor.clear();editor.putString("Card", strJson);editor.apply();}/*** 获取userItemList*//*** 保存userItemList*/}

注意代码中我写了两个获取和两个保存方法(后面两个还没有写相关代码)。
这里将Card和UserItem分开考虑,它们的关系为:

card_data.xml(加密保存)user_data.xml(加密保存)
特点内容量少,体积更小,适合快速装载,可用于全文搜索内容详细,点击卡片后展示,用于详情页显示
包含对象1. 标题
2. 摘要
3. 头像
4.标识码
1. 标题
2. 用户名(帐号)
3. 密码
4. Url
5. 备注
6. 头像及背景
7. 扩展信息(绑定关系、密保信息、二级密码、用户昵称等)
8. 自动生成的信息等
9.标识码
什么是标识码?每个card与useritem通过标识码建立唯一联系。
标识码的生成规则(暂定):标题(String)+账号等级(int)(示意图如下)

这个账号管理布局是通过点击右分页的“@id/userLink”按钮打开的

本章只讲cardList的绑定和加载,UserItem会在之后的学习中补上。

4. 运行测试

为了测试我们的数据保存是否成功,我们需要完成以下功能:

  1. 退出时加密保存当前数据到“/data/data/com.likianta.anykey/my_prefs/card_data.xml”(这里用一个test按钮实现)
  2. 再次启动app,进入主界面后,需要从本地(card_data.xml)取出卡片数据

具体代码如下:

1. 在activity_main.xml中添加一个测试按钮:

activity_main.xml

<RelativeLayout ...>...<Button
        android:id="@+id/test_save"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="test save"android:layout_alignParentTop="true"android:layout_alignParentStart="true"/></RelativeLayout>

2. 在MainActivity.java中添加相关业务:

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {@Overrideprotected void onCreate(Bundle savedInstanceState) {...//test saveButton button = (Button)findViewById(R.id.test_save);button.setOnClickListener(this);//为分页加载卡片列表PageRender();}@Overridepublic void onClick(View view) {switch (view.getId()) {...case R.id.test_save:    new SavedToMySharedPrefs(MainActivity.this,"card_data").setCardData(cardList);MainActivity.this.finish();break;}}//渲染分页(即加载卡片列表)public void PageRender() {cardList = new SavedToMySharedPrefs(MainActivity.this,"card_data").getCardData(); //从本地文件加载卡片数据cardAdapter = new CardAdapter(cardList); //将数组数据适配为卡片数据recyclerView1 = (RecyclerView) page1.findViewById(R.id.recyclerView);recyclerView1.setLayoutManager(new LinearLayoutManager(this)); //为recyclerView设置线性布局,使内部元素呈线性排列recyclerView1.setAdapter(cardAdapter); //开始加载卡片}}

为了保证测试能够成功,先把手机(或者模拟器)上的app给卸载掉。
然后启动调试:


至此,本章的主要内容已经讲完了。
本章的代码虽然特别多,但主要只做了两件事:一个是通过SecuritySharedPreference重新完成了登录的流程;另一个则是成功实现了卡片数据的保存以及卡片列表的加载更新。
当然我们还有很多必要的事情没有去做,比如删除卡片、点击保存按钮要把当前表单清空、以及最容易被人忽略的布局优化。

随着项目逐渐变大,对于内存的要求也开始增加。此时可能已经出现使用真机调试会发生OOM(Out
of Memory)的崩溃,下面在更新章节中提到的一些办法会在一定程度上避免OOM的问题(不同手机可能遇到的状况不同。我的红米Note2手机经过下面的修改在调试时已经不再崩溃了)。之后我们会一边继续完善当前的代码,一边严格控制运行消耗,特别是图像加载导致的oom问题需要多加注意。

在接下来的章节中将开始制作分组列表、底部导航按钮以及优化右分页布局,以减轻GPU渲染压力。


5. 更新修复

5.1. 一些小的更新修复

1. card布局微调

主要修改了距离属性。整体代码如下:

card.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android=""xmlns:app=""xmlns:tools=""android:layout_width="match_parent"android:layout_height="120dp"android:layout_margin="5dp"android:elevation="4dp"app:cardCornerRadius="8dp"><!--elevation表示卡片的高度cardCornerRadius表示卡片四个角的弧度xmlns:tools用于识别TextView中的\n换行符--><!--CardView本身是一个FrameLayout,显然不适合摆放控件。因此为了充分利用空间,要内建一个RelativeLayout来盛放子控件--><RelativeLayout
        android:layout_width="match_parent"android:layout_height="match_parent"><!--首先思路就是把RelativeLayout分为左中右三个部分,左边贴一个头像;中间占的面积最大,用来写标题和摘要;右边则放置功能按钮--><!--现在写的是中间的布局LinearLayout。在LinearLayout中上半部分显示标题,下半部分显示摘要--><LinearLayout
            android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginEnd="40dp"android:layout_marginStart="80dp"android:orientation="vertical"><!--注意设定LinearLayout的方向为vertical--><!--标题,深色,字号较大,高度比重为40%--><TextView
                android:id="@+id/cardTitle"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="4"android:gravity="bottom"android:padding="4dp"android:text="TitleTest"android:textColor="@color/mycolorText1"android:textSize="32sp" /><!--摘要,浅色或同色,字号比正常文字还要小,高度比重为60%,限制显示三行文字--><TextView
                android:id="@+id/cardSummary"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="6"android:ellipsize="end"android:lines="3"android:maxLines="3"android:text="name: ______\npassword: ______\nnote: this view limited 3 lines" /><!--maxLines表示最大行数,lines表示即使只有一行字也要占用三行字的高度空间,ellipsize表示多出的字数表示为省略号参考:;</LinearLayout><!--接下来写的是右边的布局,也是LinearLayout布局。从上到下依次显示三个按钮:菜单、复制name、复制password--><LinearLayout
            android:layout_width="40dp"android:layout_height="match_parent"android:layout_alignParentEnd="true"android:orientation="vertical"android:padding="5dp"><!--菜单按钮--><ImageView
                android:id="@+id/cardMenu"android:layout_width="32dp"android:layout_height="32dp"android:src="@drawable/icon_more" /><!-- 复制用户名按钮 --><ImageView
                android:id="@+id/copyName"android:layout_width="14dp"android:layout_height="14dp"android:layout_gravity="center"android:layout_marginTop="22dp"android:src="@drawable/shape_oval1" /><!-- 复制密码按钮 --><ImageView
                android:id="@+id/copyPassword"android:layout_width="14dp"android:layout_height="14dp"android:layout_gravity="center"android:layout_marginTop="16dp"android:src="@drawable/shape_oval1" /></LinearLayout><!--不要忘了左边部分。之所以留待最后才写,是为了让头像以及分割线最后加载,这样就处于其它二者的上方了--><!--先做分割线,非常细,非均等分割(上窄下宽)--><TextView
            android:layout_width="match_parent"android:layout_height="0.4dp"android:layout_marginTop="44dp"android:background="#000000" /><!--头像--><de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/cardHead"android:layout_width="60dp"android:layout_height="60dp"android:layout_marginStart="10dp"android:layout_marginTop="16dp"android:src="@color/mycolorText2" /></RelativeLayout></android.support.v7.widget.CardView>

2. 更新build.gradle库版本到最新

更新方式很简单:将鼠标移到compile代码上,AS会检测并提示“A newer version of … is available:25.4.0 …”(如图所示),只要把“available”后面的版本填上去,再点击“Sync Now”即可同步到最新。

5.2. OOM(内存溢出)问题的临时解决方案

由于在真机调试时发现应用一进入就发生oom崩溃,可以采取以下方法来暂时解决该问题:

AndroidManifest.xml中添加一行代码:

<manifest ...><application
        android:largeHeap="true"...></application></manifest>

虽然解决了一打开就崩溃的问题,但真正问题还是没有得到解决:
当我们左右滑动页面的时候,会发现滑动到右分页掉帧严重,这是因为GPU压力过大造成的,只能通过优化布局和图像加载来真正地解决。

在下章中将详细介绍如何通过优化右分页布局来解决此问题。


相关参考

  1. LitePal:郭霖《第一行代码 第二版》p229
  2. LogUtil:郭霖《第一行代码 第二版》p464
  3. android 怎么获取字符串中指定的字符_百度知道 .html
  4. 可动态显示圆形图像或圆形文字的AvatarImageView - Carbs的博客 - CSDN博客
  5. hdodenhof/CircleImageView: A circular ImageView for Android
  6. ImageView的scaleType设置不当,导致使用Glide时出现OOM - 泡在网上的日子 .html
  7. ImageView.ScaleType设置图解 - CSDN博客
  8. ImageView的src和background的区别、padding的使用技巧、ImageView根据屏幕对缩放 - CSDN博客
  9. Glide使用详解(一) - CSDN博客 /
  10. Android RecyclerView横向滑动 - CSDN博客
  11. Android框架之路——Glide加载图片(结合RecyclerView、CardView) - CSDN博客
  12. iamMehedi/Secured-Preference-Store:
  13. SharedPreferences保存List和对象序列化数据 - jxf_access的专栏 - CSDN博客
  14. 【Android安全】自带加密光环的SharedPreference - CSDN博客
  15. Gitbook/Markdown中插入复杂(合并单元格)的表格 - 一个Technical Writer的博客 - CSDN博客

日志

2017年11月30日

  1. 【更新】采用LogUtil管理日志
  2. 【更新】整理代码中注释
  3. 【删除】不再使用LitePal,转用SharedPreferences
  4. 【修改】削减标题栏的文字可点击区域
  5. 【修改】card.xml上的小按钮改用ImageView
  6. 【新增】使用SecurePreferenceStore加/解密
  7. 【修改】首次使用时登录界面的初始值的判断
  8. 【新增】完成登录的加密解密过程

2017年12月2日

  1. 【更新】解决真机调试时的oom问题
  2. 【修改】右分页中的HorizontalScrollView改为横向RecyclerView
  3. 【删除】右分页保存按钮的selector背景和文字颜色
  4. 【新增】Glide库
  5. 【更新】使用SecuritySharedPreference加密
  6. 【新增】SavedToMySharedPrefs类用于本地存取
  7. 【更新】剔除不需要的java类(SqliteDB、OpenHelper等)

更多推荐

06 Anykey用户数据的存储和加载SecuritySharedPreference

本文发布于:2024-02-19 17:16:31,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1765250.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:加载   数据   用户   Anykey   SecuritySharedPreference

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!