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 public 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