Note – 便签提醒工具

github项目:https://github.com/Kurarion/Noter

原本是本人安卓课的大作业,鉴于基本是个人独立完成,特此放到自己的博客中

该APP具有两大功能模块:1、“便签”功能

                                                 2、自定义短信规则生成通知

效果图:














第一部分:“便签”:

 

实现的功能:

  1. 用户可以自定义生成便签记录备忘事项。
  2. 用户可以随时在状态栏查阅自己生成的便签。
  3. 用户可以随时修改,取消便签。
  4. 系统记录上次生成的便签内容。

 

实现的思路:

 

通过设置Notification的各项属性创建一个具有ongoing(用户无法通过滑动直接关闭此通知)属性一条通知,然后设置这条通知的Intent(打开NotificationActivity),并设置延时意图PendingIntent封装Intent设置到Notification中完成Notification的设置,以此实现用户单击这条通知后可以进行修改并且保证了这条通知仍留在通知栏的需求,而NotificationActivity的思路则是尽量不影响用户的体验,设置为一个Dialog风格的有透明背景的小型对话框形式,并且对话框具有两个可编辑EditText用于对上次通知信息的恢复和本次的修改,出于对便签的实时性考虑,我们设定了这个便签通知使用唯一的ID=1防止重复创建,并且易于cancel。针对恢复上次通知的信息,我们采用SharedPreferences辅助类来进行数据的存储和读取

 

此功能模块APP示意图:

 

核心代码:

//开启便签功能
private  void startNote(){
    Intent intent = new Intent(this, NoteActivity.class);
    PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
    NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    Notification notification = new NotificationCompat.Builder(this)
            .setContentTitle("~~便签~~")
            .setContentText("点击我创建便签")
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.mipmap.ic_launcher)
            .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
            .setContentIntent(pi)
            .setDefaults(NotificationCompat.DEFAULT_ALL)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            //设置为用户无法手动关闭
            .setOngoing(true)
            .build();
    manager.notify(1, notification);
}

 

此功能模块开发中遇到的问题:

 

1、android中this、Activity.this、context区别与联系:上下文在安卓中无处不见,例如次功能模块中就有:Intent PendingIntent,由于APP当前运行在的状态不同,比如在Activity、Notification或者Service中,不能简简单单的使用惯例this,因为有可能当前状态根本没有一个可以使用的context,从而使用this.getApplicationContext()来实现上下文的获取,但同时不同的上下文有不同的生命周期,因此使用时需注意。

 

2、一开始设计这一部分通知该如何弹出窗口来供用户进行编辑等进一步操作的时,我原打算直接使用现有的dialog进行交互处理,但发现由于Notification只能设置Pendingintent(几乎和Intent差不多)但是Pendingintent 只能实现跳转到activity 而无法实现直接弹出一个dialog的方法,于是决定使用dialog风格的activity,这样的活动采用自定义的style实现:这里具体参考了:

http://blog.csdn.net/sinat_28520499/article/details/52827012

 

3、关于EditText控件无法获取到字符串的问题:在自己的Activity对话框中进行EditText交互,发现输入的文本信息为空,仔细考虑后,发现自己在onCreate中就直接getText()了,实际上是当对话框创建好之后,这个代码就已经执行过了并且不会在执行了,因此正确的做法是在按钮响应事件内完成getText()操作

 

 

第二部分:自定义短信规则生成通知:

 

实现的功能:

 

  1. 用户可以随时启动或关闭静态注册广播接收器(短信过滤通知功能)
  2. 用户可以自定义短信过滤规则,只获得所需短信内容并通知显示。
  3. 通知可以通过单击此通知开启新的Activity来cancel对应的通知,并且通过Toast得到使用的过滤条目的ID和注释。
  4. 用户自定义短信过滤规则可以使用正则表达式。
  5. 用户可以在过滤选项通过单击每个条目进行编辑或删除
  6. 用户可以在过滤选项活动中的菜单中进行增加条目,删除全部条目,获得帮助操作
  7. 用户自定义规则捕捉到的短信内容会根据设定的关键字进行显著标记

 

实现的思路:

这一功能模块实现较为复杂,用到了Android开发的四大组件:活动,服务,广播接收器,内容提供器。一开始我本不打算使用服务的,但是鉴于复杂度和对app占用资源的考虑,决定不使用一个广播接收器解决所有问题,实际上这么做也是此app能顺利编写的前提,因为广播接收器的生命周期并没有那么长,考虑到随着用户自定义规则数量的增多,一个接收器随时可能面临崩溃、执行不完、占用资源等潜在的问题(具体没有尝试),整体的工作流程如下:

