1 /* 2 * Copyright (C) 2019 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.car.companiondevicesupport.feature.notificationmsg; 18 19 import static com.android.car.connecteddevice.util.SafeLog.loge; 20 import static com.android.car.connecteddevice.util.SafeLog.logw; 21 import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_DISMISS_NOTIFICATION; 22 import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_MARK_AS_READ; 23 import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_REPLY; 24 import static com.android.car.messenger.common.BaseNotificationDelegate.EXTRA_CONVERSATION_KEY; 25 import static com.android.car.messenger.common.BaseNotificationDelegate.EXTRA_REMOTE_INPUT_KEY; 26 27 import android.annotation.Nullable; 28 import android.app.Notification; 29 import android.app.NotificationChannel; 30 import android.app.NotificationManager; 31 import android.app.Service; 32 import android.content.Intent; 33 import android.os.Binder; 34 import android.os.Bundle; 35 import android.os.IBinder; 36 37 import androidx.core.app.NotificationCompat; 38 import androidx.core.app.RemoteInput; 39 40 import com.android.car.companiondevicesupport.R; 41 import com.android.car.companiondevicesupport.api.external.CompanionDevice; 42 import com.android.car.messenger.NotificationMsgProto.NotificationMsg; 43 import com.android.car.messenger.common.ConversationKey; 44 45 /** 46 * Service responsible for handling {@link NotificationMsg} messaging events from the active user's 47 * securely paired {@link CompanionDevice}s. 48 */ 49 public class NotificationMsgService extends Service { 50 private final static String TAG = "NotificationMsgService"; 51 52 /* NOTIFICATIONS */ 53 static final String NOTIFICATION_MSG_CHANNEL_ID = "NOTIFICATION_MSG_CHANNEL_ID"; 54 private static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID"; 55 private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE; 56 57 private NotificationMsgDelegate mNotificationMsgDelegate; 58 private NotificationMsgFeature mNotificationMsgFeature; 59 private final IBinder binder = new LocalBinder(); 60 private NotificationManager mNotificationManager; 61 62 public class LocalBinder extends Binder { getService()63 NotificationMsgService getService() { 64 return NotificationMsgService.this; 65 } 66 } 67 68 @Override onBind(Intent intent)69 public IBinder onBind(Intent intent) { 70 return binder; 71 } 72 73 @Override onCreate()74 public void onCreate() { 75 super.onCreate(); 76 77 mNotificationManager = getSystemService(NotificationManager.class); 78 mNotificationMsgDelegate = new NotificationMsgDelegate(this); 79 mNotificationMsgFeature = new NotificationMsgFeature(this, mNotificationMsgDelegate); 80 mNotificationMsgFeature.start(); 81 sendServiceRunningNotification(); 82 } 83 84 @Override onDestroy()85 public void onDestroy() { 86 super.onDestroy(); 87 mNotificationMsgFeature.stop(); 88 } 89 90 @Override onStartCommand(Intent intent, int flags, int startId)91 public int onStartCommand(Intent intent, int flags, int startId) { 92 if (intent == null || intent.getAction() == null) return START_STICKY; 93 94 String action = intent.getAction(); 95 96 switch (action) { 97 case ACTION_REPLY: 98 handleReplyIntent(intent); 99 break; 100 case ACTION_DISMISS_NOTIFICATION: 101 handleDismissNotificationIntent(intent); 102 break; 103 case ACTION_MARK_AS_READ: 104 handleMarkAsReadIntent(intent); 105 break; 106 default: 107 logw(TAG, "Unsupported action: " + action); 108 } 109 110 return START_STICKY; 111 } 112 113 /** 114 * Posts a service running (silent/hidden) notification, so we don't throw ANR after service 115 * is started. 116 */ sendServiceRunningNotification()117 private void sendServiceRunningNotification() { 118 if (mNotificationManager == null) { 119 loge(TAG, "Failed to get NotificationManager instance"); 120 return; 121 } 122 123 // Create notification channel for app running notification 124 NotificationChannel appRunningNotificationChannel = 125 new NotificationChannel(APP_RUNNING_CHANNEL_ID, 126 getString(R.string.app_running_msg_channel_name), 127 NotificationManager.IMPORTANCE_MIN); 128 mNotificationManager.createNotificationChannel(appRunningNotificationChannel); 129 130 final Notification notification = 131 new NotificationCompat.Builder(this, APP_RUNNING_CHANNEL_ID) 132 .setSmallIcon(R.drawable.ic_message) 133 .setContentTitle(getString(R.string.app_running_msg_notification_title)) 134 .setContentText(getString(R.string.app_running_msg_notification_content)) 135 .build(); 136 startForeground(SERVICE_STARTED_NOTIFICATION_ID, notification); 137 } 138 139 handleDismissNotificationIntent(Intent intent)140 private void handleDismissNotificationIntent(Intent intent) { 141 ConversationKey key = getConversationKey(intent); 142 if (key == null) { 143 logw(TAG, "Dropping dismiss intent. Received null conversation key."); 144 return; 145 } 146 mNotificationMsgFeature.sendData(key.getDeviceId(), 147 mNotificationMsgDelegate.dismiss(key).toByteArray()); 148 } 149 handleMarkAsReadIntent(Intent intent)150 private void handleMarkAsReadIntent(Intent intent) { 151 ConversationKey key = getConversationKey(intent); 152 if (key == null) { 153 logw(TAG, "Dropping mark as read intent. Received null conversation key."); 154 return; 155 } 156 mNotificationMsgFeature.sendData(key.getDeviceId(), 157 mNotificationMsgDelegate.markAsRead(key).toByteArray()); 158 } 159 handleReplyIntent(Intent intent)160 private void handleReplyIntent(Intent intent) { 161 ConversationKey key = getConversationKey(intent); 162 Bundle bundle = RemoteInput.getResultsFromIntent(intent); 163 if (bundle == null || key == null) { 164 logw(TAG, "Dropping voice reply intent. Received null arguments."); 165 return; 166 } 167 CharSequence message = bundle.getCharSequence(EXTRA_REMOTE_INPUT_KEY); 168 mNotificationMsgFeature.sendData(key.getDeviceId(), 169 mNotificationMsgDelegate.reply(key, message.toString()).toByteArray()); 170 } 171 172 @Nullable getConversationKey(Intent intent)173 private ConversationKey getConversationKey(Intent intent) { 174 return intent.getParcelableExtra(EXTRA_CONVERSATION_KEY); 175 } 176 } 177