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