用户打开app通过滑动按钮开启短信接收器->手机收到一条新的短信->广播接收器响应这条短信,并获得短信的发送者,发送时间和短信正文->广播接收器封装信息到Intent并StartService->短信过滤服务对短信内容进行处理,适合用户自定义的过滤规则则对正文进行关键字处理,通过通知反馈给用户,否则不进行任何显示操作

其中为了能在app关闭的时候也能响应短信接收和处理,使用了静态注册短信接收广播接收器,而针对过滤条目,采用了数据库存储,其中不免使用一些存储辅助类SharedPreferences进行编辑操作的记住恢复功能。

 

此功能模块开发中遇到的问题:

 

1、一开始短信广播接收器(静态注册)无法执行接收器功能,经查阅资料发现:安卓6.0以上需要对危险权限的动态申请,即使在Manifest.xml进行了权限申请也要动态申请。解决方法,在主活动中的onCreate中增加了运行时权限申请。

参考:1、http://androidcookie.com/717.html

2、http://blog.csdn.net/zengxyuyu/article/details/72792990

 

2、在测试APP的时候经常需要测试一些数据,我常用到Toast,但是初次使用Android Studio,就按照了上课老师使用的Eclipse的DDMS进行短信的发送结果遇到了Toast中文显示乱码的问题,因为要读取短信的内容从而进行数据处理,在DDMS向模拟发送中文短信就会得到“???”的乱码,一开始以为短信乱码是由于程序编码问题,然后尝试在bulid.gradle中增加android{compileOptions.encoding=”GBK”}的声明。但问题依旧,反而造成了麻烦。后来打开短信一看,收到的短信内容本身就是乱码….然后查询DDMS中文短信乱码解决,发现无解,最后在一篇博客中得知andorid studio可以在模拟器右侧的工具栏中发送正确的中文短信。之后模拟都不再使用DDMS了…

 

3、一开始有着这样的疑惑“静态注册的广播说是常驻的,但具体是怎样的?到底生命周期多久?”当前总结的结论:只要不被列为“黑名单”,重启之后没有开启应用就能正常使用广播接收器(API24实测,以及使用API25的实机的“黑域”APP测试)也可能是不到系统不得不清理内存的时候或者是因为我将优先级设为了1000的缘故。

 

4、采用静态注册可以实现常驻提醒,但是如何关闭这个设定,经查阅,发现可以利用PackageManager中的setComponentEnableSetting()这个函数来解决,以下是我的具体实现方法:

//取消短信广播静态注册
private void unRegister(){
    getPackageManager().setComponentEnabledSetting( new ComponentName("com.example.cocoa.myapplication", SMSReceiver.class.getName()),
            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
            PackageManager.DONT_KILL_APP);
}
//重新静态注册短信广播
private void register(){
    getPackageManager().setComponentEnabledSetting( new ComponentName("com.example.cocoa.myapplication", SMSReceiver.class.getName()),
            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
            PackageManager.DONT_KILL_APP);
    //第一次创建数据库
    Connector.getDatabase();
}

 

5、当初决定使用服务来进行过滤处理而广播接收器只进行服务的开启的做法时,查阅了广播和服务之间的区别,得知:广播的周期很短,因为不能在广播内做耗时操作,但实际上服务的声明周期也很短,原因是Activity和Service处在同一个线程中,所以都不能出现长循环和耗时操作。大多数广播的作用就是传递一些通知,或者启动一条服务去处理一些耗时的不需要UI的后台操作。需要注意:如果在Service的onCreate或者onStart做一些很耗时间的事情,最好在Service里启动一个线程来完成,因为Service是跑在主线程中,会影响到你的UI操作或者阻塞主线程中的其他事情。

 

6、为了美观起见,决定使用滑动开关来控制广播接收器的开启和关闭。显然使用一个开源的项目会让开发过程更加稳定和高效:

https://github.com/kyleduo/SwitchButton

虽然只用到了一个滑动开关,但是这也是值得的,因为从中发现Android Studio导入一个github上的开源项目是如此的方便。

 

7、查阅资料的时候发现this使用频繁,之前上一个功能模块也提到了this的参数,在编写此功能模块的时候发现可能会被this迷惑,每一个活动都有一个this,对象也有this,但到底用哪一个实际是可以变换的,因此写这个上下文参数的时候使用正确的this。参考资料:

