滅入るんるん

何か書きます

【Android】DownloadManagerでDLしたファイルをIntentで表示する方法

Androidではファイルをダウンロードする方法としてDownloadManagerが提供されています。DownloadManagerでダウンロードしたファイルをIntentでACTION_VIEWを投げ外部アプリで表示するというコードを書くことがあったのでやり方を書いておきます。

github.com

いつものようにサンプルコード置いています。

Android NからfileをIntentで飛ばせない

kit-lab.hatenablog.jp

こちらのサイトでかなり丁寧に説明されているので詳細はそちらをどうぞ。

回避策として、FileProviderを利用して、ダウンロードしたファイルをfile://からcontent://に変換しIntentを発行するようにします。

DownloadManagerの使い方

始めにですが、諸事情(自分がUPPER_SNAKE_CASEを使わない派閥に属してる)により、UPPER_SNAKE_CASEな名前はすべてlowerCamelCaseにimport asしています。ちょっと量が多いですが、以下のコードではimport asされているコードだと思ってください;;

import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE as downloadManagerActionDownloadComplete
import android.app.DownloadManager.COLUMN_LOCAL_URI as downloadManagerColumnLocalUri
import android.app.DownloadManager.COLUMN_STATUS as downloadManagerColumnStatus
import android.app.DownloadManager.EXTRA_DOWNLOAD_ID as downloadManagerExtraDownloadId
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED as downloadManagerNotificationVisibilityVisibleNotifyCompleted
import android.app.DownloadManager.STATUS_SUCCESSFUL as downloadManagerStatusSuccessful
import android.content.Intent.ACTION_VIEW as intentActionView
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK as intentFlagActivityNewTask
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION as intentFlagGrantReadUriPermission
import android.content.pm.PackageManager.MATCH_ALL as packageManagerMatchAll
import android.os.Environment.DIRECTORY_DOWNLOADS as environmentDirectoryDownloads

ファイルをダウンロードする際はContext.getSystemServiceから取ってきたDownloadManagerに対してRequestを投げるという感じになります。サンプルでは外部ストレージのアプリ領域に配置するように指示しています。

    fun download(url: String, fileName: String) {
        val uri = Uri.parse(url)
        val request = DownloadManager.Request(uri).apply {
            setDestinationInExternalFilesDir(context, environmentDirectoryDownloads, fileName)
            setNotificationVisibility(downloadManagerNotificationVisibilityVisibleNotifyCompleted)
            setVisibleInDownloadsUi(true)
        }
        downloadManager.enqueue(request)
    }

また、ファイルがダウンロードされたタイミングを知るにはBroadcastReceiverを登録することになります。

context.registerReceiver(onDownloadCompleted, IntentFilter(downloadManagerActionDownloadComplete))

登録したBroadcastReceiverのonReceiveでダウンロードされたファイルのパスを取得し、Intentに投げるというフローになります。

DLしたファイルのIntentを発行する

onReceiveではintent.getLongExtra(downloadManagerExtraDownloadId, 0)によってDL対象のIDを取得することができます。

val cursor = downloadManager.query(query)

if (cursor.moveToFirst().not()) {
    return
}

val status = cursor.getInt(cursor.getColumnIndex(downloadManagerColumnStatus))

if (status != downloadManagerStatusSuccessful) {
    return
}

val path = cursor.getString(cursor.getColumnIndex(downloadManagerColumnLocalUri))

IDを取得したら上のコードのようにCursor経由でfile pathを取得する必要があります。DownloadManager.getUriForDownloadedFile(long id)というちょうどよさそうなメソッドが提供されていますが、stack overflow - Android DownloadManager class: getUriForDownloadedFile return wrong pathにもあるように、このメソッドを使うとファイルパスを知ることはできません。

また、Cursor経由で取得したfile pathにはfile://が含まれているためFileProviderでうまく判定されません。

val uri = Uri.parse(path)
val contentUri = FileProvider.getUriForFile(this@DownloadService.context, authorities, uri.toFile())

そのため、Uriを経由することでfile://が入っていないpathをFileProviderに渡すことができます。

ここで、FileProviderを利用するにはmanifestにproviderとして記載する必要があります

res/xml/content_provider.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path name="download" path="Download/"/>
</paths>

AndroidManifest.xml:

    <application>
        <provider
            android:authorities="net.meilcli.donloadmanagerintentsample.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true"
            android:name="androidx.core.content.FileProvider">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/content_provider"/>
        </provider>
    </application>

android:authoritiesの値はユニークになればどのような感じでもいいらしいので、package名を使うといいでしょう。ここで指定したandroid:authoritiesと同じ値を、FileProvider.getUriForFileの第二引数に指定します。

あとはIntentを発行するだけです。

        val intent = Intent().apply {
            action = intentActionView
            addFlags(intentFlagGrantReadUriPermission)
            addFlags(intentFlagActivityNewTask)
            setDataAndType(contentUri, mimeType)
        }

actionとdataとintentFlagGrantReadUriPermissionを指定することを忘れないようにしてください。これによって外部アプリにコンテンツの読み取り許可を与えます。これがないと外部アプリで読み取れないことになります。

以上でDownloadManagerからDLしたファイルをIntentで表示する方法です。
ちょっとコードの全体像がわからないかもしれないですがそのあたりはサンプルコードを見ていただければと