1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.egg.neko; 18 19 import static com.android.egg.neko.Cat.PURR; 20 import static com.android.egg.neko.NekoLand.CHAN_ID; 21 22 import android.app.Notification; 23 import android.app.NotificationChannel; 24 import android.app.NotificationManager; 25 import android.app.job.JobInfo; 26 import android.app.job.JobParameters; 27 import android.app.job.JobScheduler; 28 import android.app.job.JobService; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.util.Log; 34 35 import com.android.egg.R; 36 37 import java.util.List; 38 import java.util.Random; 39 40 public class NekoService extends JobService { 41 42 private static final String TAG = "NekoService"; 43 44 public static int JOB_ID = 42; 45 46 public static int CAT_NOTIFICATION = 1; 47 public static int DEBUG_NOTIFICATION = 1234; 48 49 public static float CAT_CAPTURE_PROB = 1.0f; // generous 50 51 public static long SECONDS = 1000; 52 public static long MINUTES = 60 * SECONDS; 53 54 //public static long INTERVAL_FLEX = 15 * SECONDS; 55 public static long INTERVAL_FLEX = 5 * MINUTES; 56 57 public static float INTERVAL_JITTER_FRAC = 0.25f; 58 setupNotificationChannels(Context context)59 private static void setupNotificationChannels(Context context) { 60 NotificationManager noman = context.getSystemService(NotificationManager.class); 61 NotificationChannel eggChan = new NotificationChannel(CHAN_ID, 62 context.getString(R.string.notification_channel_name), 63 NotificationManager.IMPORTANCE_DEFAULT); 64 eggChan.setSound(Uri.EMPTY, Notification.AUDIO_ATTRIBUTES_DEFAULT); // cats are quiet 65 eggChan.setVibrationPattern(PURR); // not totally quiet though 66 //eggChan.setBlockableSystem(true); // unlike a real cat, you can push this one off your lap 67 eggChan.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // cats sit in the window 68 noman.createNotificationChannel(eggChan); 69 } 70 71 @Override onStartJob(JobParameters params)72 public boolean onStartJob(JobParameters params) { 73 Log.v(TAG, "Starting job: " + String.valueOf(params)); 74 75 if (NekoLand.DEBUG_NOTIFICATIONS) { 76 NotificationManager noman = getSystemService(NotificationManager.class); 77 final Bundle extras = new Bundle(); 78 extras.putString("android.substName", getString(R.string.notification_name)); 79 final int size = getResources() 80 .getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 81 final Cat cat = Cat.create(this); 82 final Notification.Builder builder 83 = cat.buildNotification(this) 84 .setContentTitle("DEBUG") 85 .setChannelId(NekoLand.CHAN_ID) 86 .setContentText("Ran job: " + params); 87 88 noman.notify(DEBUG_NOTIFICATION, builder.build()); 89 } 90 91 triggerFoodResponse(this); 92 cancelJob(this); 93 return false; 94 } 95 triggerFoodResponse(Context context)96 private static void triggerFoodResponse(Context context) { 97 final PrefState prefs = new PrefState(context); 98 int food = prefs.getFoodState(); 99 if (food != 0) { 100 prefs.setFoodState(0); // nom 101 final Random rng = new Random(); 102 if (rng.nextFloat() <= CAT_CAPTURE_PROB) { 103 Cat cat; 104 List<Cat> cats = prefs.getCats(); 105 final int[] probs = context.getResources().getIntArray(R.array.food_new_cat_prob); 106 final float waterLevel100 = prefs.getWaterState() / 2; // water is 0..200 107 final float new_cat_prob = (float) ((food < probs.length) 108 ? probs[food] 109 : waterLevel100) / 100f; 110 Log.v(TAG, "Food type: " + food); 111 Log.v(TAG, "New cat probability: " + new_cat_prob); 112 113 if (cats.size() == 0 || rng.nextFloat() <= new_cat_prob) { 114 cat = newRandomCat(context, prefs); 115 Log.v(TAG, "A new cat is here: " + cat.getName()); 116 } else { 117 cat = getExistingCat(prefs); 118 Log.v(TAG, "A cat has returned: " + cat.getName()); 119 } 120 121 notifyCat(context, cat); 122 } 123 } 124 } 125 notifyCat(Context context, Cat cat)126 static void notifyCat(Context context, Cat cat) { 127 NotificationManager noman = context.getSystemService(NotificationManager.class); 128 final Notification.Builder builder = cat.buildNotification(context); 129 noman.notify(cat.getShortcutId(), CAT_NOTIFICATION, builder.build()); 130 } 131 newRandomCat(Context context, PrefState prefs)132 static Cat newRandomCat(Context context, PrefState prefs) { 133 final Cat cat = Cat.create(context); 134 prefs.addCat(cat); 135 cat.logAdd(context); 136 return cat; 137 } 138 getExistingCat(PrefState prefs)139 static Cat getExistingCat(PrefState prefs) { 140 final List<Cat> cats = prefs.getCats(); 141 if (cats.size() == 0) return null; 142 return cats.get(new Random().nextInt(cats.size())); 143 } 144 145 @Override onStopJob(JobParameters jobParameters)146 public boolean onStopJob(JobParameters jobParameters) { 147 return false; 148 } 149 registerJobIfNeeded(Context context, long intervalMinutes)150 public static void registerJobIfNeeded(Context context, long intervalMinutes) { 151 JobScheduler jss = context.getSystemService(JobScheduler.class); 152 JobInfo info = jss.getPendingJob(JOB_ID); 153 if (info == null) { 154 registerJob(context, intervalMinutes); 155 } 156 } 157 registerJob(Context context, long intervalMinutes)158 public static void registerJob(Context context, long intervalMinutes) { 159 setupNotificationChannels(context); 160 161 JobScheduler jss = context.getSystemService(JobScheduler.class); 162 jss.cancel(JOB_ID); 163 long interval = intervalMinutes * MINUTES; 164 long jitter = (long) (INTERVAL_JITTER_FRAC * interval); 165 interval += (long) (Math.random() * (2 * jitter)) - jitter; 166 final JobInfo jobInfo = new JobInfo.Builder(JOB_ID, 167 new ComponentName(context, NekoService.class)) 168 .setPeriodic(interval, INTERVAL_FLEX) 169 .build(); 170 171 Log.v(TAG, "A cat will visit in " + interval + "ms: " + String.valueOf(jobInfo)); 172 jss.schedule(jobInfo); 173 174 if (NekoLand.DEBUG_NOTIFICATIONS) { 175 NotificationManager noman = context.getSystemService(NotificationManager.class); 176 noman.notify(DEBUG_NOTIFICATION, new Notification.Builder(context) 177 .setSmallIcon(R.drawable.stat_icon) 178 .setContentTitle(String.format("Job scheduled in %d min", (interval / MINUTES))) 179 .setContentText(String.valueOf(jobInfo)) 180 .setPriority(Notification.PRIORITY_MIN) 181 .setCategory(Notification.CATEGORY_SERVICE) 182 .setChannelId(NekoLand.CHAN_ID) 183 .setShowWhen(true) 184 .build()); 185 } 186 } 187 cancelJob(Context context)188 public static void cancelJob(Context context) { 189 JobScheduler jss = context.getSystemService(JobScheduler.class); 190 Log.v(TAG, "Canceling job"); 191 jss.cancel(JOB_ID); 192 } 193 } 194