“ this” refers to your current object. In your case you must have implemented the intent in an inner class ClickEvent, and thats what it points to.

“ Activity.this ”points to the instance of the Activity you are currently in.

 

8、因为编辑框的需要,采用Dialog的方式实现

参考的资料:http://blog.csdn.net/u014302433/article/details/49101435

 

9、测试过程中,需要用到真机测试,自然使用apk更加方便,Android Studio中导出apk的方式一共有四种:一种是未签名调试版apk,一种是未签名发行版apk,一种是已签名调试版apk,还有一种是已签名发行版apk。

(1) debug程序通常比release程序要慢,尤其是处理视频方便release要比debug快很多。在release模式对程序进行调试的时候经常会遇到变量虽然初始化了,但是在查看其值的时候却发现是一个随机的数并不是初始化的值,有时候在对变量进行监视的时候了,会出现找不到变量的情况。

(2) debug跟release在初始化变量时所做的操作是不同的,debug是将每个字节位都赋成0xcc, 而release的赋值近似于随机。在声明变量后马上对其初始化一个默认的值是最简单有效的办法,否则项目大了你找都没地方找。代码存在错误在debug方式下可能会忽略而不被察觉到。debug方式下数组越界也大多不会出错,在release中就暴露出来了,这个找起来就比较难了。

(3) 只有DEBUG版的程序才能设置断点、单步执行、使用 TRACE/ASSERT等调试输出语句。REALEASE不包含任何调试信息,所以体积小、运行速度快。

参考资料:http://blog.csdn.net/u013524014/article/details/71537308

 

10、上下文的正确传参不是一件容易的事情,不同的对象下有不同的context参数获得方法:

1.getApplicationContext() :

这个函数返回的这个Application的上下文,所以是与app挂钩的,所以在整个生命周期里面都是不变的,这个好理解,但是使用的时候要注意,该context是和引用的生命周期一致的,所以和activity生命周期挂钩的任务不要使用该context,比如网络访问,防止内存泄露

 

2.getBasecontext():

stackoverflow上面写的是,这个函数不应该被使用,用Context代替,而Context是与activity相关连,所以当activity死亡后可能会被destroyed,我举个我自己写的例子

 

3.getApplication():

getApplication只能被Activity和Services使用,虽然在现在的Android的实现中,getApplication和getApplicationContext返回一样的对象,但也不能保证这两个函数一样(例如在特殊的提供者来说),所以如果你想得到你在Manifest文件里面注册的App class,你不要去调用getApplicationContext,因为可能得不到你所要的app实例

原文:

getApplication() is available to Activity and Services only. Although in current Android Activity and Service implementations, getApplication() and getApplicationContext() return the same object, there is no guarantee that this will always be the case (for example, in a specific vendor implementation). So if you want the Application class you registered in the Manifest, you should never call getApplicationContext() and cast it to your application, because it may not be the application instance (which you obviously experienced with the test framework).

 

4.getParent() :

返回activity的上下文,如果这个子视图的话,换句话说,就是当在子视图里面调用的话就返回一个带有子视图的activity对象,一目了然。。。

 

5.getActivity():

在fragment中使用,返回该fragment所依附的activity上下文

 

6.this

记住Activity,Service类,Application类是继承自Context类的,所以在有的时候需要上下文,只需要使用this关键字即可,但是有的时候再线程里面,this关键字的意义就改变了,但这个时候如果需要上下文,则需要使用 类名.this,这样就可以了

参考资料:https://www.cnblogs.com/chenxibobo/p/6136693.html

 

其中需要注意的是getApplicationContext() :

 

这个函数返回的这个Application的上下文,所以是与app挂钩的,所以在整个生命周期里面都是不变的,但是使用的时候要注意,该context是和引用的生命周期一致的,所以和activity生命周期挂钩的任务不要使用该context,比如网络访问,防止内存泄露

 

 

11、碰到了关闭主程序会导致app意外终止的情况

但程序log显示此问题出在了intent的身上

使用Toast发现开启app后打开短信监听开关后然后onCreate执行了一次,但是关闭app后发现这条Toast又出现了一次,结论:服务启动了两次 生命期奇怪

解决方法:

使用stopSelf()方法结束服务,不然服务的状态一直不定,导致问题很多

 

12、数据库使用方面,采用LitePal第三方库进行数据库操作,设置方法参考第一行代码(第二版)230页

 

