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