1 /* 2 * 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 static com.android.egg.neko.NekoLand.CHAN_ID; 20 21 import android.app.Notification; 22 import android.app.PendingIntent; 23 import android.app.Person; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.ShortcutInfo; 27 import android.content.pm.ShortcutManager; 28 import android.content.res.Resources; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.ColorFilter; 33 import android.graphics.PixelFormat; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.Icon; 36 import android.os.Bundle; 37 38 import com.android.egg.R; 39 import com.android.internal.logging.MetricsLogger; 40 41 import java.io.ByteArrayOutputStream; 42 import java.lang.reflect.InvocationTargetException; 43 import java.util.List; 44 import java.util.Random; 45 import java.util.concurrent.ThreadLocalRandom; 46 47 /** It's a cat. */ 48 public class Cat extends Drawable { 49 public static final long[] PURR = {0, 40, 20, 40, 20, 40, 20, 40, 20, 40, 20, 40}; 50 51 public static final boolean ALL_CATS_IN_ONE_CONVERSATION = true; 52 53 public static final String GLOBAL_SHORTCUT_ID = "com.android.egg.neko:allcats"; 54 public static final String SHORTCUT_ID_PREFIX = "com.android.egg.neko:cat:"; 55 56 private Random mNotSoRandom; 57 private Bitmap mBitmap; 58 private long mSeed; 59 private String mName; 60 private int mBodyColor; 61 private int mFootType; 62 private boolean mBowTie; 63 private String mFirstMessage; 64 notSoRandom(long seed)65 private synchronized Random notSoRandom(long seed) { 66 if (mNotSoRandom == null) { 67 mNotSoRandom = new Random(); 68 mNotSoRandom.setSeed(seed); 69 } 70 return mNotSoRandom; 71 } 72 frandrange(Random r, float a, float b)73 public static final float frandrange(Random r, float a, float b) { 74 return (b - a) * r.nextFloat() + a; 75 } 76 choose(Random r, Object... l)77 public static final Object choose(Random r, Object... l) { 78 return l[r.nextInt(l.length)]; 79 } 80 chooseP(Random r, int[] a)81 public static final int chooseP(Random r, int[] a) { 82 return chooseP(r, a, 1000); 83 } 84 chooseP(Random r, int[] a, int sum)85 public static final int chooseP(Random r, int[] a, int sum) { 86 int pct = r.nextInt(sum); 87 final int stop = a.length - 2; 88 int i = 0; 89 while (i < stop) { 90 pct -= a[i]; 91 if (pct < 0) break; 92 i += 2; 93 } 94 return a[i + 1]; 95 } 96 getColorIndex(int q, int[] a)97 public static final int getColorIndex(int q, int[] a) { 98 for (int i = 1; i < a.length; i += 2) { 99 if (a[i] == q) { 100 return i / 2; 101 } 102 } 103 return -1; 104 } 105 106 public static final int[] P_BODY_COLORS = { 107 180, 0xFF212121, // black 108 180, 0xFFFFFFFF, // white 109 140, 0xFF616161, // gray 110 140, 0xFF795548, // brown 111 100, 0xFF90A4AE, // steel 112 100, 0xFFFFF9C4, // buff 113 100, 0xFFFF8F00, // orange 114 5, 0xFF29B6F6, // blue..? 115 5, 0xFFFFCDD2, // pink!? 116 5, 0xFFCE93D8, // purple?!?!? 117 4, 0xFF43A047, // yeah, why not green 118 1, 0, // ?!?!?! 119 }; 120 121 public static final int[] P_COLLAR_COLORS = { 122 250, 0xFFFFFFFF, 123 250, 0xFF000000, 124 250, 0xFFF44336, 125 50, 0xFF1976D2, 126 50, 0xFFFDD835, 127 50, 0xFFFB8C00, 128 50, 0xFFF48FB1, 129 50, 0xFF4CAF50, 130 }; 131 132 public static final int[] P_BELLY_COLORS = { 133 750, 0, 134 250, 0xFFFFFFFF, 135 }; 136 137 public static final int[] P_DARK_SPOT_COLORS = { 138 700, 0, 139 250, 0xFF212121, 140 50, 0xFF6D4C41, 141 }; 142 143 public static final int[] P_LIGHT_SPOT_COLORS = { 144 700, 0, 145 300, 0xFFFFFFFF, 146 }; 147 148 private CatParts D; 149 tint(int color, Drawable... ds)150 public static void tint(int color, Drawable... ds) { 151 for (Drawable d : ds) { 152 if (d != null) { 153 d.mutate().setTint(color); 154 } 155 } 156 } 157 isDark(int color)158 public static boolean isDark(int color) { 159 final int r = (color & 0xFF0000) >> 16; 160 final int g = (color & 0x00FF00) >> 8; 161 final int b = color & 0x0000FF; 162 return (r + g + b) < 0x80; 163 } 164 Cat(Context context, long seed)165 public Cat(Context context, long seed) { 166 D = new CatParts(context); 167 mSeed = seed; 168 169 setName(context.getString(R.string.default_cat_name, 170 String.valueOf(mSeed % 1000))); 171 172 final Random nsr = notSoRandom(seed); 173 174 // body color 175 mBodyColor = chooseP(nsr, P_BODY_COLORS); 176 if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[]{ 177 nsr.nextFloat() * 360f, frandrange(nsr, 0.5f, 1f), frandrange(nsr, 0.5f, 1f)}); 178 179 tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail, 180 D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap); 181 tint(0x20000000, D.leg2Shadow, D.tailShadow); 182 if (isDark(mBodyColor)) { 183 tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose); 184 } 185 tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside); 186 187 tint(chooseP(nsr, P_BELLY_COLORS), D.belly); 188 tint(chooseP(nsr, P_BELLY_COLORS), D.back); 189 final int faceColor = chooseP(nsr, P_BELLY_COLORS); 190 tint(faceColor, D.faceSpot); 191 if (!isDark(faceColor)) { 192 tint(0xFF000000, D.mouth, D.nose); 193 } 194 195 mFootType = 0; 196 if (nsr.nextFloat() < 0.25f) { 197 mFootType = 4; 198 tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4); 199 } else { 200 if (nsr.nextFloat() < 0.25f) { 201 mFootType = 2; 202 tint(0xFFFFFFFF, D.foot1, D.foot3); 203 } else if (nsr.nextFloat() < 0.25f) { 204 mFootType = 3; // maybe -2 would be better? meh. 205 tint(0xFFFFFFFF, D.foot2, D.foot4); 206 } else if (nsr.nextFloat() < 0.1f) { 207 mFootType = 1; 208 tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4)); 209 } 210 } 211 212 tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap); 213 214 final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS); 215 tint(capColor, D.cap); 216 //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose); 217 218 final int collarColor = chooseP(nsr, P_COLLAR_COLORS); 219 tint(collarColor, D.collar); 220 mBowTie = nsr.nextFloat() < 0.1f; 221 tint(mBowTie ? collarColor : 0, D.bowtie); 222 223 String[] messages = context.getResources().getStringArray( 224 nsr.nextFloat() < 0.1f ? R.array.rare_cat_messages : R.array.cat_messages); 225 mFirstMessage = (String) choose(nsr, (Object[]) messages); 226 if (nsr.nextFloat() < 0.5f) mFirstMessage = mFirstMessage + mFirstMessage + mFirstMessage; 227 } 228 229 public static Cat fromShortcutId(Context context, String shortcutId) { 230 if (shortcutId.startsWith(SHORTCUT_ID_PREFIX)) { 231 return new Cat(context, Long.parseLong(shortcutId.replace(SHORTCUT_ID_PREFIX, ""))); 232 } 233 return null; 234 } 235 236 public static Cat create(Context context) { 237 return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt())); 238 } 239 240 public Notification.Builder buildNotification(Context context) { 241 final Bundle extras = new Bundle(); 242 extras.putString("android.substName", context.getString(R.string.notification_name)); 243 244 final Icon notificationIcon = createNotificationLargeIcon(context); 245 246 final Intent intent = new Intent(Intent.ACTION_MAIN) 247 .setClass(context, NekoLand.class) 248 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 249 250 ShortcutInfo shortcut = new ShortcutInfo.Builder(context, getShortcutId()) 251 .setActivity(intent.getComponent()) 252 .setIntent(intent) 253 .setShortLabel(getName()) 254 .setIcon(createShortcutIcon(context)) 255 .setLongLived(true) 256 .build(); 257 context.getSystemService(ShortcutManager.class).addDynamicShortcuts(List.of(shortcut)); 258 259 Notification.BubbleMetadata bubbs = new Notification.BubbleMetadata.Builder() 260 .setIntent( 261 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE)) 262 .setIcon(notificationIcon) 263 .setSuppressNotification(false) 264 .setDesiredHeight(context.getResources().getDisplayMetrics().heightPixels) 265 .build(); 266 267 return new Notification.Builder(context, CHAN_ID) 268 .setSmallIcon(Icon.createWithResource(context, R.drawable.stat_icon)) 269 .setLargeIcon(notificationIcon) 270 .setColor(getBodyColor()) 271 .setContentTitle(context.getString(R.string.notification_title)) 272 .setShowWhen(true) 273 .setCategory(Notification.CATEGORY_STATUS) 274 .setContentText(getName()) 275 .setContentIntent( 276 PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)) 277 .setAutoCancel(true) 278 .setStyle(new Notification.MessagingStyle(createPerson()) 279 .addMessage(mFirstMessage, System.currentTimeMillis(), createPerson()) 280 .setConversationTitle(getName()) 281 ) 282 .setBubbleMetadata(bubbs) 283 .setShortcutId(getShortcutId()) 284 .addExtras(extras); 285 } 286 287 private Person createPerson() { 288 return new Person.Builder() 289 .setName(getName()) 290 .setBot(true) 291 .setKey(getShortcutId()) 292 .build(); 293 } 294 295 public long getSeed() { 296 return mSeed; 297 } 298 299 @Override 300 public void draw(Canvas canvas) { 301 final int w = Math.min(canvas.getWidth(), canvas.getHeight()); 302 final int h = w; 303 304 if (mBitmap == null || mBitmap.getWidth() != w || mBitmap.getHeight() != h) { 305 mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 306 final Canvas bitCanvas = new Canvas(mBitmap); 307 slowDraw(bitCanvas, 0, 0, w, h); 308 } 309 canvas.drawBitmap(mBitmap, 0, 0, null); 310 } 311 312 private void slowDraw(Canvas canvas, int x, int y, int w, int h) { 313 for (int i = 0; i < D.drawingOrder.length; i++) { 314 final Drawable d = D.drawingOrder[i]; 315 if (d != null) { 316 d.setBounds(x, y, x + w, y + h); 317 d.draw(canvas); 318 } 319 } 320 321 } 322 323 public Bitmap createBitmap(int w, int h) { 324 if (mBitmap != null && mBitmap.getWidth() == w && mBitmap.getHeight() == h) { 325 return mBitmap.copy(mBitmap.getConfig(), true); 326 } 327 Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 328 slowDraw(new Canvas(result), 0, 0, w, h); 329 return result; 330 } 331 332 public static Icon recompressIcon(Icon bitmapIcon) { 333 if (bitmapIcon.getType() != Icon.TYPE_BITMAP) return bitmapIcon; 334 try { 335 final Bitmap bits = (Bitmap) Icon.class.getDeclaredMethod("getBitmap").invoke(bitmapIcon); 336 final ByteArrayOutputStream ostream = new ByteArrayOutputStream( 337 bits.getWidth() * bits.getHeight() * 2); // guess 50% compression 338 final boolean ok = bits.compress(Bitmap.CompressFormat.PNG, 100, ostream); 339 if (!ok) return null; 340 return Icon.createWithData(ostream.toByteArray(), 0, ostream.size()); 341 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { 342 return bitmapIcon; 343 } 344 } 345 346 public Icon createNotificationLargeIcon(Context context) { 347 final Resources res = context.getResources(); 348 final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 349 final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 350 return recompressIcon(createIcon(context, w, h)); 351 } 352 353 public Icon createShortcutIcon(Context context) { 354 // shortcuts do not support compressed bitmaps 355 final Resources res = context.getResources(); 356 final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 357 final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 358 return createIcon(context, w, h); 359 } 360 361 public Icon createIcon(Context context, int w, int h) { 362 Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 363 final Canvas canvas = new Canvas(result); 364 float[] hsv = new float[3]; 365 Color.colorToHSV(mBodyColor, hsv); 366 hsv[2] = (hsv[2] > 0.5f) 367 ? (hsv[2] - 0.25f) 368 : (hsv[2] + 0.25f); 369 //final Paint pt = new Paint(); 370 //pt.setColor(Color.HSVToColor(hsv)); 371 //float r = w/2; 372 //canvas.drawCircle(r, r, r, pt); 373 // int m = w/10; 374 375 // Adaptive bitmaps! 376 canvas.drawColor(Color.HSVToColor(hsv)); 377 int m = w / 4; 378 379 slowDraw(canvas, m, m, w - m - m, h - m - m); 380 381 return Icon.createWithAdaptiveBitmap(result); 382 } 383 384 @Override 385 public void setAlpha(int i) { 386 387 } 388 389 @Override 390 public void setColorFilter(ColorFilter colorFilter) { 391 392 } 393 394 @Override 395 public int getOpacity() { 396 return PixelFormat.TRANSLUCENT; 397 } 398 399 public String getName() { 400 return mName; 401 } 402 403 public void setName(String name) { 404 this.mName = name; 405 } 406 407 public int getBodyColor() { 408 return mBodyColor; 409 } 410 411 public void logAdd(Context context) { 412 logCatAction(context, "egg_neko_add"); 413 } 414 415 public void logRename(Context context) { 416 logCatAction(context, "egg_neko_rename"); 417 } 418 419 public void logRemove(Context context) { 420 logCatAction(context, "egg_neko_remove"); 421 } 422 423 public void logShare(Context context) { 424 logCatAction(context, "egg_neko_share"); 425 } 426 427 private void logCatAction(Context context, String prefix) { 428 MetricsLogger.count(context, prefix, 1); 429 MetricsLogger.histogram(context, prefix + "_color", 430 getColorIndex(mBodyColor, P_BODY_COLORS)); 431 MetricsLogger.histogram(context, prefix + "_bowtie", mBowTie ? 1 : 0); 432 MetricsLogger.histogram(context, prefix + "_feet", mFootType); 433 } 434 435 public String getShortcutId() { 436 return ALL_CATS_IN_ONE_CONVERSATION 437 ? GLOBAL_SHORTCUT_ID 438 : (SHORTCUT_ID_PREFIX + mSeed); 439 } 440 441 public static class CatParts { 442 public Drawable leftEar; 443 public Drawable rightEar; 444 public Drawable rightEarInside; 445 public Drawable leftEarInside; 446 public Drawable head; 447 public Drawable faceSpot; 448 public Drawable cap; 449 public Drawable mouth; 450 public Drawable body; 451 public Drawable foot1; 452 public Drawable leg1; 453 public Drawable foot2; 454 public Drawable leg2; 455 public Drawable foot3; 456 public Drawable leg3; 457 public Drawable foot4; 458 public Drawable leg4; 459 public Drawable tail; 460 public Drawable leg2Shadow; 461 public Drawable tailShadow; 462 public Drawable tailCap; 463 public Drawable belly; 464 public Drawable back; 465 public Drawable rightEye; 466 public Drawable leftEye; 467 public Drawable nose; 468 public Drawable bowtie; 469 public Drawable collar; 470 public Drawable[] drawingOrder; 471 472 public CatParts(Context context) { 473 body = context.getDrawable(R.drawable.body); 474 head = context.getDrawable(R.drawable.head); 475 leg1 = context.getDrawable(R.drawable.leg1); 476 leg2 = context.getDrawable(R.drawable.leg2); 477 leg3 = context.getDrawable(R.drawable.leg3); 478 leg4 = context.getDrawable(R.drawable.leg4); 479 tail = context.getDrawable(R.drawable.tail); 480 leftEar = context.getDrawable(R.drawable.left_ear); 481 rightEar = context.getDrawable(R.drawable.right_ear); 482 rightEarInside = context.getDrawable(R.drawable.right_ear_inside); 483 leftEarInside = context.getDrawable(R.drawable.left_ear_inside); 484 faceSpot = context.getDrawable(R.drawable.face_spot); 485 cap = context.getDrawable(R.drawable.cap); 486 mouth = context.getDrawable(R.drawable.mouth); 487 foot4 = context.getDrawable(R.drawable.foot4); 488 foot3 = context.getDrawable(R.drawable.foot3); 489 foot1 = context.getDrawable(R.drawable.foot1); 490 foot2 = context.getDrawable(R.drawable.foot2); 491 leg2Shadow = context.getDrawable(R.drawable.leg2_shadow); 492 tailShadow = context.getDrawable(R.drawable.tail_shadow); 493 tailCap = context.getDrawable(R.drawable.tail_cap); 494 belly = context.getDrawable(R.drawable.belly); 495 back = context.getDrawable(R.drawable.back); 496 rightEye = context.getDrawable(R.drawable.right_eye); 497 leftEye = context.getDrawable(R.drawable.left_eye); 498 nose = context.getDrawable(R.drawable.nose); 499 collar = context.getDrawable(R.drawable.collar); 500 bowtie = context.getDrawable(R.drawable.bowtie); 501 drawingOrder = getDrawingOrder(); 502 } 503 504 private Drawable[] getDrawingOrder() { 505 return new Drawable[]{ 506 collar, 507 leftEar, leftEarInside, rightEar, rightEarInside, 508 head, 509 faceSpot, 510 cap, 511 leftEye, rightEye, 512 nose, mouth, 513 tail, tailCap, tailShadow, 514 foot1, leg1, 515 foot2, leg2, 516 foot3, leg3, 517 foot4, leg4, 518 leg2Shadow, 519 body, belly, 520 bowtie 521 }; 522 } 523 } 524 } 525