blob: 939e85c07d068edfcde93916173a0c968be3d641 [file] [log] [blame]
Dan Sandlerf4e83e02020-05-12 21:25:31 -04001/*
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
17package com.android.egg.neko;
18
19import static com.android.egg.neko.Cat.PURR;
20import static com.android.egg.neko.NekoLand.CHAN_ID;
21
22import android.app.Notification;
23import android.app.NotificationChannel;
24import android.app.NotificationManager;
25import android.app.job.JobInfo;
26import android.app.job.JobParameters;
27import android.app.job.JobScheduler;
28import android.app.job.JobService;
29import android.content.ComponentName;
30import android.content.Context;
31import android.net.Uri;
32import android.os.Bundle;
33import android.util.Log;
34
35import com.android.egg.R;
36
37import java.util.List;
38import java.util.Random;
39
40public 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
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
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
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
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
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
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
146 public boolean onStopJob(JobParameters jobParameters) {
147 return false;
148 }
149
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
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
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}