1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.egg.neko;
16
17import android.app.Notification;
18import android.app.PendingIntent;
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.graphics.*;
23import android.graphics.drawable.Drawable;
24import android.graphics.drawable.Icon;
25import android.os.Bundle;
26
27import java.io.ByteArrayOutputStream;
28import java.util.Random;
29import java.util.concurrent.ThreadLocalRandom;
30
31import com.android.egg.R;
32import com.android.internal.logging.MetricsLogger;
33
34public class Cat extends Drawable {
35    public static final long[] PURR = {0, 40, 20, 40, 20, 40, 20, 40, 20, 40, 20, 40};
36
37    private Random mNotSoRandom;
38    private Bitmap mBitmap;
39    private long mSeed;
40    private String mName;
41    private int mBodyColor;
42    private int mFootType;
43    private boolean mBowTie;
44
45    private synchronized Random notSoRandom(long seed) {
46        if (mNotSoRandom == null) {
47            mNotSoRandom = new Random();
48            mNotSoRandom.setSeed(seed);
49        }
50        return mNotSoRandom;
51    }
52
53    public static final float frandrange(Random r, float a, float b) {
54        return (b-a)*r.nextFloat() + a;
55    }
56
57    public static final Object choose(Random r, Object...l) {
58        return l[r.nextInt(l.length)];
59    }
60
61    public static final int chooseP(Random r, int[] a) {
62        int pct = r.nextInt(1000);
63        final int stop = a.length-2;
64        int i=0;
65        while (i<stop) {
66            pct -= a[i];
67            if (pct < 0) break;
68            i+=2;
69        }
70        return a[i+1];
71    }
72
73    public static final int getColorIndex(int q, int[] a) {
74        for(int i = 1; i < a.length; i+=2) {
75            if (a[i] == q) {
76                return i/2;
77            }
78        }
79        return -1;
80    }
81
82    public static final int[] P_BODY_COLORS = {
83            180, 0xFF212121, // black
84            180, 0xFFFFFFFF, // white
85            140, 0xFF616161, // gray
86            140, 0xFF795548, // brown
87            100, 0xFF90A4AE, // steel
88            100, 0xFFFFF9C4, // buff
89            100, 0xFFFF8F00, // orange
90              5, 0xFF29B6F6, // blue..?
91              5, 0xFFFFCDD2, // pink!?
92              5, 0xFFCE93D8, // purple?!?!?
93              4, 0xFF43A047, // yeah, why not green
94              1, 0,          // ?!?!?!
95    };
96
97    public static final int[] P_COLLAR_COLORS = {
98            250, 0xFFFFFFFF,
99            250, 0xFF000000,
100            250, 0xFFF44336,
101             50, 0xFF1976D2,
102             50, 0xFFFDD835,
103             50, 0xFFFB8C00,
104             50, 0xFFF48FB1,
105             50, 0xFF4CAF50,
106    };
107
108    public static final int[] P_BELLY_COLORS = {
109            750, 0,
110            250, 0xFFFFFFFF,
111    };
112
113    public static final int[] P_DARK_SPOT_COLORS = {
114            700, 0,
115            250, 0xFF212121,
116             50, 0xFF6D4C41,
117    };
118
119    public static final int[] P_LIGHT_SPOT_COLORS = {
120            700, 0,
121            300, 0xFFFFFFFF,
122    };
123
124    private CatParts D;
125
126    public static void tint(int color, Drawable ... ds) {
127        for (Drawable d : ds) {
128            if (d != null) {
129                d.mutate().setTint(color);
130            }
131        }
132    }
133
134    public static boolean isDark(int color) {
135        final int r = (color & 0xFF0000) >> 16;
136        final int g = (color & 0x00FF00) >> 8;
137        final int b = color & 0x0000FF;
138        return (r + g + b) < 0x80;
139    }
140
141    public Cat(Context context, long seed) {
142        D = new CatParts(context);
143        mSeed = seed;
144
145        setName(context.getString(R.string.default_cat_name,
146                String.valueOf(mSeed % 1000)));
147
148        final Random nsr = notSoRandom(seed);
149
150        // body color
151        mBodyColor = chooseP(nsr, P_BODY_COLORS);
152        if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[] {
153                nsr.nextFloat()*360f, frandrange(nsr,0.5f,1f), frandrange(nsr,0.5f, 1f)});
154
155        tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail,
156                D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap);
157        tint(0x20000000, D.leg2Shadow, D.tailShadow);
158        if (isDark(mBodyColor)) {
159            tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose);
160        }
161        tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside);
162
163        tint(chooseP(nsr, P_BELLY_COLORS), D.belly);
164        tint(chooseP(nsr, P_BELLY_COLORS), D.back);
165        final int faceColor = chooseP(nsr, P_BELLY_COLORS);
166        tint(faceColor, D.faceSpot);
167        if (!isDark(faceColor)) {
168            tint(0xFF000000, D.mouth, D.nose);
169        }
170
171        mFootType = 0;
172        if (nsr.nextFloat() < 0.25f) {
173            mFootType = 4;
174            tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4);
175        } else {
176            if (nsr.nextFloat() < 0.25f) {
177                mFootType = 2;
178                tint(0xFFFFFFFF, D.foot1, D.foot3);
179            } else if (nsr.nextFloat() < 0.25f) {
180                mFootType = 3; // maybe -2 would be better? meh.
181                tint(0xFFFFFFFF, D.foot2, D.foot4);
182            } else if (nsr.nextFloat() < 0.1f) {
183                mFootType = 1;
184                tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4));
185            }
186        }
187
188        tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap);
189
190        final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS);
191        tint(capColor, D.cap);
192        //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose);
193
194        final int collarColor = chooseP(nsr, P_COLLAR_COLORS);
195        tint(collarColor, D.collar);
196        mBowTie = nsr.nextFloat() < 0.1f;
197        tint(mBowTie ? collarColor : 0, D.bowtie);
198    }
199
200    public static Cat create(Context context) {
201        return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt()));
202    }
203
204    public Notification.Builder buildNotification(Context context) {
205        final Bundle extras = new Bundle();
206        extras.putString("android.substName", context.getString(R.string.notification_name));
207        final Intent intent = new Intent(Intent.ACTION_MAIN)
208                .setClass(context, NekoLand.class)
209                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
210        return new Notification.Builder(context)
211                .setSmallIcon(Icon.createWithResource(context, R.drawable.stat_icon))
212                .setLargeIcon(createNotificationLargeIcon(context))
213                .setColor(getBodyColor())
214                .setPriority(Notification.PRIORITY_LOW)
215                .setContentTitle(context.getString(R.string.notification_title))
216                .setShowWhen(true)
217                .setCategory(Notification.CATEGORY_STATUS)
218                .setContentText(getName())
219                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0))
220                .setAutoCancel(true)
221                .setVibrate(PURR)
222                .addExtras(extras);
223    }
224
225    public long getSeed() {
226        return mSeed;
227    }
228
229    @Override
230    public void draw(Canvas canvas) {
231        final int w = Math.min(canvas.getWidth(), canvas.getHeight());
232        final int h = w;
233
234        if (mBitmap == null || mBitmap.getWidth() != w || mBitmap.getHeight() != h) {
235            mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
236            final Canvas bitCanvas = new Canvas(mBitmap);
237            slowDraw(bitCanvas, 0, 0, w, h);
238        }
239        canvas.drawBitmap(mBitmap, 0, 0, null);
240    }
241
242    private void slowDraw(Canvas canvas, int x, int y, int w, int h) {
243        for (int i = 0; i < D.drawingOrder.length; i++) {
244            final Drawable d = D.drawingOrder[i];
245            if (d != null) {
246                d.setBounds(x, y, x+w, y+h);
247                d.draw(canvas);
248            }
249        }
250
251    }
252
253    public Bitmap createBitmap(int w, int h) {
254        if (mBitmap != null && mBitmap.getWidth() == w && mBitmap.getHeight() == h) {
255            return mBitmap.copy(mBitmap.getConfig(), true);
256        }
257        Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
258        slowDraw(new Canvas(result), 0, 0, w, h);
259        return result;
260    }
261
262    public static Icon recompressIcon(Icon bitmapIcon) {
263        if (bitmapIcon.getType() != Icon.TYPE_BITMAP) return bitmapIcon;
264        final Bitmap bits = bitmapIcon.getBitmap();
265        final ByteArrayOutputStream ostream = new ByteArrayOutputStream(
266                bits.getWidth() * bits.getHeight() * 2); // guess 50% compression
267        final boolean ok = bits.compress(Bitmap.CompressFormat.PNG, 100, ostream);
268        if (!ok) return null;
269        return Icon.createWithData(ostream.toByteArray(), 0, ostream.size());
270    }
271
272    public Icon createNotificationLargeIcon(Context context) {
273        final Resources res = context.getResources();
274        final int w = 2*res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
275        final int h = 2*res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
276        return recompressIcon(createIcon(context, w, h));
277    }
278
279    public Icon createIcon(Context context, int w, int h) {
280        Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
281        final Canvas canvas = new Canvas(result);
282        final Paint pt = new Paint();
283        float[] hsv = new float[3];
284        Color.colorToHSV(mBodyColor, hsv);
285        hsv[2] = (hsv[2]>0.5f)
286                ? (hsv[2] - 0.25f)
287                : (hsv[2] + 0.25f);
288        pt.setColor(Color.HSVToColor(hsv));
289        float r = w/2;
290        canvas.drawCircle(r, r, r, pt);
291        int m = w/10;
292
293        slowDraw(canvas, m, m, w-m-m, h-m-m);
294
295        return Icon.createWithBitmap(result);
296    }
297
298    @Override
299    public void setAlpha(int i) {
300
301    }
302
303    @Override
304    public void setColorFilter(ColorFilter colorFilter) {
305
306    }
307
308    @Override
309    public int getOpacity() {
310        return PixelFormat.TRANSLUCENT;
311    }
312
313    public String getName() {
314        return mName;
315    }
316
317    public void setName(String name) {
318        this.mName = name;
319    }
320
321    public int getBodyColor() {
322        return mBodyColor;
323    }
324
325    public void logAdd(Context context) {
326        logCatAction(context, "egg_neko_add");
327    }
328
329    public void logRename(Context context) {
330        logCatAction(context, "egg_neko_rename");
331    }
332
333    public void logRemove(Context context) {
334        logCatAction(context, "egg_neko_remove");
335    }
336
337    public void logShare(Context context) {
338        logCatAction(context, "egg_neko_share");
339    }
340
341    private void logCatAction(Context context, String prefix) {
342        MetricsLogger.count(context, prefix, 1);
343        MetricsLogger.histogram(context, prefix +"_color",
344                getColorIndex(mBodyColor, P_BODY_COLORS));
345        MetricsLogger.histogram(context, prefix + "_bowtie", mBowTie ? 1 : 0);
346        MetricsLogger.histogram(context, prefix + "_feet", mFootType);
347    }
348
349    public static class CatParts {
350        public Drawable leftEar;
351        public Drawable rightEar;
352        public Drawable rightEarInside;
353        public Drawable leftEarInside;
354        public Drawable head;
355        public Drawable faceSpot;
356        public Drawable cap;
357        public Drawable mouth;
358        public Drawable body;
359        public Drawable foot1;
360        public Drawable leg1;
361        public Drawable foot2;
362        public Drawable leg2;
363        public Drawable foot3;
364        public Drawable leg3;
365        public Drawable foot4;
366        public Drawable leg4;
367        public Drawable tail;
368        public Drawable leg2Shadow;
369        public Drawable tailShadow;
370        public Drawable tailCap;
371        public Drawable belly;
372        public Drawable back;
373        public Drawable rightEye;
374        public Drawable leftEye;
375        public Drawable nose;
376        public Drawable bowtie;
377        public Drawable collar;
378        public Drawable[] drawingOrder;
379
380        public CatParts(Context context) {
381            body = context.getDrawable(R.drawable.body);
382            head = context.getDrawable(R.drawable.head);
383            leg1 = context.getDrawable(R.drawable.leg1);
384            leg2 = context.getDrawable(R.drawable.leg2);
385            leg3 = context.getDrawable(R.drawable.leg3);
386            leg4 = context.getDrawable(R.drawable.leg4);
387            tail = context.getDrawable(R.drawable.tail);
388            leftEar = context.getDrawable(R.drawable.left_ear);
389            rightEar = context.getDrawable(R.drawable.right_ear);
390            rightEarInside = context.getDrawable(R.drawable.right_ear_inside);
391            leftEarInside = context.getDrawable(R.drawable.left_ear_inside);
392            faceSpot = context.getDrawable(R.drawable.face_spot);
393            cap = context.getDrawable(R.drawable.cap);
394            mouth = context.getDrawable(R.drawable.mouth);
395            foot4 = context.getDrawable(R.drawable.foot4);
396            foot3 = context.getDrawable(R.drawable.foot3);
397            foot1 = context.getDrawable(R.drawable.foot1);
398            foot2 = context.getDrawable(R.drawable.foot2);
399            leg2Shadow = context.getDrawable(R.drawable.leg2_shadow);
400            tailShadow = context.getDrawable(R.drawable.tail_shadow);
401            tailCap = context.getDrawable(R.drawable.tail_cap);
402            belly = context.getDrawable(R.drawable.belly);
403            back = context.getDrawable(R.drawable.back);
404            rightEye = context.getDrawable(R.drawable.right_eye);
405            leftEye = context.getDrawable(R.drawable.left_eye);
406            nose = context.getDrawable(R.drawable.nose);
407            collar = context.getDrawable(R.drawable.collar);
408            bowtie = context.getDrawable(R.drawable.bowtie);
409            drawingOrder = getDrawingOrder();
410        }
411        private Drawable[] getDrawingOrder() {
412            return new Drawable[] {
413                    collar,
414                    leftEar, leftEarInside, rightEar, rightEarInside,
415                    head,
416                    faceSpot,
417                    cap,
418                    leftEye, rightEye,
419                    nose, mouth,
420                    tail, tailCap, tailShadow,
421                    foot1, leg1,
422                    foot2, leg2,
423                    foot3, leg3,
424                    foot4, leg4,
425                    leg2Shadow,
426                    body, belly,
427                    bowtie
428            };
429        }
430    }
431}
432