This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / hermes-voice / android / src / us / lassul / assist / CallService.java
7.7 kB 234 lines
1package us.lassul.assist; 2 3import android.app.Notification; 4import android.app.NotificationChannel; 5import android.app.NotificationManager; 6import android.app.Service; 7import android.content.ComponentName; 8import android.content.Intent; 9import android.content.pm.ServiceInfo; 10import android.net.Uri; 11import android.os.Bundle; 12import android.os.Handler; 13import android.os.IBinder; 14import android.os.Looper; 15import android.os.OutcomeReceiver; 16import android.telecom.CallAttributes; 17import android.telecom.CallControl; 18import android.telecom.CallControlCallback; 19import android.telecom.CallEndpoint; 20import android.telecom.CallEventCallback; 21import android.telecom.CallException; 22import android.telecom.DisconnectCause; 23import android.telecom.PhoneAccount; 24import android.telecom.PhoneAccountHandle; 25import android.telecom.TelecomManager; 26import android.util.Log; 27import java.util.List; 28import java.util.function.Consumer; 29 30/** 31 * Holds each call of the page as a self-managed call in Telecom, so a Bluetooth headset sees a 32 * call and its hang-up (AT+CHUP) reaches this app, which tells the page. In the foreground, as 33 * otherwise Android freezes the process once the page is in front. Stops when idle: a call the 34 * page starts after that is not held (the app cannot start this service from the background). 35 */ 36public class CallService extends Service { 37 private static final String CHANNEL = "call"; 38 private static final long IDLE_MS = 2 * 60 * 1000; 39 40 private static CallService running; 41 42 private final Handler handler = new Handler(Looper.getMainLooper()); 43 private final Runnable idleStop = new Runnable() { 44 @Override 45 public void run() { 46 stopSelf(); 47 } 48 }; 49 private PhoneAccountHandle account; 50 private CallControl call; 51 // The page's call runs; false while adding means: disconnect once added. 52 private boolean wanted; 53 54 static void pageStarted() { 55 if (running != null) { 56 running.startCall(); 57 } 58 } 59 60 static void pageEnded() { 61 if (running != null) { 62 running.endCall(); 63 } 64 } 65 66 @Override 67 public void onCreate() { 68 super.onCreate(); 69 running = this; 70 NotificationManager notifications = getSystemService(NotificationManager.class); 71 notifications.createNotificationChannel( 72 new NotificationChannel(CHANNEL, "Voice call", NotificationManager.IMPORTANCE_LOW)); 73 Notification notification = new Notification.Builder(this, CHANNEL) 74 .setSmallIcon(android.R.drawable.sym_action_call) 75 .setContentTitle("Assist") 76 .setContentText("The headset button hangs up") 77 .setOngoing(true) 78 .build(); 79 startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL); 80 account = new PhoneAccountHandle(new ComponentName(this, CallService.class), "assist"); 81 getSystemService(TelecomManager.class).registerPhoneAccount( 82 PhoneAccount.builder(account, "Assist") 83 .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED 84 | PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS) 85 .setSupportedUriSchemes(List.of(PhoneAccount.SCHEME_SIP)) 86 .build()); 87 } 88 89 @Override 90 public int onStartCommand(Intent intent, int flags, int startId) { 91 if (call == null && !wanted) { 92 idle(); 93 } 94 return START_NOT_STICKY; 95 } 96 97 @Override 98 public void onDestroy() { 99 handler.removeCallbacksAndMessages(null); 100 wanted = false; 101 disconnect(); 102 Link.close(); 103 running = null; 104 super.onDestroy(); 105 } 106 107 @Override 108 public IBinder onBind(Intent intent) { 109 return null; 110 } 111 112 private void idle() { 113 handler.removeCallbacks(idleStop); 114 handler.postDelayed(idleStop, IDLE_MS); 115 } 116 117 private void startCall() { 118 handler.removeCallbacks(idleStop); 119 if (wanted) { 120 return; 121 } 122 wanted = true; 123 if (call != null) { 124 return; 125 } 126 CallAttributes attributes = new CallAttributes.Builder( 127 account, 128 CallAttributes.DIRECTION_OUTGOING, 129 "Hermes", 130 Uri.fromParts(PhoneAccount.SCHEME_SIP, "hermes@assist.lassul.us", null)) 131 .setCallType(CallAttributes.AUDIO_CALL) 132 .build(); 133 getSystemService(TelecomManager.class).addCall( 134 attributes, 135 getMainExecutor(), 136 new OutcomeReceiver<CallControl, CallException>() { 137 @Override 138 public void onResult(CallControl control) { 139 Log.i(Link.TAG, "telecom call added"); 140 call = control; 141 if (wanted) { 142 control.setActive(getMainExecutor(), logged("setActive")); 143 } else { 144 disconnect(); 145 } 146 } 147 148 @Override 149 public void onError(CallException error) { 150 Log.w(Link.TAG, "telecom call refused", error); 151 wanted = false; 152 idle(); 153 } 154 }, 155 new Control(), 156 new Events()); 157 } 158 159 private void endCall() { 160 wanted = false; 161 disconnect(); 162 idle(); 163 } 164 165 private void disconnect() { 166 if (call != null) { 167 call.disconnect( 168 new DisconnectCause(DisconnectCause.LOCAL), getMainExecutor(), logged("disconnect")); 169 call = null; 170 } 171 } 172 173 private static OutcomeReceiver<Void, CallException> logged(String what) { 174 return new OutcomeReceiver<Void, CallException>() { 175 @Override 176 public void onResult(Void result) {} 177 178 @Override 179 public void onError(CallException error) { 180 Log.w(Link.TAG, what + " failed", error); 181 } 182 }; 183 } 184 185 private final class Control implements CallControlCallback { 186 @Override 187 public void onSetActive(Consumer<Boolean> done) { 188 done.accept(true); 189 } 190 191 @Override 192 public void onSetInactive(Consumer<Boolean> done) { 193 done.accept(true); 194 } 195 196 @Override 197 public void onAnswer(int videoState, Consumer<Boolean> done) { 198 done.accept(true); 199 } 200 201 // Telecom ended the call: the headset hung up (AT+CHUP) or a call took over. 202 @Override 203 public void onDisconnect(DisconnectCause cause, Consumer<Boolean> done) { 204 Log.i(Link.TAG, "telecom disconnect: " + cause); 205 done.accept(true); 206 call = null; 207 wanted = false; 208 Link.hangUp(); 209 idle(); 210 } 211 212 @Override 213 public void onCallStreamingStarted(Consumer<Boolean> done) { 214 done.accept(false); 215 } 216 } 217 218 private static final class Events implements CallEventCallback { 219 @Override 220 public void onCallEndpointChanged(CallEndpoint endpoint) {} 221 222 @Override 223 public void onAvailableCallEndpointsChanged(List<CallEndpoint> endpoints) {} 224 225 @Override 226 public void onMuteStateChanged(boolean muted) {} 227 228 @Override 229 public void onCallStreamingFailed(int reason) {} 230 231 @Override 232 public void onEvent(String event, Bundle extras) {} 233 } 234}