From 99183fe7d4a5d4329f50744f04faa70feec9088d Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Mon, 14 Dec 2020 22:47:44 -0800 Subject: Add service to keep countdown when ui is backgrounded This change introduced a few new components to make countdown work in the background. - EmergencyActionForegroundService: posts a notification with remaining seconds. * It also starts/stops sound associated with the notification - EmergencyActionBroadcastReceiver: handles 2 action * calling emergency number and * cancelling the count down notification When UI is closed, it start a foreground service to take over the count down. Then the service sends an notification immediately, and schedules a broadcast when countdown reaches 0. This broadcast will trigger emergency calling. The notification has a cancel button, which also is a broadcast to cancel ongoing foreground service. When foreground service stops, the vibration, sound and notification all stops with it. Bug: 172075832 Test: manual Change-Id: I04f33848b9532534f1551f20320683331207aa7e --- .../emergency/action/EmergencyActionFragment.java | 9 +- .../EmergencyActionBroadcastReceiver.java | 95 +++++++++++ .../service/EmergencyActionForegroundService.java | 184 +++++++++++++++++++++ 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/com/android/emergency/action/broadcast/EmergencyActionBroadcastReceiver.java create mode 100644 src/com/android/emergency/action/service/EmergencyActionForegroundService.java (limited to 'src') diff --git a/src/com/android/emergency/action/EmergencyActionFragment.java b/src/com/android/emergency/action/EmergencyActionFragment.java index f0b36d4e..e8ea31d8 100644 --- a/src/com/android/emergency/action/EmergencyActionFragment.java +++ b/src/com/android/emergency/action/EmergencyActionFragment.java @@ -42,6 +42,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.emergency.R; +import com.android.emergency.action.service.EmergencyActionForegroundService; import com.android.emergency.widgets.countdown.CountDownAnimationView; import com.android.emergency.widgets.slider.OnSlideCompleteListener; import com.android.emergency.widgets.slider.SliderView; @@ -68,6 +69,7 @@ public class EmergencyActionFragment extends Fragment implements OnSlideComplete @Override public void onAttach(Context context) { super.onAttach(context); + EmergencyActionForegroundService.stopService(context); mAudioManager = context.getSystemService(AudioManager.class); mEmergencyNumberUtils = new EmergencyNumberUtils(context); mTelecomManager = context.getSystemService(TelecomManager.class); @@ -134,13 +136,18 @@ public class EmergencyActionFragment extends Fragment implements OnSlideComplete Log.d(TAG, "Emergency countdown UI dismissed without being cancelled/finished, " + "continuing countdown in background"); - // TODO(b/172075832): Continue countdown in a foreground service. + + Context context = getContext(); + context.startService( + EmergencyActionForegroundService.newStartCountdownIntent(context, + mCountDownMillisLeft)); } } @Override public void onSlideComplete() { mCountdownCancelled = true; + EmergencyActionForegroundService.stopService(getActivity()); getActivity().finish(); } diff --git a/src/com/android/emergency/action/broadcast/EmergencyActionBroadcastReceiver.java b/src/com/android/emergency/action/broadcast/EmergencyActionBroadcastReceiver.java new file mode 100644 index 00000000..6c1c1b27 --- /dev/null +++ b/src/com/android/emergency/action/broadcast/EmergencyActionBroadcastReceiver.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emergency.action.broadcast; + +import static android.telecom.TelecomManager.EXTRA_CALL_SOURCE; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.telecom.PhoneAccount; +import android.telecom.TelecomManager; +import android.util.Log; + +import com.android.emergency.action.service.EmergencyActionForegroundService; +import com.android.settingslib.emergencynumber.EmergencyNumberUtils; + +/** + * Broadcast receiver to handle actions for emergency gesture foreground service and notification + */ +public class EmergencyActionBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "EmergencyActionRcvr"; + + + private static final String ACTION_START_EMERGENCY_CALL = + "com.android.emergency.broadcast.MAKE_EMERGENCY_CALL"; + private static final String ACTION_CANCEL_COUNTDOWN = + "com.android.emergency.broadcast.CANCEL_EMERGENCY_COUNTDOWN"; + + public static PendingIntent newCallEmergencyPendingIntent(Context context) { + return PendingIntent.getBroadcast(context, 0, + new Intent(ACTION_START_EMERGENCY_CALL).setClass(context, + EmergencyActionBroadcastReceiver.class), + PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static PendingIntent newCancelCountdownPendingIntent(Context context) { + return PendingIntent.getBroadcast(context, 0, + new Intent(ACTION_CANCEL_COUNTDOWN).setClass(context, + EmergencyActionBroadcastReceiver.class), + PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public void onReceive(Context context, Intent intent) { + AlarmManager alarmManager = context.getSystemService(AlarmManager.class); + String action = intent.getAction(); + switch (action) { + case ACTION_START_EMERGENCY_CALL: + Log.i(TAG, "Starting to call emergency number"); + placeEmergencyCall(context); + // Intentionally fall through to cancel service + case ACTION_CANCEL_COUNTDOWN: + Log.i(TAG, "Cancelling scheduled emergency calls and foreground service"); + alarmManager.cancel(newCallEmergencyPendingIntent(context)); + EmergencyActionForegroundService.stopService(context); + break; + default: + Log.w(TAG, "Unknown action received, skipping: " + action); + } + } + + private void placeEmergencyCall(Context context) { + if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + Log.i(TAG, "Telephony is not supported, skipping."); + return; + } + Bundle extras = new Bundle(); + extras.putBoolean(TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL, true); + extras.putInt(EXTRA_CALL_SOURCE, TelecomManager.CALL_SOURCE_EMERGENCY_SHORTCUT); + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + EmergencyNumberUtils emergencyNumberUtils = new EmergencyNumberUtils(context); + telecomManager.placeCall( + Uri.fromParts(PhoneAccount.SCHEME_TEL, emergencyNumberUtils.getPoliceNumber(), + /* fragment= */ null), extras); + } +} diff --git a/src/com/android/emergency/action/service/EmergencyActionForegroundService.java b/src/com/android/emergency/action/service/EmergencyActionForegroundService.java new file mode 100644 index 00000000..dac8550e --- /dev/null +++ b/src/com/android/emergency/action/service/EmergencyActionForegroundService.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emergency.action.service; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.os.SystemClock; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.telecom.TelecomManager; +import android.util.Log; +import android.widget.RemoteViews; + +import com.android.emergency.R; +import com.android.emergency.action.broadcast.EmergencyActionBroadcastReceiver; +import com.android.settingslib.emergencynumber.EmergencyNumberUtils; + +/** + * A service that counts down for emergency gesture. + */ +public class EmergencyActionForegroundService extends Service { + private static final String TAG = "EmergencyActionSvc"; + /** The notification that current service should be started with. */ + private static final String SERVICE_EXTRA_NOTIFICATION = "service.extra.notification"; + /** The remaining time in milliseconds before taking emergency action */ + private static final String SERVICE_EXTRA_REMAINING_TIME_MS = "service.extra.remaining_time_ms"; + /** Random unique number for the notification */ + private static final int COUNT_DOWN_NOTIFICATION_ID = 0x112; + + private TelecomManager mTelecomManager; + private Vibrator mVibrator; + private EmergencyNumberUtils mEmergencyNumberUtils; + private NotificationManager mNotificationManager; + + + @Override + public void onCreate() { + super.onCreate(); + PackageManager pm = getPackageManager(); + mVibrator = getSystemService(Vibrator.class); + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + mTelecomManager = getSystemService(TelecomManager.class); + mEmergencyNumberUtils = new EmergencyNumberUtils(this); + } + mNotificationManager = getSystemService(NotificationManager.class); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "Service started"); + if (mTelecomManager == null || mEmergencyNumberUtils == null) { + Log.d(TAG, "Device does not have telephony support, nothing to do"); + stopSelf(); + return START_NOT_STICKY; + } + long remainingTimeMs = intent.getLongExtra(SERVICE_EXTRA_REMAINING_TIME_MS, -1); + if (remainingTimeMs <= 0) { + Log.d(TAG, "Invalid remaining countdown time, nothing to do"); + stopSelf(); + return START_NOT_STICKY; + } + mNotificationManager.createNotificationChannel(buildNotificationChannel(this)); + Notification notification = intent.getParcelableExtra(SERVICE_EXTRA_NOTIFICATION); + + // Immediately show notification And now put the service in foreground mode + startForeground(COUNT_DOWN_NOTIFICATION_ID, notification); + scheduleEmergencyCallBroadcast(remainingTimeMs); + // vibration + // TODO(b/175401642): Use correct vibrate pattern + mVibrator.vibrate( + VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK)); + // TODO(b/172075832): sound + + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + // Take notification down + mNotificationManager.cancel(COUNT_DOWN_NOTIFICATION_ID); + // TODO(b/172075832): Stop sound + // Stop vibrate + mVibrator.cancel(); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Build {@link Intent} that launches foreground service for emergency gesture's countdown + * action + */ + public static Intent newStartCountdownIntent( + Context context, long remainingTimeMs) { + return new Intent(context, EmergencyActionForegroundService.class) + .putExtra(SERVICE_EXTRA_REMAINING_TIME_MS, remainingTimeMs) + .putExtra(SERVICE_EXTRA_NOTIFICATION, + buildCountDownNotification(context, remainingTimeMs)); + } + + /** End all work in this service and remove the foreground notification. */ + public static void stopService(Context context) { + context.stopService(new Intent(context, EmergencyActionForegroundService.class)); + } + + /** + * Creates a {@link NotificationChannel} object for emergency action notifications. + * + *

Note this does not create notification channel in the system. + */ + private static NotificationChannel buildNotificationChannel(Context context) { + NotificationChannel channel = new NotificationChannel("EmergencyGesture", + context.getString(R.string.emergency_action_title), IMPORTANCE_HIGH); + return channel; + } + + private static Notification buildCountDownNotification(Context context, long remainingTimeMs) { + NotificationChannel channel = buildNotificationChannel(context); + EmergencyNumberUtils emergencyNumberUtils = new EmergencyNumberUtils(context); + long targetTimeMs = SystemClock.elapsedRealtime() + remainingTimeMs; + // TODO(b/172075832): Make UI prettier + RemoteViews contentView = + new RemoteViews(context.getPackageName(), + R.layout.emergency_action_count_down_notification); + contentView.setTextViewText(R.id.notification_text, + context.getString(R.string.emergency_action_subtitle, + emergencyNumberUtils.getPoliceNumber())); + contentView.setChronometerCountDown(R.id.chronometer, true); + contentView.setChronometer( + R.id.chronometer, + targetTimeMs, + /* format= */ null, + /* started= */ true); + return new Notification.Builder(context, channel.getId()) + .setSmallIcon(R.drawable.ic_launcher_settings) + .setStyle(new Notification.DecoratedCustomViewStyle()) + .setAutoCancel(false) + .setOngoing(true) + // This is set to make sure that device doesn't vibrate twice when client + // attempts to post currently displayed notification again + .setOnlyAlertOnce(true) + .setCategory(Notification.CATEGORY_ALARM) + .setCustomContentView(contentView) + .addAction(new Notification.Action.Builder(null, context.getText(R.string.cancel), + EmergencyActionBroadcastReceiver.newCancelCountdownPendingIntent( + context)).build()) + .build(); + } + + private void scheduleEmergencyCallBroadcast(long remainingTimeMs) { + long alarmTimeMs = System.currentTimeMillis() + remainingTimeMs; + AlarmManager alarmManager = getSystemService(AlarmManager.class); + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, alarmTimeMs, + EmergencyActionBroadcastReceiver.newCallEmergencyPendingIntent(this)); + } + +} -- cgit v1.2.3