今天带咱们完成一款依据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")
列表功用运用BRVAH
implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.6")
运行时权限申请运用XXPermissions
implementation("com.github.getActivity:XXPermissions:18.2")
图片加载运用Glide
implementation("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()
}
}
以换肤界面为例,另外换肤能够看我这篇文章/post/725848… ,我这里就不多说了。运用dora.BaseActivity和dora.BaseFragment,能够统一数据加载都在initData中。
开源项目地址
github.com/dora4/DoraM…