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