1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.M; 4 import static android.os.Build.VERSION_CODES.N_MR1; 5 import static android.os.Build.VERSION_CODES.P; 6 import static org.robolectric.shadow.api.Shadow.invokeConstructor; 7 import static org.robolectric.util.reflector.Reflector.reflector; 8 9 import android.annotation.TargetApi; 10 import android.bluetooth.BluetoothDevice; 11 import android.os.Build.VERSION; 12 import android.os.Bundle; 13 import android.os.Handler; 14 import android.telecom.Call; 15 import android.telecom.CallAudioState; 16 import android.telecom.InCallAdapter; 17 import android.telecom.InCallService; 18 import android.telecom.ParcelableCall; 19 import android.telecom.Phone; 20 import com.android.internal.os.SomeArgs; 21 import com.android.internal.telecom.IInCallAdapter; 22 import org.robolectric.annotation.Implementation; 23 import org.robolectric.annotation.Implements; 24 import org.robolectric.annotation.RealObject; 25 import org.robolectric.shadow.api.Shadow; 26 import org.robolectric.util.ReflectionHelpers; 27 import org.robolectric.util.ReflectionHelpers.ClassParameter; 28 import org.robolectric.util.reflector.Accessor; 29 import org.robolectric.util.reflector.Direct; 30 import org.robolectric.util.reflector.ForType; 31 32 /** Shadow for {@link android.telecom.InCallService}. */ 33 @Implements(value = InCallService.class, minSdk = M) 34 public class ShadowInCallService extends ShadowService { 35 @RealObject private InCallService inCallService; 36 private static final int MSG_ADD_CALL = 2; 37 private static final int MSG_SET_POST_DIAL_WAIT = 4; 38 private static final int MSG_ON_CONNECTION_EVENT = 9; 39 40 private ShadowPhone shadowPhone; 41 private boolean canAddCall; 42 private boolean muted; 43 private int audioRoute = CallAudioState.ROUTE_EARPIECE; 44 private BluetoothDevice bluetoothDevice; 45 private int supportedRouteMask; 46 47 @Implementation __constructor__()48 protected void __constructor__() { 49 InCallAdapter adapter = Shadow.newInstanceOf(InCallAdapter.class); 50 Phone phone; 51 if (VERSION.SDK_INT > N_MR1) { 52 phone = 53 ReflectionHelpers.callConstructor( 54 Phone.class, 55 ClassParameter.from(InCallAdapter.class, adapter), 56 ClassParameter.from(String.class, ""), 57 ClassParameter.from(int.class, 0)); 58 } else { 59 phone = 60 ReflectionHelpers.callConstructor( 61 Phone.class, ClassParameter.from(InCallAdapter.class, adapter)); 62 } 63 shadowPhone = Shadow.extract(phone); 64 ReflectionHelpers.setField(inCallService, "mPhone", phone); 65 invokeConstructor(InCallService.class, inCallService); 66 } 67 addCall(Call call)68 public void addCall(Call call) { 69 shadowPhone.addCall(call); 70 } 71 addCall(ParcelableCall parcelableCall)72 public void addCall(ParcelableCall parcelableCall) { 73 getHandler().obtainMessage(MSG_ADD_CALL, parcelableCall).sendToTarget(); 74 } 75 76 /** 77 * Exposes {@link IIInCallService.Stub#setPostDialWait}. This is normally invoked by Telecom but 78 * in Robolectric, Telecom doesn't exist, so tests can invoke this to simulate Telecom's actions. 79 */ setPostDialWait(String callId, String remaining)80 public void setPostDialWait(String callId, String remaining) { 81 SomeArgs args = SomeArgs.obtain(); 82 args.arg1 = callId; 83 args.arg2 = remaining; 84 getHandler().obtainMessage(MSG_SET_POST_DIAL_WAIT, args).sendToTarget(); 85 } 86 87 /** 88 * Exposes {@link IIInCallService.Stub#onConnectionEvent}. This is normally invoked by Telecom but 89 * in Robolectric, Telecom doesn't exist, so tests can invoke this to simulate Telecom's actions. 90 */ onConnectionEvent(String callId, String event, Bundle extras)91 public void onConnectionEvent(String callId, String event, Bundle extras) { 92 SomeArgs args = SomeArgs.obtain(); 93 args.arg1 = callId; 94 args.arg2 = event; 95 args.arg3 = extras; 96 getHandler().obtainMessage(MSG_ON_CONNECTION_EVENT, args).sendToTarget(); 97 } 98 removeCall(Call call)99 public void removeCall(Call call) { 100 shadowPhone.removeCall(call); 101 } 102 103 @Implementation canAddCall()104 protected boolean canAddCall() { 105 return canAddCall; 106 } 107 108 /** Set the value that {@code canAddCall()} method should return. */ setCanAddCall(boolean canAddCall)109 public void setCanAddCall(boolean canAddCall) { 110 this.canAddCall = canAddCall; 111 } 112 113 @Implementation setMuted(boolean muted)114 protected void setMuted(boolean muted) { 115 this.muted = muted; 116 if (isInCallAdapterSet()) { 117 reflector(ReflectorInCallService.class, inCallService).setMuted(muted); 118 } 119 } 120 121 @Implementation setAudioRoute(int audioRoute)122 protected void setAudioRoute(int audioRoute) { 123 this.audioRoute = audioRoute; 124 if (isInCallAdapterSet()) { 125 reflector(ReflectorInCallService.class, inCallService).setAudioRoute(audioRoute); 126 } 127 } 128 129 @Implementation getCallAudioState()130 protected CallAudioState getCallAudioState() { 131 if (isInCallAdapterSet()) { 132 return reflector(ReflectorInCallService.class, inCallService).getCallAudioState(); 133 } 134 return new CallAudioState(muted, audioRoute, supportedRouteMask); 135 } 136 setSupportedRouteMask(int mask)137 public void setSupportedRouteMask(int mask) { 138 this.supportedRouteMask = mask; 139 } 140 141 @Implementation(minSdk = P) requestBluetoothAudio(BluetoothDevice bluetoothDevice)142 protected void requestBluetoothAudio(BluetoothDevice bluetoothDevice) { 143 this.bluetoothDevice = bluetoothDevice; 144 if (isInCallAdapterSet()) { 145 reflector(ReflectorInCallService.class, inCallService).requestBluetoothAudio(bluetoothDevice); 146 } 147 } 148 149 /** @return the last value provided to {@code requestBluetoothAudio()}. */ 150 @TargetApi(P) getBluetoothAudio()151 public BluetoothDevice getBluetoothAudio() { 152 return bluetoothDevice; 153 } 154 getHandler()155 private Handler getHandler() { 156 return reflector(ReflectorInCallService.class, inCallService).getHandler(); 157 } 158 159 /** 160 * Checks if the InCallService was bound using {@link 161 * com.android.internal.telecom.IInCallService#setInCallAdapter(IInCallAdapter)}. 162 * 163 * <p>If it was bound using this interface, the internal InCallAdapter will be set and it will 164 * forward invocations to FakeTelecomServer. 165 * 166 * <p>Otherwise, invoking these methods will yield NullPointerExceptions, so we will avoid 167 * forwarding the calls to the real objects. 168 */ isInCallAdapterSet()169 private boolean isInCallAdapterSet() { 170 Phone phone = reflector(ReflectorInCallService.class, inCallService).getPhone(); 171 InCallAdapter inCallAdapter = reflector(ReflectorPhone.class, phone).getInCallAdapter(); 172 Object internalAdapter = 173 reflector(ReflectorInCallAdapter.class, inCallAdapter).getInternalInCallAdapter(); 174 return internalAdapter != null; 175 } 176 177 @ForType(InCallService.class) 178 interface ReflectorInCallService { 179 @Accessor("mHandler") getHandler()180 Handler getHandler(); 181 182 @Accessor("mPhone") getPhone()183 Phone getPhone(); 184 185 @Direct requestBluetoothAudio(BluetoothDevice bluetoothDevice)186 void requestBluetoothAudio(BluetoothDevice bluetoothDevice); 187 188 @Direct setAudioRoute(int audioRoute)189 void setAudioRoute(int audioRoute); 190 191 @Direct setMuted(boolean muted)192 void setMuted(boolean muted); 193 194 @Direct getCallAudioState()195 CallAudioState getCallAudioState(); 196 } 197 198 @ForType(Phone.class) 199 interface ReflectorPhone { 200 @Accessor("mInCallAdapter") getInCallAdapter()201 InCallAdapter getInCallAdapter(); 202 } 203 204 @ForType(InCallAdapter.class) 205 interface ReflectorInCallAdapter { 206 @Accessor("mAdapter") getInternalInCallAdapter()207 Object getInternalInCallAdapter(); 208 } 209 } 210