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 notSoRandom(long seed)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 frandrange(Random r, float a, float b)55 public static final float frandrange(Random r, float a, float b) { 56 return (b-a)*r.nextFloat() + a; 57 } 58 choose(Random r, Object...l)59 public static final Object choose(Random r, Object...l) { 60 return l[r.nextInt(l.length)]; 61 } 62 chooseP(Random r, int[] a)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 getColorIndex(int q, int[] a)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 tint(int color, Drawable ... ds)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 isDark(int color)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 Cat(Context context, long seed)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