1 /*
<lambda>null2  * Copyright (C) 2020 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.egg.neko
18 
19 import android.app.PendingIntent
20 import android.content.Intent
21 import android.content.res.ColorStateList
22 import android.graphics.drawable.Icon
23 import android.service.controls.Control
24 import android.service.controls.ControlsProviderService
25 import android.service.controls.DeviceTypes
26 import android.service.controls.actions.ControlAction
27 import android.service.controls.actions.FloatAction
28 import android.service.controls.templates.ControlButton
29 import android.service.controls.templates.RangeTemplate
30 import android.service.controls.templates.StatelessTemplate
31 import android.service.controls.templates.ToggleTemplate
32 import android.text.SpannableStringBuilder
33 import android.text.style.ForegroundColorSpan
34 import android.util.Log
35 import androidx.annotation.RequiresApi
36 import com.android.internal.logging.MetricsLogger
37 import java.util.Random
38 import java.util.concurrent.Flow
39 import java.util.function.Consumer
40 
41 import com.android.egg.R
42 
43 const val CONTROL_ID_WATER = "water"
44 const val CONTROL_ID_FOOD = "food"
45 const val CONTROL_ID_TOY = "toy"
46 
47 const val FOOD_SPAWN_CAT_DELAY_MINS = 5L
48 
49 const val COLOR_FOOD_FG = 0xFFFF8000.toInt()
50 const val COLOR_FOOD_BG = COLOR_FOOD_FG and 0x40FFFFFF.toInt()
51 const val COLOR_WATER_FG = 0xFF0080FF.toInt()
52 const val COLOR_WATER_BG = COLOR_WATER_FG and 0x40FFFFFF.toInt()
53 const val COLOR_TOY_FG = 0xFFFF4080.toInt()
54 const val COLOR_TOY_BG = COLOR_TOY_FG and 0x40FFFFFF.toInt()
55 
56 val P_TOY_ICONS = intArrayOf(
57         1, R.drawable.ic_toy_mouse,
58         1, R.drawable.ic_toy_fish,
59         1, R.drawable.ic_toy_ball,
60         1, R.drawable.ic_toy_laser
61 )
62 
63 @RequiresApi(30)
64 fun Control_toString(control: Control): String {
65     val hc = String.format("0x%08x", control.hashCode())
66     return ("Control($hc id=${control.controlId}, type=${control.deviceType}, " +
67         "title=${control.title}, template=${control.controlTemplate})")
68 }
69 
70 @RequiresApi(30)
71 public class NekoControlsService : ControlsProviderService(), PrefState.PrefsListener {
72     private val TAG = "NekoControls"
73 
74     private val controls = HashMap<String, Control>()
75     private val publishers = ArrayList<UglyPublisher>()
76     private val rng = Random()
77     private val metricsLogger = MetricsLogger()
78 
79     private var lastToyIcon: Icon? = null
80 
81     private lateinit var prefs: PrefState
82 
onCreatenull83     override fun onCreate() {
84         super.onCreate()
85 
86         prefs = PrefState(this)
87         prefs.setListener(this)
88 
89         createDefaultControls()
90     }
91 
onPrefsChangednull92     override fun onPrefsChanged() {
93         createDefaultControls()
94     }
95 
createDefaultControlsnull96     private fun createDefaultControls() {
97         val foodState: Int = prefs.foodState
98         if (foodState != 0) {
99             NekoService.registerJobIfNeeded(this, FOOD_SPAWN_CAT_DELAY_MINS)
100         }
101 
102         val water = prefs.waterState
103 
104         controls[CONTROL_ID_WATER] = makeWaterBowlControl(water)
105         controls[CONTROL_ID_FOOD] = makeFoodBowlControl(foodState != 0)
106         controls[CONTROL_ID_TOY] = makeToyControl(currentToyIcon(), false)
107     }
108 
currentToyIconnull109     private fun currentToyIcon(): Icon {
110         val icon = lastToyIcon ?: randomToyIcon()
111         lastToyIcon = icon
112         return icon
113     }
114 
randomToyIconnull115     private fun randomToyIcon(): Icon {
116         return Icon.createWithResource(resources, Cat.chooseP(rng, P_TOY_ICONS, 4))
117     }
118 
colorizenull119     private fun colorize(s: CharSequence, color: Int): CharSequence {
120         val ssb = SpannableStringBuilder(s)
121         ssb.setSpan(ForegroundColorSpan(color), 0, s.length, 0)
122         return ssb
123     }
124 
makeToyControlnull125     private fun makeToyControl(icon: Icon?, thrown: Boolean): Control {
126         return Control.StatefulBuilder(CONTROL_ID_TOY, getPendingIntent())
127                 .setDeviceType(DeviceTypes.TYPE_UNKNOWN)
128                 .setCustomIcon(icon)
129                         //  ?.setTint(COLOR_TOY_FG)) // TODO(b/159559045): uncomment when fixed
130                 .setCustomColor(ColorStateList.valueOf(COLOR_TOY_BG))
131                 .setTitle(colorize(getString(R.string.control_toy_title), COLOR_TOY_FG))
132                 .setStatusText(colorize(
133                         if (thrown) getString(R.string.control_toy_status) else "",
134                         COLOR_TOY_FG))
135                 .setControlTemplate(StatelessTemplate("toy"))
136                 .setStatus(Control.STATUS_OK)
137                 .setSubtitle(if (thrown) "" else getString(R.string.control_toy_subtitle))
138                 .setAppIntent(getAppIntent())
139                 .build()
140     }
141 
makeWaterBowlControlnull142     private fun makeWaterBowlControl(fillLevel: Float): Control {
143         return Control.StatefulBuilder(CONTROL_ID_WATER, getPendingIntent())
144                 .setDeviceType(DeviceTypes.TYPE_KETTLE)
145                 .setTitle(colorize(getString(R.string.control_water_title), COLOR_WATER_FG))
146                 .setCustomColor(ColorStateList.valueOf(COLOR_WATER_BG))
147                 .setCustomIcon(Icon.createWithResource(resources,
148                         if (fillLevel >= 100f) R.drawable.ic_water_filled else R.drawable.ic_water))
149                         //.setTint(COLOR_WATER_FG)) // TODO(b/159559045): uncomment when fixed
150                 .setControlTemplate(RangeTemplate("waterlevel", 0f, 200f, fillLevel, 10f,
151                         "%.0f mL"))
152                 .setStatus(Control.STATUS_OK)
153                 .setSubtitle(if (fillLevel == 0f) getString(R.string.control_water_subtitle) else "")
154                 .build()
155     }
156 
makeFoodBowlControlnull157     private fun makeFoodBowlControl(filled: Boolean): Control {
158         return Control.StatefulBuilder(CONTROL_ID_FOOD, getPendingIntent())
159                 .setDeviceType(DeviceTypes.TYPE_UNKNOWN)
160                 .setCustomColor(ColorStateList.valueOf(COLOR_FOOD_BG))
161                 .setTitle(colorize(getString(R.string.control_food_title), COLOR_FOOD_FG))
162                 .setCustomIcon(Icon.createWithResource(resources,
163                         if (filled) R.drawable.ic_foodbowl_filled else R.drawable.ic_bowl))
164                         // .setTint(COLOR_FOOD_FG)) // TODO(b/159559045): uncomment when fixed
165                 .setStatusText(
166                         if (filled) colorize(
167                                 getString(R.string.control_food_status_full), 0xCCFFFFFF.toInt())
168                         else colorize(
169                                 getString(R.string.control_food_status_empty), 0x80FFFFFF.toInt()))
170                 .setControlTemplate(ToggleTemplate("foodbowl", ControlButton(filled, "Refill")))
171                 .setStatus(Control.STATUS_OK)
172                 .setSubtitle(if (filled) "" else getString(R.string.control_food_subtitle))
173                 .build()
174     }
175 
getPendingIntentnull176     private fun getPendingIntent(): PendingIntent {
177         val intent = Intent(Intent.ACTION_MAIN)
178                 .setClass(this, NekoLand::class.java)
179                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
180         return PendingIntent.getActivity(this, 0, intent,
181             PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
182     }
183 
getAppIntentnull184     private fun getAppIntent(): PendingIntent {
185         return getPendingIntent()
186     }
187 
performControlActionnull188     override fun performControlAction(
189         controlId: String,
190         action: ControlAction,
191         consumer: Consumer<Int>
192     ) {
193         when (controlId) {
194             CONTROL_ID_FOOD -> {
195                 // refill bowl
196                 controls[CONTROL_ID_FOOD] = makeFoodBowlControl(true)
197                 Log.v(TAG, "Bowl refilled. (Registering job.)")
198                 NekoService.registerJob(this, FOOD_SPAWN_CAT_DELAY_MINS)
199                 metricsLogger.histogram("egg_neko_offered_food", 11)
200                 prefs.foodState = 11
201             }
202             CONTROL_ID_TOY -> {
203                 Log.v(TAG, "Toy tossed.")
204                 controls[CONTROL_ID_TOY] =
205                     makeToyControl(currentToyIcon(), true)
206                 // TODO: re-enable toy
207                 Thread() {
208                     Thread.sleep((1 + Random().nextInt(4)) * 1000L)
209                     NekoService.getExistingCat(prefs)?.let {
210                         NekoService.notifyCat(this, it)
211                     }
212                     controls[CONTROL_ID_TOY] = makeToyControl(randomToyIcon(), false)
213                     pushControlChanges()
214                 }.start()
215             }
216             CONTROL_ID_WATER -> {
217                 if (action is FloatAction) {
218                     controls[CONTROL_ID_WATER] = makeWaterBowlControl(action.newValue)
219                     Log.v(TAG, "Water level set to " + action.newValue)
220                     prefs.waterState = action.newValue
221                 }
222             }
223             else -> {
224                 return
225             }
226         }
227         consumer.accept(ControlAction.RESPONSE_OK)
228         pushControlChanges()
229     }
230 
pushControlChangesnull231     private fun pushControlChanges() {
232         Thread() {
233             publishers.forEach { it.refresh() }
234         }.start()
235     }
236 
makeStatelessnull237     private fun makeStateless(c: Control?): Control? {
238         if (c == null) return null
239         return Control.StatelessBuilder(c.controlId, c.appIntent)
240                 .setTitle(c.title)
241                 .setSubtitle(c.subtitle)
242                 .setStructure(c.structure)
243                 .setDeviceType(c.deviceType)
244                 .setCustomIcon(c.customIcon)
245                 .setCustomColor(c.customColor)
246                 .build()
247     }
248 
createPublisherFornull249     override fun createPublisherFor(list: MutableList<String>): Flow.Publisher<Control> {
250         createDefaultControls()
251 
252         val publisher = UglyPublisher(list, true)
253         publishers.add(publisher)
254         return publisher
255     }
256 
createPublisherForAllAvailablenull257     override fun createPublisherForAllAvailable(): Flow.Publisher<Control> {
258         createDefaultControls()
259 
260         val publisher = UglyPublisher(controls.keys, false)
261         publishers.add(publisher)
262         return publisher
263     }
264 
265     private inner class UglyPublisher(
266         val controlKeys: Iterable<String>,
267         val indefinite: Boolean
268     ) : Flow.Publisher<Control> {
269         val subscriptions = ArrayList<UglySubscription>()
270 
271         private inner class UglySubscription(
272             val initialControls: Iterator<Control>,
273             var subscriber: Flow.Subscriber<in Control>?
274         ) : Flow.Subscription {
cancelnull275             override fun cancel() {
276                 Log.v(TAG, "cancel subscription: $this for subscriber: $subscriber " +
277                         "to publisher: $this@UglyPublisher")
278                 subscriber = null
279                 unsubscribe(this)
280             }
281 
requestnull282             override fun request(p0: Long) {
283                 (0 until p0).forEach { _ ->
284                     if (initialControls.hasNext()) {
285                         send(initialControls.next())
286                     } else {
287                         if (!indefinite) subscriber?.onComplete()
288                     }
289                 }
290             }
291 
sendnull292             fun send(c: Control) {
293                 Log.v(TAG, "sending update: " + Control_toString(c) + " => " + subscriber)
294                 subscriber?.onNext(c)
295             }
296         }
297 
subscribenull298         override fun subscribe(subscriber: Flow.Subscriber<in Control>) {
299             Log.v(TAG, "subscribe to publisher: $this by subscriber: $subscriber")
300             val sub = UglySubscription(controlKeys.mapNotNull { controls[it] }.iterator(),
301             subscriber)
302             subscriptions.add(sub)
303             subscriber.onSubscribe(sub)
304         }
305 
unsubscribenull306         fun unsubscribe(sub: UglySubscription) {
307             Log.v(TAG, "no more subscriptions, removing subscriber: $sub")
308             subscriptions.remove(sub)
309             if (subscriptions.size == 0) {
310                 Log.v(TAG, "no more subscribers, removing publisher: $this")
311                 publishers.remove(this)
312             }
313         }
314 
refreshnull315         fun refresh() {
316             controlKeys.mapNotNull { controls[it] }.forEach { control ->
317                 subscriptions.forEach { sub ->
318                     sub.send(control)
319                 }
320             }
321         }
322     }
323 }
324