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