現在、個人で音楽プレイヤーアプリを作っていて、
楽曲の一覧表示まで、できているのですが、
そのとき使った楽曲検索方法について、ご紹介したいと思います。
UIスレッドを妨げず、楽曲の検索処理を分離したかったので、
JobIntentServiceを使いました。
ちなみに、検索した楽曲を表示する部分については書いてないので、
そこはお好みでレイアウトを組んでいただければと思います。
JobIntentServiceって何?
JobIntentServiceとは、IntentServiceの親戚のようなもので、
Intentに情報を詰め込んで、JobIntentServiceに渡すと、
バックグラウンドで、よしなに処理してくれるものです。
Oreo以降とそれ以前で、挙動が違うみたいですが、
今回の内容にはあまり関係ないので、詳しくは説明しません。
JobIntentServiceについて詳しく知りたい方は、こちら
使い方としては、こんな感じです。
まず、JobIntentServiceを継承したクラスを作ります。
class MyJobIntentService: JobIntentService() {
/** ユニークID */
private const val JOB_ID = 100
override fun onHandleWork(intent: Intent) {
// ここにJobIntentServiceで行いたい処理を書く。
}
}
かならず onHandleWork
をOverrideします。
Intentを渡すとこのメソッドに処理が入ってきますので、
ここに、バックグラウンドで行いたい処理を記述します。
JOB_ID
は、サービスを識別するユニークなIDです。
同じJobIntentServiceを呼び出すときには、同じjobidを使用するようにします。
次にIntentを生成して、JobIntentServiceのenqueueWorkを呼び出します。
val Intent = Intent(context, MyJobIntentService::class.java)
intent.putExtra("name", "value")
MyJobIntentService.enqueueWork(context, MyJobIntentService::class.java, JOB_ID, intent)
ここで渡したIntentが、onHandleWorkに渡されます。
今回は、onHandleWork内で、楽曲ファイルの検索を行います。
楽曲ファイルの検索には、ContentResolver
楽曲にかぎらず、端末内のファイルを取得するときには、
ContentResolverを使います。
ContentResolverは、Context経由で取得することができます。
ActivityやFragment以外でも呼び出せるように、
下記のようにApplicationクラスを作っておくと便利です。
class MyApplication : Application() {
companion object {
private lateinit var sInstance: Application
fun getInstance() = sInstance
}
override fun onCreate() {
super.onCreate()
sInstance = this
}
}
private val mResolver: ContentResolver = MyApplication .getInstance().contentResolver
ContentResolverが取得できたら、queryメソッドを使って検索を行います。
public final Cursor query (Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder)
返り値であるCursorを操作して、情報を取得します。
各引数の説明は以下のとおりです。
uri
取り出したいコンテンツの場所を表します。
楽曲だと、content://media/external/audio/media
になります。
projection
取得したいカラム名を指定します。
楽曲だと、タイトルやアーティスト名などです。
すべてのカラムを取得したいときは、nullを渡します。
selection
検索条件を指定します。指定した検索条件に合致するものだけが抽出されます。
例 "WHERE title = ?"
selectionArgs
selectionの ? が、指定したselectionArgsで置き換わります。
sortOrder
並び替え条件です。
例 "ORDER BY title"
使うクラスの説明は大体終わったので、
ここから検索ロジックを書いていきたいと思います。
Cursorクラスの拡張関数を定義する
queryで返ってくるCursorクラスには、カラム名を指定して直接Valueを取るような関数が存在しません。
例えば、title
というカラム名のValueを取得したければ、
まずgetColumnIndex(column : String)で、カラムのインデックスを取得し、
それをgetString(columnIndex : Int)に渡す必要があります。
これは非常に面倒なので、
CursorExt.ktファイルを作って、そこにCursorの拡張関数を定義します。
/**
* Cursorクラスの拡張関数群
*/
fun Cursor.getIntFromColumn(columnName: String) = this.getInt(this.getColumnIndex(columnName))
fun Cursor.getLongFromColumn(columnName: String) = this.getLong(this.getColumnIndex(columnName))
fun Cursor.getFloatFromColumn(columnName: String) = this.getFloat(this.getColumnIndex(columnName))
fun Cursor.getStringFromColumn(columnName: String) = this.getString(this.getColumnIndex(columnName))
こうすることで、カラム名を渡して直接Valueを取れるようになります。
扱う型を増やしたいときには、適宜、拡張関数を追加してください。
(例:getByteFromColumn()など)
楽曲のデータクラスを作る
取得した楽曲情報を格納するクラスを作成します。
イミュータブルなデータクラスを作る場合、
Javaだと、フィールドごとに、Setterを作らなくてはいけないので大変ですが、
Kotlinには、data class
があるので簡単に書くことができます。
/**
* 楽曲メタデータ
*/
data class MusicMetadata(
val mId: Long = -1, // ID
val mArtist: String = "", // アーティスト名
val mAlbum: String = "", // アルバム名
val mAlbumId: Long = -1, // アルバムID
val mAlbumKey: String = "", // アルバムキー
val mDurationInMs: Long = -1, // 楽曲の再生時間
val mTrackNumber: Int = -1, // アルバムの収録インデックス
val mTitle: String = "", // 楽曲名
val mFilePath: String = "" // ファイルパス
)
内部でequals()やhashCode()といったメソッドが自動生成されるので、便利です。
定数クラスを作る
Intentに情報を詰めるとき、Intentから情報を引き出すときに使う、
定数を保持するクラスを作成します。
/**
* MyJobIntentServiceに渡すIntentのキー群
*
* 例 :
* intent = Intent()
* intent.purString(QUERY_URI, queryUri)
*/
object ContentResolverConst {
/** ContentResolverが管理しているuri */
const val QUERY_URI = "queryUri"
/** 取得したいカラム名 */
const val QUERY_PROJECTION = "projection"
/** 絞り込みたいカラム名 */
const val QUERY_SELECTION = "selection"
/** 指定したカラム名の条件 */
const val QUERY_SELECTION_ARGS = "selectionArgs"
/** ソートしたいカラム名 */
const val QUERY_SORT_ORDER = "sortOrder"
/**
* リストに追加するか、上書きするかを指定するフラグ
* 追加 True : 上書き False
*/
const val APPEND_FLAG = "appendFlag"
/** 検索の種別 */
const val SEARCH_ACTION_KEY = "actionKey"
/** 楽曲検索時 */
const val ACTION_SEARCH_MUSIC = "searchMusic"
}
JobIntentServiceを作る
JobIntentServiceクラスを継承した、MyIntentServiceを作成します。
class MyJobIntentService: JobIntentService() {
/** ユニークID */
private const val JOB_ID = 100
/** 楽曲の検索に使うURI */
private val TRACK_EXTERNAL_URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
/** 楽曲を検索する際に使用するカラム */
private val TRACK_COLUMNS: Array<String> = arrayOf(
MediaStore.Audio.Media._ID, // シーケンスID
MediaStore.Audio.Media.ARTIST, // アーティスト名
MediaStore.Audio.Media.ALBUM, // アルバム名
MediaStore.Audio.Media.ALBUM_ID, // アルバムID
MediaStore.Audio.Media.ALBUM_KEY, // アルバムキー。アルバムの検索に使用する
MediaStore.Audio.Media.DURATION, // 楽曲の再生時間
MediaStore.Audio.Media.TRACK, // 楽曲のアルバム内でのトラックナンバー
MediaStore.Audio.Media.TITLE, // 楽曲名
MediaStore.Audio.Media.DATA // ファイルパス
)
/** MediaStoreへのアクセッサ */
private val mResolver: ContentResolver = MyApplication.getInstance().contentResolver
/** アプリケーションコンテキスト */
private val mContext: Context = MyApplication.getInstance().applicationContext
override fun onHandleWork(intent: Intent) {
}
}
クラスを作成したら、AndroidManifest.xmlに記述を追加します。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="...">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppThemeNoActionBar">
~ 中略 ~
<service
android:name="MyJobIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application>
</manifest>
そしたら、MyJobIntenetService内に、Intentを生成する
createIntentメソッドを実装します。
/**
* Intentを作成する
* 作成したIntentは、onHandleIntent内で使用される
*
* @param queryUri ContentResolverが管理しているuri
* @param projection 取得したいカラム名
* @param selection 絞り込みたいカラム名
* @param selectionArgs 指定したカラム名の条件
* @param sortOrder ソートしたいカラム名
* @param appendFlag リストに追加するか、上書きするかを指定するフラグ ⇒ 追加 True : 上書き False
*/
private fun createIntent(
searchAction: String,
queryUri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?,
appendFlag: Boolean
): Intent {
return Intent(mContext, MyJobIntentService::class.java).apply {
putExtra(ContentResolverConst.SEARCH_ACTION_KEY, searchAction)
putExtra(ContentResolverConst.QUERY_URI, queryUri)
putExtra(ContentResolverConst.QUERY_PROJECTION, projection)
putExtra(ContentResolverConst.QUERY_SELECTION, selection)
putExtra(ContentResolverConst.QUERY_SELECTION_ARGS, selectionArgs)
putExtra(ContentResolverConst.QUERY_SORT_ORDER, sortOrder)
putExtra(ContentResolverConst.APPEND_FLAG, appendFlag)
}
}
渡された情報をIntentに詰めるだけです。
次は、外から呼ばれる楽曲検索のトリガーとなるメソッドを実装します。
/**
* SDカードと端末内部に保存されている、全ての楽曲を取得する
*/
fun searchAllMusic(context: Context) {
searchAllMusicFromUri(context, TRACK_EXTERNAL_URI, false)
}
/**
* Uriから楽曲を取得する
*
* @param queryUri 検索するフォルダパスを表すURI
* @param appendFlag リストに追加するか、上書きするかを指定するフラグ
*/
private fun searchAllMusicFromUri(context: Context, queryUri: Uri, appendFlag: Boolean) {
val intent: Intent = createIntent(
searchAction = ContentResolverConst.ACTION_SEARCH_MUSIC,
queryUri = queryUri,
projection = TRACK_COLUMNS,
selection = null,
selectionArgs = null,
sortOrder = MediaStore.Audio.Media.TITLE,
appendFlag = appendFlag
)
enqueueWork(context, ContentResolverService::class.java, JOB_ID, intent)
}
最後に、onHandleWork内に処理を書いていきます。
override fun onHandleWork(intent: Intent) {
val searchAction: String = intent.getStringExtra(ContentResolverConst.SEARCH_ACTION_KEY)
val appendFlag: Boolean = intent.getBooleanExtra(ContentResolverConst.APPEND_FLAG, false)
val queryUri: Uri = intent.getParcelableExtra(ContentResolverConst.QUERY_URI)
val projection: Array<String>? = intent.getStringArrayExtra(ContentResolverConst.QUERY_PROJECTION)
val selection: String? = intent.getStringExtra(ContentResolverConst.QUERY_SELECTION)
val selectionArgs: Array<String>? = intent.getStringArrayExtra(ContentResolverConst.QUERY_SELECTION_ARGS)
val sortOrder: String? = intent.getStringExtra(ContentResolverConst.QUERY_SORT_ORDER)
val cursor: Cursor = mResolver.query(queryUri, projection, selection, selectionArgs, sortOrder)
if (!cursor.moveToFirst()) {
// 最初の行に、カーソルを動かせない場合
cursor.close()
return
}
// 楽曲リストを取得する場合
val musicList: List<MusicMetadata> = createMusicMetadataFromCursor(cursor)
cursor.close()
}
/**
* 検索結果を楽曲メタデータに格納する
* cursorのmoveToFirstが呼び出されていることを、前提にしている
*/
private fun createMusicMetadataFromCursor(cursor: Cursor): List<MusicMetadata> =
mutableListOf<MusicMetadata>().apply {
while (cursor.moveToNext()) {
add(
MusicMetadata(
mId = cursor.getLongFromColumn(MediaStore.Audio.Media._ID),
mArtist = cursor.getStringFromColumn(MediaStore.Audio.Media.ARTIST),
mAlbum = cursor.getStringFromColumn(MediaStore.Audio.Media.ALBUM),
mAlbumId = cursor.getLongFromColumn(MediaStore.Audio.Media.ALBUM_ID),
mAlbumKey = cursor.getStringFromColumn(MediaStore.Audio.Media.ALBUM_KEY),
mDurationInMs = cursor.getLongFromColumn(MediaStore.Audio.Media.DURATION),
mTrackNumber = cursor.getIntFromColumn(MediaStore.Audio.Media.TRACK),
mTitle = cursor.getStringFromColumn(MediaStore.Audio.Media.TITLE),
mFilePath = cursor.getStringFromColumn(MediaStore.Audio.Media.DATA)
)
)
}
}
これで楽曲の検索結果が、リストに格納されました。
あとはこのリストを、RecyclerViewなどに入れ込めば、
楽曲リストの表示が行なえます。
ここまでお読み頂きありがとうございました。