All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.egg.neko.Cat Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.egg.neko;

import static com.android.egg.neko.NekoLand.CHAN_ID;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;

import com.android.egg.R;
import com.android.internal.logging.MetricsLogger;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/** It's a cat. */
public class Cat extends Drawable {
    public static final long[] PURR = {0, 40, 20, 40, 20, 40, 20, 40, 20, 40, 20, 40};

    public static final boolean ALL_CATS_IN_ONE_CONVERSATION = true;

    public static final String GLOBAL_SHORTCUT_ID = "com.android.egg.neko:allcats";
    public static final String SHORTCUT_ID_PREFIX = "com.android.egg.neko:cat:";

    private Random mNotSoRandom;
    private Bitmap mBitmap;
    private long mSeed;
    private String mName;
    private int mBodyColor;
    private int mFootType;
    private boolean mBowTie;
    private String mFirstMessage;

    private synchronized Random notSoRandom(long seed) {
        if (mNotSoRandom == null) {
            mNotSoRandom = new Random();
            mNotSoRandom.setSeed(seed);
        }
        return mNotSoRandom;
    }

    public static final float frandrange(Random r, float a, float b) {
        return (b - a) * r.nextFloat() + a;
    }

    public static final Object choose(Random r, Object... l) {
        return l[r.nextInt(l.length)];
    }

    public static final int chooseP(Random r, int[] a) {
        return chooseP(r, a, 1000);
    }

    public static final int chooseP(Random r, int[] a, int sum) {
        int pct = r.nextInt(sum);
        final int stop = a.length - 2;
        int i = 0;
        while (i < stop) {
            pct -= a[i];
            if (pct < 0) break;
            i += 2;
        }
        return a[i + 1];
    }

    public static final int getColorIndex(int q, int[] a) {
        for (int i = 1; i < a.length; i += 2) {
            if (a[i] == q) {
                return i / 2;
            }
        }
        return -1;
    }

    public static final int[] P_BODY_COLORS = {
            180, 0xFF212121, // black
            180, 0xFFFFFFFF, // white
            140, 0xFF616161, // gray
            140, 0xFF795548, // brown
            100, 0xFF90A4AE, // steel
            100, 0xFFFFF9C4, // buff
            100, 0xFFFF8F00, // orange
            5, 0xFF29B6F6, // blue..?
            5, 0xFFFFCDD2, // pink!?
            5, 0xFFCE93D8, // purple?!?!?
            4, 0xFF43A047, // yeah, why not green
            1, 0,          // ?!?!?!
    };

    public static final int[] P_COLLAR_COLORS = {
            250, 0xFFFFFFFF,
            250, 0xFF000000,
            250, 0xFFF44336,
            50, 0xFF1976D2,
            50, 0xFFFDD835,
            50, 0xFFFB8C00,
            50, 0xFFF48FB1,
            50, 0xFF4CAF50,
    };

    public static final int[] P_BELLY_COLORS = {
            750, 0,
            250, 0xFFFFFFFF,
    };

    public static final int[] P_DARK_SPOT_COLORS = {
            700, 0,
            250, 0xFF212121,
            50, 0xFF6D4C41,
    };

    public static final int[] P_LIGHT_SPOT_COLORS = {
            700, 0,
            300, 0xFFFFFFFF,
    };

    private CatParts D;

    public static void tint(int color, Drawable... ds) {
        for (Drawable d : ds) {
            if (d != null) {
                d.mutate().setTint(color);
            }
        }
    }

    public static boolean isDark(int color) {
        final int r = (color & 0xFF0000) >> 16;
        final int g = (color & 0x00FF00) >> 8;
        final int b = color & 0x0000FF;
        return (r + g + b) < 0x80;
    }

