自己经常会忘记一些密码什么的,想把这些密码保存下来,但是别人做的软件总有一点不安全的感觉,所以自己动手做了一个带有指纹加密的笔记本。
以下是本工程用到的一些第三方包
compile 'org.greenrobot:greendao:3.2.0'
compile 'net.zetetic:android-database-sqlcipher:3.5.1'
compile 'com.getbase:floatingactionbutton:1.10.1'
其中greendao是一款比较好用的开源数据库,具体和其他开源数据库相比好在哪里我就不介绍了,百度上面会有很多分析什么的,我就简单说一下用法就好。
需要在build.gradle中添加一下内容
#位置在build.gradle的第一行
apply plugin: 'org.greenrobot.greendao'
#位置在android{}中,主要用于管理本地数据库版本,不同本地数据库版本之
#间升级时候需要对版本进行判断
greendao {
schemaVersion 2
targetGenDir 'src/main/java'
}
#这个是在工程的build.gradle中进行配置的
#bulidscript{
dependencies {
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.0'
}
}
以上就完成greendao的配置,可以使用greendao了。
net.zetetic:android-database-sqlcipher:3.5.1
这个是用于数据库加密的一个第三方包,可以考虑使用,也可以不使用,毕竟只要你不自己主动将本地数据库提供出去,别人就不能直接从数据库中获得相应的信息
最后一个第三方库是一个floatingActionButton,提供了floatingActionMenu,是那种可以弹出悬浮按钮的效果
下面看一下工程的结构
当然之后可能还会变化,这个是我目前的进度。
下面开始介绍项目各个部分的实现,首先是主页面,主页面是一个比较简单,除了toolbar之外只有一个RecyclerView,用来以列表的方式展示便签的简要信息。资源文件内容如下
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.leaveme.notebook.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fab_icon="@drawable/ic_add_white_24dp"
/>
</android.support.design.widget.CoordinatorLayout>
下面的内容就比较重要了,因为现在android版本都已经在5.0以上了,所以需要进行权限申请,需要将项目中用到的权限,比如读写存储卡的权限,指纹识别的权限,这就需要除了在Manifest中列清楚之外还要动态申请。
private void Permissinit(){
//需要请求的权限请求字符串列表
List<String> permissionsNeeded = new ArrayList<String>();
//权限请求列表
final List<String> permissionsList = new ArrayList<String>();
//添加读写存储空间、读取手机状态、拨打电话、读取位置信息、读取精确位置信息这些权限到权限请求列表中
if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE))
permissionsNeeded.add("\n\r读存储空间");
if (!addPermission(permissionsList, Manifest.permission.READ_EXTERNAL_STORAGE))
permissionsNeeded.add("\n\r写存储空间");
if (!addPermission(permissionsList, Manifest.permission.INTERNET))
permissionsNeeded.add("\n\r联网");
if (!addPermission(permissionsList, Manifest.permission.USE_FINGERPRINT))
permissionsNeeded.add("\n\r使用指纹识别");
//如果权限请求列表中的内容大于0个,则开始请求权限
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) {
//获取到第一个需要添加请求列表的权限
String message = "你需要获取已下权限:" + permissionsNeeded.get(0);
//循环将剩余需要请求的权限加入到请求列表
for (int i = 1; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
//开始向系统请求权限
ActivityCompat.requestPermissions(this,permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
}
先将需要请求的权限打包放在权限求情列表中,当然首先判断是否需要请求该权限,毕竟有的权限不需要动态请求也是可以获得的,这个时候就需要
private boolean addPermission(List<String> permissionsList, String permission) {
//判断该应用是否具备要请求的权限
if (ContextCompat.checkSelfPermission(this,permission) != PackageManager.PERMISSION_GRANTED) {
//没有该权限,则加入到请求列表
permissionsList.add(permission);
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,permission))
return false;
}
return true;
}
checkSelfPermission就是判断该APP是否可以使用某种权限。当所有需要的权限都加入到权限请求列表后,并且该列表的大小不是0个,就可以向用户请求者写权限了。
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
这个UI对话框的UI界面可以简单地向用户描述为什么需要这些权限。
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
//判断是否为该软件系统请求的权限信息
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
//判断是否请求成功
{
//请求成功
if(grantResults.length>0&&grantResults[0] == PackageManager.PERMISSION_GRANTED){
}
else{
// //请求失败,提示用户“请求权限失败
Log.e("TAG","请求权限失败");
// Toast.makeText(this,"请求权限失败,请手动设置",Toast.LENGTH_LONG).show();
// this.finish();
}
}break;
}
}
当请求获得反馈信息后,会执行onRequestPermissionsResult函数,然后可以根据是否请求成功进行相应的操作。
这样就完成了一整套动态权限获取的流程。
然后是列表页面的实现,采用RecyclerView的主要原因是其在实现了listview功能的基础上,加入了更加规范的viewHolder,而且item的复用工作不需要手动管理。前期的实现代码如下,在创建的时候初始化加载数据,然后通过onbindViewHolder将数据绑定到每一个item上,逻辑实现比较清晰。资源文件比较简单,不单独列出了,在文章的末尾提供了整个项目的github地址,方便下载查看
public class NoteAdapter extends RecyclerView.Adapter<NoteAdapter.ItemViewHolder> implements ItemTouchHelperAdapter{
private List<Note> notes = new ArrayList<>();
private Context context;
private DaoSession session;
private NoteDao noteDao;
private final OnStartDragListener mDragStartListener;
public NoteAdapter(Context context, OnStartDragListener dragStartListener){
mDragStartListener = dragStartListener;
this.context = context;
init();
}
private void init(){
session = GreenDaoHelper.getDaoSession(context);
noteDao= session.getNoteDao();
notes = noteDao.loadAll();
}
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list, parent, false);
ItemViewHolder itemViewHolder = new ItemViewHolder(view);
return itemViewHolder;
}
@Override
public void onBindViewHolder(ItemViewHolder holder, final int position) {
RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)holder;
ViewGroup.LayoutParams layoutParams = viewHolder.itemView.getLayoutParams();
layoutParams.height = LinearLayout.LayoutParams.WRAP_CONTENT;
holder.content.setText(notes.get(position).getContent());
holder.time.setText(dateFormatString.transform(notes.get(position).getTimeStamp()));
holder.title.setText(notes.get(position).getTitle());
((RecyclerView.ViewHolder) holder).itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setClass(context, NoteActivity.class);
intent.setAction(notes.get(position).getTimeStamp()+"");
context.startActivity(intent);
}
});
}
@Override
public int getItemCount() {
if(noteDao.count()!=notes.size()){
notes = noteDao.loadAll();
return notes.size();
}
return notes.size();
}
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
Collections.swap(notes, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return true;
}
@Override
public void onItemDismiss(final int position) {
new CommomDialog(context, R.style.dialog, "您确定删除此条记录?", new CommomDialog.OnCloseListener() {
@Override
public void onClick(Dialog dialog, boolean confirm) {
if(confirm){
session.getNoteDao().delete(notes.get(position));
notes.remove(position);
notifyItemRemoved(position);
dialog.dismiss();
}
}
}).setTitle("提示").show();
}
public static class ItemViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder{
public final TextView title;
public final TextView time;
public final TextView content;
public ItemViewHolder(View itemView) {
super(itemView);
title = (TextView)itemView.findViewById(R.id.tv_item_tile);
time = (TextView)itemView.findViewById(R.id.tv_item_time);
content = (TextView)itemView.findViewById(R.id.tv_item_content);
}
@Override
public void onItemSelected() {
itemView.setBackgroundColor(Color.LTGRAY);
}
@Override
public void onItemClear() {
itemView.setBackgroundColor(0);
}
}
}
在这里用到了greendao数据库初始化加载数据,具体的greendao的使用,我这里不介绍了,网上有很多教程可以找到,例如:Android GreenDao使用教程。列一下我的数据库格式,包括了以下这些column。其中@entity关键字表示这是一个实体Bean,会被greendao自动编译成一个数据库表,并形成一个notedao来管理数据库的增删查改等操作。
@Entity
public class Note {
@Id(autoincrement = true)
private long id;
private String title;//笔记标题
private String content;//笔记内容
private long timeStamp;//笔记时间
private int state;//笔记状态 0:正常状态 1:加密状态 -1:已删除状态
private String pictureId;//笔记中的图片id
private long index;//显示顺序排序
@Generated(hash = 587745031)
public Note(long id, String title, String content, long timeStamp, int state,
String pictureId, long index) {
this.id = id;
this.title = title;
this.content = content;
this.timeStamp = timeStamp;
this.state = state;
this.pictureId = pictureId;
this.index = index;
}
@Generated(hash = 1272611929)
public Note() {
}
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
public long getTimeStamp() {
return this.timeStamp;
}
public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp;
}
public int getState() {
return this.state;
}
public void setState(int state) {
this.state = state;
}
public String getPictureId() {
return this.pictureId;
}
public void setPictureId(String pictureId) {
this.pictureId = pictureId;
}
public long getIndex() {
return this.index;
}
public void setIndex(long index) {
this.index = index;
}
}
最后是NoteActivity的实现,就是写一些笔记的Activity。实现起来非常容易,页面上只有两个edittext,对其进行简单的配置即可。值得注意的是需要对启动来源进行判断,是来自直接新增一个条目,还是要修改一个条目,这里我采用的是传入一个timestamp参数,如果这个参数为空则表示是一个新增条目,则新建一个笔记条目。如果有这个timestamp参数,且不为空,那么就从数据库中获取该条笔记,并加载其中的内容,以备修改和查看。保存则是在推出的时候自动保存,包括点击android系统返回键退出或者点击上面的返回图表退出。
public class NoteActivity extends AppCompatActivity {
private EditText title;
private EditText content;
private Note note;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note);
DaoSession session = GreenDaoHelper.getDaoSession(this);
NoteDao noteDao= session.getNoteDao();
Intent intent = getIntent();
String time = intent.getAction();
long currentTime = 0;
//判断启动来源,决定是否需要新建一条笔记
if(null==time||time.equals("")){
//需要新建一条笔记
currentTime = new Date().getTime();
note = new Note();
note.setId(noteDao.count());
note.setContent("");
note.setTitle("");
note.setPictureId("");
note.setState(0);
note.setTimeStamp(currentTime);
}else {
//获取数据库中已有笔记
currentTime = Long.parseLong(time.trim());
List<Note> n = noteDao.queryBuilder().where(NoteDao.Properties.TimeStamp.eq(currentTime)).list();
if(n.size()>0) {
note = n.get(0);
}else {
Toast.makeText(this,"something error",Toast.LENGTH_SHORT).show();
}
}
initView();
}
private void initView(){
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
storeNote();
NoteActivity.this.finish();
}
});
title = (EditText) findViewById(R.id.edt_title);
content = (EditText)findViewById(R.id.edt_content);
title.setText(note.getTitle());
content.setText(note.getContent());
}
private void storeNote(){
DaoSession session = GreenDaoHelper.getDaoSession(this);
NoteDao noteDao= session.getNoteDao();
note.setContent(content.getText().toString());
note.setTitle(title.getText().toString());
//插入或者替换笔记
noteDao.insertOrReplace(note);
}
@Override
protected void onPause() {
super.onPause();
//在这里写保存笔记的目的是为了防止因为activity的生命周期执行到onstop的时候,主界面的列表已经刷新了,进而导致数据同步延迟
storeNote();
}
}