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