1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.icons 18 19 import android.content.Context 20 import android.graphics.Bitmap 21 import android.graphics.drawable.BitmapDrawable 22 import android.graphics.drawable.Drawable 23 import android.os.AsyncTask 24 import android.os.UserHandle 25 import android.util.SparseArray 26 import androidx.annotation.GuardedBy 27 import androidx.lifecycle.DefaultLifecycleObserver 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.LifecycleOwner 30 import com.android.intentresolver.Flags.targetHoverAndKeyboardFocusStates 31 import com.android.intentresolver.R 32 import com.android.intentresolver.SimpleIconFactory 33 import com.android.intentresolver.TargetPresentationGetter 34 import com.android.intentresolver.chooser.DisplayResolveInfo 35 import com.android.intentresolver.chooser.SelectableTargetInfo 36 import com.android.intentresolver.inject.ActivityOwned 37 import dagger.assisted.Assisted 38 import dagger.assisted.AssistedFactory 39 import dagger.assisted.AssistedInject 40 import dagger.hilt.android.qualifiers.ActivityContext 41 import java.util.concurrent.atomic.AtomicInteger 42 import java.util.function.Consumer 43 import javax.inject.Provider 44 import kotlinx.coroutines.Dispatchers 45 import kotlinx.coroutines.asExecutor 46 47 /** An actual [TargetDataLoader] implementation. */ 48 // TODO: replace async tasks with coroutines. 49 class DefaultTargetDataLoader 50 @AssistedInject 51 constructor( 52 @ActivityContext private val context: Context, 53 @ActivityOwned private val lifecycle: Lifecycle, 54 private val iconFactoryProvider: Provider<SimpleIconFactory>, 55 private val presentationFactory: TargetPresentationGetter.Factory, 56 @Assisted private val isAudioCaptureDevice: Boolean, 57 ) : TargetDataLoader { 58 private val nextTaskId = AtomicInteger(0) 59 @GuardedBy("self") private val activeTasks = SparseArray<AsyncTask<*, *, *>>() 60 private val executor = Dispatchers.IO.asExecutor() 61 62 init { 63 lifecycle.addObserver( 64 object : DefaultLifecycleObserver { 65 override fun onDestroy(owner: LifecycleOwner) { 66 lifecycle.removeObserver(this) 67 destroy() 68 } 69 } 70 ) 71 } 72 73 override fun getOrLoadAppTargetIcon( 74 info: DisplayResolveInfo, 75 userHandle: UserHandle, 76 callback: Consumer<Drawable>, 77 ): Drawable? { 78 val taskId = nextTaskId.getAndIncrement() 79 LoadIconTask(context, info, presentationFactory) { bitmap -> 80 removeTask(taskId) 81 callback.accept(bitmap?.toDrawable() ?: loadIconPlaceholder()) 82 } 83 .also { addTask(taskId, it) } 84 .executeOnExecutor(executor) 85 return null 86 } 87 88 override fun getOrLoadDirectShareIcon( 89 info: SelectableTargetInfo, 90 userHandle: UserHandle, 91 callback: Consumer<Drawable>, 92 ): Drawable? { 93 val taskId = nextTaskId.getAndIncrement() 94 LoadDirectShareIconTask( 95 context.createContextAsUser(userHandle, 0), 96 info, 97 presentationFactory, 98 iconFactoryProvider, 99 ) { bitmap -> 100 removeTask(taskId) 101 callback.accept(bitmap?.toDrawable() ?: loadIconPlaceholder()) 102 } 103 .also { addTask(taskId, it) } 104 .executeOnExecutor(executor) 105 return null 106 } 107 108 override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) { 109 val taskId = nextTaskId.getAndIncrement() 110 LoadLabelTask(context, info, isAudioCaptureDevice, presentationFactory) { result -> 111 removeTask(taskId) 112 callback.accept(result) 113 } 114 .also { addTask(taskId, it) } 115 .executeOnExecutor(executor) 116 } 117 118 override fun getOrLoadLabel(info: DisplayResolveInfo) { 119 if (!info.hasDisplayLabel()) { 120 val result = 121 LoadLabelTask.loadLabel(context, info, isAudioCaptureDevice, presentationFactory) 122 info.displayLabel = result.label 123 info.extendedInfo = result.subLabel 124 } 125 } 126 127 private fun addTask(id: Int, task: AsyncTask<*, *, *>) { 128 synchronized(activeTasks) { activeTasks.put(id, task) } 129 } 130 131 private fun removeTask(id: Int) { 132 synchronized(activeTasks) { activeTasks.remove(id) } 133 } 134 135 private fun loadIconPlaceholder(): Drawable = 136 requireNotNull(context.getDrawable(R.drawable.resolver_icon_placeholder)) 137 138 private fun destroy() { 139 synchronized(activeTasks) { 140 for (i in 0 until activeTasks.size()) { 141 activeTasks.valueAt(i).cancel(false) 142 } 143 activeTasks.clear() 144 } 145 } 146 147 private fun Bitmap.toDrawable(): Drawable { 148 return if (targetHoverAndKeyboardFocusStates()) { 149 HoverBitmapDrawable(this) 150 } else { 151 BitmapDrawable(context.resources, this) 152 } 153 } 154 155 @AssistedFactory 156 interface Factory { 157 fun create(isAudioCaptureDevice: Boolean): DefaultTargetDataLoader 158 } 159 } 160