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