1 /* 2 * Copyright (C) 2021 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.cts.servicekilltestapp; 18 19 import android.annotation.SuppressLint; 20 import android.app.AlarmManager; 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.app.Service; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.os.AsyncTask; 31 import android.os.Build; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.PowerManager; 35 import android.util.Log; 36 import android.os.SystemClock; 37 38 import java.io.ByteArrayOutputStream; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.ObjectInputStream; 43 import java.io.ObjectOutputStream; 44 import java.io.Serializable; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.TimeZone; 51 import java.util.concurrent.CancellationException; 52 import java.util.concurrent.ExecutorService; 53 import java.util.concurrent.ScheduledFuture; 54 import java.util.concurrent.Executors; 55 import java.util.concurrent.ScheduledExecutorService; 56 import java.util.concurrent.TimeUnit; 57 58 public class ServiceKillTestService extends Service { 59 60 /** 61 * Execution times for each measure 62 */ 63 public static final long HOUR_IN_MS = TimeUnit.HOURS.toMillis(1); 64 public static final long ALARM_REPEAT_MS = TimeUnit.MINUTES.toMillis(10); 65 public static final long ALARM_REPEAT_MARGIN_MS = TimeUnit.SECONDS.toMillis(30); 66 public static final long PERSIST_BENCHMARK_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30); 67 public static final long WORK_REPEAT_MS = TimeUnit.SECONDS.toMillis(10); 68 public static final long MAIN_REPEAT_MS = TimeUnit.SECONDS.toMillis(10); 69 70 /** 71 * Actions and extras 72 */ 73 public static final String TEST_CASE_PACKAGE_NAME = "com.android.cts.servicekilltest"; 74 public static final String TEST_APP_PACKAGE_NAME = TEST_CASE_PACKAGE_NAME + "app"; 75 public static final String ACTION_START = TEST_CASE_PACKAGE_NAME + ".ACTION_START"; 76 public static final String ACTION_STOP = TEST_CASE_PACKAGE_NAME + ".ACTION_STOP"; 77 public static final String ACTION_RESULT = TEST_CASE_PACKAGE_NAME + ".ACTION_RESULT"; 78 private static final String ACTION_ALARM = TEST_CASE_PACKAGE_NAME + ".ACTION_ALARM"; 79 public static final String EXTRA_TEST_ID = "test_id"; 80 81 82 public static final String APP = "CTSServiceKillTest"; 83 public static final String TAG = "ServiceKillTest"; 84 85 public static String NOTIFICATION_CHANNEL_FOREGROUND = "foreground"; 86 87 private PowerManager.WakeLock mWakeLock; 88 private Handler mHandler; 89 private ScheduledExecutorService mScheduledExecutor; 90 private ExecutorService mExecutor; 91 92 93 private Benchmark mCurrentBenchmark; 94 95 private boolean mStarted = false; 96 97 private ScheduledFuture<?> mScheduledFuture; 98 99 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 100 101 private BroadcastReceiver mAlarmReceiver = new BroadcastReceiver() { 102 103 @Override 104 public void onReceive(Context context, Intent intent) { 105 if (intent != null && ACTION_ALARM.equals(intent.getAction())) { 106 logDebug("Alarm"); 107 mCurrentBenchmark.addEvent(Benchmark.Measure.ALARM, SystemClock.elapsedRealtime()); 108 scheduleAlarm(); 109 saveBenchmarkIfRequired(mCurrentBenchmark); 110 } 111 112 } 113 }; 114 115 private Runnable mMainRunnable = new Runnable() { 116 @Override 117 public void run() { 118 logDebug("Main"); 119 if (mWakeLock.isHeld()) { 120 mCurrentBenchmark.addEvent(Benchmark.Measure.MAIN, SystemClock.elapsedRealtime()); 121 saveBenchmarkIfRequired(mCurrentBenchmark); 122 } else { 123 Log.w(TAG, "Wake lock broken"); 124 } 125 mHandler.postDelayed(this, MAIN_REPEAT_MS); 126 } 127 }; 128 129 @Override onBind(Intent intent)130 public IBinder onBind(Intent intent) { 131 return null; 132 } 133 134 135 @Override onCreate()136 public void onCreate() { 137 super.onCreate(); 138 logDebug("onCreate()"); 139 PowerManager powerManager = getSystemService(PowerManager.class); 140 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, APP + "::" + TAG); 141 mWakeLock.acquire(); 142 mExecutor = Executors.newSingleThreadExecutor(); 143 mHandler = new Handler(); 144 startForeground(); 145 loadBenchmarkAsync(benchmark -> { 146 logDebug("Loading Benchmark " + benchmark); 147 148 if (benchmark == null || !benchmark.isRunning()) { 149 mCurrentBenchmark = new Benchmark(); 150 logDebug("New Benchmark " + mCurrentBenchmark); 151 } else { 152 mCurrentBenchmark = benchmark; 153 } 154 startBenchmark(); 155 }); 156 157 } 158 getTestId(Intent i)159 private String getTestId(Intent i) { 160 return i == null ? null : i.getStringExtra(EXTRA_TEST_ID); 161 } 162 isAction(Intent i, String action)163 private boolean isAction(Intent i, String action) { 164 if (i != null && action != null) { 165 return action.equals(i.getAction()); 166 } 167 return i == null && action == null; 168 } 169 170 @Override onStartCommand(Intent intent, int flags, int startId)171 public int onStartCommand(Intent intent, int flags, int startId) { 172 startForeground(); 173 174 final String id = getTestId(intent); 175 if (id != null) { 176 logDebug("onStartCommand TEST " + id + " action " + intent.getAction()); 177 mHandler.post(new Runnable() { 178 @Override 179 public void run() { 180 if (mCurrentBenchmark != null) { 181 if (isAction(intent, ACTION_START)) { 182 logDebug("Starting TEST " + id); 183 mCurrentBenchmark.startTest(id); 184 } else if (isAction(intent, ACTION_STOP)) { 185 logDebug("Stopping TEST " + id); 186 sendResult(id, mCurrentBenchmark.finishTest(id)); 187 188 if (!mCurrentBenchmark.isRunning()) { 189 logDebug("No TEST running, stopping benchmark"); 190 saveBenchmarkAsync(mCurrentBenchmark, () -> { 191 logDebug("No TEST running, stopping service"); 192 stopSelf(); 193 }); 194 } 195 } else if (isAction(intent, ACTION_RESULT)) { 196 logDebug("Getting results for TEST " + id); 197 sendResult(id, mCurrentBenchmark.getAllResults(id)); 198 } 199 } else { 200 mHandler.postDelayed(this, 1000); 201 } 202 } 203 }); 204 } else { 205 Log.w(TAG, "Ignoring start request without test ID"); 206 } 207 return START_STICKY; 208 } 209 sendResult(String testId, Map<Benchmark.Measure, Float> result)210 private void sendResult(String testId, Map<Benchmark.Measure, Float> result) { 211 logDebug("Sending result"); 212 Intent intent = new Intent(ACTION_RESULT); 213 intent.putExtra(EXTRA_TEST_ID, testId); 214 intent.setPackage(TEST_CASE_PACKAGE_NAME); 215 if (result != null) { 216 for (Benchmark.Measure measure : result.keySet()) { 217 intent.putExtra(measure.name(), result.get(measure)); 218 logDebug("Result " + measure.name() + "=" + result.get(measure)); 219 } 220 } 221 sendBroadcast(intent); 222 } 223 startForeground()224 private void startForeground() { 225 NotificationManager notificationManager = getSystemService(NotificationManager.class); 226 227 NotificationChannel notificationChannel = 228 new NotificationChannel(NOTIFICATION_CHANNEL_FOREGROUND, TAG, 229 NotificationManager.IMPORTANCE_LOW); 230 notificationManager.createNotificationChannel(notificationChannel); 231 startForeground(12, new Notification.Builder(this, NOTIFICATION_CHANNEL_FOREGROUND) 232 .setSmallIcon(android.R.drawable.ic_media_play) 233 .setChannelId(NOTIFICATION_CHANNEL_FOREGROUND) 234 .setContentText("Foreground Service Kill Test Running").build()); 235 } 236 getAlarmIntent()237 private PendingIntent getAlarmIntent() { 238 Intent i = new Intent(ACTION_ALARM); 239 i.setPackage(getPackageName()); 240 return PendingIntent.getBroadcast(this, 0, i, 241 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 242 } 243 scheduleAlarm()244 private void scheduleAlarm() { 245 long alarmTime = SystemClock.elapsedRealtime() + ALARM_REPEAT_MS; 246 logDebug(String.format("Scheduling alarm at %d", alarmTime)); 247 AlarmManager alarmManager = getSystemService(AlarmManager.class); 248 if (alarmManager.canScheduleExactAlarms()) { 249 alarmManager.setExactAndAllowWhileIdle( 250 AlarmManager.ELAPSED_REALTIME_WAKEUP, 251 alarmTime, 252 getAlarmIntent() 253 ); 254 } else { 255 Log.w(TAG, "Cannot schedule exact alarms"); 256 } 257 } 258 cancelAlarm()259 private void cancelAlarm() { 260 logDebug("Cancel alarm "); 261 AlarmManager alarmManager = getSystemService(AlarmManager.class); 262 alarmManager.cancel(getAlarmIntent()); 263 } 264 265 startBenchmark()266 private void startBenchmark() { 267 mHandler.post(mMainRunnable); 268 scheduleAlarm(); 269 registerReceiver(mAlarmReceiver, new IntentFilter(ACTION_ALARM)); 270 mScheduledExecutor = Executors.newScheduledThreadPool(1); 271 mScheduledFuture = mScheduledExecutor.scheduleAtFixedRate(() -> { 272 try { 273 logDebug("Work"); 274 long now = SystemClock.elapsedRealtime(); 275 mHandler.post(() -> { 276 if (mWakeLock.isHeld()) { 277 mCurrentBenchmark.addEvent(Benchmark.Measure.WORK, now); 278 saveBenchmarkIfRequired(mCurrentBenchmark); 279 } else { 280 Log.w(TAG, "Wake lock broken"); 281 } 282 }); 283 } catch (Throwable t) { 284 Log.e(TAG, "Error in scheduled execution ", t); 285 } 286 }, WORK_REPEAT_MS, WORK_REPEAT_MS, TimeUnit.MILLISECONDS); 287 288 mStarted = true; 289 } 290 stopBenchmark()291 private void stopBenchmark() { 292 try { 293 unregisterReceiver(mAlarmReceiver); 294 } catch (Exception e) { 295 Log.w(TAG, "Receiver not registered", e); 296 } 297 cancelAlarm(); 298 mHandler.removeCallbacks(mMainRunnable); 299 if (mScheduledExecutor != null) { 300 mScheduledExecutor.shutdown(); 301 if (mScheduledFuture.isDone()) { 302 try { 303 mScheduledFuture.get(); 304 } catch (CancellationException e) { 305 } catch (Exception e) { 306 Log.e(TAG, "Error in scheduled execution ", e); 307 } 308 } 309 } 310 } 311 312 @Override onDestroy()313 public void onDestroy() { 314 super.onDestroy(); 315 logDebug("onDestroy()"); 316 if (mStarted) { 317 stopBenchmark(); 318 } 319 mExecutor.shutdown(); 320 mWakeLock.release(); 321 } 322 getServiceIntent(Context context)323 private static Intent getServiceIntent(Context context) { 324 return new Intent(context, ServiceKillTestService.class); 325 } 326 loadBenchmarkAsync(Consumer<Benchmark> consumer)327 private void loadBenchmarkAsync(Consumer<Benchmark> consumer) { 328 mExecutor.execute(() -> { 329 final Benchmark benchmark = loadBenchmark(); 330 mHandler.post(() -> { 331 consumer.accept(benchmark); 332 }); 333 }); 334 } 335 336 public interface Consumer<T> { accept(T consumable)337 void accept(T consumable); 338 } 339 loadBenchmark()340 private synchronized Benchmark loadBenchmark() { 341 ObjectInputStream in = null; 342 try { 343 in = new ObjectInputStream(openFileInput(TAG)); 344 return (Benchmark) in.readObject(); 345 } catch (FileNotFoundException e) { 346 logDebug("File not found"); 347 } catch (ClassNotFoundException e) { 348 Log.e(TAG, "Class no found", e); 349 } catch (IOException e) { 350 Log.e(TAG, "I/O error", e); 351 } finally { 352 try { 353 if (in != null) { 354 in.close(); 355 } 356 } catch (IOException e) { 357 Log.e(TAG, "Cannot close benchmark file", e); 358 } 359 } 360 return null; 361 } 362 363 clearBenchmark()364 private synchronized void clearBenchmark() { 365 deleteFile(TAG); 366 } 367 saveBenchmarkIfRequired(Benchmark benchmark)368 private void saveBenchmarkIfRequired(Benchmark benchmark) { 369 if (SystemClock.elapsedRealtime() - benchmark.lastPersisted > PERSIST_BENCHMARK_TIMEOUT_MS) { 370 saveBenchmarkAsync(benchmark, null); 371 } 372 } 373 374 saveBenchmarkAsync(Benchmark benchmark, Runnable runnable)375 private void saveBenchmarkAsync(Benchmark benchmark, Runnable runnable) { 376 final byte[] bytes = benchmark.toBytes(); 377 378 mExecutor.execute(() -> { 379 save(bytes); 380 mHandler.post(() -> { 381 logDebug("SAVED " + benchmark); 382 benchmark.setPersisted(); 383 if (runnable != null) { 384 runnable.run(); 385 } 386 }); 387 }); 388 } 389 save(byte[] bytes)390 private synchronized void save(byte[] bytes) { 391 FileOutputStream fileOut = null; 392 try { 393 fileOut = openFileOutput(TAG, MODE_PRIVATE); 394 fileOut.write(bytes); 395 fileOut.flush(); 396 } catch (FileNotFoundException e) { 397 Log.e(TAG, "File not found", e); 398 } catch (IOException e) { 399 Log.e(TAG, "I/O error", e); 400 } finally { 401 try { 402 if (fileOut != null) { 403 fileOut.close(); 404 } 405 } catch (IOException e) { 406 Log.e(TAG, "Cannot close benchmark file", e); 407 } 408 } 409 } 410 411 private static class Range { 412 public final long from; 413 public final long to; 414 Range(long from, long to)415 public Range(long from, long to) { 416 if (to < from || to < 0 || from < 0) { 417 throw new IllegalArgumentException("FROM: " + from + " before TO: " + to); 418 } 419 this.from = from; 420 this.to = to; 421 } 422 inRange(long timestamp)423 public boolean inRange(long timestamp) { 424 return timestamp >= from && timestamp <= to; 425 } 426 getDuration()427 public long getDuration() { 428 return to - from + 1; 429 } 430 431 @Override toString()432 public String toString() { 433 return String.format("[%d-%d]", from, to); 434 } 435 } 436 437 public static class Benchmark implements Serializable { 438 439 public enum Measure { 440 TOTAL, 441 WORK(WORK_REPEAT_MS), 442 MAIN(MAIN_REPEAT_MS), 443 ALARM(ALARM_REPEAT_MS + ALARM_REPEAT_MARGIN_MS); 444 445 private final long interval; 446 Measure()447 Measure() { 448 interval = -1; 449 } 450 Measure(long interval)451 Measure(long interval) { 452 this.interval = interval; 453 } 454 } 455 456 private static final long serialVersionUID = -2939643983335136263L; 457 458 private long lastPersisted = -1; 459 460 private long startTime; 461 Benchmark()462 public Benchmark() { 463 startTime = SystemClock.elapsedRealtime(); 464 } 465 466 private final Map<Measure, List<Long>> eventMap = new HashMap<>(); 467 private final Map<String, Long> tests = new HashMap<>(); 468 isRunning()469 public boolean isRunning() { 470 return tests.size() > 0; 471 } 472 startTest(String id)473 public void startTest(String id) { 474 if (!tests.containsKey(id)) { 475 tests.put(id, SystemClock.elapsedRealtime()); 476 } 477 } 478 finishTest(String id)479 public Map<Measure, Float> finishTest(String id) { 480 if (tests.containsKey(id)) { 481 Long startTime = tests.remove(id); 482 return getAllResults(new Range(startTime, SystemClock.elapsedRealtime())); 483 } 484 Log.w(TAG, "Missing results for test " + id); 485 return null; 486 } 487 getAllResults(String id)488 public Map<Measure, Float> getAllResults(String id) { 489 if (tests.containsKey(id)) { 490 Long startTime = tests.get(id); 491 return getAllResults(new Range(startTime, SystemClock.elapsedRealtime())); 492 } 493 return null; 494 } 495 getAllResults(Range range)496 private Map<Measure, Float> getAllResults(Range range) { 497 Map<Measure, Float> results = new HashMap<>(); 498 for (Measure measure : Measure.values()) { 499 results.put(measure, getResult(measure, range)); 500 } 501 return results; 502 } 503 getLastPersisted()504 public long getLastPersisted() { 505 return lastPersisted; 506 } 507 setPersisted()508 public void setPersisted() { 509 this.lastPersisted = SystemClock.elapsedRealtime(); 510 } 511 filter(List<Long> source, Range range)512 private List<Long> filter(List<Long> source, Range range) { 513 List<Long> result = new ArrayList<>(source); 514 515 if (range == null) { 516 return source; 517 } 518 519 Iterator<Long> i = result.iterator(); 520 while (i.hasNext()) { 521 if (!range.inRange(i.next())) { 522 i.remove(); 523 } 524 } 525 return result; 526 } 527 addEvent(Measure measure, long timestamp)528 public void addEvent(Measure measure, long timestamp) { 529 List<Long> events = getEvents(measure); 530 events.add(timestamp); 531 if (!eventMap.containsKey(measure)) { 532 eventMap.put(measure, events); 533 } 534 } 535 addEvent(Measure measure)536 public void addEvent(Measure measure) { 537 addEvent(measure, SystemClock.elapsedRealtime()); 538 } 539 getEvents(Measure measure)540 private List<Long> getEvents(Measure measure) { 541 List<Long> events = eventMap.get(measure); 542 return events == null ? new ArrayList<>() : events; 543 } 544 getResult(Measure measure)545 public float getResult(Measure measure) { 546 return getResult(measure, null); 547 } 548 getResult(Measure measure, Range range)549 public float getResult(Measure measure, Range range) { 550 551 if (measure == Measure.TOTAL) { 552 return (getResult(Measure.WORK, range) + (2 * getResult(Measure.ALARM, range)) + 553 getResult(Measure.MAIN, range)) / 4f; 554 } 555 556 List<Long> events = filter(getEvents(measure), range); 557 558 return Math 559 .min(1, events.size() / (getDuration(range) / (float) measure.interval)); 560 } 561 getDuration()562 private long getDuration() { 563 return SystemClock.elapsedRealtime() - startTime; 564 } 565 getDuration(Range range)566 private long getDuration(Range range) { 567 if (range == null) { 568 return getDuration(); 569 } 570 return range.getDuration(); 571 } 572 toBytes()573 private byte[] toBytes() { 574 try { 575 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 576 ObjectOutputStream out = new ObjectOutputStream(bos); 577 out.writeObject(this); 578 out.flush(); 579 out.close(); 580 bos.close(); 581 return bos.toByteArray(); 582 } catch (IOException e) { 583 Log.e(TAG, "Cannot serialize benchmark: " + this, e); 584 return null; 585 } 586 } 587 588 @SuppressLint("DefaultLocale") 589 @Override toString()590 public String toString() { 591 return toReportString().replaceAll("\n", ""); 592 } 593 594 @SuppressLint("DefaultLocale") toReportString()595 public String toReportString() { 596 return String 597 .format("Benchmark TIME: %tT TESTS: %d \n\nMAIN:\n%.1f%% %d \n\nWORK:\n%.1f%%" + 598 " %d \n\nALARM:\n%.1f%% %d \n\n%s", 599 getDuration() - TimeZone.getDefault().getOffset(0), 600 tests.size(), getResult(Measure.MAIN) * 100, 601 getEvents(Measure.MAIN).size(), getResult(Measure.WORK) * 100, 602 getEvents(Measure.WORK).size(), getResult(Measure.ALARM) * 100, 603 getEvents(Measure.ALARM).size(), isRunning() ? "RUNNING..." : 604 getResult(Measure.TOTAL) >= 0.9f ? "TEST PASSED!" : 605 "TEST FAILED!"); 606 } 607 } 608 logDebug(String s)609 public static void logDebug(String s) { 610 if (DEBUG) { 611 Log.d(TAG, s); 612 } 613 } 614 }