From f4e83e0a4acd548bf1edd02bb845931a60bdb8da Mon Sep 17 00:00:00 2001 From: Dan Sandler Date: Tue, 12 May 2020 21:25:31 -0400 Subject: [PATCH] Fixed bug: Not everyone has home controls yet, or pets. Fixes: 156301524 Test: # to enable controls component adb shell am start -n com.android.egg.test/com.android.egg.neko.NekoActivationActivity # manual step: activate controls from GlobalActions # to visit the cat list adb shell am start -n com.android.egg.test/com.android.egg.neko.NekoLand # to check on the status of the food bowl job (once # the food bowl control has been tapped) adb shell cmd jobscheduler get-job-state com.android.egg.test 42 # to trigger the food immediately adb shell cmd jobscheduler run com.android.egg.test 42 Change-Id: I985a930bb5dd56d99eb57a340e4affe08c09724b --- .../internal/app/PlatLogoActivity.java | 34 +- packages/EasterEgg/Android.bp | 18 +- packages/EasterEgg/AndroidManifest.xml | 94 +++- packages/EasterEgg/build.gradle | 82 +++ packages/EasterEgg/gradle.properties | 23 + .../res/drawable/android_11_dial.xml | 63 +++ packages/EasterEgg/res/drawable/back.xml | 22 + packages/EasterEgg/res/drawable/belly.xml | 22 + packages/EasterEgg/res/drawable/body.xml | 22 + packages/EasterEgg/res/drawable/bowtie.xml | 22 + packages/EasterEgg/res/drawable/cap.xml | 22 + packages/EasterEgg/res/drawable/collar.xml | 22 + packages/EasterEgg/res/drawable/face_spot.xml | 22 + packages/EasterEgg/res/drawable/food_bits.xml | 33 ++ .../EasterEgg/res/drawable/food_chicken.xml | 39 ++ .../EasterEgg/res/drawable/food_cookie.xml | 35 ++ packages/EasterEgg/res/drawable/food_dish.xml | 24 + .../EasterEgg/res/drawable/food_donut.xml | 24 + .../EasterEgg/res/drawable/food_sysuituna.xml | 24 + packages/EasterEgg/res/drawable/foot1.xml | 22 + packages/EasterEgg/res/drawable/foot2.xml | 22 + packages/EasterEgg/res/drawable/foot3.xml | 22 + packages/EasterEgg/res/drawable/foot4.xml | 22 + packages/EasterEgg/res/drawable/head.xml | 22 + packages/EasterEgg/res/drawable/ic_bowl.xml | 34 ++ packages/EasterEgg/res/drawable/ic_close.xml | 24 + .../res/drawable/ic_foodbowl_filled.xml | 44 ++ .../res/drawable/ic_fullcat_icon.xml | 108 ++++ packages/EasterEgg/res/drawable/ic_share.xml | 24 + .../EasterEgg/res/drawable/ic_toy_ball.xml | 29 + .../EasterEgg/res/drawable/ic_toy_fish.xml | 41 ++ .../EasterEgg/res/drawable/ic_toy_laser.xml | 42 ++ .../EasterEgg/res/drawable/ic_toy_mouse.xml | 45 ++ packages/EasterEgg/res/drawable/ic_water.xml | 27 + .../res/drawable/ic_water_filled.xml | 24 + .../res/drawable/ic_waterbowl_filled.xml | 33 ++ packages/EasterEgg/res/drawable/icon.xml | 19 + packages/EasterEgg/res/drawable/icon_bg.xml | 8 +- packages/EasterEgg/res/drawable/left_ear.xml | 22 + .../res/drawable/left_ear_inside.xml | 22 + packages/EasterEgg/res/drawable/left_eye.xml | 22 + packages/EasterEgg/res/drawable/leg1.xml | 22 + packages/EasterEgg/res/drawable/leg2.xml | 22 + .../EasterEgg/res/drawable/leg2_shadow.xml | 22 + packages/EasterEgg/res/drawable/leg3.xml | 22 + packages/EasterEgg/res/drawable/leg4.xml | 22 + packages/EasterEgg/res/drawable/mouth.xml | 27 + packages/EasterEgg/res/drawable/nose.xml | 22 + packages/EasterEgg/res/drawable/octo_bg.xml | 8 + packages/EasterEgg/res/drawable/right_ear.xml | 22 + .../res/drawable/right_ear_inside.xml | 23 + packages/EasterEgg/res/drawable/right_eye.xml | 22 + packages/EasterEgg/res/drawable/stat_icon.xml | 30 + packages/EasterEgg/res/drawable/tail.xml | 26 + packages/EasterEgg/res/drawable/tail_cap.xml | 22 + .../EasterEgg/res/drawable/tail_shadow.xml | 22 + .../EasterEgg/res/layout/activity_paint.xml | 4 +- packages/EasterEgg/res/layout/cat_view.xml | 82 +++ packages/EasterEgg/res/layout/edit_text.xml | 30 + packages/EasterEgg/res/layout/food_layout.xml | 31 ++ .../EasterEgg/res/layout/neko_activity.xml | 25 + packages/EasterEgg/res/values/cat_strings.xml | 71 +++ packages/EasterEgg/res/values/dimens.xml | 19 + packages/EasterEgg/res/values/strings.xml | 4 +- packages/EasterEgg/res/xml/filepaths.xml | 19 + .../src/com/android/egg/neko/Cat.java | 524 ++++++++++++++++++ .../src/com/android/egg/neko/Food.java | 61 ++ .../egg/neko/NekoActivationActivity.java | 66 +++ .../android/egg/neko/NekoControlsService.kt | 323 +++++++++++ .../src/com/android/egg/neko/NekoDialog.java | 109 ++++ .../src/com/android/egg/neko/NekoLand.java | 340 ++++++++++++ .../android/egg/neko/NekoLockedActivity.java | 48 ++ .../src/com/android/egg/neko/NekoService.java | 193 +++++++ .../src/com/android/egg/neko/NekoTile.java | 116 ++++ .../src/com/android/egg/neko/PrefState.java | 104 ++++ 75 files changed, 3795 insertions(+), 33 deletions(-) create mode 100644 packages/EasterEgg/build.gradle create mode 100644 packages/EasterEgg/gradle.properties create mode 100644 packages/EasterEgg/res/drawable/android_11_dial.xml create mode 100644 packages/EasterEgg/res/drawable/back.xml create mode 100644 packages/EasterEgg/res/drawable/belly.xml create mode 100644 packages/EasterEgg/res/drawable/body.xml create mode 100644 packages/EasterEgg/res/drawable/bowtie.xml create mode 100644 packages/EasterEgg/res/drawable/cap.xml create mode 100644 packages/EasterEgg/res/drawable/collar.xml create mode 100644 packages/EasterEgg/res/drawable/face_spot.xml create mode 100644 packages/EasterEgg/res/drawable/food_bits.xml create mode 100644 packages/EasterEgg/res/drawable/food_chicken.xml create mode 100644 packages/EasterEgg/res/drawable/food_cookie.xml create mode 100644 packages/EasterEgg/res/drawable/food_dish.xml create mode 100644 packages/EasterEgg/res/drawable/food_donut.xml create mode 100644 packages/EasterEgg/res/drawable/food_sysuituna.xml create mode 100644 packages/EasterEgg/res/drawable/foot1.xml create mode 100644 packages/EasterEgg/res/drawable/foot2.xml create mode 100644 packages/EasterEgg/res/drawable/foot3.xml create mode 100644 packages/EasterEgg/res/drawable/foot4.xml create mode 100644 packages/EasterEgg/res/drawable/head.xml create mode 100644 packages/EasterEgg/res/drawable/ic_bowl.xml create mode 100644 packages/EasterEgg/res/drawable/ic_close.xml create mode 100644 packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml create mode 100644 packages/EasterEgg/res/drawable/ic_fullcat_icon.xml create mode 100644 packages/EasterEgg/res/drawable/ic_share.xml create mode 100644 packages/EasterEgg/res/drawable/ic_toy_ball.xml create mode 100644 packages/EasterEgg/res/drawable/ic_toy_fish.xml create mode 100644 packages/EasterEgg/res/drawable/ic_toy_laser.xml create mode 100644 packages/EasterEgg/res/drawable/ic_toy_mouse.xml create mode 100644 packages/EasterEgg/res/drawable/ic_water.xml create mode 100644 packages/EasterEgg/res/drawable/ic_water_filled.xml create mode 100644 packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml create mode 100644 packages/EasterEgg/res/drawable/icon.xml create mode 100644 packages/EasterEgg/res/drawable/left_ear.xml create mode 100644 packages/EasterEgg/res/drawable/left_ear_inside.xml create mode 100644 packages/EasterEgg/res/drawable/left_eye.xml create mode 100644 packages/EasterEgg/res/drawable/leg1.xml create mode 100644 packages/EasterEgg/res/drawable/leg2.xml create mode 100644 packages/EasterEgg/res/drawable/leg2_shadow.xml create mode 100644 packages/EasterEgg/res/drawable/leg3.xml create mode 100644 packages/EasterEgg/res/drawable/leg4.xml create mode 100644 packages/EasterEgg/res/drawable/mouth.xml create mode 100644 packages/EasterEgg/res/drawable/nose.xml create mode 100644 packages/EasterEgg/res/drawable/octo_bg.xml create mode 100644 packages/EasterEgg/res/drawable/right_ear.xml create mode 100644 packages/EasterEgg/res/drawable/right_ear_inside.xml create mode 100644 packages/EasterEgg/res/drawable/right_eye.xml create mode 100644 packages/EasterEgg/res/drawable/stat_icon.xml create mode 100644 packages/EasterEgg/res/drawable/tail.xml create mode 100644 packages/EasterEgg/res/drawable/tail_cap.xml create mode 100644 packages/EasterEgg/res/drawable/tail_shadow.xml create mode 100644 packages/EasterEgg/res/layout/cat_view.xml create mode 100644 packages/EasterEgg/res/layout/edit_text.xml create mode 100644 packages/EasterEgg/res/layout/food_layout.xml create mode 100644 packages/EasterEgg/res/layout/neko_activity.xml create mode 100644 packages/EasterEgg/res/values/cat_strings.xml create mode 100644 packages/EasterEgg/res/values/dimens.xml create mode 100644 packages/EasterEgg/res/xml/filepaths.xml create mode 100644 packages/EasterEgg/src/com/android/egg/neko/Cat.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/Food.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoLand.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoService.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/NekoTile.java create mode 100644 packages/EasterEgg/src/com/android/egg/neko/PrefState.java diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java index 2a7eae6267951..986bbc8628ec1 100644 --- a/core/java/com/android/internal/app/PlatLogoActivity.java +++ b/core/java/com/android/internal/app/PlatLogoActivity.java @@ -55,6 +55,10 @@ import org.json.JSONObject; public class PlatLogoActivity extends Activity { private static final boolean WRITE_SETTINGS = true; + private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r"; + + private static final int UNLOCK_TRIES = 3; + BigDialView mDialView; @Override @@ -77,8 +81,10 @@ public class PlatLogoActivity extends Activity { mDialView = new BigDialView(this, null); if (Settings.System.getLong(getContentResolver(), - "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) { - mDialView.setUnlockTries(3); + R_EGG_UNLOCK_SETTING, 0) == 0) { + mDialView.setUnlockTries(UNLOCK_TRIES); + } else { + mDialView.setUnlockTries(0); } final FrameLayout layout = new FrameLayout(this); @@ -91,18 +97,16 @@ public class PlatLogoActivity extends Activity { private void launchNextStage(boolean locked) { final ContentResolver cr = getContentResolver(); - if (Settings.System.getLong(cr, "egg_mode" /* Settings.System.EGG_MODE */, 0) == 0) { - // For posterity: the moment this user unlocked the easter egg - try { - if (WRITE_SETTINGS) { - Settings.System.putLong(cr, - "egg_mode", // Settings.System.EGG_MODE, - locked ? 0 : System.currentTimeMillis()); - } - } catch (RuntimeException e) { - Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e); + try { + if (WRITE_SETTINGS) { + Settings.System.putLong(cr, + R_EGG_UNLOCK_SETTING, + locked ? 0 : System.currentTimeMillis()); } + } catch (RuntimeException e) { + Log.e("com.android.internal.app.PlatLogoActivity", "Can't write settings", e); } + try { startActivity(new Intent(Intent.ACTION_MAIN) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK @@ -235,8 +239,8 @@ public class PlatLogoActivity extends Activity { } return true; case MotionEvent.ACTION_UP: - if (mWasLocked && !mDialDrawable.isLocked()) { - launchNextStage(false); + if (mWasLocked != mDialDrawable.isLocked()) { + launchNextStage(mDialDrawable.isLocked()); } return true; } @@ -404,6 +408,8 @@ public class PlatLogoActivity extends Activity { if (isLocked() && oldUserLevel != STEPS - 1 && getUserLevel() == STEPS - 1) { mUnlockTries--; + } else if (!isLocked() && getUserLevel() == 0) { + mUnlockTries = UNLOCK_TRIES; } if (!isLocked()) { diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp index 43ed810b56744..b858ab01ffd96 100644 --- a/packages/EasterEgg/Android.bp +++ b/packages/EasterEgg/Android.bp @@ -23,11 +23,23 @@ android_app { name: "EasterEgg", + platform_apis: true, certificate: "platform", - sdk_version: "current", - optimize: { enabled: false, - } + }, + + static_libs: [ + "androidx.core_core", + "androidx.recyclerview_recyclerview", + "androidx.annotation_annotation", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + //"kotlinx-coroutines-reactive", + ], + + manifest: "AndroidManifest.xml", + + kotlincflags: ["-Xjvm-default=enable"], } diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml index 7f76a45299635..57c459b6f0fd4 100644 --- a/packages/EasterEgg/AndroidManifest.xml +++ b/packages/EasterEgg/AndroidManifest.xml @@ -6,19 +6,24 @@ + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/EasterEgg/build.gradle b/packages/EasterEgg/build.gradle new file mode 100644 index 0000000000000..20b469898498b --- /dev/null +++ b/packages/EasterEgg/build.gradle @@ -0,0 +1,82 @@ +buildscript { + ext.kotlin_version = '1.3.71' + + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +final String ANDROID_ROOT = "${rootDir}/../../../.." + +android { + compileSdkVersion COMPILE_SDK + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + applicationId "com.android.egg" + minSdkVersion 28 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + } + + signingConfigs { + debug.storeFile file("${ANDROID_ROOT}/vendor/google/certs/devkeys/platform.keystore") + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' + implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}" + implementation "androidx.dynamicanimation:dynamicanimation:${ANDROID_X_VERSION}" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation "androidx.annotation:annotation:${ANDROID_X_VERSION}" +} + diff --git a/packages/EasterEgg/gradle.properties b/packages/EasterEgg/gradle.properties new file mode 100644 index 0000000000000..e8e6450e2943f --- /dev/null +++ b/packages/EasterEgg/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +android.enableJetifier=true +kotlin.code.style=official + +ANDROID_X_VERSION=1+ +COMPILE_SDK=android-30 +BUILD_TOOLS_VERSION=28.0.3 diff --git a/packages/EasterEgg/res/drawable/android_11_dial.xml b/packages/EasterEgg/res/drawable/android_11_dial.xml new file mode 100644 index 0000000000000..73fd37f1bdd60 --- /dev/null +++ b/packages/EasterEgg/res/drawable/android_11_dial.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/back.xml b/packages/EasterEgg/res/drawable/back.xml new file mode 100644 index 0000000000000..b55d65cdf76d8 --- /dev/null +++ b/packages/EasterEgg/res/drawable/back.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/belly.xml b/packages/EasterEgg/res/drawable/belly.xml new file mode 100644 index 0000000000000..8b0e9afac4632 --- /dev/null +++ b/packages/EasterEgg/res/drawable/belly.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/body.xml b/packages/EasterEgg/res/drawable/body.xml new file mode 100644 index 0000000000000..86087209eff5f --- /dev/null +++ b/packages/EasterEgg/res/drawable/body.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/bowtie.xml b/packages/EasterEgg/res/drawable/bowtie.xml new file mode 100644 index 0000000000000..33fa9216712fb --- /dev/null +++ b/packages/EasterEgg/res/drawable/bowtie.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/cap.xml b/packages/EasterEgg/res/drawable/cap.xml new file mode 100644 index 0000000000000..d8b4cc58a261f --- /dev/null +++ b/packages/EasterEgg/res/drawable/cap.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/collar.xml b/packages/EasterEgg/res/drawable/collar.xml new file mode 100644 index 0000000000000..5e4d0fd4f8865 --- /dev/null +++ b/packages/EasterEgg/res/drawable/collar.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/face_spot.xml b/packages/EasterEgg/res/drawable/face_spot.xml new file mode 100644 index 0000000000000..a89fb4fdaadd5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/face_spot.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/food_bits.xml b/packages/EasterEgg/res/drawable/food_bits.xml new file mode 100644 index 0000000000000..1b2bb6f369470 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_bits.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/packages/EasterEgg/res/drawable/food_chicken.xml b/packages/EasterEgg/res/drawable/food_chicken.xml new file mode 100644 index 0000000000000..95b2fb55b7967 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_chicken.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/food_cookie.xml b/packages/EasterEgg/res/drawable/food_cookie.xml new file mode 100644 index 0000000000000..74dd134355e23 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_cookie.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/EasterEgg/res/drawable/food_dish.xml b/packages/EasterEgg/res/drawable/food_dish.xml new file mode 100644 index 0000000000000..3fff6a90fad2b --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_dish.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/food_donut.xml b/packages/EasterEgg/res/drawable/food_donut.xml new file mode 100644 index 0000000000000..eaf831ea560c0 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_donut.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/food_sysuituna.xml b/packages/EasterEgg/res/drawable/food_sysuituna.xml new file mode 100644 index 0000000000000..28cf4a2c76836 --- /dev/null +++ b/packages/EasterEgg/res/drawable/food_sysuituna.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/foot1.xml b/packages/EasterEgg/res/drawable/foot1.xml new file mode 100644 index 0000000000000..0d9085998a182 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot1.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/foot2.xml b/packages/EasterEgg/res/drawable/foot2.xml new file mode 100644 index 0000000000000..364ba0cd861c4 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot2.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/foot3.xml b/packages/EasterEgg/res/drawable/foot3.xml new file mode 100644 index 0000000000000..e3a512a2568d9 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot3.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/foot4.xml b/packages/EasterEgg/res/drawable/foot4.xml new file mode 100644 index 0000000000000..66b78fa266490 --- /dev/null +++ b/packages/EasterEgg/res/drawable/foot4.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/head.xml b/packages/EasterEgg/res/drawable/head.xml new file mode 100644 index 0000000000000..df600a8613cd6 --- /dev/null +++ b/packages/EasterEgg/res/drawable/head.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/ic_bowl.xml b/packages/EasterEgg/res/drawable/ic_bowl.xml new file mode 100644 index 0000000000000..d55565d92988d --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_bowl.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_close.xml b/packages/EasterEgg/res/drawable/ic_close.xml new file mode 100644 index 0000000000000..60ea36b11fccd --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_close.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml new file mode 100644 index 0000000000000..54961af68aef5 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_foodbowl_filled.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml new file mode 100644 index 0000000000000..5dca3d18f2d45 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_fullcat_icon.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_share.xml b/packages/EasterEgg/res/drawable/ic_share.xml new file mode 100644 index 0000000000000..8cebc7ed46dea --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_share.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/ic_toy_ball.xml b/packages/EasterEgg/res/drawable/ic_toy_ball.xml new file mode 100644 index 0000000000000..411084b2a272d --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_ball.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_toy_fish.xml b/packages/EasterEgg/res/drawable/ic_toy_fish.xml new file mode 100644 index 0000000000000..bb01e9f32bfbf --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_fish.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_toy_laser.xml b/packages/EasterEgg/res/drawable/ic_toy_laser.xml new file mode 100644 index 0000000000000..8fe84ffbd38c4 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_laser.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_toy_mouse.xml b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml new file mode 100644 index 0000000000000..ba3dc33220836 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_toy_mouse.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_water.xml b/packages/EasterEgg/res/drawable/ic_water.xml new file mode 100644 index 0000000000000..7d94b24096361 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_water.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_water_filled.xml b/packages/EasterEgg/res/drawable/ic_water_filled.xml new file mode 100644 index 0000000000000..eed171d05668a --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_water_filled.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml new file mode 100644 index 0000000000000..28b1fa8240605 --- /dev/null +++ b/packages/EasterEgg/res/drawable/ic_waterbowl_filled.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/packages/EasterEgg/res/drawable/icon.xml b/packages/EasterEgg/res/drawable/icon.xml new file mode 100644 index 0000000000000..7f8d4fa8833ff --- /dev/null +++ b/packages/EasterEgg/res/drawable/icon.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/EasterEgg/res/drawable/icon_bg.xml b/packages/EasterEgg/res/drawable/icon_bg.xml index 659f98be4f435..31b2a7f9a333d 100644 --- a/packages/EasterEgg/res/drawable/icon_bg.xml +++ b/packages/EasterEgg/res/drawable/icon_bg.xml @@ -1,8 +1,7 @@ - + android:color="#073042" /> + diff --git a/packages/EasterEgg/res/drawable/left_ear.xml b/packages/EasterEgg/res/drawable/left_ear.xml new file mode 100644 index 0000000000000..2b98736df039b --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_ear.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/left_ear_inside.xml b/packages/EasterEgg/res/drawable/left_ear_inside.xml new file mode 100644 index 0000000000000..1d947edc31e21 --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_ear_inside.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/left_eye.xml b/packages/EasterEgg/res/drawable/left_eye.xml new file mode 100644 index 0000000000000..4dde1b661393c --- /dev/null +++ b/packages/EasterEgg/res/drawable/left_eye.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/leg1.xml b/packages/EasterEgg/res/drawable/leg1.xml new file mode 100644 index 0000000000000..d72c746b62322 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg1.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/leg2.xml b/packages/EasterEgg/res/drawable/leg2.xml new file mode 100644 index 0000000000000..a772a870af7d1 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg2.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/leg2_shadow.xml b/packages/EasterEgg/res/drawable/leg2_shadow.xml new file mode 100644 index 0000000000000..b01bd6995c0b1 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg2_shadow.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/leg3.xml b/packages/EasterEgg/res/drawable/leg3.xml new file mode 100644 index 0000000000000..d471236687b58 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg3.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/leg4.xml b/packages/EasterEgg/res/drawable/leg4.xml new file mode 100644 index 0000000000000..e5868eb80c598 --- /dev/null +++ b/packages/EasterEgg/res/drawable/leg4.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/mouth.xml b/packages/EasterEgg/res/drawable/mouth.xml new file mode 100644 index 0000000000000..ddcf2e82f976b --- /dev/null +++ b/packages/EasterEgg/res/drawable/mouth.xml @@ -0,0 +1,27 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/nose.xml b/packages/EasterEgg/res/drawable/nose.xml new file mode 100644 index 0000000000000..d403cd1baadfe --- /dev/null +++ b/packages/EasterEgg/res/drawable/nose.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/octo_bg.xml b/packages/EasterEgg/res/drawable/octo_bg.xml new file mode 100644 index 0000000000000..1e46cf434a8be --- /dev/null +++ b/packages/EasterEgg/res/drawable/octo_bg.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/EasterEgg/res/drawable/right_ear.xml b/packages/EasterEgg/res/drawable/right_ear.xml new file mode 100644 index 0000000000000..b9fb4d1c74708 --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_ear.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/right_ear_inside.xml b/packages/EasterEgg/res/drawable/right_ear_inside.xml new file mode 100644 index 0000000000000..86b6e3428d1f3 --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_ear_inside.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/EasterEgg/res/drawable/right_eye.xml b/packages/EasterEgg/res/drawable/right_eye.xml new file mode 100644 index 0000000000000..a1871a62c25bb --- /dev/null +++ b/packages/EasterEgg/res/drawable/right_eye.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/stat_icon.xml b/packages/EasterEgg/res/drawable/stat_icon.xml new file mode 100644 index 0000000000000..608cb2017c3f0 --- /dev/null +++ b/packages/EasterEgg/res/drawable/stat_icon.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/packages/EasterEgg/res/drawable/tail.xml b/packages/EasterEgg/res/drawable/tail.xml new file mode 100644 index 0000000000000..0cca23c3e16cb --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail.xml @@ -0,0 +1,26 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/tail_cap.xml b/packages/EasterEgg/res/drawable/tail_cap.xml new file mode 100644 index 0000000000000..b82f6f9b478ad --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail_cap.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/drawable/tail_shadow.xml b/packages/EasterEgg/res/drawable/tail_shadow.xml new file mode 100644 index 0000000000000..bb1ff12b3afed --- /dev/null +++ b/packages/EasterEgg/res/drawable/tail_shadow.xml @@ -0,0 +1,22 @@ + + + + diff --git a/packages/EasterEgg/res/layout/activity_paint.xml b/packages/EasterEgg/res/layout/activity_paint.xml index a4c17afd15313..8e916b021bbdc 100644 --- a/packages/EasterEgg/res/layout/activity_paint.xml +++ b/packages/EasterEgg/res/layout/activity_paint.xml @@ -16,7 +16,7 @@ --> - \ No newline at end of file + diff --git a/packages/EasterEgg/res/layout/cat_view.xml b/packages/EasterEgg/res/layout/cat_view.xml new file mode 100644 index 0000000000000..85b494d2e68d3 --- /dev/null +++ b/packages/EasterEgg/res/layout/cat_view.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/EasterEgg/res/layout/edit_text.xml b/packages/EasterEgg/res/layout/edit_text.xml new file mode 100644 index 0000000000000..9f7ac802bad4a --- /dev/null +++ b/packages/EasterEgg/res/layout/edit_text.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/EasterEgg/res/layout/food_layout.xml b/packages/EasterEgg/res/layout/food_layout.xml new file mode 100644 index 0000000000000..d0ca0c8899aae --- /dev/null +++ b/packages/EasterEgg/res/layout/food_layout.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/packages/EasterEgg/res/layout/neko_activity.xml b/packages/EasterEgg/res/layout/neko_activity.xml new file mode 100644 index 0000000000000..c258137ca7108 --- /dev/null +++ b/packages/EasterEgg/res/layout/neko_activity.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/packages/EasterEgg/res/values/cat_strings.xml b/packages/EasterEgg/res/values/cat_strings.xml new file mode 100644 index 0000000000000..5214fc1ab01d9 --- /dev/null +++ b/packages/EasterEgg/res/values/cat_strings.xml @@ -0,0 +1,71 @@ + + + Android Neko + New cats + \???? + A cat is here. + Cat #%s + Cats + Forget %s? + + Empty dish + Bits + Fish + Chicken + Treat + + + @drawable/food_dish + @drawable/food_bits + @drawable/food_sysuituna + @drawable/food_chicken + @drawable/food_cookie + + + 0 + 15 + 30 + 60 + 120 + + + 0 + 5 + 35 + 65 + 90 + + + 😸 + 😹 + 😺 + 😻 + 😼 + 😽 + 😾 + 😿 + 🙀 + 💩 + 🐁 + + + 🍩 + 🍭 + 🍫 + 🍨 + 🔔 + 🐝 + 🍪 + 🥧 + + Toy + Tap to use + Cat attracted! + Water bubbler + Swipe to fill + Food bowl + Tap to refill + Full + Empty + + diff --git a/packages/EasterEgg/res/values/dimens.xml b/packages/EasterEgg/res/values/dimens.xml new file mode 100644 index 0000000000000..e9dcebd27f7b2 --- /dev/null +++ b/packages/EasterEgg/res/values/dimens.xml @@ -0,0 +1,19 @@ + + + + 64dp + diff --git a/packages/EasterEgg/res/values/strings.xml b/packages/EasterEgg/res/values/strings.xml index b95ec6be4c845..25f94215d433d 100644 --- a/packages/EasterEgg/res/values/strings.xml +++ b/packages/EasterEgg/res/values/strings.xml @@ -14,11 +14,13 @@ Copyright (C) 2018 The Android Open Source Project limitations under the License. --> - Android Q Easter Egg + Android R Easter Egg Icon Quiz PAINT.APK + + Cat Controls diff --git a/packages/EasterEgg/res/xml/filepaths.xml b/packages/EasterEgg/res/xml/filepaths.xml new file mode 100644 index 0000000000000..2130025e92659 --- /dev/null +++ b/packages/EasterEgg/res/xml/filepaths.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/packages/EasterEgg/src/com/android/egg/neko/Cat.java b/packages/EasterEgg/src/com/android/egg/neko/Cat.java new file mode 100644 index 0000000000000..cd59a735068bb --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/Cat.java @@ -0,0 +1,524 @@ +/* + * 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 + }; + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/Food.java b/packages/EasterEgg/src/com/android/egg/neko/Food.java new file mode 100644 index 0000000000000..aeffc4adfd3a6 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/Food.java @@ -0,0 +1,61 @@ +/* + * 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 android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Icon; + +import com.android.egg.R; + +public class Food { + private final int mType; + + private static int[] sIcons; + private static String[] sNames; + + public Food(int type) { + mType = type; + } + + public Icon getIcon(Context context) { + if (sIcons == null) { + TypedArray icons = context.getResources().obtainTypedArray(R.array.food_icons); + sIcons = new int[icons.length()]; + for (int i = 0; i < sIcons.length; i++) { + sIcons[i] = icons.getResourceId(i, 0); + } + icons.recycle(); + } + return Icon.createWithResource(context, sIcons[mType]); + } + + public String getName(Context context) { + if (sNames == null) { + sNames = context.getResources().getStringArray(R.array.food_names); + } + return sNames[mType]; + } + + public long getInterval(Context context) { + return context.getResources().getIntArray(R.array.food_intervals)[mType]; + } + + public int getType() { + return mType; + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java new file mode 100644 index 0000000000000..df461c6878f0c --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoActivationActivity.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 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 android.app.Activity; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.util.Log; +import android.widget.Toast; + +import com.android.internal.logging.MetricsLogger; + +public class NekoActivationActivity extends Activity { + private static final String R_EGG_UNLOCK_SETTING = "egg_mode_r"; + + private void toastUp(String s) { + Toast toast = Toast.makeText(this, s, Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + public void onStart() { + super.onStart(); + + final PackageManager pm = getPackageManager(); + final ComponentName cn = new ComponentName(this, NekoControlsService.class); + final boolean componentEnabled = pm.getComponentEnabledSetting(cn) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + if (Settings.System.getLong(getContentResolver(), + R_EGG_UNLOCK_SETTING, 0) == 0) { + if (componentEnabled) { + Log.v("Neko", "Disabling controls."); + pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + MetricsLogger.histogram(this, "egg_neko_enable", 0); + toastUp("\uD83D\uDEAB"); + } else { + Log.v("Neko", "Controls already disabled."); + } + } else { + if (!componentEnabled) { + Log.v("Neko", "Enabling controls."); + pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP); + MetricsLogger.histogram(this, "egg_neko_enable", 1); + toastUp("\uD83D\uDC31"); + } else { + Log.v("Neko", "Controls already enabled."); + } + } + finish(); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt new file mode 100644 index 0000000000000..56f599a3a2198 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoControlsService.kt @@ -0,0 +1,323 @@ +/* + * 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 android.app.PendingIntent +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Icon +import android.service.controls.Control +import android.service.controls.ControlsProviderService +import android.service.controls.DeviceTypes +import android.service.controls.actions.ControlAction +import android.service.controls.actions.FloatAction +import android.service.controls.templates.ControlButton +import android.service.controls.templates.RangeTemplate +import android.service.controls.templates.StatelessTemplate +import android.service.controls.templates.ToggleTemplate +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.util.Log +import androidx.annotation.RequiresApi +import com.android.internal.logging.MetricsLogger +import java.util.Random +import java.util.concurrent.Flow +import java.util.function.Consumer + +import com.android.egg.R + +const val CONTROL_ID_WATER = "water" +const val CONTROL_ID_FOOD = "food" +const val CONTROL_ID_TOY = "toy" + +const val FOOD_SPAWN_CAT_DELAY_MINS = 5L + +const val COLOR_FOOD_FG = 0xFFFF8000.toInt() +const val COLOR_FOOD_BG = COLOR_FOOD_FG and 0x40FFFFFF.toInt() +const val COLOR_WATER_FG = 0xFF0080FF.toInt() +const val COLOR_WATER_BG = COLOR_WATER_FG and 0x40FFFFFF.toInt() +const val COLOR_TOY_FG = 0xFFFF4080.toInt() +const val COLOR_TOY_BG = COLOR_TOY_FG and 0x40FFFFFF.toInt() + +val P_TOY_ICONS = intArrayOf( + 1, R.drawable.ic_toy_mouse, + 1, R.drawable.ic_toy_fish, + 1, R.drawable.ic_toy_ball, + 1, R.drawable.ic_toy_laser +) + +@RequiresApi(30) +fun Control_toString(control: Control): String { + val hc = String.format("0x%08x", control.hashCode()) + return ("Control($hc id=${control.controlId}, type=${control.deviceType}, " + + "title=${control.title}, template=${control.controlTemplate})") +} + +@RequiresApi(30) +public class NekoControlsService : ControlsProviderService(), PrefState.PrefsListener { + private val TAG = "NekoControls" + + private val controls = HashMap() + private val publishers = ArrayList() + private val rng = Random() + + private var lastToyIcon: Icon? = null + + private lateinit var prefs: PrefState + + override fun onCreate() { + super.onCreate() + + prefs = PrefState(this) + prefs.setListener(this) + + createDefaultControls() + } + + override fun onPrefsChanged() { + createDefaultControls() + } + + private fun createDefaultControls() { + val foodState: Int = prefs.foodState + if (foodState != 0) { + NekoService.registerJobIfNeeded(this, FOOD_SPAWN_CAT_DELAY_MINS) + } + + val water = prefs.waterState + + controls[CONTROL_ID_WATER] = makeWaterBowlControl(water) + controls[CONTROL_ID_FOOD] = makeFoodBowlControl(foodState != 0) + controls[CONTROL_ID_TOY] = makeToyControl(currentToyIcon(), false) + } + + private fun currentToyIcon(): Icon { + val icon = lastToyIcon ?: randomToyIcon() + lastToyIcon = icon + return icon + } + + private fun randomToyIcon(): Icon { + return Icon.createWithResource(resources, Cat.chooseP(rng, P_TOY_ICONS, 4)) + } + + private fun colorize(s: CharSequence, color: Int): CharSequence { + val ssb = SpannableStringBuilder(s) + ssb.setSpan(ForegroundColorSpan(color), 0, s.length, 0) + return ssb + } + + private fun makeToyControl(icon: Icon?, thrown: Boolean): Control { + return Control.StatefulBuilder(CONTROL_ID_TOY, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_UNKNOWN) + .setCustomIcon(icon) + // ?.setTint(COLOR_TOY_FG)) // TODO(b/159559045): uncomment when fixed + .setCustomColor(ColorStateList.valueOf(COLOR_TOY_BG)) + .setTitle(colorize(getString(R.string.control_toy_title), COLOR_TOY_FG)) + .setStatusText(colorize( + if (thrown) getString(R.string.control_toy_status) else "", + COLOR_TOY_FG)) + .setControlTemplate(StatelessTemplate("toy")) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (thrown) "" else getString(R.string.control_toy_subtitle)) + .setAppIntent(getAppIntent()) + .build() + } + + private fun makeWaterBowlControl(fillLevel: Float): Control { + return Control.StatefulBuilder(CONTROL_ID_WATER, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_KETTLE) + .setTitle(colorize(getString(R.string.control_water_title), COLOR_WATER_FG)) + .setCustomColor(ColorStateList.valueOf(COLOR_WATER_BG)) + .setCustomIcon(Icon.createWithResource(resources, + if (fillLevel >= 100f) R.drawable.ic_water_filled else R.drawable.ic_water)) + //.setTint(COLOR_WATER_FG)) // TODO(b/159559045): uncomment when fixed + .setControlTemplate(RangeTemplate("waterlevel", 0f, 200f, fillLevel, 10f, + "%.0f mL")) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (fillLevel == 0f) getString(R.string.control_water_subtitle) else "") + .build() + } + + private fun makeFoodBowlControl(filled: Boolean): Control { + return Control.StatefulBuilder(CONTROL_ID_FOOD, getPendingIntent()) + .setDeviceType(DeviceTypes.TYPE_UNKNOWN) + .setCustomColor(ColorStateList.valueOf(COLOR_FOOD_BG)) + .setTitle(colorize(getString(R.string.control_food_title), COLOR_FOOD_FG)) + .setCustomIcon(Icon.createWithResource(resources, + if (filled) R.drawable.ic_foodbowl_filled else R.drawable.ic_bowl)) + // .setTint(COLOR_FOOD_FG)) // TODO(b/159559045): uncomment when fixed + .setStatusText( + if (filled) colorize( + getString(R.string.control_food_status_full), 0xCCFFFFFF.toInt()) + else colorize( + getString(R.string.control_food_status_empty), 0x80FFFFFF.toInt())) + .setControlTemplate(ToggleTemplate("foodbowl", ControlButton(filled, "Refill"))) + .setStatus(Control.STATUS_OK) + .setSubtitle(if (filled) "" else getString(R.string.control_food_subtitle)) + .build() + } + + private fun getPendingIntent(): PendingIntent { + val intent = Intent(Intent.ACTION_MAIN) + .setClass(this, NekoLand::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return PendingIntent.getActivity(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + private fun getAppIntent(): PendingIntent { + return getPendingIntent() + } + + + override fun performControlAction( + controlId: String, + action: ControlAction, + consumer: Consumer + ) { + when (controlId) { + CONTROL_ID_FOOD -> { + // refill bowl + controls[CONTROL_ID_FOOD] = makeFoodBowlControl(true) + Log.v(TAG, "Bowl refilled. (Registering job.)") + NekoService.registerJob(this, FOOD_SPAWN_CAT_DELAY_MINS) + MetricsLogger.histogram(this, "egg_neko_offered_food", 11) + prefs.foodState = 11 + } + CONTROL_ID_TOY -> { + Log.v(TAG, "Toy tossed.") + controls[CONTROL_ID_TOY] = + makeToyControl(currentToyIcon(), true) + // TODO: re-enable toy + Thread() { + Thread.sleep((1 + Random().nextInt(4)) * 1000L) + NekoService.getExistingCat(prefs)?.let { + NekoService.notifyCat(this, it) + } + controls[CONTROL_ID_TOY] = makeToyControl(randomToyIcon(), false) + pushControlChanges() + }.start() + } + CONTROL_ID_WATER -> { + if (action is FloatAction) { + controls[CONTROL_ID_WATER] = makeWaterBowlControl(action.newValue) + Log.v(TAG, "Water level set to " + action.newValue) + prefs.waterState = action.newValue + } + } + else -> { + return + } + } + consumer.accept(ControlAction.RESPONSE_OK) + pushControlChanges() + } + + private fun pushControlChanges() { + Thread() { + publishers.forEach { it.refresh() } + }.start() + } + + private fun makeStateless(c: Control?): Control? { + if (c == null) return null + return Control.StatelessBuilder(c.controlId, c.appIntent) + .setTitle(c.title) + .setSubtitle(c.subtitle) + .setStructure(c.structure) + .setDeviceType(c.deviceType) + .setCustomIcon(c.customIcon) + .setCustomColor(c.customColor) + .build() + } + + override fun createPublisherFor(list: MutableList): Flow.Publisher { + createDefaultControls() + + val publisher = UglyPublisher(list, true) + publishers.add(publisher) + return publisher + } + + override fun createPublisherForAllAvailable(): Flow.Publisher { + createDefaultControls() + + val publisher = UglyPublisher(controls.keys, false) + publishers.add(publisher) + return publisher + } + + private inner class UglyPublisher( + val controlKeys: Iterable, + val indefinite: Boolean + ) : Flow.Publisher { + val subscriptions = ArrayList() + + private inner class UglySubscription( + val initialControls: Iterator, + var subscriber: Flow.Subscriber? + ) : Flow.Subscription { + override fun cancel() { + Log.v(TAG, "cancel subscription: $this for subscriber: $subscriber " + + "to publisher: $this@UglyPublisher") + subscriber = null + unsubscribe(this) + } + + override fun request(p0: Long) { + (0 until p0).forEach { _ -> + if (initialControls.hasNext()) { + send(initialControls.next()) + } else { + if (!indefinite) subscriber?.onComplete() + } + } + } + + fun send(c: Control) { + Log.v(TAG, "sending update: " + Control_toString(c) + " => " + subscriber) + subscriber?.onNext(c) + } + } + + override fun subscribe(subscriber: Flow.Subscriber) { + Log.v(TAG, "subscribe to publisher: $this by subscriber: $subscriber") + val sub = UglySubscription(controlKeys.mapNotNull { controls[it] }.iterator(), + subscriber) + subscriptions.add(sub) + subscriber.onSubscribe(sub) + } + + fun unsubscribe(sub: UglySubscription) { + Log.v(TAG, "no more subscriptions, removing subscriber: $sub") + subscriptions.remove(sub) + if (subscriptions.size == 0) { + Log.v(TAG, "no more subscribers, removing publisher: $this") + publishers.remove(this) + } + } + + fun refresh() { + controlKeys.mapNotNull { controls[it] }.forEach { control -> + subscriptions.forEach { sub -> + sub.send(control) + } + } + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java new file mode 100644 index 0000000000000..2bd2228e7bf21 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoDialog.java @@ -0,0 +1,109 @@ +/* + * 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 android.app.Dialog; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.egg.R; + +import java.util.ArrayList; + +public class NekoDialog extends Dialog { + + private final Adapter mAdapter; + + public NekoDialog(@NonNull Context context) { + super(context, android.R.style.Theme_Material_Dialog_NoActionBar); + RecyclerView view = new RecyclerView(getContext()); + mAdapter = new Adapter(getContext()); + view.setLayoutManager(new GridLayoutManager(getContext(), 2)); + view.setAdapter(mAdapter); + final float dp = context.getResources().getDisplayMetrics().density; + final int pad = (int)(16*dp); + view.setPadding(pad, pad, pad, pad); + setContentView(view); + } + + private void onFoodSelected(Food food) { + PrefState prefs = new PrefState(getContext()); + int currentState = prefs.getFoodState(); + if (currentState == 0 && food.getType() != 0) { + NekoService.registerJob(getContext(), food.getInterval(getContext())); + } +// MetricsLogger.histogram(getContext(), "egg_neko_offered_food", food.getType()); + prefs.setFoodState(food.getType()); + dismiss(); + } + + private class Adapter extends RecyclerView.Adapter { + + private final Context mContext; + private final ArrayList mFoods = new ArrayList<>(); + + public Adapter(Context context) { + mContext = context; + int[] foods = context.getResources().getIntArray(R.array.food_names); + // skip food 0, you can't choose it + for (int i=1; i list = mPrefs.getCats(); + Collections.sort(list, new Comparator() { + @Override + public int compare(Cat cat, Cat cat2) { + Color.colorToHSV(cat.getBodyColor(), hsv); + float bodyH1 = hsv[0]; + Color.colorToHSV(cat2.getBodyColor(), hsv); + float bodyH2 = hsv[0]; + return Float.compare(bodyH1, bodyH2); + } + }); + cats = list.toArray(new Cat[0]); + } + mAdapter.setCats(cats); + return cats.length; + } + + private void onCatClick(Cat cat) { + if (CAT_GEN) { + mPrefs.addCat(cat); + new AlertDialog.Builder(NekoLand.this) + .setTitle("Cat added") + .setPositiveButton(android.R.string.ok, null) + .show(); + } else { + showNameDialog(cat); + } + } + + private void onCatRemove(Cat cat) { + cat.logRemove(this); + mPrefs.removeCat(cat); + } + + private void showNameDialog(final Cat cat) { + final Context context = new ContextThemeWrapper(this, + android.R.style.Theme_Material_Light_Dialog_NoActionBar); + // TODO: Move to XML, add correct margins. + View view = LayoutInflater.from(context).inflate(R.layout.edit_text, null); + final EditText text = (EditText) view.findViewById(android.R.id.edit); + text.setText(cat.getName()); + text.setSelection(cat.getName().length()); + final int size = context.getResources() + .getDimensionPixelSize(android.R.dimen.app_icon_size); + Drawable catIcon = cat.createIcon(this, size, size).loadDrawable(this); + new AlertDialog.Builder(context) + .setTitle(" ") + .setIcon(catIcon) + .setView(view) + .setPositiveButton(android.R.string.ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + cat.logRename(context); + cat.setName(text.getText().toString().trim()); + mPrefs.addCat(cat); + } + }).show(); + } + + @Override + public void onPrefsChanged() { + updateCats(); + } + + private class CatAdapter extends RecyclerView.Adapter { + + private Cat[] mCats; + + public void setCats(Cat[] cats) { + mCats = cats; + notifyDataSetChanged(); + } + + @Override + public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new CatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.cat_view, parent, false)); + } + + private void setContextGroupVisible(final CatHolder holder, boolean vis) { + final View group = holder.contextGroup; + if (vis && group.getVisibility() != View.VISIBLE) { + group.setAlpha(0); + group.setVisibility(View.VISIBLE); + group.animate().alpha(1.0f).setDuration(333); + Runnable hideAction = new Runnable() { + @Override + public void run() { + setContextGroupVisible(holder, false); + } + }; + group.setTag(hideAction); + group.postDelayed(hideAction, 5000); + } else if (!vis && group.getVisibility() == View.VISIBLE) { + group.removeCallbacks((Runnable) group.getTag()); + group.animate().alpha(0f).setDuration(250).withEndAction(new Runnable() { + @Override + public void run() { + group.setVisibility(View.INVISIBLE); + } + }); + } + } + + @Override + public void onBindViewHolder(final CatHolder holder, int position) { + Context context = holder.itemView.getContext(); + final int size = context.getResources().getDimensionPixelSize(R.dimen.neko_display_size); + holder.imageView.setImageIcon(mCats[position].createIcon(context, size, size)); + holder.textView.setText(mCats[position].getName()); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onCatClick(mCats[holder.getAdapterPosition()]); + } + }); + holder.itemView.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + setContextGroupVisible(holder, true); + return true; + } + }); + holder.delete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setContextGroupVisible(holder, false); + new AlertDialog.Builder(NekoLand.this) + .setTitle(getString(R.string.confirm_delete, mCats[position].getName())) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onCatRemove(mCats[holder.getAdapterPosition()]); + } + }) + .show(); + } + }); + holder.share.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setContextGroupVisible(holder, false); + Cat cat = mCats[holder.getAdapterPosition()]; + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + mPendingShareCat = cat; + requestPermissions( + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + STORAGE_PERM_REQUEST); + return; + } + shareCat(cat); + } + }); + } + + @Override + public int getItemCount() { + return mCats.length; + } + } + + private void shareCat(Cat cat) { + final File dir = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "Cats"); + if (!dir.exists() && !dir.mkdirs()) { + Log.e("NekoLand", "save: error: can't create Pictures directory"); + return; + } + final File png = new File(dir, cat.getName().replaceAll("[/ #:]+", "_") + ".png"); + Bitmap bitmap = cat.createBitmap(EXPORT_BITMAP_SIZE, EXPORT_BITMAP_SIZE); + if (bitmap != null) { + try { + OutputStream os = new FileOutputStream(png); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, os); + os.close(); + MediaScannerConnection.scanFile( + this, + new String[]{png.toString()}, + new String[]{"image/png"}, + null); + Log.v("Neko", "cat file: " + png); + Uri uri = FileProvider.getUriForFile(this, "com.android.egg.fileprovider", png); + Log.v("Neko", "cat uri: " + uri); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.putExtra(Intent.EXTRA_SUBJECT, cat.getName()); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, null) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)); + cat.logShare(this); + } catch (IOException e) { + Log.e("NekoLand", "save: error: " + e); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (requestCode == STORAGE_PERM_REQUEST) { + if (mPendingShareCat != null) { + shareCat(mPendingShareCat); + mPendingShareCat = null; + } + } + } + + private static class CatHolder extends RecyclerView.ViewHolder { + private final ImageView imageView; + private final TextView textView; + private final View contextGroup; + private final View delete; + private final View share; + + public CatHolder(View itemView) { + super(itemView); + imageView = (ImageView) itemView.findViewById(android.R.id.icon); + textView = (TextView) itemView.findViewById(android.R.id.title); + contextGroup = itemView.findViewById(R.id.contextGroup); + delete = itemView.findViewById(android.R.id.closeButton); + share = itemView.findViewById(android.R.id.shareText); + } + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java new file mode 100644 index 0000000000000..ca89adc7b6e4d --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoLockedActivity.java @@ -0,0 +1,48 @@ +/* + * 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 android.app.Activity; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.view.WindowManager; + +import androidx.annotation.Nullable; + +public class NekoLockedActivity extends Activity implements OnDismissListener { + + private NekoDialog mDialog; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + + mDialog = new NekoDialog(this); + mDialog.setOnDismissListener(this); + mDialog.show(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoService.java b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java new file mode 100644 index 0000000000000..939e85c07d068 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoService.java @@ -0,0 +1,193 @@ +/* + * 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.Cat.PURR; +import static com.android.egg.neko.NekoLand.CHAN_ID; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.android.egg.R; + +import java.util.List; +import java.util.Random; + +public class NekoService extends JobService { + + private static final String TAG = "NekoService"; + + public static int JOB_ID = 42; + + public static int CAT_NOTIFICATION = 1; + public static int DEBUG_NOTIFICATION = 1234; + + public static float CAT_CAPTURE_PROB = 1.0f; // generous + + public static long SECONDS = 1000; + public static long MINUTES = 60 * SECONDS; + + //public static long INTERVAL_FLEX = 15 * SECONDS; + public static long INTERVAL_FLEX = 5 * MINUTES; + + public static float INTERVAL_JITTER_FRAC = 0.25f; + + private static void setupNotificationChannels(Context context) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + NotificationChannel eggChan = new NotificationChannel(CHAN_ID, + context.getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT); + eggChan.setSound(Uri.EMPTY, Notification.AUDIO_ATTRIBUTES_DEFAULT); // cats are quiet + eggChan.setVibrationPattern(PURR); // not totally quiet though + //eggChan.setBlockableSystem(true); // unlike a real cat, you can push this one off your lap + eggChan.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // cats sit in the window + noman.createNotificationChannel(eggChan); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.v(TAG, "Starting job: " + String.valueOf(params)); + + if (NekoLand.DEBUG_NOTIFICATIONS) { + NotificationManager noman = getSystemService(NotificationManager.class); + final Bundle extras = new Bundle(); + extras.putString("android.substName", getString(R.string.notification_name)); + final int size = getResources() + .getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + final Cat cat = Cat.create(this); + final Notification.Builder builder + = cat.buildNotification(this) + .setContentTitle("DEBUG") + .setChannelId(NekoLand.CHAN_ID) + .setContentText("Ran job: " + params); + + noman.notify(DEBUG_NOTIFICATION, builder.build()); + } + + triggerFoodResponse(this); + cancelJob(this); + return false; + } + + private static void triggerFoodResponse(Context context) { + final PrefState prefs = new PrefState(context); + int food = prefs.getFoodState(); + if (food != 0) { + prefs.setFoodState(0); // nom + final Random rng = new Random(); + if (rng.nextFloat() <= CAT_CAPTURE_PROB) { + Cat cat; + List cats = prefs.getCats(); + final int[] probs = context.getResources().getIntArray(R.array.food_new_cat_prob); + final float waterLevel100 = prefs.getWaterState() / 2; // water is 0..200 + final float new_cat_prob = (float) ((food < probs.length) + ? probs[food] + : waterLevel100) / 100f; + Log.v(TAG, "Food type: " + food); + Log.v(TAG, "New cat probability: " + new_cat_prob); + + if (cats.size() == 0 || rng.nextFloat() <= new_cat_prob) { + cat = newRandomCat(context, prefs); + Log.v(TAG, "A new cat is here: " + cat.getName()); + } else { + cat = getExistingCat(prefs); + Log.v(TAG, "A cat has returned: " + cat.getName()); + } + + notifyCat(context, cat); + } + } + } + + static void notifyCat(Context context, Cat cat) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + final Notification.Builder builder = cat.buildNotification(context); + noman.notify(cat.getShortcutId(), CAT_NOTIFICATION, builder.build()); + } + + static Cat newRandomCat(Context context, PrefState prefs) { + final Cat cat = Cat.create(context); + prefs.addCat(cat); + cat.logAdd(context); + return cat; + } + + static Cat getExistingCat(PrefState prefs) { + final List cats = prefs.getCats(); + if (cats.size() == 0) return null; + return cats.get(new Random().nextInt(cats.size())); + } + + @Override + public boolean onStopJob(JobParameters jobParameters) { + return false; + } + + public static void registerJobIfNeeded(Context context, long intervalMinutes) { + JobScheduler jss = context.getSystemService(JobScheduler.class); + JobInfo info = jss.getPendingJob(JOB_ID); + if (info == null) { + registerJob(context, intervalMinutes); + } + } + + public static void registerJob(Context context, long intervalMinutes) { + setupNotificationChannels(context); + + JobScheduler jss = context.getSystemService(JobScheduler.class); + jss.cancel(JOB_ID); + long interval = intervalMinutes * MINUTES; + long jitter = (long) (INTERVAL_JITTER_FRAC * interval); + interval += (long) (Math.random() * (2 * jitter)) - jitter; + final JobInfo jobInfo = new JobInfo.Builder(JOB_ID, + new ComponentName(context, NekoService.class)) + .setPeriodic(interval, INTERVAL_FLEX) + .build(); + + Log.v(TAG, "A cat will visit in " + interval + "ms: " + String.valueOf(jobInfo)); + jss.schedule(jobInfo); + + if (NekoLand.DEBUG_NOTIFICATIONS) { + NotificationManager noman = context.getSystemService(NotificationManager.class); + noman.notify(DEBUG_NOTIFICATION, new Notification.Builder(context) + .setSmallIcon(R.drawable.stat_icon) + .setContentTitle(String.format("Job scheduled in %d min", (interval / MINUTES))) + .setContentText(String.valueOf(jobInfo)) + .setPriority(Notification.PRIORITY_MIN) + .setCategory(Notification.CATEGORY_SERVICE) + .setChannelId(NekoLand.CHAN_ID) + .setShowWhen(true) + .build()); + } + } + + public static void cancelJob(Context context) { + JobScheduler jss = context.getSystemService(JobScheduler.class); + Log.v(TAG, "Canceling job"); + jss.cancel(JOB_ID); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java new file mode 100644 index 0000000000000..d02433f40e897 --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/NekoTile.java @@ -0,0 +1,116 @@ +/* + * 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 android.content.Intent; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; + +import com.android.egg.neko.PrefState.PrefsListener; +import com.android.internal.logging.MetricsLogger; + +public class NekoTile extends TileService implements PrefsListener { + + private static final String TAG = "NekoTile"; + + private PrefState mPrefs; + + @Override + public void onCreate() { + super.onCreate(); + mPrefs = new PrefState(this); + } + + @Override + public void onStartListening() { + super.onStartListening(); + mPrefs.setListener(this); + updateState(); + } + + @Override + public void onStopListening() { + super.onStopListening(); + mPrefs.setListener(null); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + MetricsLogger.count(this, "egg_neko_tile_added", 1); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + MetricsLogger.count(this, "egg_neko_tile_removed", 1); + } + + @Override + public void onPrefsChanged() { + updateState(); + } + + private void updateState() { + Tile tile = getQsTile(); + int foodState = mPrefs.getFoodState(); + Food food = new Food(foodState); + if (foodState != 0) { + NekoService.registerJobIfNeeded(this, food.getInterval(this)); + } + tile.setIcon(food.getIcon(this)); + tile.setLabel(food.getName(this)); + tile.setState(foodState != 0 ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.updateTile(); + } + + @Override + public void onClick() { + if (mPrefs.getFoodState() != 0) { + // there's already food loaded, let's empty it + MetricsLogger.count(this, "egg_neko_empty_food", 1); + mPrefs.setFoodState(0); + NekoService.cancelJob(this); + } else { + // time to feed the cats + if (isLocked()) { + if (isSecure()) { + Log.d(TAG, "startActivityAndCollapse"); + Intent intent = new Intent(this, NekoLockedActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityAndCollapse(intent); + } else { + unlockAndRun(new Runnable() { + @Override + public void run() { + showNekoDialog(); + } + }); + } + } else { + showNekoDialog(); + } + } + } + + private void showNekoDialog() { + Log.d(TAG, "showNekoDialog"); + MetricsLogger.count(this, "egg_neko_select_food", 1); + showDialog(new NekoDialog(this)); + } +} diff --git a/packages/EasterEgg/src/com/android/egg/neko/PrefState.java b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java new file mode 100644 index 0000000000000..49ff315392b4d --- /dev/null +++ b/packages/EasterEgg/src/com/android/egg/neko/PrefState.java @@ -0,0 +1,104 @@ +/* + * 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PrefState implements OnSharedPreferenceChangeListener { + + private static final String FILE_NAME = "mPrefs"; + + private static final String FOOD_STATE = "food"; + + private static final String WATER_STATE = "water"; + + private static final String CAT_KEY_PREFIX = "cat:"; + + private final Context mContext; + private final SharedPreferences mPrefs; + private PrefsListener mListener; + + public PrefState(Context context) { + mContext = context; + mPrefs = mContext.getSharedPreferences(FILE_NAME, 0); + } + + // Can also be used for renaming. + public void addCat(Cat cat) { + mPrefs.edit() + .putString(CAT_KEY_PREFIX + String.valueOf(cat.getSeed()), cat.getName()) + .apply(); + } + + public void removeCat(Cat cat) { + mPrefs.edit().remove(CAT_KEY_PREFIX + String.valueOf(cat.getSeed())).apply(); + } + + public List getCats() { + ArrayList cats = new ArrayList<>(); + Map map = mPrefs.getAll(); + for (String key : map.keySet()) { + if (key.startsWith(CAT_KEY_PREFIX)) { + long seed = Long.parseLong(key.substring(CAT_KEY_PREFIX.length())); + Cat cat = new Cat(mContext, seed); + cat.setName(String.valueOf(map.get(key))); + cats.add(cat); + } + } + return cats; + } + + public int getFoodState() { + return mPrefs.getInt(FOOD_STATE, 0); + } + + public void setFoodState(int foodState) { + mPrefs.edit().putInt(FOOD_STATE, foodState).apply(); + } + + public float getWaterState() { + return mPrefs.getFloat(WATER_STATE, 0f); + } + + public void setWaterState(float waterState) { + mPrefs.edit().putFloat(WATER_STATE, waterState).apply(); + } + + public void setListener(PrefsListener listener) { + mListener = listener; + if (mListener != null) { + mPrefs.registerOnSharedPreferenceChangeListener(this); + } else { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + mListener.onPrefsChanged(); + } + + public interface PrefsListener { + void onPrefsChanged(); + } +}