    public Cat(Context context, long seed) {
        D = new CatParts(context);
        mSeed = seed;

        setName(context.getString(R.string.default_cat_name,
                String.valueOf(mSeed % 1000)));

        final Random nsr = notSoRandom(seed);

        // body color
        mBodyColor = chooseP(nsr, P_BODY_COLORS);
        if (mBodyColor == 0) mBodyColor = Color.HSVToColor(new float[]{
                nsr.nextFloat() * 360f, frandrange(nsr, 0.5f, 1f), frandrange(nsr, 0.5f, 1f)});

        tint(mBodyColor, D.body, D.head, D.leg1, D.leg2, D.leg3, D.leg4, D.tail,
                D.leftEar, D.rightEar, D.foot1, D.foot2, D.foot3, D.foot4, D.tailCap);
        tint(0x20000000, D.leg2Shadow, D.tailShadow);
        if (isDark(mBodyColor)) {
            tint(0xFFFFFFFF, D.leftEye, D.rightEye, D.mouth, D.nose);
        }
        tint(isDark(mBodyColor) ? 0xFFEF9A9A : 0x20D50000, D.leftEarInside, D.rightEarInside);

        tint(chooseP(nsr, P_BELLY_COLORS), D.belly);
        tint(chooseP(nsr, P_BELLY_COLORS), D.back);
        final int faceColor = chooseP(nsr, P_BELLY_COLORS);
        tint(faceColor, D.faceSpot);
        if (!isDark(faceColor)) {
            tint(0xFF000000, D.mouth, D.nose);
        }

        mFootType = 0;
        if (nsr.nextFloat() < 0.25f) {
            mFootType = 4;
            tint(0xFFFFFFFF, D.foot1, D.foot2, D.foot3, D.foot4);
        } else {
            if (nsr.nextFloat() < 0.25f) {
                mFootType = 2;
                tint(0xFFFFFFFF, D.foot1, D.foot3);
            } else if (nsr.nextFloat() < 0.25f) {
                mFootType = 3; // maybe -2 would be better? meh.
                tint(0xFFFFFFFF, D.foot2, D.foot4);
            } else if (nsr.nextFloat() < 0.1f) {
                mFootType = 1;
                tint(0xFFFFFFFF, (Drawable) choose(nsr, D.foot1, D.foot2, D.foot3, D.foot4));
            }
        }

        tint(nsr.nextFloat() < 0.333f ? 0xFFFFFFFF : mBodyColor, D.tailCap);

        final int capColor = chooseP(nsr, isDark(mBodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS);
        tint(capColor, D.cap);
        //tint(chooseP(nsr, isDark(bodyColor) ? P_LIGHT_SPOT_COLORS : P_DARK_SPOT_COLORS), D.nose);

        final int collarColor = chooseP(nsr, P_COLLAR_COLORS);
        tint(collarColor, D.collar);
        mBowTie = nsr.nextFloat() < 0.1f;
        tint(mBowTie ? collarColor : 0, D.bowtie);

        String[] messages = context.getResources().getStringArray(
                nsr.nextFloat() < 0.1f ? R.array.rare_cat_messages : R.array.cat_messages);
        mFirstMessage = (String) choose(nsr, (Object[]) messages);
        if (nsr.nextFloat() < 0.5f) mFirstMessage = mFirstMessage + mFirstMessage + mFirstMessage;
    }

    public static Cat fromShortcutId(Context context, String shortcutId) {
        if (shortcutId.startsWith(SHORTCUT_ID_PREFIX)) {
            return new Cat(context, Long.parseLong(shortcutId.replace(SHORTCUT_ID_PREFIX, "")));
        }
        return null;
    }

    public static Cat create(Context context) {
        return new Cat(context, Math.abs(ThreadLocalRandom.current().nextInt()));
    }

    public Notification.Builder buildNotification(Context context) {
        final Bundle extras = new Bundle();
        extras.putString("android.substName", context.getString(R.string.notification_name));

        final Icon notificationIcon = createNotificationLargeIcon(context);

        final Intent intent = new Intent(Intent.ACTION_MAIN)
                .setClass(context, NekoLand.class)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        ShortcutInfo shortcut = new ShortcutInfo.Builder(context, getShortcutId())
                .setActivity(intent.getComponent())
                .setIntent(intent)
                .setShortLabel(getName())
                .setIcon(createShortcutIcon(context))
                .setLongLived(true)
                .build();
        context.getSystemService(ShortcutManager.class).addDynamicShortcuts(List.of(shortcut));

        Notification.BubbleMetadata bubbs = new Notification.BubbleMetadata.Builder()
                .setIntent(
                        PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE))
                .setIcon(notificationIcon)
                .setSuppressNotification(false)
                .setDesiredHeight(context.getResources().getDisplayMetrics().heightPixels)
                .build();

        return new Notification.Builder(context, CHAN_ID)
                .setSmallIcon(Icon.createWithResource(context, R.drawable.stat_icon))
                .setLargeIcon(notificationIcon)
                .setColor(getBodyColor())
                .setContentTitle(context.getString(R.string.notification_title))
                .setShowWhen(true)
                .setCategory(Notification.CATEGORY_STATUS)
                .setContentText(getName())
                .setContentIntent(
                        PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE))
                .setAutoCancel(true)
                .setStyle(new Notification.MessagingStyle(createPerson())
                        .addMessage(mFirstMessage, System.currentTimeMillis(), createPerson())
                        .setConversationTitle(getName())
                )
                .setBubbleMetadata(bubbs)
                .setShortcutId(getShortcutId())
                .addExtras(extras);
    }

    private Person createPerson() {
        return new Person.Builder()
                .setName(getName())
                .setBot(true)
                .setKey(getShortcutId())
                .build();
    }

    public long getSeed() {
        return mSeed;
    }

    @Override
    public void draw(Canvas canvas) {
        final int w = Math.min(canvas.getWidth(), canvas.getHeight());
        final int h = w;

        if (mBitmap == null || mBitmap.getWidth() != w || mBitmap.getHeight() != h) {
            mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
            final Canvas bitCanvas = new Canvas(mBitmap);
            slowDraw(bitCanvas, 0, 0, w, h);
        }
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }

    private void slowDraw(Canvas canvas, int x, int y, int w, int h) {
        for (int i = 0; i < D.drawingOrder.length; i++) {
            final Drawable d = D.drawingOrder[i];
            if (d != null) {
                d.setBounds(x, y, x + w, y + h);
                d.draw(canvas);
            }
        }

    }

    public Bitmap createBitmap(int w, int h) {
        if (mBitmap != null && mBitmap.getWidth() == w && mBitmap.getHeight() == h) {
            return mBitmap.copy(mBitmap.getConfig(), true);
        }
        Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        slowDraw(new Canvas(result), 0, 0, w, h);
        return result;
    }

    public static Icon recompressIcon(Icon bitmapIcon) {
        if (bitmapIcon.getType() != Icon.TYPE_BITMAP) return bitmapIcon;
        try {
            final Bitmap bits = (Bitmap) Icon.class.getDeclaredMethod("getBitmap").invoke(bitmapIcon);
            final ByteArrayOutputStream ostream = new ByteArrayOutputStream(
                    bits.getWidth() * bits.getHeight() * 2); // guess 50% compression
            final boolean ok = bits.compress(Bitmap.CompressFormat.PNG, 100, ostream);
            if (!ok) return null;
            return Icon.createWithData(ostream.toByteArray(), 0, ostream.size());
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
            return bitmapIcon;
        }
    }

    public Icon createNotificationLargeIcon(Context context) {
        final Resources res = context.getResources();
        final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
        final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
        return recompressIcon(createIcon(context, w, h));
    }

    public Icon createShortcutIcon(Context context) {
        // shortcuts do not support compressed bitmaps
        final Resources res = context.getResources();
        final int w = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
        final int h = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
        return createIcon(context, w, h);
    }

    public Icon createIcon(Context context, int w, int h) {
        Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(result);
        float[] hsv = new float[3];
        Color.colorToHSV(mBodyColor, hsv);
        hsv[2] = (hsv[2] > 0.5f)
                ? (hsv[2] - 0.25f)
                : (hsv[2] + 0.25f);
        //final Paint pt = new Paint();
        //pt.setColor(Color.HSVToColor(hsv));
        //float r = w/2;
        //canvas.drawCircle(r, r, r, pt);
        // int m = w/10;

        // Adaptive bitmaps!
        canvas.drawColor(Color.HSVToColor(hsv));
        int m = w / 4;

        slowDraw(canvas, m, m, w - m - m, h - m - m);

        return Icon.createWithAdaptiveBitmap(result);
    }

    @Override
    public void setAlpha(int i) {

    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    public String getName() {
        return mName;
    }

    public void setName(String name) {
        this.mName = name;
    }

    public int getBodyColor() {
        return mBodyColor;
    }

    public void logAdd(Context context) {
        logCatAction(context, "egg_neko_add");
    }

    public void logRename(Context context) {
        logCatAction(context, "egg_neko_rename");
    }

    public void logRemove(Context context) {
        logCatAction(context, "egg_neko_remove");
    }

    public void logShare(Context context) {
        logCatAction(context, "egg_neko_share");
    }

    private void logCatAction(Context context, String prefix) {
        MetricsLogger.count(context, prefix, 1);
        MetricsLogger.histogram(context, prefix + "_color",
                getColorIndex(mBodyColor, P_BODY_COLORS));
        MetricsLogger.histogram(context, prefix + "_bowtie", mBowTie ? 1 : 0);
        MetricsLogger.histogram(context, prefix + "_feet", mFootType);
    }

    public String getShortcutId() {
        return ALL_CATS_IN_ONE_CONVERSATION
                ? GLOBAL_SHORTCUT_ID
                : (SHORTCUT_ID_PREFIX + mSeed);
    }

    public static class CatParts {
        public Drawable leftEar;
        public Drawable rightEar;
        public Drawable rightEarInside;
        public Drawable leftEarInside;
        public Drawable head;
        public Drawable faceSpot;
        public Drawable cap;
        public Drawable mouth;
        public Drawable body;
        public Drawable foot1;
        public Drawable leg1;
        public Drawable foot2;
        public Drawable leg2;
        public Drawable foot3;
        public Drawable leg3;
        public Drawable foot4;
        public Drawable leg4;
        public Drawable tail;
        public Drawable leg2Shadow;
        public Drawable tailShadow;
        public Drawable tailCap;
        public Drawable belly;
        public Drawable back;
        public Drawable rightEye;
        public Drawable leftEye;
        public Drawable nose;
        public Drawable bowtie;
        public Drawable collar;
        public Drawable[] drawingOrder;

        public CatParts(Context context) {
            body = context.getDrawable(R.drawable.body);
            head = context.getDrawable(R.drawable.head);
            leg1 = context.getDrawable(R.drawable.leg1);
            leg2 = context.getDrawable(R.drawable.leg2);
            leg3 = context.getDrawable(R.drawable.leg3);
            leg4 = context.getDrawable(R.drawable.leg4);
            tail = context.getDrawable(R.drawable.tail);
            leftEar = context.getDrawable(R.drawable.left_ear);
            rightEar = context.getDrawable(R.drawable.right_ear);
            rightEarInside = context.getDrawable(R.drawable.right_ear_inside);
            leftEarInside = context.getDrawable(R.drawable.left_ear_inside);
            faceSpot = context.getDrawable(R.drawable.face_spot);
            cap = context.getDrawable(R.drawable.cap);
            mouth = context.getDrawable(R.drawable.mouth);
            foot4 = context.getDrawable(R.drawable.foot4);
            foot3 = context.getDrawable(R.drawable.foot3);
            foot1 = context.getDrawable(R.drawable.foot1);
            foot2 = context.getDrawable(R.drawable.foot2);
            leg2Shadow = context.getDrawable(R.drawable.leg2_shadow);
            tailShadow = context.getDrawable(R.drawable.tail_shadow);
            tailCap = context.getDrawable(R.drawable.tail_cap);
            belly = context.getDrawable(R.drawable.belly);
            back = context.getDrawable(R.drawable.back);
            rightEye = context.getDrawable(R.drawable.right_eye);
            leftEye = context.getDrawable(R.drawable.left_eye);
            nose = context.getDrawable(R.drawable.nose);
            collar = context.getDrawable(R.drawable.collar);
            bowtie = context.getDrawable(R.drawable.bowtie);
            drawingOrder = getDrawingOrder();
        }

        private Drawable[] getDrawingOrder() {
            return new Drawable[]{
                    collar,
                    leftEar, leftEarInside, rightEar, rightEarInside,
                    head,
                    faceSpot,
                    cap,
                    leftEye, rightEye,
                    nose, mouth,
                    tail, tailCap, tailShadow,
                    foot1, leg1,
                    foot2, leg2,
                    foot3, leg3,
                    foot4, leg4,
                    leg2Shadow,
                    body, belly,
                    bowtie
            };
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy