现有一个安卓app,通过usb和电脑连接后,需要互相传输一些文件和json数据。请问可以使用什么方式传输?此过程不能使用网络。 安卓版本是10及以上
上传后出现下面的图,在appuplode上传提交界面success表示已经上传成功了。 "image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250105/635389e0ab9e89ae5fed6b33d81624a6.png)
淘宝上看到巡更系统的巡更点用的RFID射频技术,安卓手机可以开发APP识别这种巡更点设备吗?
前言由于项目是使用的是MVVM架构,使用DataStore等组件时会发现返回的是Flow喂!我LiveData都还没有用熟呢,咋就开始用Flow了,既然官方这样推广,当然是道理所在,那本章就说一说Flow。为了看到第一手准确资料,我是硬着头皮看完了Google官方英语介绍的视频,虽然很多都没有听懂,不过Google的大佬做的图还是很容易理解,本章很多图都是截取视频。背景在日常开发中,尤其是APP端经常有同时发起多个请求然后进行展示内容的需求,比如我先拿到标题(因子)先展示出来,再根据因子去加载数据,拿到数据后再进行分类、处理后再进行展示,这种复杂的业务,如果使用Java原来的回调方式,那就会一层套一层,还要不断地切换线程那当然不用,前面文章也说了使用Kotlin协程可以方便地切换线程以及使用阻塞式地方式写出非阻塞式的代码,那这个问题解决了。还有一个问题就是数据处理问题,这时你当然会说LiveData也有数据处理的API,比如map等等,说的很有道理,确实也是这个道理,那为什么Google官方又要整这么一出呢?等我后面细细说来。LiveData有什么不足?什么是LiveData,它就是一个基于观察者模式的可观察的数据存储器类,同时和普通的观察者又不太一样,它结合了Android生命周期,能感知Android界面的生命周期变化,这种能力可以确保LiveData仅更新处于活跃生命周期状态的应用组件观察者。所以,LiveData是一个简单易用的组件,同时也就是因为它的简单易用,和之前同是观察者模式的老大哥RxJava比功能就欠缺了不少,所以处理比较复杂的场景会比较麻烦。由于LiveData设计比较简单,所以有以下不足:LiveData只能在主线程更新数据。LiveData操作符不够多,处理复杂数据时不够用。虽然我也是刚用Jetpack,我咋记得可以子线程中更新数据呢,不少有个postValue方法吗?是的,但是postValue方法也需要切换到主线程,再进行更新数据,所以在处理复杂业务时,我从子线程获取的数据,我就想在子线程中先展示一下,然后接着再处理再请求,不想老是切换线程,这时就可以使用Flow了。注:由于本人没有真正了解、使用过RxJava,所以关于Flow和RxJava的比较就不细说了,主要也就是RxJava入门门槛高,在Android中需要自己处理生命周期,Flow也支持线程切换、有协程Scope兜底不会出现性能浪费等。Flow简介因为关于Flow的文章挺少,我就直接看了官方的视频介绍,先不谈什么API和原理,先按照官方文档来普及一波先。问题现在有3个耗时任务,分别是A、B、C,在协程中进行,那么使用不同的返回值来接收3个任务的结果会是什么区别呢?使用List如图所示,调用foo时会切换线程,很显然这里的缺点就是必须要等3个任务都执行完,然后把结果放入到一个List中,返回给调用者,如果某个任务执行很久,则需要等待。示例代码:runBlocking{ val list = foo() for (x in list) Log.i(TAG, "initData: $x") }suspend fun foo(): List<String> = buildList { Log.i(TAG, "foo: 开始发送数据") delay(1000) add("A") delay(1000) add("B") delay(1000) add("C") Log.i(TAG, "foo: 结束发送数据") }再看一下打印结果:`2021-08-25 14:34:45.307 15141-15141/: foo: 开始发送数据2021-08-25 14:34:48.309 15141-15141/: foo: 结束发送数据2021-08-25 14:34:48.311 15141-15141/: initData: A2021-08-25 14:34:48.312 15141-15141/: initData: B2021-08-25 14:34:48.312 15141-15141/: initData: C`这里耗时任务执行了3S多,然后数据全部返回,调用者再进行处理。使用Channel接着看一下Channel,什么是Channel的具体定义以及使用我之前也没有使用过,不过从代码注释可以知道它实际就是一个队列,而且是并发安全的,可以用来连接协程,实现不同协程的通信,还是看官方的截图:从这个图更可以看出Channel就是一个队列,不过这个队列相当于管道一样,一边生产数据,一边消费数据,最主要是它还可以在跨协程工作。那同样写个小例子://先定义一个变量,也就是这个Channel val channel = Channel<String>()//开启协程,不断地receive也就是消费管道里地数据 lifecycleScope.launch(Dispatchers.IO) { while (true){ Log.i(TAG, "initData: receive: ${channel.receive()}") } }//开启协程发送数据 lifecycleScope.launch { foo() }//模拟耗时操作 suspend fun foo(){ Log.i(TAG, "foo: 开始发送数据") delay(1000) channel.send("A") delay(1000) channel.send("B") delay(1000) channel.send("C") Log.i(TAG, "foo: 结束发送数据") }结果可想而知是什么样子的:`2021-08-25 14:58:42.236 16024-16024/: foo: 开始发送数据2021-08-25 14:58:43.240 16024-16062/: initData: receive: A2021-08-25 14:58:44.247 16024-16061/: initData: receive: B2021-08-25 14:58:45.252 16024-16024/: foo: 结束发送数据2021-08-25 14:58:45.254 16024-16062/: initData: receive: C`可以看出在第一个耗时任务结束发送时,消费者已经开始工作了,不用等待所有任务都结束。使用Flow关于Flow可以叫做流,这个和管道Channel设计的很像,也是生产者、消费者模型,一边生产数据,一边消耗数据。不过官方视频里说Flow是冷流,在有订阅对象时才开始产生数据也就是emit事件,所以在没有collect之前,Flow内部没有任何协程被激活,不会造成资源泄漏。直接看一下官方截图:示例代码://返回一个Flow实例 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) emit("A") delay(1000) emit("B") delay(1000) emit("C") Log.i(TAG, "foo: 结束发送数据") }//进行collect lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: $it") } } //发送数据 lifecycleScope.launch { foo() }可以预见,这个结果肯定和使用Channel是一样的,打印:2021-08-25 15:15:08.507 16419-16458/: foo: 开始发送数据2021-08-25 15:15:09.515 16419-16457/: initData: A2021-08-25 15:15:10.522 16419-16458/: initData: B2021-08-25 15:15:11.529 16419-16457/: initData: C2021-08-25 15:15:11.529 16419-16457/: foo: 结束发送数据到这里会发现和使用Channel一模一样的,区别就是Flow这里先拿到了Flow引用,只有去collect的时候,上游才去emit数据。为了加深印象和区别,总结一下:Flow介绍1.Flow是序列形式的,这个是区分List的,下面这个图可以很容易的看出区别。2.Flow是冷流,什么是冷流?Flow默认是冷流,如果要使用热流,可以使用SharedFlow,关于2者的区别,可以看下面图:(1)冷流冷流可以保证不必要的内存浪费,因为只有去collect时才会触发发射端的协程代码运行,如果有2个Collector,那另一个Collector也只有在collect时才会触发发射端协程运行,且会都跑一遍,如图:看一下代码://foo是冷流Flow var foo: Flow<String>? = null//在init函数里 开启线程,去emit数据 lifecycleScope.launch { foo() } //开启2个协程 都拿到foo,然后进行collect lifecycleScope.launch(Dispatchers.IO) { foo?.collect { Log.i(TAG, "initData: A开始收集 $it") } } //这里的协程,先延迟了2s 再去进行collect lifecycleScope.launch { delay(2000) foo?.collect { Log.i(TAG, "initData: B开始收集 $it") } }foo()函数如下://这里会每隔1s发送一个数据 suspend fun foo(){ foo = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") } }然后看一下打印:2021-08-30 11:49:13.862 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:14.868 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:15.868 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:15.874 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:16.870 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送数据 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:18.876 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印我们不难看出:A收集者开始调用collect时,发射协程开始工作,在13.862秒开始等待2s后,B收集者开始调用collect时,发射协程又开始工作一遍,在15.868,2者相隔2s多虽然A收集者已经开始收集了,B收集者开始时,依然又跑一遍(2)热流热流是一对多的关系,当有多个collector时,这时发射端发射一个数据,每个collector都能接收到,这个很像那个LiveData观察者模型,数据能得到共享,所以也是被称为SharedFlow。同时和冷流只有在collect时才去跑发射端的协程代码不同,热流会在对象创建出时便开始执行。同样也是看一下示例代码:val _events = MutableSharedFlow<String>()//先开启协程,创建出SharedFlow lifecycleScope.launch { foo1() } //立马进行收集 lifecycleScope.launch(Dispatchers.IO) { _events.collect { Log.i(TAG, "initData: A开始收集 $it") } } //延迟2秒再进行收集 lifecycleScope.launch { delay(2000) _events.collect { Log.i(TAG, "initData: B开始收集 $it") } }//一开始就发射A,后面每延迟1s发射一次 suspend fun foo1(){ Log.i(TAG, "foo: 开始发送数据") Log.i(TAG, "foo: 开始发送A") _events.emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") _events.emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") _events.emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }打印数据:2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 14:04:53.405 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 14:04:54.406 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 14:04:54.407 8383-8424/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 14:04:54.407 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 14:04:55.415 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 14:04:55.421 8383-8426/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 14:04:55.423 8383-8383/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 14:04:55.425 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 14:04:55.426 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印不难看出:热流在创建时,便开始发射数据由于A收集器在发射器发射完一个数据才开始collect,所以A收集器也收集不到A2s后B收集器开始收集,这时它只能被迫接受C,因为A和B数据都错过了,也不会重新再跑一遍发射代码Flow的详细解析从前面的代码,我们肯定了解了什么是Flow,以及冷流和热流的概念,那现在我们根据来官方的源码,来介绍一下Flow是如何使用和常见API。上面图中可以大概说出Flow的特性和使用了,另外补充几点:1、改善的Flow操作符前面说了Flow就是一个数据流,在中间可以对它进行各种操作,所以Flow的API为了更好的使用链式调用,把一些API进行了改善,比如在每个数据发送时进行延迟、开始进行延迟等等,这里就使用官方的一张图来表示:2、背压在生产者、消费者模型中都避免不了背压,在RxJava中使用了很多策略来应对,不过Flow在设计之处就解决了这个问题,其一是Flow发射和收集数据都是异步的,其二就是在发射或者收集添加延迟,来达到缓解背压的情况:说道背压,这个也是和Channel有很大的区别,Channel的策略是缓冲区,但是Flow自带承压机制,因为是Cold事件源,如果没有消费者,事件源不会主动emit事件。collect方法以及flow的构造方法均为suspend,所以均可以延迟,这样如果某端没有准备好,也可以通过延迟来承压,在上图也可以看出,简而言之就是没做事情处理背压。3、buffer操作符前面的总结图里说了,Flow是顺序队列,也就是开始collect,发射A,处理A,然后发射B,处理B,这里如果收集端有耗时操作,整个耗时就非常长了,如图代码例子://这里消费者,先collect,但是我消费也需要时间,所以延迟1s lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: 消费 $it") delay(1000) } } lifecycleScope.launch { foo() }//为了更好的看出执行顺序,每个发送都加了打印 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }可以看出最后的执行打印:2021-08-25 15:37:44.640 16997-17035/: foo: 开始发送A 2021-08-25 15:37:44.640 16997-17035/: initData: 消费 A 2021-08-25 15:37:45.641 16997-17036/: foo: 结束发送A 2021-08-25 15:37:46.643 16997-17035/: foo: 开始发送B 2021-08-25 15:37:46.643 16997-17035/: initData: 消费 B 2021-08-25 15:37:47.643 16997-17036/: foo: 结束发送B 2021-08-25 15:37:48.645 16997-17035/: foo: 开始发送C 2021-08-25 15:37:48.645 16997-17035/: initData: 消费 C 2021-08-25 15:37:49.648 16997-17036/: foo: 结束发送C 2021-08-25 15:37:49.649 16997-17036/: foo: 结束发送数据可以看出这里的操作一共花了6s,如何进行改善呢,就是使用buffer:SharedFlow的详细解析说完了Flow是冷流,冷流固然很好,但是用的多的还是SharedFlow,也就是热流,也可以叫成共享流。比如在Android的MVVM架构中,需要使用数据驱动来完成,这时就可以替换LiveData为SharedFlow(当然是后面要说的StateFlow),对于一个flow会有多个订阅者,这时就可以进行观察,从而达到多个订阅者都可以根据数据变化而做出变化。还是简单看一下总结图:下面我们来具体分析一波。(1)、构造函数public fun <T> MutableSharedFlow( replay: Int = 0, extraBufferCapacity: Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow<T>replay:表示当有新的订阅者collect时,发送几个已经发送过的数据给它,默认是0,即默认新订阅者不会获取到订阅之前的数据。 -extraBufferCapacity:表示减去replay,这个Flow还可以缓存多少个数据,默认是0; -onBufferOverflow:表示缓存策略,即缓冲区满了后,如何处理,默认是挂起。(2)、ShareIn函数前面说了,如果使用SharedFlow最好使用MutableSharedFlow然后使用emit来发射数据,但是经常有需求是返回一个Flow,这时就需要使用ShareIn函数把Flow转成SharedFlow,ShareIn函数是Flow的扩展函数,看一下参数:public fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T>scope,这个很好理解,协程范围,表示共享开始时所在的协程作用域范围,之前说过SharedFlow很像LiveData,这里的收集者就是观察者,所以为了控制范围,需要传递一个观察者执行操作的协程范围。replay,这个就是当有一个新的观察者订阅时,需要重新传递给这个新的观察者的数量。zstarted,这个控制共享的开始和结束的策略。这里有3种策略,分别说一下:(3)、ShareIn函数上游flow操作符在前面我们说了一些操作符,这里就可以给应用上,比如我在上游结束、异常等情况onCompletionval flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }retry可以对一些指定的异常进行处理,比如IO异常时进行重连等。val flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }onStart 可以在上游数据开始之前,做一些操作。val flow = loginRepository.getLoginName() flow.onStart { emit("start") } .onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }StateFlow的详细解析终于到了喜闻乐见的StateFlow了,官方就是希望用这个来替代LiveData,那么它到底是如何来替代LiveData的呢?(1)、特性StateFlow是SharedFlow的子类,根据前面说的那么它是个热流,可以被多个观察者观察,同时可以设置它消失的scope以及条件。StateFlow只更新最新的数据,也就是它是一个replay为0的SharedFlow。StateFlow里面和LiveData很像,都有个value来保存其值,也可以通过这个属性来获取或者设置它的值。(2)、使用使用MutableStateFlow就和LiveData一样,不过它需要一个默认值。也可以使用stateIn函数把一个Flow转成StateFlow,直接看这个函数:val result = userId.mapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading )从这个最基本的使用,我们可以看出以下信息:它的范围是viewModelScope,当viewModelScope结束时,流会停止。当观察者都被移除后,超过5s,为了不浪费性能,流会停止。会有个默认值,也就是这个initivalValue。(3)、观察从上面来看,确实很像LiveData,不过LiveData有个很重要的功能就是具有生命周期感知性能力,在UI处于活跃状态时才会更新数据,那这个StateFlow在收集时并没有传递lifecycleOwner,那如何达到一样的效果呢?首先是观察的操作执行在协程中,这个协程的范围是lifecycleScope不错,但是是直接launch还是launchWhenStarted呢,看下面这个图:从这个图我们发现如果我们直接使用launch的话,可能会在onStop执行时这时来个数据更新,我的View根本没法更新,所以会造成错误,这里要达到和LiveData一样的效果在界面活跃时进行更新,所以这里启动协程就需要使用launchWhenStarted。这个看起来没啥问题,不过我们前面不是有个WhileSubscribed这个优化项呢,表示没有观察者时5s后停止上游数据发射,那现在这个则无法做到,这时就需要使用repeatOnLifecycle,直接看图:这个repeatOnLifecycle的作用就是当满足特定状态时启动协程,并且在生命周期退出这个状态时停止协程。那这就可以比如我APP退到后台,这时如果超过5S,则不会再订阅,协程终止,这时上游发射逻辑也会停止,提高性能。当APP再切换到前台时,会执行onStart,这时又会有观察者,这时上游逻辑又会开始,所以最佳的写法如下:onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... } } } }总结看完了关于Flow的介绍,我们会发现它不仅有类似RxJava的强大操作符,也可以和JetPack一起使用,在处理复杂数据上会比LiveData方便很多。
今天带大家实现一款基于Dora SDK的Android本地音乐播放器app,本项目也作为Dora SDK的实践项目或使用教程。使用到开源库有[github.com/dora4/dora] 、[github.com/dora4/dcach…] 等。先声明一点,本项目主要作为框架的使用教程,界面风格不喜勿喷。效果演示实现功能基本播放功能,包括播放、暂停、缓冲、后台播放等播放模式切换均衡器和重低音增强耳机拔出暂停音频焦点处理,和其他音乐播放器互斥摇一摇切换歌曲更换皮肤知识产权框架搭建我们要开发一款Android App,首先要搭建基础框架,比如使用MVP还是MVVM架构?使用什么网络库?使用什么ORM库?很显然,作为Dora SDK的使用教程,肯定是要依赖Dora SDK的。 // Dora全家桶 implementation("com.github.dora4:dcache-android:1.7.9") implementation("com.github.dora4:dora:1.1.9") implementation("com.github.dora4:dora-arouter-support:1.1") implementation("com.github.dora4:dora-apollo-support:1.1") implementation("com.github.dora4:dora-pgyer-support:1.0") // implementation 'com.github.dora4:dora-eventbus-support:1.1' implementation("com.github.dora4:dview-toggle-button:1.0") implementation("com.github.dora4:dview-alert-dialog:1.0") implementation("com.github.dora4:dview-loading-dialog:1.2") implementation("com.github.dora4:dview-colors:1.0") implementation("com.github.dora4:dview-skins:1.4") implementation("com.github.dora4:dview-bottom-dialog:1.1") // implementation 'com.github.dora4:dview-avatar:1.4' implementation("com.github.dora4:dview-titlebar:1.9")列表功能使用BRVAHimplementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.6")运行时权限申请使用XXPermissionsimplementation("com.github.getActivity:XXPermissions:18.2")图片加载使用Glideimplementation("com.github.bumptech.glide:glide:4.11.0")主要依赖的就是这些库。应用入口MusicApp类编写package site.doramusic.app import dora.BaseApplication import dora.db.Orm import dora.db.OrmConfig import dora.http.log.FormatLogInterceptor import dora.http.retrofit.RetrofitManager import site.doramusic.app.base.conf.AppConfig import site.doramusic.app.db.Album import site.doramusic.app.db.Artist import site.doramusic.app.db.Folder import site.doramusic.app.db.Music import site.doramusic.app.http.service.CommonService import site.doramusic.app.http.service.MusicService import site.doramusic.app.http.service.UserService import site.doramusic.app.media.MediaManager class MusicApp : BaseApplication(), AppConfig { /** * 全局的音乐播放控制管理器。 */ var mediaManager: MediaManager? = null private set companion object { /** * 全局Application单例。 */ var instance: MusicApp? = null private set } override fun onCreate() { super.onCreate() instance = this init() } private fun init() { initHttp() // 初始化网络框架 initDb() // 初始化SQLite数据库的表 initMedia() // 初始化媒体管理器 } private fun initMedia() { mediaManager = MediaManager(this) } private fun initHttp() { RetrofitManager.initConfig { okhttp { interceptors().add(FormatLogInterceptor()) build() } mappingBaseUrl(MusicService::class.java, AppConfig.URL_APP_SERVER) mappingBaseUrl(UserService::class.java, AppConfig.URL_APP_SERVER) mappingBaseUrl(CommonService::class.java, AppConfig.URL_CHAT_SERVER) } } private fun initDb() { Orm.init(this, OrmConfig.Builder() .database(AppConfig.DB_NAME) .version(AppConfig.DB_VERSION) .tables(Music::class.java, Artist::class.java, Album::class.java, Folder::class.java) .build()) } }网络和ORM库都是来自于dcache-android库。首先初始化4张表,music、artist、album、folder,用来保存一些音乐信息。初始化网络库的时候添加一个FormatLogInterceptor日志拦截器,方便格式化输出网络请求日志。在Application中保存一个MediaManager单例,用来全局控制音乐的播放、暂停等。MediaManager与整体媒体框架我们使用MediaManager来统一管理媒体。由于要支持app后台运行时也能继续播放,所以我们考虑使用Service,而我们这不是一个简简单单的服务,而是要实时控制和反馈数据的。对于这样的一种场景,我们考虑将服务运行在单独的进程,并使用AIDL在主进程进行跨进程调用。 /** * 通过它调用AIDL远程服务接口。 */ class MediaManager(internal val context: Context) : IMediaService.Stub(), AppConfig { private var mediaService: IMediaService? = null private val serviceConnection: ServiceConnection private var onCompletionListener: MusicControl.OnConnectCompletionListener? = null init { this.serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { mediaService = asInterface(service) if (mediaService != null) { //音频服务启动的标志 LogUtils.i("MediaManager:connected") onCompletionListener!!.onConnectCompletion(mediaService) } } override fun onServiceDisconnected(name: ComponentName) { //音频服务断开的标志 LogUtils.i("MediaManager:disconnected") } } } fun setOnCompletionListener(l: MusicControl.OnConnectCompletionListener) { onCompletionListener = l } fun connectService() { val intent = Intent(AppConfig.MEDIA_SERVICE) intent.setClass(context, MediaService::class.java) context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) } fun disconnectService() { context.unbindService(serviceConnection) context.stopService(Intent(AppConfig.MEDIA_SERVICE)) } override fun play(pos: Int): Boolean { try { return mediaService?.play(pos) ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun playById(id: Int): Boolean { try { return mediaService?.playById(id) ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun playByPath(path: String) { try { mediaService?.playByPath(path) } catch (e: RemoteException) { e.printStackTrace() } } override fun playByUrl(music: Music, url: String) { try { mediaService?.playByUrl(music, url) } catch (e: RemoteException) { e.printStackTrace() } } override fun replay(): Boolean { try { return mediaService?.replay() ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun pause(): Boolean { try { return mediaService?.pause() ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun prev(): Boolean { try { return mediaService?.prev() ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun next(): Boolean { try { return mediaService?.next() ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun stop() { try { mediaService?.stop() ?: false } catch (e: RemoteException) { e.printStackTrace() } } override fun duration(): Int { try { return mediaService?.duration() ?: 0 } catch (e: RemoteException) { e.printStackTrace() } return 0 } override fun setCurMusic(music: Music) { try { mediaService?.setCurMusic(music) ?: false } catch (e: RemoteException) { e.printStackTrace() } } override fun position(): Int { try { return mediaService?.position() ?: 0 } catch (e: RemoteException) { e.printStackTrace() } return 0 } override fun pendingProgress(): Int { try { return mediaService?.pendingProgress() ?: 0 } catch (e: RemoteException) { e.printStackTrace() } return 0 } override fun seekTo(progress: Int): Boolean { try { return mediaService?.seekTo(progress) ?: false } catch (e: RemoteException) { e.printStackTrace() } return false } override fun refreshPlaylist(playlist: MutableList<Music>?) { try { mediaService?.refreshPlaylist(playlist) } catch (e: RemoteException) { e.printStackTrace() } } override fun setBassBoost(strength: Int) { try { mediaService?.setBassBoost(strength) } catch (e: RemoteException) { e.printStackTrace() } } override fun setEqualizer(bandLevels: IntArray) { try { mediaService?.setEqualizer(bandLevels) } catch (e: RemoteException) { e.printStackTrace() } } override fun getEqualizerFreq(): IntArray? { try { return mediaService?.equalizerFreq } catch (e: RemoteException) { e.printStackTrace() } return null } override fun getPlayState(): Int { try { return mediaService?.playState ?: 0 } catch (e: RemoteException) { e.printStackTrace() } return 0 } override fun getPlayMode(): Int { try { return mediaService?.playMode ?: 0 } catch (e: RemoteException) { e.printStackTrace() } return 0 } override fun setPlayMode(mode: Int) { try { mediaService?.playMode = mode } catch (e: RemoteException) { e.printStackTrace() } } override fun getCurMusicId(): Int { try { return mediaService?.curMusicId ?: -1 } catch (e: Exception) { e.printStackTrace() } return -1 } override fun loadCurMusic(music: Music): Boolean { try { return mediaService?.loadCurMusic(music) ?: false } catch (e: Exception) { e.printStackTrace() } return false } override fun getCurMusic(): Music? { try { return mediaService?.curMusic } catch (e: RemoteException) { e.printStackTrace() } return null } override fun getPlaylist(): MutableList<Music>? { try { return mediaService?.playlist } catch (e: Exception) { e.printStackTrace() } return null } override fun updateNotification(bitmap: Bitmap, title: String, name: String) { try { mediaService?.updateNotification(bitmap, title, name) } catch (e: RemoteException) { e.printStackTrace() } } override fun cancelNotification() { try { mediaService?.cancelNotification() } catch (e: RemoteException) { e.printStackTrace() } } }我们将服务配置在单独的进程,需要在AndroidManifest.xml中给service标签指定android:process,也就是进程标识,这样就分出了区别于应用主进程的一个新的进程。<service android:name=".media.MediaService" android:process=":doramedia" android:exported="true" android:label="DoraMusic Media"> <intent-filter> <action android:name="site.doramusic.app.service.ACTION_MEDIA_SERVICE" /> </intent-filter> </service>与媒体信息相关表的定义Music歌曲表package site.doramusic.app.db; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import dora.db.constraint.AssignType; import dora.db.constraint.PrimaryKey; import dora.db.migration.OrmMigration; import dora.db.table.Column; import dora.db.table.Ignore; import dora.db.table.OrmTable; import dora.db.table.PrimaryKeyEntry; import dora.db.table.Table; import site.doramusic.app.sort.Sort; /** * 歌曲表。 */ @Table("music") public class Music implements OrmTable, Parcelable, Sort { public static final String COLUMN_ID = "_id"; public static final String COLUMN_SONG_ID = "song_id"; public static final String COLUMN_ALBUM_ID = "album_id"; public static final String COLUMN_DURATION = "duration"; public static final String COLUMN_MUSIC_NAME = "music_name"; public static final String COLUMN_ARTIST = "artist"; public static final String COLUMN_DATA = "data"; public static final String COLUMN_FOLDER = "folder"; public static final String COLUMN_MUSIC_NAME_KEY = "music_name_key"; public static final String COLUMN_ARTIST_KEY = "artist_key"; public static final String COLUMN_FAVORITE = "favorite"; public static final String COLUMN_LAST_PLAY_TIME = "last_play_time"; /** * 数据库中的_id */ @Column(COLUMN_ID) @PrimaryKey(AssignType.AUTO_INCREMENT) public int id; @Column(COLUMN_SONG_ID) public int songId = -1; @Column(COLUMN_ALBUM_ID) public int albumId = -1; @Column(COLUMN_DURATION) public int duration; @Column(COLUMN_MUSIC_NAME) public String musicName; @Column(COLUMN_ARTIST) public String artist; @Column(COLUMN_DATA) public String data; @Column(COLUMN_FOLDER) public String folder; @Column(COLUMN_MUSIC_NAME_KEY) public String musicNameKey; @Column(COLUMN_ARTIST_KEY) public String artistKey; @Column(COLUMN_FAVORITE) public int favorite; @Column(COLUMN_LAST_PLAY_TIME) public long lastPlayTime; @Ignore private String sortLetter; @Ignore private Type type; /** * 封面路径,在线歌曲用。 */ @Ignore private String coverPath; @NonNull @Override public OrmMigration[] getMigrations() { return new OrmMigration[0]; } public enum Type { LOCAL, ONLINE } public Music() { } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { Bundle bundle = new Bundle(); bundle.putInt(COLUMN_ID, id); bundle.putInt(COLUMN_SONG_ID, songId); bundle.putInt(COLUMN_ALBUM_ID, albumId); bundle.putInt(COLUMN_DURATION, duration); bundle.putString(COLUMN_MUSIC_NAME, musicName); bundle.putString(COLUMN_ARTIST, artist); bundle.putString(COLUMN_DATA, data); bundle.putString(COLUMN_FOLDER, folder); bundle.putString(COLUMN_MUSIC_NAME_KEY, musicNameKey); bundle.putString(COLUMN_ARTIST_KEY, artistKey); bundle.putInt(COLUMN_FAVORITE, favorite); bundle.putLong(COLUMN_LAST_PLAY_TIME, lastPlayTime); dest.writeBundle(bundle); } public static final Creator<Music> CREATOR = new Creator<Music>() { @Override public Music createFromParcel(Parcel source) { Music music = new Music(); Bundle bundle = source.readBundle(getClass().getClassLoader()); music.id = bundle.getInt(COLUMN_ID); music.songId = bundle.getInt(COLUMN_SONG_ID); music.albumId = bundle.getInt(COLUMN_ALBUM_ID); music.duration = bundle.getInt(COLUMN_DURATION); music.musicName = bundle.getString(COLUMN_MUSIC_NAME); music.artist = bundle.getString(COLUMN_ARTIST); music.data = bundle.getString(COLUMN_DATA); music.folder = bundle.getString(COLUMN_FOLDER); music.musicNameKey = bundle.getString(COLUMN_MUSIC_NAME_KEY); music.artistKey = bundle.getString(COLUMN_ARTIST_KEY); music.favorite = bundle.getInt(COLUMN_FAVORITE); music.lastPlayTime = bundle.getLong(COLUMN_LAST_PLAY_TIME); return music; } @Override public Music[] newArray(int size) { return new Music[size]; } }; @NonNull @Override public String toString() { return "DoraMusic{" + "id=" + id + ", songId=" + songId + ", albumId=" + albumId + ", duration=" + duration + ", musicName='" + musicName + ''' + ", artist='" + artist + ''' + ", data='" + data + ''' + ", folder='" + folder + ''' + ", musicNameKey='" + musicNameKey + ''' + ", artistKey='" + artistKey + ''' + ", favorite=" + favorite + ", lastPlayTime=" + lastPlayTime + '}'; } @NonNull @Override public PrimaryKeyEntry getPrimaryKey() { return new PrimaryKeyEntry(COLUMN_ID, id); } @Override public boolean isUpgradeRecreated() { return false; } @Override public String getSortLetter() { return sortLetter; } @Override public void setSortLetter(String sortLetter) { this.sortLetter = sortLetter; } public void setType(Type type) { this.type = type; } public Type getType() { return type; } public void setCoverPath(String coverPath) { this.coverPath = coverPath; } public String getCoverPath() { return coverPath; } @Override public int compareTo(Sort sort) { return getSortLetter().compareTo(sort.getSortLetter()); } }Artist歌手表package site.doramusic.app.db; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import dora.db.constraint.AssignType; import dora.db.constraint.PrimaryKey; import dora.db.migration.OrmMigration; import dora.db.table.Column; import dora.db.table.Ignore; import dora.db.table.OrmTable; import dora.db.table.PrimaryKeyEntry; import dora.db.table.Table; import site.doramusic.app.sort.Sort; /** * 歌手表。 */ @Table("artist") public class Artist implements OrmTable, Parcelable, Sort { public static final String COLUMN_ID = "_id"; public static final String COLUMN_ARTIST_NAME = "artist_name"; public static final String COLUMN_NUMBER_OF_TRACKS = "number_of_tracks"; @Ignore private String sortLetter; @Column(COLUMN_ID) @PrimaryKey(AssignType.AUTO_INCREMENT) public int id; @Column(COLUMN_ARTIST_NAME) public String name; /** * 曲目数。 */ @Column(COLUMN_NUMBER_OF_TRACKS) public int number_of_tracks; @Override public int describeContents() { return 0; } public Artist() { } @Override public void writeToParcel(Parcel dest, int flags) { Bundle bundle = new Bundle(); bundle.putInt(COLUMN_ID, id); bundle.putString(COLUMN_ARTIST_NAME, name); bundle.putInt(COLUMN_NUMBER_OF_TRACKS, number_of_tracks); dest.writeBundle(bundle); } public static final Creator<Artist> CREATOR = new Creator<Artist>() { @Override public Artist createFromParcel(Parcel source) { Bundle bundle = source.readBundle(getClass().getClassLoader()); Artist artist = new Artist(); artist.id = bundle.getInt(COLUMN_ID); artist.name = bundle.getString(COLUMN_ARTIST_NAME); artist.number_of_tracks = bundle.getInt(COLUMN_NUMBER_OF_TRACKS); return artist; } @Override public Artist[] newArray(int size) { return new Artist[size]; } }; @NonNull @Override public PrimaryKeyEntry getPrimaryKey() { return new PrimaryKeyEntry(COLUMN_ID, id); } @Override public boolean isUpgradeRecreated() { return false; } @Override public String getSortLetter() { return sortLetter; } @Override public void setSortLetter(String sortLetter) { this.sortLetter = sortLetter; } public int compareTo(Sort sort) { return getSortLetter().compareTo(sort.getSortLetter()); } @NonNull @Override public OrmMigration[] getMigrations() { return new OrmMigration[0]; } }Album专辑表package site.doramusic.app.db; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import dora.db.constraint.AssignType; import dora.db.constraint.PrimaryKey; import dora.db.migration.OrmMigration; import dora.db.table.Column; import dora.db.table.Ignore; import dora.db.table.OrmTable; import dora.db.table.PrimaryKeyEntry; import dora.db.table.Table; import site.doramusic.app.sort.Sort; /** * 专辑表。 */ @Table("album") public class Album implements OrmTable, Parcelable, Sort { public static final String COLUMN_ID = "_id"; public static final String COLUMN_ALBUM_NAME = "album_name"; public static final String COLUMN_ALBUM_ID = "album_id"; public static final String COLUMN_NUMBER_OF_SONGS = "number_of_songs"; public static final String COLUMN_ALBUM_COVER_PATH = "album_cover_path"; @Column(COLUMN_ID) @PrimaryKey(AssignType.AUTO_INCREMENT) public int id; @Ignore private String sortLetter; //专辑名称 @Column(COLUMN_ALBUM_NAME) public String album_name; //专辑在数据库中的id @Column(COLUMN_ALBUM_ID) public int album_id = -1; //专辑的歌曲数目 @Column(COLUMN_NUMBER_OF_SONGS) public int number_of_songs = 0; //专辑封面图片路径 @Column(COLUMN_ALBUM_COVER_PATH) public String album_cover_path; @Override public int describeContents() { return 0; } public Album() { } @Override public void writeToParcel(Parcel dest, int flags) { Bundle bundle = new Bundle(); bundle.putInt(COLUMN_ID, id); bundle.putString(COLUMN_ALBUM_NAME, album_name); bundle.putString(COLUMN_ALBUM_COVER_PATH, album_cover_path); bundle.putInt(COLUMN_NUMBER_OF_SONGS, number_of_songs); bundle.putInt(COLUMN_ALBUM_ID, album_id); dest.writeBundle(bundle); } public static final Creator<Album> CREATOR = new Creator<Album>() { @Override public Album createFromParcel(Parcel source) { Album album = new Album(); Bundle bundle = source.readBundle(getClass().getClassLoader()); album.id = bundle.getInt(COLUMN_ID); album.album_name = bundle.getString(COLUMN_ALBUM_NAME); album.album_cover_path = bundle.getString(COLUMN_ALBUM_COVER_PATH); album.number_of_songs = bundle.getInt(COLUMN_NUMBER_OF_SONGS); album.album_id = bundle.getInt(COLUMN_ALBUM_ID); return album; } @Override public Album[] newArray(int size) { return new Album[size]; } }; @NonNull @Override public PrimaryKeyEntry getPrimaryKey() { return new PrimaryKeyEntry(COLUMN_ID, id); } @Override public boolean isUpgradeRecreated() { return false; } @Override public String getSortLetter() { return sortLetter; } @Override public void setSortLetter(String sortLetter) { this.sortLetter = sortLetter; } @Override public int compareTo(Sort sort) { return getSortLetter().compareTo(sort.getSortLetter()); } @NonNull @Override public OrmMigration[] getMigrations() { return new OrmMigration[0]; } }Folder文件夹表package site.doramusic.app.db; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import dora.db.constraint.AssignType; import dora.db.constraint.NotNull; import dora.db.constraint.PrimaryKey; import dora.db.constraint.Unique; import dora.db.migration.OrmMigration; import dora.db.table.Column; import dora.db.table.Ignore; import dora.db.table.OrmTable; import dora.db.table.PrimaryKeyEntry; import dora.db.table.Table; import site.doramusic.app.sort.Sort; /** * 文件夹表。 */ @Table("folder") public class Folder implements OrmTable, Parcelable, Sort { public static final String COLUMN_ID = "_id"; public static final String COLUMN_FOLDER_NAME = "folder_name"; public static final String COLUMN_FOLDER_PATH = "folder_path"; @Ignore private String sortLetter; @Column(COLUMN_ID) @PrimaryKey(AssignType.AUTO_INCREMENT) public int id; @Column(COLUMN_FOLDER_NAME) public String name; @Unique @NotNull @Column(COLUMN_FOLDER_PATH) public String path; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { Bundle bundle = new Bundle(); bundle.putInt(COLUMN_ID, id); bundle.putString(COLUMN_FOLDER_NAME, name); bundle.putString(COLUMN_FOLDER_PATH, path); dest.writeBundle(bundle); } public Folder() { } public static Creator<Folder> CREATOR = new Creator<Folder>() { @Override public Folder createFromParcel(Parcel source) { Folder folder = new Folder(); Bundle bundle = source.readBundle(getClass().getClassLoader()); folder.id = bundle.getInt(COLUMN_ID); folder.name = bundle.getString(COLUMN_FOLDER_NAME); folder.path = bundle.getString(COLUMN_FOLDER_PATH); return folder; } @Override public Folder[] newArray(int size) { return new Folder[size]; } }; @NonNull @Override public PrimaryKeyEntry getPrimaryKey() { return new PrimaryKeyEntry(COLUMN_ID, id); } @Override public boolean isUpgradeRecreated() { return false; } @Override public String getSortLetter() { return sortLetter; } @Override public void setSortLetter(String sortLetter) { this.sortLetter = sortLetter; } @Override public int compareTo(Sort sort) { return getSortLetter().compareTo(sort.getSortLetter()); } @NonNull @Override public OrmMigration[] getMigrations() { return new OrmMigration[0]; } }这4张表的类主要演示dcache-android库的orm功能。我们可以看到@Table和@Column可以给表和列重命名,当然,不一定就会使用默认的表和列名规则。不是表字段的属性加上@Ignore。也可以通过@Unique配置唯一约束,通过@NotNull配置非空约束,使用@PrimaryKey配置主键约束。MusicScanner本地歌曲扫描package site.doramusic.app.media import android.annotation.SuppressLint import android.content.Context import android.database.Cursor import android.provider.MediaStore import dora.db.Orm import dora.db.Transaction import dora.db.dao.DaoFactory import dora.db.table.TableManager import dora.util.PinyinUtils import dora.util.TextUtils import site.doramusic.app.base.conf.AppConfig import site.doramusic.app.db.Album import site.doramusic.app.db.Artist import site.doramusic.app.db.Folder import site.doramusic.app.db.Music import site.doramusic.app.util.MusicUtils import site.doramusic.app.util.PreferencesManager import java.io.File import java.util.* import kotlin.collections.ArrayList /** * 媒体扫描器,用来扫描手机中的歌曲文件。 */ @SuppressLint("Range") object MusicScanner : AppConfig { private val proj_music = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.DURATION) private val proj_album = arrayOf(MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.NUMBER_OF_SONGS, MediaStore.Audio.Albums._ID, MediaStore.Audio.Albums.ALBUM_ART) private val proj_artist = arrayOf( MediaStore.Audio.Artists.ARTIST, MediaStore.Audio.Artists.NUMBER_OF_TRACKS) private val proj_folder = arrayOf(MediaStore.Files.FileColumns.DATA) private val musicDao = DaoFactory.getDao(Music::class.java) private val artistDao = DaoFactory.getDao(Artist::class.java) private val albumDao = DaoFactory.getDao(Album::class.java) private val folderDao = DaoFactory.getDao(Folder::class.java) private fun recreateTables() { TableManager.recreateTable(Music::class.java) TableManager.recreateTable(Artist::class.java) TableManager.recreateTable(Album::class.java) TableManager.recreateTable(Folder::class.java) } @JvmStatic fun scan(context: Context): List<Music> { recreateTables() var musics = arrayListOf<Music>() Transaction.execute(Music::class.java) { musics = queryMusic(context, AppConfig.ROUTE_START_FROM_LOCAL) as ArrayList<Music> it.insert(musics) } if (musics.size > 0) { // 歌曲都没有就没有必要查询歌曲信息了 Transaction.execute { val artists = queryArtist(context) artistDao.insert(artists) val albums = queryAlbum(context) albumDao.insert(albums) val folders = queryFolder(context) folderDao.insert(folders) } } return musics } @JvmStatic fun queryMusic(context: Context, from: Int): List<Music> { return queryMusic(context, null, null, from) } @JvmStatic fun queryMusic(context: Context, selections: String?, selection: String?, from: Int): List<Music> { val sp = PreferencesManager(context) val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI val cr = context.contentResolver val select = StringBuffer(" 1=1 ") // 查询语句:检索出.mp3为后缀名,时长大于1分钟,文件大小大于1MB的媒体文件 if (sp.getFilterSize()) { select.append(" and ${MediaStore.Audio.Media.SIZE} > " + "${AppConfig.SCANNER_FILTER_SIZE}") } if (sp.getFilterTime()) { select.append(" and ${MediaStore.Audio.Media.DURATION} > " + "${AppConfig.SCANNER_FILTER_DURATION}") } if (TextUtils.isNotEmpty(selections)) { select.append(selections) } return when (from) { AppConfig.ROUTE_START_FROM_LOCAL -> if (musicDao.count() > 0) { musicDao.selectAll() } else { getMusicList(cr.query(uri, proj_music, select.toString(), null, MediaStore.Audio.Media.ARTIST_KEY)) } AppConfig.ROUTE_START_FROM_ARTIST -> if (musicDao.count() > 0) { queryMusic(selection, AppConfig.ROUTE_START_FROM_ARTIST) } else { getMusicList(cr.query(uri, proj_music, select.toString(), null, MediaStore.Audio.Media.ARTIST_KEY)) } AppConfig.ROUTE_START_FROM_ALBUM -> { if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_ALBUM) } if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_FOLDER) } if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE) } if (musicDao.count() > 0) { queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST) } else arrayListOf() } AppConfig.ROUTE_START_FROM_FOLDER -> { if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_FOLDER) } if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE) } if (musicDao.count() > 0) { queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST) } else arrayListOf() } AppConfig.ROUTE_START_FROM_FAVORITE -> { if (musicDao.count() > 0) { return queryMusic(selection, AppConfig.ROUTE_START_FROM_FAVORITE) } if (musicDao.count() > 0) { queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST) } else arrayListOf() } AppConfig.ROUTE_START_FROM_LATEST -> { if (musicDao.count() > 0) { queryMusic(selection, AppConfig.ROUTE_START_FROM_LATEST) } else arrayListOf() } else -> arrayListOf() } } @JvmStatic fun queryMusic(selection: String?, type: Int): List<Music> { val db = Orm.getDB() var sql = "" when (type) { AppConfig.ROUTE_START_FROM_ARTIST -> { sql = "select * from music where ${Music.COLUMN_ARTIST} = ?" } AppConfig.ROUTE_START_FROM_ALBUM -> { sql = "select * from music where ${Music.COLUMN_ALBUM_ID} = ?" } AppConfig.ROUTE_START_FROM_FOLDER -> { sql = "select * from music where ${Music.COLUMN_FOLDER} = ?" } AppConfig.ROUTE_START_FROM_FAVORITE -> { sql = "select * from music where ${Music.COLUMN_FAVORITE} = ?" // } else if (type == ROUTE_START_FROM_DOWNLOAD) { // sql = "select * from music where download = ?"; } AppConfig.ROUTE_START_FROM_LATEST -> { sql = "select * from music where ${Music.COLUMN_LAST_PLAY_TIME} > ? order by " + "${Music.COLUMN_LAST_PLAY_TIME} desc limit 100" } } return parseCursor(db.rawQuery(sql, arrayOf(selection))) } private fun parseCursor(cursor: Cursor): List<Music> { val list: MutableList<Music> = ArrayList() while (cursor.moveToNext()) { val music = Music() music.id = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_ID)) music.songId = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_SONG_ID)) music.albumId = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_ALBUM_ID)) music.duration = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_DURATION)) music.musicName = cursor.getString(cursor.getColumnIndex( Music.COLUMN_MUSIC_NAME)) music.artist = cursor.getString(cursor.getColumnIndex(Music.COLUMN_ARTIST)) music.data = cursor.getString(cursor.getColumnIndex(Music.COLUMN_DATA)) music.folder = cursor.getString(cursor.getColumnIndex(Music.COLUMN_FOLDER)) music.musicNameKey = cursor.getString(cursor.getColumnIndex( Music.COLUMN_MUSIC_NAME_KEY)) music.artistKey = cursor.getString(cursor.getColumnIndex( Music.COLUMN_ARTIST_KEY)) music.favorite = cursor.getInt(cursor.getColumnIndex(Music.COLUMN_FAVORITE)) music.lastPlayTime = cursor.getLong(cursor.getColumnIndex( Music.COLUMN_LAST_PLAY_TIME)) list.add(music) } cursor.close() return list } /** * 获取包含音频文件的文件夹信息。 * * @param context * @return */ @JvmStatic fun queryFolder(context: Context): List<Folder> { val sp = PreferencesManager(context) val uri = MediaStore.Files.getContentUri("external") val cr = context.contentResolver val selection = StringBuilder(MediaStore.Files.FileColumns.MEDIA_TYPE + " = " + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO + " and " + "(" + MediaStore.Files.FileColumns.DATA + " like '%.mp3' or " + MediaStore.Files.FileColumns.DATA + " like '%.flac' or " + MediaStore.Files.FileColumns.DATA + " like '%.wav' or " + MediaStore.Files.FileColumns.DATA + " like '%.ape' or " + MediaStore.Files.FileColumns.DATA + " like '%.m4a' or " + MediaStore.Files.FileColumns.DATA + " like '%.aac')") // 查询语句:检索出.mp3为后缀名,时长大于1分钟,文件大小大于1MB的媒体文件 if (sp.getFilterSize()) { selection.append(" and " + MediaStore.Audio.Media.SIZE + " > " + AppConfig.SCANNER_FILTER_SIZE) } if (sp.getFilterTime()) { selection.append(" and " + MediaStore.Audio.Media.DURATION + " > " + AppConfig.SCANNER_FILTER_DURATION) } // selection.append(") group by ( " + MediaStore.Files.FileColumns.PARENT) return if (folderDao.count() > 0) { folderDao.selectAll() } else { getFolderList(cr.query(uri, proj_folder, selection.toString(), null, null)) } } /** * 获取歌手信息。 * * @param context * @return */ @JvmStatic fun queryArtist(context: Context): List<Artist> { val uri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI val cr = context.contentResolver return if (artistDao.count() > 0) { artistDao.selectAll() } else { getArtistList(cr.query(uri, proj_artist, null, null, MediaStore.Audio.Artists.NUMBER_OF_TRACKS + " desc")) } } /** * 获取专辑信息。 * * @param context * @return */ @JvmStatic fun queryAlbum(context: Context): List<Album> { val sp = PreferencesManager(context) val uri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI val cr = context.contentResolver val where = StringBuilder(MediaStore.Audio.Albums._ID + " in (select distinct " + MediaStore.Audio.Media.ALBUM_ID + " from audio_meta where (1=1 ") if (sp.getFilterSize()) { where.append(" and " + MediaStore.Audio.Media.SIZE + " > " + AppConfig.SCANNER_FILTER_SIZE) } if (sp.getFilterTime()) { where.append(" and " + MediaStore.Audio.Media.DURATION + " > " + AppConfig.SCANNER_FILTER_DURATION) } where.append("))") return if (albumDao.count() > 0) { albumDao.selectAll() } else { // Media.ALBUM_KEY 按专辑名称排序 // FIXME: Android11的Invalid token select问题 getAlbumList(cr.query(uri, proj_album, null, null, MediaStore.Audio.Media.ALBUM_KEY)) } } private fun getMusicList(cursor: Cursor?): List<Music> { val list: MutableList<Music> = ArrayList() if (cursor == null) { return list } while (cursor.moveToNext()) { val music = Music() val filePath = cursor.getString(cursor .getColumnIndex(MediaStore.Audio.Media.DATA)) music.songId = cursor.getInt(cursor .getColumnIndex(MediaStore.Audio.Media._ID)) music.albumId = cursor.getInt(cursor .getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)) val duration = cursor.getInt(cursor .getColumnIndex(MediaStore.Audio.Media.DURATION)) if (duration > 0) { music.duration = duration } else { try { music.duration = MusicUtils.getDuration(filePath) } catch (e: RuntimeException) { continue } } music.musicName = cursor.getString(cursor .getColumnIndex(MediaStore.Audio.Media.TITLE)) music.artist = cursor.getString(cursor .getColumnIndex(MediaStore.Audio.Media.ARTIST)) music.data = filePath val folderPath = filePath.substring(0, filePath.lastIndexOf(File.separator)) music.folder = folderPath music.musicNameKey = PinyinUtils.getPinyinFromSentence(music.musicName) music.artistKey = PinyinUtils.getPinyinFromSentence(music.artist) list.add(music) } cursor.close() return list } private fun getAlbumList(cursor: Cursor?): List<Album> { val list: MutableList<Album> = ArrayList() if (cursor == null) { return list } while (cursor.moveToNext()) { val album = Album() album.album_name = cursor.getString( cursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM)) album.album_id = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Albums._ID)) album.number_of_songs = cursor.getInt(cursor .getColumnIndex(MediaStore.Audio.Albums.NUMBER_OF_SONGS)) album.album_cover_path = cursor.getString(cursor .getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART)) list.add(album) } cursor.close() return list } private fun getArtistList(cursor: Cursor?): List<Artist> { val list: MutableList<Artist> = ArrayList() if (cursor == null) { return list } while (cursor.moveToNext()) { val artist = Artist() artist.name = cursor.getString(cursor .getColumnIndex(MediaStore.Audio.Artists.ARTIST)) artist.number_of_tracks = cursor.getInt(cursor .getColumnIndex(MediaStore.Audio.Artists.NUMBER_OF_TRACKS)) list.add(artist) } cursor.close() return list } private fun getFolderList(cursor: Cursor?): List<Folder> { val list: MutableList<Folder> = ArrayList() if (cursor == null) { return list } while (cursor.moveToNext()) { val folder = Folder() val filePath = cursor.getString( cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)) folder.path = filePath.substring(0, filePath.lastIndexOf(File.separator)) folder.name = folder.path.substring(folder.path .lastIndexOf(File.separator) + 1) list.add(folder) } cursor.close() return list } }我们可以看到,使用DaoFactory.getDao拿到dao对象就可以以ORM的方式操作数据库中的表了。MusicControl媒体控制的具体实现package site.doramusic.app.media; import android.content.Context; import android.content.Intent; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.audiofx.BassBoost; import android.media.audiofx.Equalizer; import android.os.Build; import android.os.PowerManager; import com.lsxiao.apollo.core.Apollo; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Random; import dora.db.builder.WhereBuilder; import dora.db.dao.DaoFactory; import dora.db.dao.OrmDao; import dora.util.LogUtils; import dora.util.TextUtils; import dora.util.ToastUtils; import site.doramusic.app.base.conf.ApolloEvent; import site.doramusic.app.base.conf.AppConfig; import site.doramusic.app.db.Music; import site.doramusic.app.util.PreferencesManager; /** * 音乐播放流程控制。 */ public class MusicControl implements MediaPlayer.OnCompletionListener, AppConfig { private final Random mRandom; private int mPlayMode; private final MediaPlayerProxy mMediaPlayer; private final List<Music> mPlaylist; private final Context mContext; private int mCurPlayIndex; private int mPlayState; private int mPendingProgress; private final int mCurMusicId; private Music mCurMusic; private boolean mPlaying; private final AudioManager mAudioManager; private final OrmDao<Music> mDao; private final PreferencesManager mPrefsManager; public MusicControl(Context context) { this.mContext = context; this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.mPrefsManager = new PreferencesManager(context); this.mPlayMode = MPM_LIST_LOOP_PLAY; //默认列表循环 this.mPlayState = MPS_NO_FILE; //默认没有音频文件播放 this.mCurPlayIndex = -1; this.mCurMusicId = -1; this.mPlaylist = new ArrayList<>(); this.mDao = DaoFactory.INSTANCE.getDao(Music.class); this.mMediaPlayer = new MediaPlayerProxy(); this.mMediaPlayer.setNeedCacheAudio(true); this.mMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); //播放音频的时候加锁,防止CPU休眠 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AudioAttributes attrs = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); this.mMediaPlayer.setAudioAttributes(attrs); } else { this.mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); } this.mMediaPlayer.setOnCompletionListener(this); this.mRandom = new Random(); this.mRandom.setSeed(System.currentTimeMillis()); } /** * 设置重低音参数。 * * @param strength */ public void setBassBoost(int strength) { int audioSessionId = mMediaPlayer.getAudioSessionId(); BassBoost bassBoost = new BassBoost(0, audioSessionId); BassBoost.Settings settings = new BassBoost.Settings(); settings.strength = (short) strength; bassBoost.setProperties(settings); bassBoost.setEnabled(true); bassBoost.setParameterListener(new BassBoost.OnParameterChangeListener() { @Override public void onParameterChange(BassBoost effect, int status, int param, short value) { LogUtils.i("重低音参数改变"); } }); } /** * 获取均衡器支持的频率。 * * @return */ public int[] getEqualizerFreq() { int audioSessionId = mMediaPlayer.getAudioSessionId(); Equalizer equalizer = new Equalizer(0, audioSessionId); short bands = equalizer.getNumberOfBands(); int[] freqs = new int[bands]; for (short i = 0; i < bands; i++) { int centerFreq = equalizer.getCenterFreq(i) / 1000; freqs[i] = centerFreq; } return freqs; } /** * 设置均衡器。 * * @param bandLevels */ public void setEqualizer(int[] bandLevels) { int audioSessionId = mMediaPlayer.getAudioSessionId(); Equalizer equalizer = new Equalizer(1, audioSessionId); // 获取均衡控制器支持最小值和最大值 short minEQLevel = equalizer.getBandLevelRange()[0];//第一个下标为最低的限度范围 short maxEQLevel = equalizer.getBandLevelRange()[1]; // 第二个下标为最高的限度范围 int distanceEQLevel = maxEQLevel - minEQLevel; int singleEQLevel = distanceEQLevel / 25; for (short i = 0; i < bandLevels.length; i++) { equalizer.setBandLevel(i, (short) (singleEQLevel * bandLevels[i])); } equalizer.setEnabled(true); equalizer.setParameterListener(new Equalizer.OnParameterChangeListener() { @Override public void onParameterChange(Equalizer effect, int status, int param1, int param2, int value) { LogUtils.i("均衡器参数改变:" + status + "," + param1 + "," + param2 + "," + value); } }); } /** * 保存收藏。 * * @param music */ private void saveFavorite(Music music) { music.favorite = 1; mDao.update(WhereBuilder.Companion.create().addWhereEqualTo("_id", music.id), music); } /** * 保存最近播放。 * * @param music */ private void saveLatest(Music music) { //更新本地缓存歌曲 music.lastPlayTime = System.currentTimeMillis(); mDao.update(WhereBuilder.Companion.create().addWhereEqualTo("_id", music.id), music); } /** * 设置播放。 * * @param playState */ public void setPlaying(int playState) { switch (playState) { case MPS_PLAYING: mPlaying = true; break; default: mPlaying = false; } } /** * 设置当前播放的歌曲。 * * @param music * @return */ public boolean loadCurMusic(Music music) { if (prepare(seekPosById(mPlaylist, music.songId))) { this.mCurMusic = music; return true; } return false; } /** * 修改当前播放歌曲的信息。 * * @param music * @return */ public void setCurMusic(Music music) { this.mPlaylist.set(mCurPlayIndex, music); this.mCurMusic = music; } /** * 缓冲准备。 * * @param pos * @return */ public boolean prepare(int pos) { mCurPlayIndex = pos; mPendingProgress = 0; mMediaPlayer.reset(); if (mPrefsManager.getBassBoost()) { setBassBoost(1000); } else { setBassBoost(1); } if (!mPrefsManager.getEqualizerDecibels().equals("")) { int[] equalizerFreq = getEqualizerFreq(); int[] decibels = new int[equalizerFreq.length]; String[] values = mPrefsManager.getEqualizerDecibels().split(","); for (int i = 0; i < decibels.length; i++) { decibels[i] = Integer.valueOf(values[i]); } setEqualizer(decibels); } String path = mPlaylist.get(pos).data; if (TextUtils.isNotEmpty(path)) { try { mMediaPlayer.setDataSource(path); mMediaPlayer.prepare(); mPlayState = MPS_PREPARE; } catch (Exception e) { mPlayState = MPS_INVALID; if (pos < mPlaylist.size()) { pos++; playById(mPlaylist.get(pos).songId); } return false; } } else { ToastUtils.showShort(mContext, "歌曲路径为空"); } mCurMusic = mPlaylist.get(mCurPlayIndex); sendMusicPlayBroadcast(); return true; } /** * 根据歌曲的id来播放。 * * @param id * @return */ public boolean playById(int id) { if (requestFocus()) { int position = seekPosById(mPlaylist, id); mCurPlayIndex = position; if (mCurMusicId == id) { if (!mMediaPlayer.isPlaying()) { mMediaPlayer.start(); mPlayState = MPS_PLAYING; sendMusicPlayBroadcast(); mCurMusic = mPlaylist.get(mCurPlayIndex); saveLatest(mCurMusic); } else { pause(); } return true; } if (!prepare(position)) { return false; } return replay(); } else { return false; } } /** * 根据URL播放歌曲。 * * @param music * @param url */ public void playByUrl(Music music, String url) { if (requestFocus()) { try { mMediaPlayer.setAudioCachePath(music.data); mMediaPlayer.setOnCachedProgressUpdateListener(new MediaPlayerProxy.OnCachedProgressUpdateListener() { @Override public void updateCachedProgress(int progress) { mPendingProgress = progress; } }); String localProxyUrl = mMediaPlayer.getLocalURLAndSetRemoteSocketAddress(url); mPlaylist.add(mCurPlayIndex, music); //插入到当前播放位置 mCurMusic = music; mMediaPlayer.startProxy(); mMediaPlayer.reset(); mMediaPlayer.setDataSource(localProxyUrl); mMediaPlayer.prepareAsync(); mMediaPlayer.start(); mPlayState = MPS_PLAYING; sendMusicPlayBroadcast(); } catch (Exception e) { e.printStackTrace(); } } } /** * 根据本地文件路径播放歌曲。 * * @param path */ public void play(String path) { if (requestFocus()) { try { mMediaPlayer.stop(); mMediaPlayer.reset(); mMediaPlayer.setDataSource(path); mMediaPlayer.prepare(); mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mMediaPlayer.start(); sendMusicPlayBroadcast(); } }); } catch (IOException e) { e.printStackTrace(); } } } /** * 停止播放歌曲。 */ public void stop() { if (mMediaPlayer.isPlaying()) { mMediaPlayer.stop(); } } AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() { public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { // Pause playback pause(); } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Resume playback replay(); } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { mAudioManager.abandonAudioFocus(audioFocusListener); pause(); } } }; /** * 请求音频焦点。 * * @return */ private boolean requestFocus() { // Request audio focus for playback int result = mAudioManager.requestAudioFocus(audioFocusListener, // Use the music stream. AudioManager.STREAM_MUSIC, // Request permanent focus. AudioManager.AUDIOFOCUS_GAIN); return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } /** * 根据位置播放列表中的歌曲。 * * @param pos * @return */ public boolean play(int pos) { if (requestFocus()) { if (mCurPlayIndex == pos) { if (!mMediaPlayer.isPlaying()) { mMediaPlayer.start(); mPlayState = MPS_PLAYING; sendMusicPlayBroadcast(); mCurMusic = mPlaylist.get(mCurPlayIndex); saveLatest(mCurMusic); } else { pause(); } return true; } if (!prepare(pos)) { return false; } return replay(); } else { return false; } } /** * 获取当前播放歌曲的索引。 * * @return */ public int getCurPlayIndex() { return mCurPlayIndex; } /** * 保证索引在播放列表索引范围内。 * * @param index * @return */ private int reviseIndex(int index) { if (index < 0) { index = mPlaylist.size() - 1; } if (index >= mPlaylist.size()) { index = 0; } return index; } /** * 获取当前歌曲播放的位置。 * * @return */ public int position() { if (mPlayState == MPS_PLAYING || mPlayState == MPS_PAUSE) { return mMediaPlayer.getCurrentPosition(); } return 0; } /** * 获取当前歌曲的时长。 * * @return 毫秒 */ public int duration() { if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) { return 0; } return mMediaPlayer.getDuration(); } /** * 跳到指定进度播放歌曲。 * * @param progress * @return */ public boolean seekTo(int progress) { if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) { return false; } int pro = reviseSeekValue(progress); int time = mMediaPlayer.getDuration(); int curTime = (int) ((float) pro / 100 * time); mMediaPlayer.seekTo(curTime); return true; } /** * 获取歌曲的播放模式。 * * @return */ public int getPlayMode() { return mPlayMode; } /** * 设置歌曲的播放模式。 * * @param mode */ public void setPlayMode(int mode) { this.mPlayMode = mode; } /** * 清空播放列表。 */ public void clear() { mMediaPlayer.stop(); mMediaPlayer.reset(); } /** * 在线缓冲进度。 * * @return */ public int pendingProgress() { return mPendingProgress; } public interface OnConnectCompletionListener { void onConnectCompletion(IMediaService service); } /** * 获取当前正在播放的歌曲。 * * @return */ public Music getCurMusic() { return mCurMusic; } /** * 检测当前歌曲是否正在播放中。 * * @return */ public boolean isPlaying() { return mPlaying; } /** * 暂停当前歌曲的播放。 * * @return */ public boolean pause() { if (mPlayState != MPS_PLAYING) { return false; } mMediaPlayer.pause(); mPlayState = MPS_PAUSE; mCurMusic = mPlaylist.get(mCurPlayIndex); sendMusicPlayBroadcast(); return true; } /** * 播放上一首。 * * @return */ public boolean prev() { switch (mPlayMode) { case AppConfig.MPM_LIST_LOOP_PLAY: //列表循环 return moveLeft(); case AppConfig.MPM_ORDER_PLAY: //顺序播放 if (mCurPlayIndex != 0) { return moveLeft(); } else { return prepare(mCurPlayIndex); } case AppConfig.MPM_RANDOM_PLAY: //随机播放 int index = getRandomIndex(); if (index != -1) { mCurPlayIndex = index; } else { mCurPlayIndex = 0; } if (prepare(mCurPlayIndex)) { return replay(); } return false; case AppConfig.MPM_SINGLE_LOOP_PLAY: //单曲循环 prepare(mCurPlayIndex); return replay(); default: return false; } } /** * 播放下一首。 * * @return */ public boolean next() { switch (mPlayMode) { case MPM_LIST_LOOP_PLAY: //列表循环 return moveRight(); case MPM_ORDER_PLAY: //顺序播放 if (mCurPlayIndex != mPlaylist.size() - 1) { return moveRight(); } else { return prepare(mCurPlayIndex); } case MPM_RANDOM_PLAY: //随机播放 int index = getRandomIndex(); if (index != -1) { mCurPlayIndex = index; } else { mCurPlayIndex = 0; } if (prepare(mCurPlayIndex)) { return replay(); } return false; case MPM_SINGLE_LOOP_PLAY: //单曲循环 prepare(mCurPlayIndex); return replay(); default: return false; } } @Override public void onCompletion(MediaPlayer mp) { next(); } /** * 随机播放模式下获取播放索引。 * * @return */ private int getRandomIndex() { int size = mPlaylist.size(); if (size == 0) { return -1; } return Math.abs(mRandom.nextInt() % size); } /** * 修正缓冲播放的进度在合理的范围内。 * * @param progress * @return */ private int reviseSeekValue(int progress) { if (progress < 0) { progress = 0; } else if (progress > 100) { progress = 100; } return progress; } /** * 刷新播放列表的歌曲。 * * @param playlist */ public void refreshPlaylist(List<Music> playlist) { mPlaylist.clear(); mPlaylist.addAll(playlist); if (mPlaylist.size() == 0) { mPlayState = MPS_NO_FILE; mCurPlayIndex = -1; return; } } /** * 在当前播放模式下播放上一首。 * * @return */ public boolean moveLeft() { if (mPlayState == MPS_NO_FILE) { return false; } mCurPlayIndex--; mCurPlayIndex = reviseIndex(mCurPlayIndex); if (!prepare(mCurPlayIndex)) { return false; } return replay(); } /** * 在当前播放模式下播放下一首。 * * @return */ public boolean moveRight() { if (mPlayState == MPS_NO_FILE) { return false; } mCurPlayIndex++; mCurPlayIndex = reviseIndex(mCurPlayIndex); if (!prepare(mCurPlayIndex)) { return false; } return replay(); } /** * 重头开始播放当前歌曲。 * * @return */ public boolean replay() { if (requestFocus()) { if (mPlayState == MPS_INVALID || mPlayState == MPS_NO_FILE) { return false; } mMediaPlayer.start(); mPlayState = MPS_PLAYING; sendMusicPlayBroadcast(); mCurMusic = mPlaylist.get(mCurPlayIndex); saveLatest(mCurMusic); return true; } else { return false; } } /** * 发送音乐播放/暂停的广播。 */ private void sendMusicPlayBroadcast() { setPlaying(mPlayState); Intent intent = new Intent(ACTION_PLAY); intent.putExtra("play_state", mPlayState); mContext.sendBroadcast(intent); Apollo.emit(ApolloEvent.REFRESH_LOCAL_NUMS); } /** * 获取当前的播放状态。 * * @return */ public int getPlayState() { return mPlayState; } /** * 获取播放列表。 * * @return */ public List<Music> getPlaylist() { return mPlaylist; } /** * 退出媒体播放。 */ public void exit() { mMediaPlayer.stop(); mMediaPlayer.release(); mCurPlayIndex = -1; mPlaylist.clear(); } /** * 根据歌曲的ID,寻找出歌曲在当前播放列表中的位置。 * * @param playlist * @param id * @return */ public int seekPosById(List<Music> playlist, int id) { if (id == -1) { return -1; } int result = -1; if (playlist != null) { for (int i = 0; i < playlist.size(); i++) { if (id == playlist.get(i).songId) { result = i; break; } } } return result; } }前面我们提到使用AIDL进行跨进程访问。那么整体调用顺序是,MediaManager->MediaService->MusicControl。MediaManager调用层,相当于一个外包装或者说是门面。MediaService中间层,用于后台访问。MusicControl实现层。ShakeDetector摇一摇切歌package site.doramusic.app.shake import android.content.Context import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.Handler import site.doramusic.app.util.PreferencesManager /** * 摇一摇切歌。 */ class ShakeDetector(context: Context) : SensorEventListener { private val sensorManager: SensorManager? private var onShakeListener: OnShakeListener? = null private val prefsManager: PreferencesManager private var lowX: Float = 0.toFloat() private var lowY: Float = 0.toFloat() private var lowZ: Float = 0.toFloat() private var shaking: Boolean = false private val shakeHandler: Handler by lazy { Handler() } companion object { private const val FILTERING_VALUE = 0.1f } init { // 获取传感器管理服务 sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager prefsManager = PreferencesManager(context) } private val r: Runnable = Runnable { shaking = false } override fun onSensorChanged(event: SensorEvent) { if (prefsManager.getShakeChangeMusic() && event.sensor.type == Sensor.TYPE_ACCELEROMETER) { if (!shaking) { shakeHandler.removeCallbacks(r) val x = event.values[SensorManager.DATA_X] val y = event.values[SensorManager.DATA_Y] val z = event.values[SensorManager.DATA_Z] lowX = x * FILTERING_VALUE + lowX * (1.0f - FILTERING_VALUE) lowY = y * FILTERING_VALUE + lowY * (1.0f - FILTERING_VALUE) lowZ = z * FILTERING_VALUE + lowZ * (1.0f - FILTERING_VALUE) val highX = x - lowX val highY = y - lowY val highZ = z - lowZ if (highX >= 10 || highY >= 10 || highZ >= 10) { shaking = true onShakeListener?.onShake() shakeHandler.postDelayed(r, 2000) } } } } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { //传感器精度改变 } /** * 启动摇晃检测--注册监听器。 */ fun start() { sensorManager?.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL) } /** * 停止摇晃检测--取消监听器。 */ fun stop() { sensorManager?.unregisterListener(this) } /** * 当摇晃事件发生时,接收通知。 */ interface OnShakeListener { /** * 当手机晃动时被调用。 */ fun onShake() } fun setOnShakeListener(l: OnShakeListener) { this.onShakeListener = l } }摇一摇功能的实现原理很简单,就是使用了Android的重力传感器,当x,y,z轴的加速度超过了预先设定的阈值,就会触发摇一摇功能,我们这里是调用MediaManager播放下一首歌。因为MediaManager管理着整个可以播放的音乐列表,所以随时都可以触发摇一摇功能,当然在设置中关掉了摇一摇功能除外。拔出耳机或断开蓝牙耳机连接暂停播放音乐package site.doramusic.app.receiver import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothHeadset import android.bluetooth.BluetoothProfile import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.media.AudioManager import android.os.Handler import site.doramusic.app.MusicApp import site.doramusic.app.R import site.doramusic.app.media.SimpleAudioPlayer /** * 耳机拨出监听。 */ class EarphoneReceiver : BroadcastReceiver() { private lateinit var player: SimpleAudioPlayer override fun onReceive(context: Context, intent: Intent) { val action = intent.action if (action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) { changeSpeakerphoneOn(context, true) // 只监听拔出耳机使用这个意图 // 耳机拔出时,暂停音乐播放 Handler().postDelayed({ player = SimpleAudioPlayer(context) player.playByRawId(R.raw.earphone) }, 1000) pauseMusic() } else if (Intent.ACTION_HEADSET_PLUG == action) { // if (intent.hasExtra("state")) { // int state = intent.getIntExtra("state", -1); // if (state == 1) { // //插入耳机 // } else if (state == 0) { // //拔出耳机 // pauseMusic(); // } // } } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED == action) { val adapter = BluetoothAdapter.getDefaultAdapter() if (BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.A2DP) || BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) || BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEALTH) || BluetoothProfile.STATE_DISCONNECTED == adapter.getProfileConnectionState(BluetoothProfile.GATT)) { changeSpeakerphoneOn(context, true) //蓝牙耳机失去连接 Handler().postDelayed({ player = SimpleAudioPlayer(context) player.playByRawId(R.raw.bluetooth) }, 1000) pauseMusic() } else if (BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) || BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEADSET) || BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.HEALTH) || BluetoothProfile.STATE_CONNECTED == adapter.getProfileConnectionState(BluetoothProfile.GATT)) { //蓝牙耳机已连接 } } } private fun pauseMusic() { MusicApp.instance!!.mediaManager!!.pause() } /** * 切换播放模式。 * * @param connected */ private fun changeSpeakerphoneOn(context: Context, connected: Boolean) { val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.isSpeakerphoneOn = connected } }我们通过监听系统广播来实现这样的功能。MusicTimer全局音乐播放界面刷新package site.doramusic.app.util; import android.os.Handler; import android.os.Message; import java.util.Timer; import java.util.TimerTask; public class MusicTimer { public final static int REFRESH_PROGRESS_EVENT = 0x100; private static final int INTERVAL_TIME = 500; private Handler[] mHandler; private Timer mTimer; private TimerTask mTimerTask; private int what; private boolean mTimerStart = false; public MusicTimer(Handler... handler) { this.mHandler = handler; this.what = REFRESH_PROGRESS_EVENT; mTimer = new Timer(); } public void startTimer() { if (mHandler == null || mTimerStart) { return; } mTimerTask = new MusicTimerTask(); mTimer.schedule(mTimerTask, INTERVAL_TIME, INTERVAL_TIME); mTimerStart = true; } public void stopTimer() { if (!mTimerStart) { return; } mTimerStart = false; if (mTimerTask != null) { mTimerTask.cancel(); mTimerTask = null; } } class MusicTimerTask extends TimerTask { @Override public void run() { if (mHandler != null) { for (Handler handler : mHandler) { Message msg = handler.obtainMessage(what); msg.sendToTarget(); } } } } }我们所有需要刷新进度条的地方都要用到这个类,一般设置为0.5秒刷新一次,既不过度刷新,又要保证歌曲时间播放进度较准确。BaseActivity写法体验package site.doramusic.app.ui.activity import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.ViewGroup import android.widget.RelativeLayout import androidx.appcompat.widget.AppCompatImageView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Route import dora.skin.SkinManager import dora.skin.base.BaseSkinActivity import dora.util.DensityUtils import dora.util.StatusBarUtils import dora.widget.DoraTitleBar import site.doramusic.app.R import site.doramusic.app.annotation.TimeTrace import site.doramusic.app.base.conf.ARoutePath import site.doramusic.app.databinding.ActivityChoiceColorBinding import site.doramusic.app.ui.adapter.ChoiceColorAdapter import site.doramusic.app.util.PreferencesManager /** * 换肤界面,选择颜色。 */ @Route(path = ARoutePath.ACTIVITY_CHOICE_COLOR) class ChoiceColorActivity : BaseSkinActivity<ActivityChoiceColorBinding>() { private lateinit var colorDrawable: ColorDrawable private var choiceColorAdapter: ChoiceColorAdapter? = null private var colorDatas: MutableList<ColorData>? = null private lateinit var prefsManager: PreferencesManager data class ColorData(val backgroundResId: Int, val backgroundColor: Int) override fun getLayoutId(): Int { return R.layout.activity_choice_color } override fun onSetStatusBar() { super.onSetStatusBar() StatusBarUtils.setTransparencyStatusBar(this) } override fun initData(savedInstanceState: Bundle?) { mBinding.statusbarChoiceColor.layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, StatusBarUtils.getStatusBarHeight()) SkinManager.getLoader().setBackgroundColor(mBinding.statusbarChoiceColor, "skin_theme_color") val imageView = AppCompatImageView(this) val dp24 = DensityUtils.dp2px(24f) imageView.layoutParams = RelativeLayout.LayoutParams(dp24, dp24) imageView.setImageResource(R.drawable.ic_save) mBinding.titlebarChoiceColor.addMenuButton(imageView) mBinding.titlebarChoiceColor.setOnIconClickListener(object : DoraTitleBar.OnIconClickListener { override fun onIconBackClick(icon: AppCompatImageView) { } override fun onIconMenuClick(position: Int, icon: AppCompatImageView) { if (position == 0) { changeSkin() } } }) prefsManager = PreferencesManager(this) colorDatas = mutableListOf( ColorData(R.drawable.cyan_bg, resources.getColor(R.color.skin_theme_color_cyan)), ColorData(R.drawable.orange_bg, resources.getColor(R.color.skin_theme_color_orange)), ColorData(R.drawable.black_bg, resources.getColor(R.color.skin_theme_color_black)), ColorData(R.drawable.green_bg, resources.getColor(R.color.skin_theme_color_green)), ColorData(R.drawable.red_bg, resources.getColor(R.color.skin_theme_color_red)), ColorData(R.drawable.blue_bg, resources.getColor(R.color.skin_theme_color_blue)), ColorData(R.drawable.purple_bg, resources.getColor(R.color.skin_theme_color_purple))) choiceColorAdapter = ChoiceColorAdapter() choiceColorAdapter!!.setList(colorDatas!!) mBinding.rvChoiceColor.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) // mBinding.rvChoiceColor.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL)) mBinding.rvChoiceColor.itemAnimator = DefaultItemAnimator() mBinding.rvChoiceColor.adapter = choiceColorAdapter choiceColorAdapter!!.selectedPosition = if (prefsManager.getSkinType() == 0) 0 else prefsManager.getSkinType() - 1 colorDrawable = ColorDrawable(ContextCompat.getColor(this, R.color.colorPrimary)) mBinding.ivChoiceColorPreview.background = colorDrawable choiceColorAdapter!!.setOnItemClickListener { adapter, view, position -> val color = colorDatas!![position].backgroundColor colorDrawable.color = color choiceColorAdapter!!.selectedPosition = position choiceColorAdapter!!.notifyDataSetChanged() } } /** * 测试AOP。 */ @TimeTrace private fun changeSkin() { when (choiceColorAdapter!!.selectedPosition) { 0 -> { prefsManager.saveSkinType(1) SkinManager.changeSkin("cyan") } 1 -> { prefsManager.saveSkinType(2) SkinManager.changeSkin("orange") } 2 -> { prefsManager.saveSkinType(3) SkinManager.changeSkin("black") } 3 -> { prefsManager.saveSkinType(4) SkinManager.changeSkin("green") } 4 -> { prefsManager.saveSkinType(5) SkinManager.changeSkin("red") } 5 -> { prefsManager.saveSkinType(6) SkinManager.changeSkin("blue") } 6 -> { prefsManager.saveSkinType(7) SkinManager.changeSkin("purple") } } SkinManager.getLoader().setBackgroundColor(mBinding.statusbarChoiceColor, "skin_theme_color") finish() } }以换肤界面为例,另外换肤可以看我这篇文章juejin.cn/post/725848… ,我这里就不多说了。使用dora.BaseActivity和dora.BaseFragment,可以统一数据加载都在initData中。开源项目地址github.com/dora4/DoraM…
架构是无形的,但对项目的影响又是无处不在的。好的代码结构可以方便业务的扩展,差的代码结构容易造成极难看出问题的bug。可想而知,一个好的架构,对项目的影响力有多大。大多数人写代码都是不考虑架构的,想到哪写到哪,这不是一个优秀的程序员的写代码的方式。优秀的程序员大多都会在写一行代码之前考虑清楚,我这行代码为什么要写?为什么要写在这里?真正好的代码,都是不多一行代码,不少一行代码,每一行代码,都用到实处。那么,怎样写出好的代码呢?首先,要逼迫自己养成代码洁癖,俗称强迫症,即看着不符合自己代码编码规范的代码会感觉比较难受,从而让自己为了解除这种痛苦,写每一行代码之前都考虑清楚。其次,你应该去系统学习一下架构设计相关的知识,这是非常有用的。以Java语言为例,你应该去了解Java的反射机制、泛型以及注解的使用。接下来,你要学习6种设计原则和23种设计模式。接下来就是不断的优化和锤炼自己的代码,在大量实践中成长。最后,你要去Github上面多读优秀的开源代码,来学习别人写代码的思路。好的架构其实并非一开始就能设计得很完善,通常都是不断的重构、优化。然后,才演化成最终非常高质量的代码。每一个优秀的框架或代码结构背后,都是日日夜夜的埋头苦干,锲而不舍的坚持,都是汗水凝聚而成的。并不是说,谁一开始就能想出最优的代码结构,除非这个人之前写过这个类似的,然后复制过来直接使用。做架构设计的通常都有自己的开源项目,把自己写好的框架,分享给别人使用,节省别人开发的时间,成就他人,升华自己!
AOP(Aspect-Oriented Program)面向切面编程,它是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。比如,我们可以通过它来拦截一些种类的方法。以点击事件为例,我们可以在点击事件处理回调之前,切一个切点,关注下它的网络数据请求情况,即检测一下当前网络是否可用,然后也可以切另外一个切点,关注下它的方法执行时长。而这个方法就是一个切面,这类方法可以有很多个,只要符合相同的特性,比如我们上面提到的点击事件处理。我们实现AOP通常最直接的方式就是使用AspectJ,当然了,只要你技术足够到位,也可以自己用动态代理模拟。我们可以定义一个gradle插件,在编译的时候辅助编译。import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main import org.gradle.api.Plugin import org.gradle.api.Project /** * @使用ajc编译java代码 , 同 时 织 入 切 片 代 码 * 使用 AspectJ 的编译器(ajc,一个java编译器的扩展) * 对所有受 aspect 影响的类进行织入。 * 在 gradle 的编译 task 中增加额外配置,使之能正确编译运行。 */ class AspectjPlugin implements Plugin<Project> { void apply(Project project) { project.dependencies { api 'org.aspectj:aspectjrt:1.8.10' } final def log = project.logger log.error "============aspectj start============" def hasApp = project.plugins.hasPlugin("com.android.application") final def variants if (hasApp) { variants = project.android.applicationVariants } else { variants = project.android.libraryVariants } variants.all { variant -> def javaCompile = variant.javaCompile javaCompile.doLast { String[] args = [ "-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator) ] log.error "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true) new Main().run(args, handler) for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown break case IMessage.WARNING: log.warn message.message, message.thrown break case IMessage.INFO: log.info message.message, message.thrown break case IMessage.DEBUG: log.debug message.message, message.thrown break } } } } log.error "============aspectj stop============" } }然后以重复点击防抖为例。先声明一个注解。@Target(AnnotationTarget.FUNCTION) annotation class AopOnClick( /** * 点击间隔时间 */ val value: Long = 1000)然后定义一个切面实现。import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Pointcut @Aspect class AopClickAspect { /** * 定义切点,标记切点为所有被@AopOnclick注解的方法 * 注意:这里com.example.annotation.AopOnClickk需要替换成 * 你自己项目中AopOnclick这个类的全路径 */ @Pointcut("execution(@com.example.annotation.AopOnClick * *(..))") fun methodAnnotated() { } /** * 定义一个切面方法,包裹切点方法 */ @Around("methodAnnotated()") @Throws(Throwable::class) fun aroundJoinPoint(joinPoint: ProceedingJoinPoint) { val methodSignature = joinPoint.signature as MethodSignature val method = methodSignature.method if (!method.isAnnotationPresent(AopOnClick::class.java)) { return } val aopOnClick = method.getAnnotation(AopOnClick::class.java) // 判断是否快速点击 if (!AopClickUtil.isFastDoubleClick(aopOnClick.value)) { // 不是快速点击,执行原方法 joinPoint.proceed() } } }用到的工具方法。object AopClickUtil { /** * 最近一次点击的时间 */ private var mLastClickTime: Long = 0 /** * 是否是快速点击 * * @param intervalMillis 时间间期(毫秒) * @return true:是,false:不是 */ fun isFastDoubleClick(intervalMillis: Long): Boolean { // long time = System.currentTimeMillis(); val time = SystemClock.elapsedRealtime() val timeInterval = Math.abs(time - mLastClickTime) return if (timeInterval < intervalMillis) { true } else { mLastClickTime = time false } } }这样就可以在所有配置了@AopOnClick注解的方法实现AOP重复点击防抖了。
描述:下拉刷新和上拉加载复杂度:★★★★☆分组:【Dora大控件组】关系:暂无技术要点:事件分发、视图动画、布局容器的布局照片动图软件包github.com/dora4/dora_…用法val pullableLayout = findViewById<PullableLayout>(R.id.pullableLayout) pullableLayout.setOnRefreshListener(object : PullableLayout.OnRefreshListener { override fun onRefresh(layout: PullableLayout) { pullableLayout.postDelayed(Runnable { pullableLayout.refreshFinish(PullableLayout.SUCCEED) }, 1000) } override fun onLoadMore(layout: PullableLayout) { pullableLayout.postDelayed(Runnable { pullableLayout.loadMoreFinish(PullableLayout.SUCCEED) }, 1000) } })
中文名:享元模式英文名:Flyweight类型:结构型模式班主任评语:享元模式,在大量创建同一个类对象的时候,对不变的属性复用,而只修改变化的属性,从而降低内存使用率。比如春节机票,出发地、目的地、航班号、起飞时间和降落时间不变的,只有价格随购买时间变化,那么出票的时候只需要修改机票对象的价格属性,而无需重新创建对象。
在空间中,有两条线段,分别是线段AB和线段CD,它们的顶点坐标分别是A(x1,y1,z1),B(x2,y2,z2),C(x3,y3,z3);D(x4,y4,z4),请问如何求线段AB与线段CD的交点坐标?