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 package com.android.car.voicecontrol; 17 18 import android.Manifest; 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.car.Car; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.os.IBinder; 29 import android.os.RemoteCallbackList; 30 import android.os.RemoteException; 31 import android.service.voice.VoiceInteractionService; 32 import android.util.Log; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 import androidx.core.app.NotificationCompat; 37 38 import com.android.car.assist.CarVoiceInteractionSession; 39 import com.android.car.telephony.common.Contact; 40 41 import java.util.Arrays; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Set; 45 46 /** 47 * Voice interaction entry point. This service is kept running for as long as this application 48 * is set as the default voice interaction service. 49 */ 50 public class InteractionService extends VoiceInteractionService { 51 private static final String TAG = "Mica.InteractionService"; 52 private static final String CHANNEL_ID = "Mica"; 53 private static final int NOTIFICATION_ID = 1; 54 private NotificationManager mNotificationManager; 55 public static final String LOCAL_SERVICE_ACTION = "com.android.car.voicecontrol.local"; 56 public static final String LOCAL_SERVICE_CMD_SETUP_CHANGED = "setup-changed"; 57 private static final List<String> SUPPORTED_VOICE_ACTIONS = Arrays.asList( 58 CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION, 59 CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION, 60 CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION, 61 CarVoiceInteractionSession.VOICE_ACTION_SEND_SMS 62 ); 63 public static final List<String> REQUIRED_PERMISSIONS = Arrays.asList( 64 Manifest.permission.RECORD_AUDIO, 65 Car.PERMISSION_SPEED, 66 Manifest.permission.CALL_PHONE, 67 Manifest.permission.READ_PHONE_STATE, 68 Manifest.permission.SEND_SMS, 69 Manifest.permission.READ_CONTACTS 70 ); 71 private ContactsProvider mContactsProvider; 72 73 private final ILocalService.Stub mBinder = new ILocalService.Stub() { 74 @Override 75 public boolean isSetupComplete() { 76 return InteractionService.this.isSetupComplete(); 77 } 78 79 @Override 80 public void registerListener(ILocalServiceListener listener) { 81 mListeners.register(listener); 82 } 83 84 @Override 85 public void unregisterListener(ILocalServiceListener listener) { 86 mListeners.unregister(listener); 87 } 88 89 @Override 90 public String getVoice() { 91 return PreferencesController.getInstance(InteractionService.this).getVoice(); 92 } 93 94 @Override 95 public void setVoice(String name) { 96 PreferencesController.getInstance(InteractionService.this).setVoice(name); 97 } 98 99 @Override 100 public String getUsername() { 101 String value = PreferencesController.getInstance(InteractionService.this).getUsername(); 102 Log.d(TAG, "getUsername(): " + value); 103 return value; 104 } 105 106 @Override 107 public void setUsername(String value) { 108 Log.d(TAG, "setUsername: " + value); 109 PreferencesController.getInstance(InteractionService.this).setUsername(value); 110 onSetupChanged(); 111 } 112 113 @Override 114 public boolean hasAllPermissions() { 115 return InteractionService.hasAllPermissions(InteractionService.this); 116 } 117 118 @Override 119 public boolean isNotificationListener() { 120 return InteractionService.this.isNotificationListener(); 121 } 122 123 @Override 124 public Contact getContact(String query, @Nullable String deviceAddress) { 125 return mContactsProvider.getContact(query, deviceAddress); 126 } 127 }; 128 private final RemoteCallbackList<ILocalServiceListener> mListeners = new RemoteCallbackList<>(); 129 130 @Override onBind(Intent intent)131 public IBinder onBind(Intent intent) { 132 Log.d(TAG, "onBind: " + intent); 133 if (LOCAL_SERVICE_ACTION.equals(intent.getAction())) { 134 return mBinder; 135 } 136 return super.onBind(intent); 137 } 138 139 @Override onStartCommand(Intent intent, int flags, int startId)140 public int onStartCommand(Intent intent, int flags, int startId) { 141 switch (intent.getAction()) { 142 case LOCAL_SERVICE_CMD_SETUP_CHANGED: 143 onSetupChanged(); 144 return START_NOT_STICKY; 145 default: 146 return super.onStartCommand(intent, flags, startId); 147 } 148 } 149 150 @Override onCreate()151 public void onCreate() { 152 super.onCreate(); 153 mContactsProvider = new ContactsProvider(this); 154 } 155 156 @Override onDestroy()157 public void onDestroy() { 158 mContactsProvider.destroy(); 159 super.onDestroy(); 160 } 161 162 @Override onReady()163 public void onReady() { 164 Log.e(TAG, "onReady()"); 165 super.onReady(); 166 updateNotification(); 167 168 // Setup always-on hot-word detector here (see: VoiceInteractionService#onReady()) for 169 // details. 170 } 171 isSetupComplete()172 private boolean isSetupComplete() { 173 boolean setupComplete = PreferencesController.getInstance(this).isUserSignedIn() 174 && hasAllPermissions(this) && isNotificationListener(); 175 Log.d(TAG, "isSetupComplete: " + setupComplete); 176 return setupComplete; 177 } 178 179 /** 180 * @return true if this application has all required dangerous permissions (permissions the 181 * user is able to grant). 182 */ hasAllPermissions(Context context)183 public static boolean hasAllPermissions(Context context) { 184 for (String permission : REQUIRED_PERMISSIONS) { 185 if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { 186 Log.d(TAG, "Missing permission: " + permission); 187 return false; 188 } 189 } 190 return true; 191 } 192 isNotificationListener()193 private boolean isNotificationListener() { 194 ComponentName component = new ComponentName(this, 195 VoiceNotificationListenerService.class); 196 NotificationManager notificationManager = getSystemService(NotificationManager.class); 197 return notificationManager.isNotificationListenerAccessGranted(component); 198 } 199 onSetupChanged()200 private void onSetupChanged() { 201 Log.d(TAG, "onSetupChanged"); 202 int items = mListeners.beginBroadcast(); 203 for (int i = 0; i < items; i++) { 204 try { 205 mListeners.getBroadcastItem(i).setupChanged(); 206 } catch (RemoteException e) { 207 Log.e(TAG, "Unable to notify broadcast item " 208 + mListeners.getBroadcastItem(i), e); 209 } 210 } 211 mListeners.finishBroadcast(); 212 updateNotification(); 213 } 214 updateNotification()215 private void updateNotification() { 216 if (mNotificationManager == null) { 217 mNotificationManager = getSystemService(NotificationManager.class); 218 NotificationChannel channel = new NotificationChannel(CHANNEL_ID, 219 getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT); 220 mNotificationManager.createNotificationChannel(channel); 221 } 222 if (isSetupComplete()) { 223 Log.d(TAG, "updateNotification(): is setup complete = true"); 224 mNotificationManager.cancel(NOTIFICATION_ID); 225 return; 226 } 227 Log.d(TAG, "updateNotification(): is setup complete = false"); 228 Intent intent = new Intent(this, SignInActivity.class); 229 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 230 PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, 231 intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 232 233 Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) 234 .setSmallIcon(R.drawable.ic_launcher) 235 .setColor(getColor(R.color.car_ui_color_accent)) 236 .setContentTitle(getString(R.string.notification_setup_title)) 237 .setContentText(getString(R.string.notification_setup_text)) 238 .setContentIntent(pendingIntent) 239 .build(); 240 241 mNotificationManager.notify(NOTIFICATION_ID, notification); 242 } 243 244 @NonNull 245 @Override onGetSupportedVoiceActions(@onNull Set<String> voiceActions)246 public Set<String> onGetSupportedVoiceActions(@NonNull Set<String> voiceActions) { 247 Set<String> result = new HashSet<>(voiceActions); 248 result.retainAll(SUPPORTED_VOICE_ACTIONS); 249 return result; 250 } 251 252 } 253