LoginSignup
6
2

More than 5 years have passed since last update.

JobIntentServiceで行う端末の楽曲検索

Posted at

現在、個人で音楽プレイヤーアプリを作っていて、
楽曲の一覧表示まで、できているのですが、

そのとき使った楽曲検索方法について、ご紹介したいと思います。

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の拡張関数を定義します。

CursorExt.kt

/**
 * 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 があるので簡単に書くことができます。

MusicMetadata.kt

/**
 * 楽曲メタデータ
 */
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から情報を引き出すときに使う、
定数を保持するクラスを作成します。

ContentResolverConst.kt
/**
 * 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を作成します。

MyJobIntentService.kt

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に記述を追加します。

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メソッドを実装します。

MyJobIntentService.kt
    /**
     * 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に詰めるだけです。

次は、外から呼ばれる楽曲検索のトリガーとなるメソッドを実装します。

MyJobIntentService.kt

    /**
     * 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内に処理を書いていきます。

MyJobIntentService.kt

    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などに入れ込めば、
楽曲リストの表示が行なえます。

ここまでお読み頂きありがとうございました。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2