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