13、在进行验证的时候需要使用adb查看数据库情况,但是却不能像第一行代码介绍的那样直接在模拟器上就获得了root权限,书中也没有详细的说明,经过查阅资料发现需要使用su命令,但发现su not found。最后看到网上有人说这是安卓7.1自身的问题,但自己没有验证是否如此,结果就没有使用ADB去查看数据库,对数据库操作只能依赖函数和重装APP实现

 

14、由于本APP的过滤选项活动中使用了RecyclerView这个布局,是当前官方推荐使用的代替LinearView的更加灵活的布局,他的实现也比较清晰易懂,创建了对象类,在后面的数据库操作中也得到了复用,关键是这个Adapter类给了很大的便利性,可以设置单击一条对象的一部分的信息使用不同的OnClick。这里需要注意适配器类中的事件处理函数中无法直接通过context使用其方法。

 

15、遇到问题:在RecyclerView的适配器类中使用dialog结束编辑数据后无法对当前activity进行数据刷新

思路:1、利用传入的context对象调用activity的刷新函数 不行,无法直接使用context

2、利用activity的onResume函数 不行 dialog不会将activity放到未运行状态

【dialog不是一个activity】

3、最终解决方法:构造函数中上下文context由 Context类型更换为FilterActivity(启动Dialog的Activity类,在调用其中的initData方法进行刷新

 

 

16、数据库操作中,使用了LitePal,它采用了面向对象的方法,使得数据库和代码连接更加紧密,使用起来也很方便,正好对象类的创建在RecycleView中也得到了复用,值得注意的是:数据库一般只是在OnCreate时导入,然后更新UI,之后再次在当前Activity中对数据库操作后需要重新导入,因此此处直接封装一个方法:

//多次调用封装为方法
public void initData(){
    //从数据库中导入
    filterList=DataSupport.findAll(Filter.class);

    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_filter);
    //使用线性的布局
    LinearLayoutManager layoutManager=new LinearLayoutManager(this);
    //StaggeredGridLayoutManager layoutManager = new
    //    StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);

    recyclerView.setLayoutManager(layoutManager);
    FilterAdapter adapter = new FilterAdapter(filterList,this);
    recyclerView.setAdapter(adapter);
}

 

 

17、问题:如何自动增加notifiction的id号以显示多条信息

http://blog.csdn.net/ccorg/article/details/51476222

最后演变为使用Filter自带的ID主键作为Notification的ID正好一举两得

 

 

18、在传递PendingIntent由通知唤起新的Activity遇到接收到bundle为null

最后查阅资料后尝试发送方直接使用intent的putExtra(“id”,ID)方法,接收方使用getIntExtra(“id”) 测试成功

其中还要注意一点:

PendingIntent  pi = PendingIntent.getActivity(context,id,intent,flag);

最后一个flag参数,分别是:

int FLAG_CANCEL_CURRENT:如果该PendingIntent已经存在,则在生成新的之前取消当前的,也就是说只有最后的PendingIntent有效,之前的都无效。

int FLAG_NO_CREATE:如果该PendingIntent不存在,直接返回null而不是创建一个PendingIntent.

int FLAG_ONE_SHOT:该PendingIntent只能用一次,在send()方法执行后,自动取消。

int FLAG_UPDATE_CURRENT:如果该PendingIntent已经存在,则用新传入的Intent更新当前的数据。

由以上四个flag可以看出,如果我们想通过PendingIntent传值,并且每次传值都不同的话,就应该使用FLAG_UPDATE_CURRENT,但是这时不要忘记第二个参数,假设第二个参数id为常量,比如0,那么所对应的Intent里面的extra的内容将更新为最新,也就是说所以的数据都是最新的;相反如果每次的id值不同,则Intent里面内

容不会被更新,所以要使你的Intent里面的内容不同就应该保持第二个参数每次都不相同。

参考资料:

http://blog.csdn.net/ccorg/article/details/51476222(尝试后未果成功后删除这里的设置仍成功,结论此贴的方法不是关键)

http://blog.csdn.net/lasttnt/article/details/40299095  …(flags参数问题)

https://stackoverflow.com/questions/11551195/intent-from-notification-does-not-have-extras(在一楼的评论区第一条得到解决)

 

19、兼容问题:

目前只有android7.1(大概)1080分辨率的可以正常使用…本人的安卓机前几天更新8.0以后,惨不忍睹,贯穿整个APP的核心功能“通知”不能用了,可能是废弃了旧的通知函数,目前没有要改的计划,源码已经上传github。