WorkManagerとは
Android Jetpack の一部
バックグラウンド処理のためのアーキテクチャコンポーネント
環境
Projectのbuild.gradleにWorkManagerのバージョンを定義
buildscript {
…
versions.work = “2.5.0”
…
}
appのbuild.gradleにWorkManagerを追記
dependencies {
…
implementation “androidx.work:work-runtime-ktx:$versions.work”
}
サンプル
ファイル構成
root
┣workers
┃┣BlurWorker.kt // ぼかし処理(workerクラス)
┃┣CleanupWorker.kt // 一時ファイル削除処理(workerクラス)
┃┣SaveImageToFileWorker.kt // ファイル保存処理(workerクラス)
┃┗WorkerUtilis.kt // 共通処理
┣BlurActivity.kt // メインアクティビティ
┣BlurViewModel.kt // ビューモデル
┗Constants.kt // 定数
概要
イメージファイルにぼかし処理を行う。
処理は「一時ファイル削除」→「ぼかし処理を行い一時ファイルに保存」→「ファイル保存」の順
WorkManagerを用いてバックグラウンドで実行
制約として一連の処理は2重起動しない、ファイル保存時はバッテリー充電中のみ。
処理状況のステータスに応じて画面表示
処理完了後は出力イメージの参照ボタンを表示
Workerクラス
BlurWorker
WorkManagerクラスとしてWorkerクラスの子クラスを作成
doWorkメソッドにバックグラウンドで実行する処理を記載する
画像にぼかしを入れる処理
Result.success()で以降の処理の入力Dataを返す
/**
* WorkManagerクラスとしてWorkerクラスの子クラスを作成
* doWorkメソッドにバックグラウンドで実行する処理を記載する
* 画像にぼかしを入れる処理
* Result.success()で以降の処理の入力Dataを返す
*/
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.text.TextUtils
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.background.KEY_IMAGE_URI
private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
// WorkManagerで実行する処理内容を記載
override fun doWork(): Result {
// WorkerオブジェクトからContextを取得
val appContext = applicationContext
// 対象のイメージファイルを取得
// 1回目はapplyBlurで設定したDataオブジェクト
// 2回以降はこのfunで設定したDataオブジェクト
val resourceUri = inputData.getString(KEY_IMAGE_URI)
// 処理状況を表示
makeStatusNotification("Blurring image", appContext)
// 動作確認のための一時停止
sleep()
return try {
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
// 入力イメージを開く
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
// ぼかしの入ったイメージに変換
val output = blurBitmap(picture, appContext)
// 出力イメージを一時ファイルに保存
val outputUri = writeBitmapToFile(appContext, output)
// 出力UriをDataオブジェクトにセット(以降の処理の入力Dataとして利用する)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
// outputDataをsuccessで返却
Result.success(outputData)
} catch (throwable: Throwable) {
Log.e(TAG, "Error applying blur")
throwable.printStackTrace()
Result.failure()
}
}
}
CleanupWorker
WorkManagerクラスとしてWorkerクラスの子クラスを作成
doWorkメソッドにバックグラウンドで実行する処理を記載する
一時ファイルを削除する処理
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
// 処理状況の表示(ContextはWorkerオブジェクトからを取得)
makeStatusNotification("Cleaning up old temporary files", applicationContext)
// 動作確認のための一時停止
sleep()
return try {
// 出力ディレクトリの取得(ContextはWorkerオブジェクトからを取得)
val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
if (outputDirectory.exists()) {
// ディレクトリが存在する場合は一覧を取得
val entries = outputDirectory.listFiles()
if (entries != null) {
for (entry in entries) {
val name = entry.name
if (name.isNotEmpty() && name.endsWith(".png")) {
val deleted = entry.delete()
Log.i(TAG, "Deleted $name - $deleted")
}
}
}
}
Result.success()
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
SaveImageToFileWorker
WorkManagerクラスとしてWorkerクラスの子クラスを作成
doWorkメソッドにバックグラウンドで実行する処理を記載する
イメージファイルとして画像を保存する処理
Result.success()でWorkManagerにDataオブジェクトを返す
/**
* WorkManagerクラスとしてWorkerクラスの子クラスを作成
* doWorkメソッドにバックグラウンドで実行する処理を記載する
* イメージファイルとして画像を保存する処理
* Result.success()でWorkManagerにDataオブジェクトを返す
*/
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
private val Title = "Blurred Image"
private val dateFormatter = SimpleDateFormat(
"yyyy.MM.dd 'at' HH:mm:ss z",
Locale.getDefault()
)
override fun doWork(): Result {
// 処理状況の表示(ContextはWorkerオブジェクトからを取得)
makeStatusNotification("Saving image", applicationContext)
// 動作確認のための一時停止
sleep()
val resolver = applicationContext.contentResolver
return try {
// DataオブジェクトからURIを取得
val resourceUri = inputData.getString(KEY_IMAGE_URI)
// イメージを取得
val bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
// イメージをファイルに保存
val imageUrl = MediaStore.Images.Media.insertImage(
resolver, bitmap, Title, dateFormatter.format(Date()))
if (!imageUrl.isNullOrEmpty()) {
val output = workDataOf(KEY_IMAGE_URI to imageUrl)
// 成功を返却
Result.success(output)
} else {
Log.e(TAG, "Writing to MediaStore failed")
Result.failure()
}
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
レイアウトxml
1画面のみ
goButtonでワークマネージャーの処理を開始する
ファイル保存処理の状況に応じてボタンやProgressBarの表示切替
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_weight="1"
android:src="@drawable/test"
android:contentDescription="@string/description_image"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/filters_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blur_title"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<RadioGroup
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/radio_blur_group"
android:checkedButton="@+id/radio_blur_lv_1">
<RadioButton android:id="@+id/radio_blur_lv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blur_lv_1" />
<RadioButton android:id="@+id/radio_blur_lv_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blur_lv_2" />
<RadioButton android:id="@+id/radio_blur_lv_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blur_lv_3" />
</RadioGroup>
<RadioGroup
android:id="@+id/destinations"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_work"
android:visibility="gone"
/>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
android:layout_gravity="center_horizontal"
/>
</LinearLayout>
<Button
android:id="@+id/go_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go"
/>
<Button
android:id="@+id/see_file_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/see_file"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>
</ScrollView>
アクティビティ
1画面のみ
goButtonでワークマネージャーの処理を開始する
ファイル保存処理の状況に応じてボタンやProgressBarの表示切替
/**
* アプリで唯一の画面
* goButtonでワークマネージャーの処理を開始する
* 処理状況に応じて画面表示
*/
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.work.WorkInfo
import com.example.background.databinding.ActivityBlurBinding
class BlurActivity : AppCompatActivity() {
private val viewModel: BlurViewModel by viewModels { BlurViewModelFactory(application) }
private lateinit var binding: ActivityBlurBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBlurBinding.inflate(layoutInflater)
setContentView(binding.root)
// ぼかし処理全体のキューをWorkManagerに登録する処理
binding.goButton.setOnClickListener { viewModel.applyBlur(blurLevel) }
// 処理完了(outputUri)が存在する時はその画像を表示
binding.seeFileButton.setOnClickListener {
viewModel.outputUri?.let { currentUri ->
val actionView = Intent(Intent.ACTION_VIEW, currentUri)
actionView.resolveActivity(packageManager)?.run {
startActivity(actionView)
}
}
}
// キャンセルボタン
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }
// WorkManagerの処理状況を監視する
viewModel.outputWorkInfos.observe(this, workInfosObserver())
}
// WorkManagerの処理状況に応じた画面状態にする
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// WorkManagerが開始されるとlistOfWorkInfoに情報が入る
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// WorkManagerの実行情報を取得
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
// ボタン状態を処理完了にする
showWorkFinished()
// workInfoから保存処理SuccessのURI情報を取得
val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)
// ファイルを見るボタンにURIをセット
if (!outputImageUri.isNullOrEmpty()) {
viewModel.setOutputUri(outputImageUri as String)
binding.seeFileButton.visibility = View.VISIBLE
}
} else {
// ボタン状態を処理中にする
showWorkInProgress()
}
}
}
/**
* 保存処理中
*/
private fun showWorkInProgress() {
with(binding) {
progressBar.visibility = View.VISIBLE
cancelButton.visibility = View.VISIBLE
goButton.visibility = View.GONE
seeFileButton.visibility = View.GONE
}
}
/**
* 処理終了
*/
private fun showWorkFinished() {
with(binding) {
progressBar.visibility = View.GONE
cancelButton.visibility = View.GONE
goButton.visibility = View.VISIBLE
}
}
/**
* ぼかしレベル
*/
private val blurLevel: Int
get() =
when (binding.radioBlurGroup.checkedRadioButtonId) {
R.id.radio_blur_lv_1 -> 1
R.id.radio_blur_lv_2 -> 2
R.id.radio_blur_lv_3 -> 3
else -> 1
}
}
ViewModel
WorkManagerのリクエストをキューに登録
リクエストは「一時ファイル削除」、ぼかし処理(3回まで)、ファイル保存の順
チェーンの2重起動しない(チェーン名にIMAGE_MANIPULATION_WORK_NAME)
ファイル保存処理は充電中の制約あり
初回のぼかし処理はアクティビティから取得したimageUriをインプット
2回目以降のぼかし処理とファイル保存処理のインプットはぼかし処理のResult.successでoutputを設定
ファイル保存処理のResult.successはワークマネージャーのステータスから利用する
チェーンのキャンセル処理あり
/**
* WorkManagerのリクエストをキューに登録
* リクエストは「一時ファイル削除」、ぼかし処理(3回まで)、ファイル保存の順
* チェーンの2重起動しない(チェーン名にIMAGE_MANIPULATION_WORK_NAME)
* ファイル保存処理は充電中の制約あり
* 初回のぼかし処理はアクティビティから取得したimageUriをインプット
* 2回目以降のぼかし処理とファイル保存処理のインプットはぼかし処理のResult.successでoutputを設定
* ファイル保存処理のResult.successはワークマネージャーのステータスから利用する
* チェーンのキャンセル処理あり
*/
import android.app.Application
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.example.background.workers.BlurWorker
import com.example.background.workers.CleanupWorker
import com.example.background.workers.SaveImageToFileWorker
class BlurViewModel(application: Application) : ViewModel() {
// internalは同じモジュール内からアクセス可能
internal var imageUri: Uri? = null
internal var outputUri: Uri? = null
// workManager
private val workManager = WorkManager.getInstance(application)
// WorkManagerの処理状況を格納
internal val outputWorkInfos: LiveData<List<WorkInfo>>
init {
// workManagerで指定タグの処理状況を監視(保存処理に付けたタグを監視)
outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
// 入力イメージのURIを取得(固定値)
imageUri = getImageUri(application.applicationContext)
}
/**
* 入力のDataオブジェクト
* keyとValueペアのオブジェクト
*/
private fun createInputDataForUri(): Data {
// Dataビルダーオブジェクトを生成
val builder = Data.Builder()
// DataオブジェクトにimageUriをセット
imageUri?.let {
builder.putString(KEY_IMAGE_URI, imageUri.toString())
}
// Dataオブジェクトを返却
return builder.build()
}
/**
* WorkManagerでWorkRequestをキューに登録する
*/
internal fun applyBlur(blurLevel: Int) {
// WorkRequestを単独ではなくチェーンとして実行(先頭がCleanupWorker)
// 処理チェーンに名前を付けて一度に複数のチェーン起動を避ける
// ExistingWorkPolicyのオプションは次のいずれか(REPLACE,KEEP,APPEND)
var continuation = workManager
.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
)
// ぼかし処理を指定回数キューに登録
for (i in 0 until blurLevel) {
// ぼかし処理のworkManagerを定義
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
if (i == 0) {
// 初回処理の入力データをセット。引数はイメージuriのデータオブジェクトを渡す
// 2回目以降の入力データはBlurWorker内で実施
blurBuilder.setInputData(createInputDataForUri())
}
// ぼかし処理をキューに登録(指定回数繰り返す)
continuation = continuation.then(blurBuilder.build())
}
// ファイル保存時に充電中の制約を付加
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
// 保存処理のworkManagerを定義
// workInfo取得用のタグをセットする
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.setConstraints(constraints)
.addTag(TAG_OUTPUT)
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
// キャンセル処理
internal fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
private fun uriOrNull(uriString: String?): Uri? {
return if (!uriString.isNullOrEmpty()) {
Uri.parse(uriString)
} else {
null
}
}
// 固定で入力イメージファイルのUriを出力
private fun getImageUri(context: Context): Uri {
val resources = context.resources
val imageUri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(resources.getResourcePackageName(R.drawable.test))
.appendPath(resources.getResourceTypeName(R.drawable.test))
.appendPath(resources.getResourceEntryName(R.drawable.test))
.build()
return imageUri
}
internal fun setOutputUri(outputImageUri: String?) {
outputUri = uriOrNull(outputImageUri)
}
}
// ViewModelのFactoryクラス
class BlurViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(BlurViewModel::class.java)) {
BlurViewModel(application) as T
} else {
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
その他
@file:JvmName("Constants")
package com.example.background
// Notification Channel constants
// Name of Notification Channel for verbose notifications of background work
@JvmField val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence =
"Verbose WorkManager Notifications"
const val VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION =
"Shows notifications whenever work starts"
@JvmField val NOTIFICATION_TITLE: CharSequence = "WorkRequest Starting"
const val CHANNEL_ID = "VERBOSE_NOTIFICATION"
const val NOTIFICATION_ID = 1
// The name of the image manipulation work
const val IMAGE_MANIPULATION_WORK_NAME = "image_manipulation_work"
// Other keys
const val OUTPUT_PATH = "blur_filter_outputs"
const val KEY_IMAGE_URI = "KEY_IMAGE_URI"
const val TAG_OUTPUT = "OUTPUT"
const val DELAY_TIME_MILLIS: Long = 3000
@file:JvmName("WorkerUtils")
package com.example.background.workers
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.renderscript.Allocation
import androidx.renderscript.Element
import androidx.renderscript.RenderScript
import androidx.renderscript.ScriptIntrinsicBlur
import com.example.background.CHANNEL_ID
import com.example.background.DELAY_TIME_MILLIS
import com.example.background.NOTIFICATION_ID
import com.example.background.NOTIFICATION_TITLE
import com.example.background.OUTPUT_PATH
import com.example.background.R
import com.example.background.VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION
import com.example.background.VERBOSE_NOTIFICATION_CHANNEL_NAME
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
private const val TAG = "WorkerUtils"
// 処理状況をNotificationで表示する
fun makeStatusNotification(message: String, context: Context) {
// Make a channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
val name = VERBOSE_NOTIFICATION_CHANNEL_NAME
val description = VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(CHANNEL_ID, name, importance)
channel.description = description
// Add the channel
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
notificationManager?.createNotificationChannel(channel)
}
// Create the notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(NOTIFICATION_TITLE)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVibrate(LongArray(0))
// Show the notification
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build())
}
/**
* 3秒の停止
*/
fun sleep() {
try {
Thread.sleep(DELAY_TIME_MILLIS, 0)
} catch (e: InterruptedException) {
Log.e(TAG, e.message)
}
}
/**
* Blurs the given Bitmap image
* @param bitmap Image to blur
* @param applicationContext Application context
* @return Blurred bitmap image
*/
@WorkerThread
fun blurBitmap(bitmap: Bitmap, applicationContext: Context): Bitmap {
lateinit var rsContext: RenderScript
try {
// Create the output bitmap
val output = Bitmap.createBitmap(
bitmap.width, bitmap.height, bitmap.config)
// Blur the image
rsContext = RenderScript.create(applicationContext, RenderScript.ContextType.DEBUG)
val inAlloc = Allocation.createFromBitmap(rsContext, bitmap)
val outAlloc = Allocation.createTyped(rsContext, inAlloc.type)
val theIntrinsic = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
theIntrinsic.apply {
setRadius(10f)
theIntrinsic.setInput(inAlloc)
theIntrinsic.forEach(outAlloc)
}
outAlloc.copyTo(output)
return output
} finally {
rsContext.finish()
}
}
/**
* Writes bitmap to a temporary file and returns the Uri for the file
* @param applicationContext Application context
* @param bitmap Bitmap to write to temp file
* @return Uri for temp file with bitmap
* @throws FileNotFoundException Throws if bitmap file cannot be found
*/
@Throws(FileNotFoundException::class)
fun writeBitmapToFile(applicationContext: Context, bitmap: Bitmap): Uri {
val name = String.format("blur-filter-output-%s.png", UUID.randomUUID().toString())
val outputDir = File(applicationContext.filesDir, OUTPUT_PATH)
if (!outputDir.exists()) {
outputDir.mkdirs() // should succeed
}
val outputFile = File(outputDir, name)
var out: FileOutputStream? = null
try {
out = FileOutputStream(outputFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, out)
} finally {
out?.let {
try {
it.close()
} catch (ignore: IOException) {
}
}
}
return Uri.fromFile(outputFile)
}