diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 01:05:30 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 01:05:30 +0000 |
commit | a0b5c5974cba9d228bd6aff6f0afab45c25f28b9 (patch) | |
tree | e1306c4f7c9f066598917260b8bec88e5c102a77 | |
parent | 4ee21038bca9e7d9599c08c1e64e545189b54cf5 (diff) | |
parent | e72d0b88c6b4dacf2789f39b418e95829a8f9145 (diff) | |
download | sl4a-a0b5c5974cba9d228bd6aff6f0afab45c25f28b9.tar.gz |
Snap for 10447354 from e72d0b88c6b4dacf2789f39b418e95829a8f9145 to mainline-cellbroadcast-releaseaml_cbr_341710000aml_cbr_341610000aml_cbr_341510010aml_cbr_341410010aml_cbr_341311010aml_cbr_341110000aml_cbr_341011000aml_cbr_340914000android14-mainline-cellbroadcast-release
Change-Id: I212428646539ce28a90ad924a5b40f41d6d373e4
22 files changed, 1126 insertions, 35 deletions
diff --git a/Common/Android.bp b/Common/Android.bp index 16f03310..41154b29 100644 --- a/Common/Android.bp +++ b/Common/Android.bp @@ -34,10 +34,12 @@ java_library { "junit", "modules-utils-build", "aosp_test_rcs_client_base", + "com.uwb.support.ccc", "com.uwb.support.fira", ], sdk_version: "core_platform", + plugins: ["auto_value_plugin"], libs: [ "framework-wifi.impl", // allow SL4A to access @hide Wifi APIs "framework-connectivity.impl", @@ -45,7 +47,7 @@ java_library { "framework-uwb.impl", "framework-bluetooth.impl", "framework", - + "auto_value_annotations", "telephony-common", "ims-common", "bouncycastle-repackaged-unbundled", diff --git a/Common/src/com/googlecode/android_scripting/facade/DataUsageController.java b/Common/src/com/googlecode/android_scripting/facade/DataUsageController.java index 6ec89ed1..3e9aa05f 100644 --- a/Common/src/com/googlecode/android_scripting/facade/DataUsageController.java +++ b/Common/src/com/googlecode/android_scripting/facade/DataUsageController.java @@ -16,8 +16,11 @@ package com.googlecode.android_scripting.facade; +import static android.app.usage.NetworkStats.Bucket.METERED_YES; import static android.net.ConnectivityManager.TYPE_MOBILE; import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkTemplate.MATCH_MOBILE; +import static android.net.NetworkTemplate.MATCH_WIFI; import static android.telephony.TelephonyManager.SIM_STATE_READY; import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; @@ -39,6 +42,7 @@ import android.util.Pair; import java.time.ZonedDateTime; import java.util.Locale; +import java.util.Set; /** * DataUsageController. @@ -87,7 +91,8 @@ public class DataUsageController { if (subscriberId == null) { return warn("no subscriber id"); } - NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(subscriberId); + NetworkTemplate template = new NetworkTemplate.Builder(MATCH_MOBILE) + .setMeteredness(METERED_YES).setSubscriberIds(Set.of(subscriberId)).build(); template = NetworkTemplate.normalize(template, mTelephonyManager.getMergedSubscriberIds()); return getDataUsageInfo(template); @@ -101,7 +106,8 @@ public class DataUsageController { if (subscriberId == null) { return warn("no subscriber id"); } - NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(subscriberId); + NetworkTemplate template = new NetworkTemplate.Builder(MATCH_MOBILE) + .setMeteredness(METERED_YES).setSubscriberIds(Set.of(subscriberId)).build(); template = NetworkTemplate.normalize(template, mTelephonyManager.getMergedSubscriberIds()); return getDataUsageInfo(template, uId); @@ -112,7 +118,7 @@ public class DataUsageController { * @return DataUsageInfo: The Wifi data usage information. */ public DataUsageInfo getWifiDataUsageInfo() { - NetworkTemplate template = NetworkTemplate.buildTemplateWifiWildcard(); + NetworkTemplate template = new NetworkTemplate.Builder(MATCH_WIFI).build(); return getDataUsageInfo(template); } diff --git a/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java b/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java index 0a7223ca..a7b514d2 100644 --- a/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java @@ -269,7 +269,6 @@ public class SettingsFacade extends RpcReceiver { String previousPassword) { // mLockPatternUtils.setLockPatternEnabled(true, UserHandle.myUserId()); mLockPatternUtils.setLockScreenDisabled(false, UserHandle.myUserId()); - mLockPatternUtils.setCredentialRequiredToDecrypt(true); mLockPatternUtils.setLockCredential(LockscreenCredential.createPassword(password), LockscreenCredential.createPasswordOrNone(previousPassword), UserHandle.myUserId()); diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java index 60e97971..33a667e3 100644 --- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java @@ -576,11 +576,15 @@ public class BluetoothConnectionFacade extends RpcReceiver { @Rpc(description = "Bluetooth init Bond by Mac Address") public boolean bluetoothBond(@RpcParameter(name = "macAddress") String macAddress) { + mContext.registerReceiver(new BondBroadcastReceiver(), + new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); return mBluetoothAdapter.getRemoteDevice(macAddress).createBond(); } @Rpc(description = "Bluetooth init LE Bond by Mac Address") public boolean bluetoothLeBond(@RpcParameter(name = "macAddress") String macAddress) { + mContext.registerReceiver(new BondBroadcastReceiver(), + new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); return mBluetoothAdapter.getRemoteDevice(macAddress).createBond(BluetoothDevice.TRANSPORT_LE); } @@ -631,27 +635,41 @@ public class BluetoothConnectionFacade extends RpcReceiver { } /** - * Bond to a device using Out of Band Data. + * Bond to a device using Out of Band Data over LE transport. Note that there is a distinction + * between the address with type supplied in the oob data and the address and type of the + * BluetoothDevice object. * - * @param address String representation of address like "00:11:22:33:44:55" + * @param oobDataAddress is the MAC address to be used in the oob data + * @param oobDataAddressType is the BluetoothDevice.AddressType for the oob data MAC address * @param transport String "1", "2", "3" to match TRANSPORT_* * @param c Hex String of the 16 octet confirmation * @param r Hex String of the 16 octet randomizer + * @param address String representation of MAC address for the BluetoothDevice object + * @param addressType the BluetoothDevice.AddressType for the BluetoothDevice object */ - @Rpc(description = "Creates and Out of Band bond.") - public void bluetoothCreateBondOutOfBand(@RpcParameter(name = "address") String address, - @RpcParameter(name = "transport") String transport, - @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r) { - Log.d("bluetoothCreateBondOutOfBand(" + address + ", " + transport + "," + c + ", " + @Rpc(description = "Creates and Out of Band LE bond.") + public boolean bluetoothCreateLeBondOutOfBand( + @RpcParameter(name = "oobDataAddress") String oobDataAddress, + @RpcParameter(name = "oobDataAddressType") Integer oobDataAddressType, + @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r, + @RpcParameter(name = "address") String address, + @RpcParameter(name = "addressType") @RpcDefault("1") Integer addressType) { + Log.d("bluetoothCreateLeBondOutOfBand(" + address + ", " + addressType + "," + c + ", " + r + ")"); - BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(address); + BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteLeDevice(address, addressType); byte[] addressBytes = new byte[7]; int i = 0; - for (String s : address.split(":")) { + for (String s : oobDataAddress.split(":")) { addressBytes[i] = hexStringToByteArray(s)[0]; i++; } - addressBytes[i] = 0x01; + + // Inserts the oob address type if one is provided + if (oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_PUBLIC + || oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_RANDOM) { + addressBytes[i] = oobDataAddressType.byteValue(); + } + OobData p192 = null; OobData p256 = new OobData.LeBuilder(hexStringToByteArray(c), addressBytes, OobData.LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL) @@ -659,7 +677,7 @@ public class BluetoothConnectionFacade extends RpcReceiver { .build(); mContext.registerReceiver(new BondBroadcastReceiver(), new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); - remoteDevice.createBondOutOfBand(Integer.parseInt(transport), p192, p256); + return remoteDevice.createBondOutOfBand(BluetoothDevice.TRANSPORT_LE, p192, p256); } private class BondBroadcastReceiver extends BroadcastReceiver { @@ -755,11 +773,17 @@ public class BluetoothConnectionFacade extends RpcReceiver { @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID) throws Exception { - BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), - deviceID); - mContext.registerReceiver(new BondBroadcastReceiver(), - new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); - return mDevice.removeBond(); + // We don't want to crash the test if the test passes an address that cannot be found. + try { + BluetoothDevice mDevice = BluetoothFacade.getDevice( + mBluetoothAdapter.getBondedDevices(), deviceID); + mContext.registerReceiver(new BondBroadcastReceiver(), + new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); + return mDevice.removeBond(); + } catch (Exception e) { + Log.d("Failed to find the device by deviceId"); + return false; + } } @Rpc(description = "Connect to a device that is already bonded.") diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidDeviceFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidDeviceFacade.java index ba61ded3..bc73e6a2 100644 --- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidDeviceFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidDeviceFacade.java @@ -151,9 +151,9 @@ public class BluetoothHidDeviceFacade extends RpcReceiver { // HID mouse movement private static final byte[] RIGHT = {0, 1, 0, 0}; - private static final byte[] DOWN = {0, 0, -1, 0}; + private static final byte[] DOWN = {0, 0, 1, 0}; private static final byte[] LEFT = {0, -1, 0, 0}; - private static final byte[] UP = {0, 0, 1, 0}; + private static final byte[] UP = {0, 0, -1, 0}; // Default values. private static final int QOS_TOKEN_RATE = 800; // 9 bytes * 1000000 us / 11250 us @@ -430,6 +430,34 @@ public class BluetoothHidDeviceFacade extends RpcReceiver { } /** + * Send a bytes array data report to a connected HID host using interrupt channel. + * @param deviceID name or MAC address or the HID input host + * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in + * descriptor. + * @param report byte array to be sent into HID device + * @return true if successfully sent the report; otherwise false + * @throws Exception error from Bluetooth HidDevService + */ + @Rpc(description = "Send bytes array report to a connected HID host using interrupt channel.") + public Boolean bluetoothHidDeviceSendBytesArrayReport( + @RpcParameter(name = "deviceID", + description = "Name or MAC address of a bluetooth device.") + String deviceID, + @RpcParameter(name = "descriptor", + description = "Descriptor of the report") + Integer id, + @RpcParameter(name = "report") + byte[] report) throws Exception { + if (sHidDeviceProfile == null) { + return false; + } + + BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), + deviceID); + return sHidDeviceProfile.sendReport(device, id, report); + } + + /** * Send a report to the connected HID host as reply for GET_REPORT request from the HID host. * @param deviceID name or MAC address or the HID input host * @param type type of the report, as in request @@ -463,6 +491,39 @@ public class BluetoothHidDeviceFacade extends RpcReceiver { } /** + * Send a bytes array report to the connected HID host as reply for GET_REPORT request + * from the HID host. + * @param deviceID name or MAC address or the HID input host + * @param type type of the report, as in request + * @param id id of the report, as in request + * @param report byte array to be sent into HID device + * @return true if successfully sent the reply report; otherwise false + * @throws Exception error from Bluetooth HidDevService + */ + @Rpc(description = "Send reply bytes array report to a connected HID..") + public Boolean bluetoothHidDeviceReplyBytesArrayReport( + @RpcParameter(name = "deviceID", + description = "Name or MAC address of a bluetooth device.") + String deviceID, + @RpcParameter(name = "type", + description = "Type as in the report.") + Integer type, + @RpcParameter(name = "id", + description = "id as in the report.") + Integer id, + @RpcParameter(name = "report") + byte[] report) throws Exception { + if (sHidDeviceProfile == null) { + return false; + } + + BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), + deviceID); + return sHidDeviceProfile.replyReport( + device, (byte) (int) type, (byte) (int) id, report); + } + + /** * Send error handshake message as reply for invalid SET_REPORT request from the HID host. * @param deviceID name or MAC address or the HID input host * @param error error byte diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java index 24f0b087..6829c90c 100644 --- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java @@ -38,6 +38,7 @@ import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcDefault; import com.googlecode.android_scripting.rpc.RpcParameter; +import java.util.Arrays; import java.util.List; /* @@ -122,9 +123,9 @@ public class BluetoothHidFacade extends RpcReceiver { } break; case BluetoothHidHost.ACTION_REPORT: { - char[] report = intent.getCharArrayExtra( + byte[] report = intent.getByteArrayExtra( BluetoothHidHost.EXTRA_REPORT); - Log.d("Received report: " + String.valueOf(report)); + Log.d("Received report: " + Arrays.toString(report)); } break; case BluetoothHidHost.ACTION_VIRTUAL_UNPLUG_STATUS: { diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothSocketConnFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothSocketConnFacade.java index 18ca8588..49efe6cf 100644 --- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothSocketConnFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothSocketConnFacade.java @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; +import android.os.Bundle; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.facade.EventFacade; @@ -55,6 +56,9 @@ public class BluetoothSocketConnFacade extends RpcReceiver { private AcceptThread mAcceptThread; private byte mTxPktIndex = 0; + private final Bundle mGoodNews; + private final Bundle mBadNews; + private static final String DEFAULT_PSM = "161"; //=0x00A1 // UUID for SL4A. @@ -68,6 +72,11 @@ public class BluetoothSocketConnFacade extends RpcReceiver { mEventFacade = manager.getReceiver(EventFacade.class); mService = manager.getService(); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + mGoodNews = new Bundle(); + mGoodNews.putBoolean("Status", true); + mBadNews = new Bundle(); + mBadNews.putBoolean("Status", false); } private BluetoothConnection getConnection(String connID) throws IOException { @@ -731,9 +740,15 @@ public class BluetoothSocketConnFacade extends RpcReceiver { mConnUuid = addConnection(conn); Log.d("ConnectThread:run: isConnected=" + mSocket.isConnected() + ", address=" + mSocket.getRemoteDevice().getAddress() + ", uuid=" + mConnUuid); + if (mSocket.isConnected()) { + mEventFacade.postEvent("BluetoothSocketConnectSuccess", mGoodNews); + } else { + mEventFacade.postEvent("BluetoothSocketConnectError", mBadNews); + } } catch (IOException connectException) { Log.e("ConnectThread::run(): Error: Connection Unsuccessful"); cancel(); + mEventFacade.postEvent("BluetoothSocketConnectError", mBadNews); return; } } diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java index e2286724..176dd303 100644 --- a/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java @@ -270,6 +270,21 @@ public class GattClientFacade extends RpcReceiver { } /** + * Reconnect to a Bluetooth GATT server + * + * @param index the bluetooth gatt index + * @throws Exception + */ + @Rpc(description = "Reconnect a bluetooth gatt") + public void gattClientReconnect(@RpcParameter(name = "index") Integer index) throws Exception { + if (mBluetoothGattList.get(index) != null) { + mBluetoothGattList.get(index).connect(); + } else { + throw new Exception("Invalid index input: " + index); + } + } + + /** * Disconnect a bluetooth gatt * * @param index the bluetooth gatt index diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java b/Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java new file mode 100644 index 00000000..bd5fd5ff --- /dev/null +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.googlecode.android_scripting.facade.telephony; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** An AutoValue class to maintain the information of an audio file. */ +@AutoValue +abstract class AudioFileInfo { + static AudioFileInfo create(String mime, int sampleRate, int channelCount) { + return new AutoValue_AudioFileInfo(mime, sampleRate, channelCount); + } + + @NonNull + @Override + public final String toString() { + return String.format("{mime:%s, sample rate:%d, channel count:%d}", + mime(), + sampleRate(), + channelCount()); + } + + abstract String mime(); + abstract int sampleRate(); + abstract int channelCount(); +} diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java index 0e1e8ca8..1629217a 100644 --- a/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java @@ -16,11 +16,21 @@ package com.googlecode.android_scripting.facade.telephony; +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.TERMINATE; +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.RUN; +import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.MONO_CHANNEL; +import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.SAMPLE_RATE_16K; +import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.SAMPLE_RATE_48K; +import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.STEREO_CHANNEL; + +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Set; +import java.io.File; +import android.content.Context; import android.telecom.Call; import android.telecom.Call.Details; import android.telecom.CallAudioState; @@ -30,6 +40,8 @@ import android.telecom.Phone; import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.telecom.VideoProfile.CameraCapabilities; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; import com.googlecode.android_scripting.Log; @@ -39,6 +51,27 @@ public class InCallServiceImpl extends InCallService { private static InCallServiceImpl sService = null; + private static PlayAudioInCall playAudioInCall; + private static RecordVoiceInCall recordVoiceInCall; + + // The call is to play an audio file or record voice. + private static Call playRecordCall = null; + + // A telephony device to route voice through mobile telphony network + private static AudioDeviceInfo audioTelephonyInfo = null; + + // Indicates if the call is playing audio or not + private static HandleVoiceThreadState playAudioInCallState = TERMINATE; + // Indicates if the call is recording voice or not + private static HandleVoiceThreadState recordVoiceInCallState = TERMINATE; + + private static AudioManager mAudioManager = null; + + // The audio file is to play audio on the route of telephony network + private static File playAudioFile; + // The audio file is to store voice wav data on the route of telephony network + private static File recordVoiceFile; + public static InCallServiceImpl getService() { return sService; } @@ -542,6 +575,14 @@ public class InCallServiceImpl extends InCallService { } } + /** Indicates and controls the state of the audio playing or voice recording thread. */ + enum HandleVoiceThreadState { + /** The audio playing/voice recording thread is terminated. */ + TERMINATE, + /** The audio playing/voice recording thread is running. */ + RUN + } + /* * TODO: b/26272583 Refactor so that these are instance members of the * incallservice. Then we can perform null checks using the design pattern @@ -560,6 +601,10 @@ public class InCallServiceImpl extends InCallService { CallCallback callCallback = new CallCallback(id, CallCallback.EVENT_NONE); call.registerCallback(callCallback); + // Make sure the first call is used to play or record voice + if (playRecordCall == null) { + playRecordCall = call; + } VideoCall videoCall = call.getVideoCall(); VideoCallCallback videoCallCallback = null; @@ -590,6 +635,7 @@ public class InCallServiceImpl extends InCallService { */ if (sService == null) { sService = this; + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); } else if (sService != this) { Log.e("Multiple InCall Services Active in SL4A!"); @@ -606,6 +652,14 @@ public class InCallServiceImpl extends InCallService { mCallContainerMap.remove(id); + if (getCallId(call).equals(getCallId(playRecordCall))) { + Log.d("Terminate the call for playing/recording."); + playAudioInCallState = TERMINATE; + playRecordCall = null; + recordVoiceInCallState = TERMINATE; + recordVoiceInCall = null; + } + CallListener.onCallRemoved(id, call); if (mCallContainerMap.size() == 0) { @@ -1450,6 +1504,140 @@ public class InCallServiceImpl extends InCallService { return propertyList; } + /** + * Plays an audio file specified by {@code audioFileName} during a phone call. + * + * The method first checks if {@link #Call}, {@code audioFileName} and {@link #AudioDeviceInfo} + * exist. Finally, it creates a {@link #PlayAudioInCall} which creates a thread to perform + * audio playing. + * The method is called by {@link TelephonyManagerFacade#telephonyPlayAudioFile()}. + * + * @return {@code true} if the audio file is successfully played. Otherwise, {@code false} + */ + public static boolean playAudioFile(String audioFileName) { + Log.d(String.format("Playing audio file \"%s\"...", audioFileName)); + if (playAudioInCallState.equals(RUN)) { + Log.d("Playing is ongoing!"); + return false; + } + if (getService() == null) { + Log.d("InCallService isn't activated yet"); + return false; + } + audioTelephonyInfo = getAudioDeviceInfo(); + if (audioTelephonyInfo == null) { + Log.d("No Telephony AudioDeviceInfo!"); + return false; + } + playAudioFile = new File(getService().getFilesDir(), audioFileName); + if (!playAudioFile.exists()) { + Log.d(String.format("%s not found in files folder!",audioFileName)); + return false; + } + playAudioInCall = new PlayAudioInCall(mEventFacade, + playRecordCall, playAudioFile, audioTelephonyInfo); + return playAudioInCall.playAudioFile(); + } + + /** Gets the audio telephony device during in a call. + * + * @return null if not found audio telephony device. Otherwise, an instance of + * {@link #AudioDeviceInfo} + * */ + public static AudioDeviceInfo getAudioDeviceInfo() { + AudioDeviceInfo[] audioDeviceInfoList = mAudioManager.getDevices( + AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo info : audioDeviceInfoList) { + if (info.getType() == AudioDeviceInfo.TYPE_TELEPHONY) { + Log.d(String.format("Found audio telephony device: %d", info.getType())); + return info; + } + } + return null; + } + + public static HandleVoiceThreadState getPlayAudioInCallState() { + return playAudioInCallState; + } + + public static void setPlayAudioInCallState(HandleVoiceThreadState state) { + playAudioInCallState = state; + } + + public static void stopPlayAudioFile() { + if (playAudioInCallState.equals(RUN) && playAudioInCall != null) { + Log.d("Stop playing audio successfully!"); + playAudioInCallState = TERMINATE; + } + } + + /** + * Records voice during a phone call. + * + * The method checks the following items before creating a thread to record voice. + * <ol> + * <li>Check recoding state. If there is already a voice recording, ignore the request.</li> + * <li>Check if call has been established.</li> + * <li>Check the input sample rate and channel count to meet constraints.</li> + * <li>Check if the record wav file is created successfully.</li> + * </ol> + * @param recordWavFile indicates the wav file name of the recording voice + * @param sampleRate indicates sampling rate of the recording voice + * @param channelCount indicates voice channel number to be recorded + * @return {@code true} if voice is successfully recorded. Otherwise, {@code false} + */ + public static boolean recordVoice( + String recordWavFile, int sampleRate, int channelCount, boolean cancelNoiseEcho) { + Log.d(String.format("Recording voice to the \"%s\" file...", recordWavFile)); + if (getRecordVoiceInCallState().equals(RUN)) { + Log.d("Recording is ongoing!"); + return false; + } + if (getService() == null) { + Log.d("InCallService isn't activated yet"); + return false; + } + if (sampleRate != SAMPLE_RATE_16K && sampleRate != SAMPLE_RATE_48K) { + Log.e(String.format("Don't support sample rate: %d", sampleRate)); + return false; + } + if (channelCount != MONO_CHANNEL && channelCount != STEREO_CHANNEL) { + Log.e(String.format("Don't support channel count: %d", channelCount)); + return false; + } + recordVoiceFile = new File(getService().getFilesDir(), recordWavFile); + if (!recordVoiceFile.exists()) { + try { + Log.d(String.format("Creates a empty %s wav file to store voice data!", + recordWavFile)); + recordVoiceFile.createNewFile(); + } catch (IOException e) { + Log.e(String.format("Failed to create %s wav file!", recordWavFile)); + return false; + } + } + Log.d(String.format("The voice recording info: wav file: %s, Sampling rate: %d, channel count: %d", + recordWavFile, sampleRate, channelCount)); + recordVoiceInCall = new RecordVoiceInCall(mEventFacade, playRecordCall, recordVoiceFile, + sampleRate, channelCount, cancelNoiseEcho); + return recordVoiceInCall.recordVoice(); + } + + public static void stopRecordVoice() { + if (getRecordVoiceInCallState().equals(RUN) && playAudioInCall != null) { + Log.d("Stop recording voice successfully!"); + setRecordVoiceInCallState(TERMINATE); + } + } + + public static HandleVoiceThreadState getRecordVoiceInCallState() { + return recordVoiceInCallState; + } + + public static void setRecordVoiceInCallState(HandleVoiceThreadState state) { + recordVoiceInCallState = state; + } + public static String getCallPresentationInfoString(int presentation) { switch (presentation) { case TelecomManager.PRESENTATION_ALLOWED: diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java b/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java new file mode 100644 index 00000000..f708f467 --- /dev/null +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.android_scripting.facade.telephony; + +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.TERMINATE; +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.RUN; +import static android.os.VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY; +import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC; +import static android.media.AudioAttributes.USAGE_MEDIA; +import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; +import static android.os.Build.VERSION; +import static android.os.Build.VERSION_CODES; + +import com.googlecode.android_scripting.facade.EventFacade; +import com.googlecode.android_scripting.Log; +import android.media.AudioDeviceInfo; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioTrack; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.telecom.Call; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.File; + +/** + * The class handles playing an audio file on the route of the telephony network during a phone + * call. + */ +public class PlayAudioInCall { + private static Thread playAudioThread = null; + private AudioDeviceInfo audioDeviceInfo; + private File audioFile; + private AudioFileInfo audioFileInfo; + private Call call; + private EventFacade eventFacade; + + PlayAudioInCall(EventFacade eventFacade, + Call call, + File audioFile, + AudioDeviceInfo audioDeviceInfo) { + this.eventFacade = eventFacade; + this.call = call; + this.audioFile = audioFile; + this.audioDeviceInfo = audioDeviceInfo; + Log.d(String.format("eventFacade=%s, call=%s, audioFile=%s, audioDeviceInfo=%d", + this.eventFacade, this.call, this.audioFile, this.audioDeviceInfo.getId())); + } + + boolean playAudioFile() { + if (!setupAudioFileInfo()) + return false; + return playAudio(); + } + + private boolean playAudio() { + AudioFormat audioFormat = getAudioFormat(); + Log.d(String.format("Audio format: %s", audioFormat.toString())); + AudioTrack audioTrack = getAudioTrack(audioFormat, getAudioTrackBufferSize(audioFormat)); + Log.d(String.format("Audio Track: %s",audioTrack.toString())); + if (!audioTrack.setPreferredDevice(audioDeviceInfo)) { + audioTrack.release(); + return false; + } + Log.d(String.format("Set the preferred audio device to %d successfully", + audioDeviceInfo.getId())); + while (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) + ; + Log.d(String.format("Audio track state: %s", audioTrack.getState())); + return createPlayAudioThread(audioTrack); + } + + private boolean createPlayAudioThread(AudioTrack audioTrack) { + playAudioThread = new Thread(() -> { + byte[] audioRaw = new byte[512]; + int readBytes; + try { + InCallServiceImpl.setPlayAudioInCallState(RUN); + InCallServiceImpl.muteCall(true); + InputStream inputStream = new FileInputStream(audioFile); + inputStream.read(audioRaw, 0, 44); + audioTrack.play(); + while ((readBytes = inputStream.read(audioRaw)) != -1) { + audioTrack.write(audioRaw, 0, readBytes); + if (stopPlayAudio()) { + break; + } + } + Log.d("End Playing audio!"); + inputStream.close(); + audioTrack.stop(); + audioTrack.release(); + eventFacade.postEvent(TelephonyConstants.EventCallPlayAudioStateChanged, + new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call), + TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_END)); + } + catch (IOException e) { + audioTrack.release(); + Log.d(String.format("Failed to read audio file \"%s\"!", audioFile.getName())); + eventFacade.postEvent(TelephonyConstants.EventCallPlayAudioStateChanged, + new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call), + TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_FAIL)); + } + finally { + InCallServiceImpl.muteCall(false); + InCallServiceImpl.setPlayAudioInCallState(TERMINATE); + } + }); + playAudioThread.start(); + return true; + } + + private AudioTrack getAudioTrack(AudioFormat audioFormat, int bufferSize) { + return new AudioTrack.Builder().setAudioFormat(audioFormat).setBufferSizeInBytes(bufferSize) + .setAudioAttributes(getAudioAttributes()).setTransferMode(AudioTrack.MODE_STREAM).build(); + } + + private int getAudioTrackBufferSize(AudioFormat audioFormat) { + return AudioTrack.getMinBufferSize( + audioFormat.getSampleRate(), + audioFormat.getChannelMask(), + audioFormat.getEncoding()); + } + + private AudioAttributes getAudioAttributes() { + if (VERSION.SDK_INT >= VERSION_CODES.Q) { + Log.d("AudioAttributes above Android Q is used."); + return new AudioAttributes.Builder() + .setUsage(USAGE_VOICE_COMMUNICATION).build(); + } else { + Log.d("AudioAttributes below Android Q is used."); + return new AudioAttributes.Builder() + .setContentType(CONTENT_TYPE_MUSIC) + .setFlags(FLAG_BYPASS_INTERRUPTION_POLICY) + .setUsage(USAGE_MEDIA).build(); + } + } + + private boolean setupAudioFileInfo() { + MediaExtractor extractor = new MediaExtractor(); + try { + extractor.setDataSource(audioFile.getAbsolutePath()); + } catch (IOException e) { + Log.d(String.format("Failed to set data source in MediaExtrator, %s", e.getMessage())); + return false; + } + extractor.selectTrack(0); + MediaFormat format = extractor.getTrackFormat(0); + audioFileInfo = AudioFileInfo.create(format.getString(MediaFormat.KEY_MIME), + format.getInteger(MediaFormat.KEY_SAMPLE_RATE), + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); + Log.d(String.format("The media format is %s", audioFileInfo)); + return true; + } + + private AudioFormat getAudioFormat() { + int channelMask = audioFileInfo.channelCount() == 1 ? AudioFormat.CHANNEL_OUT_MONO + : AudioFormat.CHANNEL_OUT_STEREO; + return new AudioFormat.Builder().setChannelMask(channelMask) + .setSampleRate(audioFileInfo.sampleRate()).setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .build(); + } + + private boolean stopPlayAudio() { + if (InCallServiceImpl.getPlayAudioInCallState().equals(RUN)) { + return false; + } + Log.d("Stop playing audio!"); + return true; + } +} diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java b/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java new file mode 100644 index 00000000..3b4adb67 --- /dev/null +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.android_scripting.facade.telephony; + +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.RUN; +import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.TERMINATE; +import static java.nio.ByteOrder.LITTLE_ENDIAN; + +import com.googlecode.android_scripting.Log; +import com.googlecode.android_scripting.facade.EventFacade; +import android.telecom.Call; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder.AudioSource; +import android.media.audiofx.AcousticEchoCanceler; +import android.media.audiofx.AudioEffect; +import android.media.audiofx.NoiseSuppressor; +import com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState; +import java.io.File; +import java.io.IOException; +import java.io.FileOutputStream; +import java.io.DataOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; + +/** + * The class handles recording voice on the route of the telephony network during a phone + * call. + */public class RecordVoiceInCall { + public static final int SAMPLE_RATE_16K = 16000; + public static final int SAMPLE_RATE_48K = 48000; + public static final int MONO_CHANNEL = 1; + public static final int STEREO_CHANNEL = 2; + private static final int PCM_16_BITS = 16; + private EventFacade eventFacade; + private Call call; + private int sampleRate; + private int channelCount; + private File recordFile; + private AudioFileInfo audioFileInfo; + private AudioFormat audioFormat; + private NoiseSuppressor noiseSuppressor; + private AcousticEchoCanceler acousticEchoCanceler; + private int minRecordAudioBufferSize; + private boolean cancelNoiseEcho; + private static Thread recordVoiceThread = null; + + RecordVoiceInCall(EventFacade eventFacade, + Call call, + File recordFile, + int sampleRate, + int channelCount, + boolean cancelNoiseEcho) { + this.eventFacade = eventFacade; + this.call = call; + this.recordFile = recordFile; + this.sampleRate = sampleRate; + this.channelCount = channelCount; + this.cancelNoiseEcho = cancelNoiseEcho; + Log.d(String.format("eventFacade=%s, call=%s, recordFile=%s, sampleRate=%d, channelCount=%d", + this.eventFacade, this.call, this.recordFile, this.sampleRate, this.channelCount)); + } + + /**Handles functional flows of voice recording and exposes to be invoked by users.*/ + boolean recordVoice() { + setRecordAudioInfo(); + setAudioFormat(); + setMinRecordAudioBufferSize(); + AudioRecord audioRecord = new AudioRecord.Builder() + .setAudioSource(AudioSource.VOICE_DOWNLINK) + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(minRecordAudioBufferSize) + .build(); + if (cancelNoiseEcho) { + enableNoiseSuppressor(audioRecord); + enableAcousticEchoCanceler(audioRecord); + } + return createRecordVoiceThread(audioRecord); + } + + private void setRecordAudioInfo() { + audioFileInfo = AudioFileInfo.create("WAVE", sampleRate, channelCount); + } + + /**Configures an object of {@code AudioFormat} needed for voice recording in a call. + * The minimal info is to provide channel count and voice encoding scheme. + */ + private void setAudioFormat() { + int channelMask = audioFileInfo.channelCount() == 1 ? AudioFormat.CHANNEL_IN_MONO + : AudioFormat.CHANNEL_IN_STEREO; + audioFormat = new AudioFormat.Builder().setChannelMask(channelMask) + .setSampleRate(audioFileInfo.sampleRate()).setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .build(); + Log.d(String.format( + "Audio format for recording, channel mask: %d, sample rate: %d, encoding: PCM_16bit", + channelMask, sampleRate)); + } + + /**Acquires minimal buffer size for sampling voice data.*/ + private void setMinRecordAudioBufferSize() { + minRecordAudioBufferSize = AudioRecord.getMinBufferSize(audioFormat.getSampleRate(), + audioFormat.getChannelCount(), + audioFormat.getEncoding()); + Log.d(String.format("Buffer size to record voice data: %d", minRecordAudioBufferSize)); + } + + private void enableNoiseSuppressor(AudioRecord audioRecord) { + if (NoiseSuppressor.isAvailable()) { + noiseSuppressor = NoiseSuppressor.create(audioRecord.getAudioSessionId()); + if (noiseSuppressor.setEnabled(true) != AudioEffect.SUCCESS) { + Log.w("Failed to enable noiseSuppressor!"); + return; + } + Log.d("Enable noiseSuppressor!"); + return; + } + Log.d("Noise suppressor is not available!"); + } + + private void enableAcousticEchoCanceler(AudioRecord audioRecord) { + if (AcousticEchoCanceler.isAvailable()) { + acousticEchoCanceler = AcousticEchoCanceler.create(audioRecord.getAudioSessionId()); + if (acousticEchoCanceler.setEnabled(true) != AudioEffect.SUCCESS) { + Log.w("Failed to enable AcousticEchoCanceler"); + return; + } + Log.d("Enable AcousticEchoCanceler!"); + return; + } + Log.d("AcousticEchoCanceler is not available!"); + } + + /**Creates a background thread to perform voice recording.*/ + private boolean createRecordVoiceThread(AudioRecord audioRecord) { + recordVoiceThread = new Thread(()-> { + int totalVoiceBytes = 0; + try { + InCallServiceImpl.setRecordVoiceInCallState(RUN); + InCallServiceImpl.muteCall(true); + audioRecord.startRecording(); + FileOutputStream outputStream = new FileOutputStream(recordFile); + DataOutputStream recordVoiceOutStream = new DataOutputStream(outputStream); + + writeWaveHeader(recordVoiceOutStream, totalVoiceBytes); + short[] buffer = new short[minRecordAudioBufferSize]; + + while (InCallServiceImpl.getRecordVoiceInCallState().equals(RUN)) { + int shortsRead = audioRecord.read(buffer, 0, minRecordAudioBufferSize); + totalVoiceBytes += shortsRead * 2; + for (int count = 0; count < shortsRead; count++) { + recordVoiceOutStream.writeShort(getShortByOrder(buffer[count], LITTLE_ENDIAN)); + } + } + recordVoiceOutStream.flush(); + recordVoiceOutStream.close(); + correctWaveHeaderChunkSize(totalVoiceBytes); + Log.d("End recording voice!"); + eventFacade.postEvent(TelephonyConstants.EventCallRecordVoiceStateChanged, + new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call), + TelephonyConstants.TELEPHONY_STATE_RECORD_VOICE_END)); + + } catch (IOException e) { + Log.d(String.format("Failed to record voice to \"%s\"!", recordFile.getName())); + eventFacade.postEvent(TelephonyConstants.EventCallPlayAudioStateChanged, + new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call), + TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_FAIL)); + + } finally { + InCallServiceImpl.muteCall(false); + releaseRecordSources(audioRecord); + } + }); + recordVoiceThread.start(); + return true; + } + + private void releaseRecordSources(AudioRecord audioRecord) { + audioRecord.stop(); + audioRecord.release(); + if (noiseSuppressor != null) { + noiseSuppressor.release(); + } + if (acousticEchoCanceler != null) { + acousticEchoCanceler.release(); + } + InCallServiceImpl.setRecordVoiceInCallState(TERMINATE); + } + /**Creates and writes wave header to the beginning of the wave file.*/ + private void writeWaveHeader(DataOutputStream dataOutputStream, int totalBytes) + throws IOException { + /* 1-4 */ + dataOutputStream.write("RIFF".getBytes(StandardCharsets.UTF_8)); + /* 5-8 Write Chunk Size ~= the byte size of voice data + 36 */ + dataOutputStream.writeInt(getIntByOrder(totalBytes + 36, LITTLE_ENDIAN)); + /* 9-12 */ + dataOutputStream.write("WAVE".getBytes(StandardCharsets.UTF_8)); + /* 13-16 */ + dataOutputStream.write("fmt ".getBytes(StandardCharsets.UTF_8)); + /* 17-20 Writes SubChunk1Size */ + dataOutputStream.writeInt(getIntByOrder(16, LITTLE_ENDIAN)); + /* 21-22 Writes audio format, PCM: 1 */ + dataOutputStream.writeShort(getShortByOrder((short) 1, LITTLE_ENDIAN)); + /* 23-24 Writes number of channels */ + dataOutputStream.writeShort( + getShortByOrder((short) audioFileInfo.channelCount(), LITTLE_ENDIAN)); + /* 25-28 Writes sampling rate */ + dataOutputStream.writeInt( + getIntByOrder(audioFileInfo.sampleRate(), LITTLE_ENDIAN)); + /* 29-32 Writes byte rate */ + int byteRate = audioFileInfo.sampleRate() * audioFileInfo.channelCount() * PCM_16_BITS/2; + dataOutputStream.writeInt( + getIntByOrder(byteRate, LITTLE_ENDIAN)); + /* 33-34 Writes block align */ + int blockAlign = audioFileInfo.channelCount() * PCM_16_BITS / 2; + dataOutputStream.writeShort( + getShortByOrder((short) blockAlign, LITTLE_ENDIAN)); + /* 35-36 Writes PCM bits */ + dataOutputStream.writeShort( + getShortByOrder((short) PCM_16_BITS, LITTLE_ENDIAN)); + /* 37-40 */ + dataOutputStream.write("data".getBytes(StandardCharsets.UTF_8)); + /* 41-44 SubChunk2Size ~= the byte size of voice data */ + dataOutputStream.writeInt( + getIntByOrder(totalBytes, LITTLE_ENDIAN)); + } + + private void correctWaveHeaderChunkSize(int totalVoiceBytes) throws IOException { + RandomAccessFile randomVoiceAccessFile = new RandomAccessFile(recordFile, "rw"); + writeWaveHeaderChunkSize(randomVoiceAccessFile, totalVoiceBytes); + writeWaveHeaderSubChunk2Size(randomVoiceAccessFile, totalVoiceBytes); + randomVoiceAccessFile.close(); + } + + /**A wav file consists of two chunks. The first chunk is the header data which describes + * the sample rate, channel count, the size of voice data and so on. See {@code #writeWaveHeader}. + */ + private void writeWaveHeaderChunkSize(RandomAccessFile randomAccessFile, int totalVoiceBytes) + throws IOException { + randomAccessFile.seek(4); + randomAccessFile.write(intToBytes(totalVoiceBytes + 36, LITTLE_ENDIAN), 0, 4); + } + + /**A wav file consists of two chunks. The second chunk is the voice data.*/ + private void writeWaveHeaderSubChunk2Size(RandomAccessFile randomAccessFile, int totalVoiceBytes) + throws IOException { + randomAccessFile.seek(40); + randomAccessFile.write(intToBytes(totalVoiceBytes, LITTLE_ENDIAN), 0, 4); + } + + /**A short type of integer can be represented with little endian or big endian.*/ + private short getShortByOrder(short value, ByteOrder byteOrder) { + byte[] bytes = ByteBuffer.allocate(2).putShort(value).array(); + return ByteBuffer.wrap(bytes).order(byteOrder).getShort(); + } + + /**Converts integer to byte array.*/ + private byte[] intToBytes(int value, ByteOrder byteOrder) { + return ByteBuffer.allocate(4).order(byteOrder).putInt(value).array(); + } + + /**An integer type of integer can be represented with little endian or big endian.*/ + private int getIntByOrder(int value, ByteOrder byteOrder) { + byte[] bytes = ByteBuffer.allocate(4).putInt(value).array(); + return ByteBuffer.wrap(bytes).order(byteOrder).getInt(); + } +} diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java index 1de17161..1c8724f2 100644 --- a/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java @@ -740,7 +740,7 @@ public class SmsFacade extends RpcReceiver { } private PendingIntent createBroadcastPendingIntent(String intentAction, Uri messageUri) { - Intent intent = new Intent(intentAction, messageUri); + Intent intent = new Intent(intentAction, messageUri).setPackage(mService.getPackageName()); return PendingIntent.getBroadcast(mService, 0, intent, PendingIntent.FLAG_MUTABLE); } diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java index d5e96335..591542f6 100644 --- a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java @@ -244,6 +244,10 @@ public class TelephonyConstants { public static final String TELEPHONY_STATE_IDLE = "IDLE"; public static final String TELEPHONY_STATE_OFFHOOK = "OFFHOOK"; public static final String TELEPHONY_STATE_UNKNOWN = "UNKNOWN"; + public static final String TELEPHONY_STATE_PLAY_AUDIO_END = "PLAYAUDIOEND"; + public static final String TELEPHONY_STATE_PLAY_AUDIO_FAIL = "PLAYAUDIOFAIL"; + public static final String TELEPHONY_STATE_RECORD_VOICE_END = "RECORDVOICEEND"; + public static final String TELEPHONY_STATE_RECORD_VOICE_FAIL = "RECORDVOICEFAIL"; /** * Constant for TTY Mode @@ -401,6 +405,8 @@ public class TelephonyConstants { public static final String EventSrvccStateChanged = "SrvccStateChanged"; public static final String EventMessageWaitingIndicatorChanged = "MessageWaitingIndicatorChanged"; public static final String EventPhysicalChannelConfigChanged = "PhysicalChannelConfigChanged"; + public static final String EventCallPlayAudioStateChanged = "CallPlayAudioStateChanged"; + public static final String EventCallRecordVoiceStateChanged = "CallRecordVoiceStateChanged"; /** * Constants for OnStartTetheringCallback diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java index e60e5f15..f68647ab 100644 --- a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java @@ -32,10 +32,12 @@ import android.telephony.CellInfo; import android.telephony.CellLocation; import android.telephony.NeighboringCellInfo; import android.telephony.PhoneStateListener; +import android.telephony.RadioAccessFamily; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.telephony.PinResult; import com.android.internal.telephony.RILConstants; @@ -190,7 +192,10 @@ public class TelephonyManagerFacade extends RpcReceiver { @Rpc(description = "Get network preference for subscription.") public String telephonyGetPreferredNetworkTypesForSubscription( @RpcParameter(name = "subId") Integer subId) { - int networkPreferenceInt = mTelephonyManager.getPreferredNetworkType(subId); + int networkPreferenceInt = + RadioAccessFamily.getNetworkTypeFromRaf( + (int) mTelephonyManager.createForSubscriptionId( + subId).getAllowedNetworkTypesBitmask()); return TelephonyUtils.getNetworkModeStringfromInt(networkPreferenceInt); } @@ -1006,6 +1011,21 @@ public class TelephonyManagerFacade extends RpcReceiver { } /** + * Check if the Subscription ID is valid. + * @param subId the subscription ID + * @return {true} if subId is valid, {false} otherwise. + */ + @Rpc(description = "Check if the Subscription ID is valid.") + public boolean telephonyIsSubscriptionIdValid( + @RpcParameter(name = "subId") Integer subId){ + if (subId == null || !SubscriptionManager.isValidSubscriptionId(subId)) { + Log.e("Invalid or null subscription ID"); + return false; + } + return true; + } + + /** * Supply the puk code and pin for locked SIM. * @param puk the puk code string * @param pin the puk pin string @@ -1019,6 +1039,25 @@ public class TelephonyManagerFacade extends RpcReceiver { } /** + * Supply the puk code and pin for locked SIM of specified subscription ID. + * @param subId the subscription ID + * @param puk the puk code string + * @param pin the puk pin string + * @return true or false for supplying the puk code and pin successfully or unsuccessfully. + */ + @Rpc(description = "Supply Puk and Pin for locked SIM " + + "for specified subscription ID.") + public boolean telephonySupplyPukForSubscription( + @RpcParameter(name = "subId") Integer subId, + @RpcParameter(name = "puk") String puk, + @RpcParameter(name = "pin") String pin) { + if (!telephonyIsSubscriptionIdValid(subId)) { + return false; + } + return mTelephonyManager.createForSubscriptionId(subId).supplyPuk(puk, pin); + } + + /** * Supply pin for locked SIM. * @param pin the puk pin string * @return true or false for supplying the pin successfully or unsuccessfully. @@ -1029,6 +1068,84 @@ public class TelephonyManagerFacade extends RpcReceiver { return mTelephonyManager.supplyPin(pin); } + /** + * Supply pin for locked SIM of specified subscription ID. + * @param subId the subscription ID + * @param pin the puk pin string + * @return true or false for supplying the pin successfully or unsuccessfully. + */ + @Rpc(description = "Supply Pin for locked SIM " + + "for specified subscription ID.") + public boolean telephonySupplyPinForSubscription( + @RpcParameter(name = "subId") Integer subId, + @RpcParameter(name = "pin") String pin) { + if (!telephonyIsSubscriptionIdValid(subId)) { + return false; + } + return mTelephonyManager.createForSubscriptionId(subId).supplyPin(pin); + } + + /** + * Enable or disable the ICC PIN lock of specified subscription ID. + * @param subId the subscription ID + * @param enabled "true" for enable, "false" for disable. + * @param pin needed to enable or disable the ICC PIN lock + * @return true or false for enable/disable the pin successfully or unsuccessfully. + */ + @Rpc(description = "Enable or disable the ICC PIN lock " + + "for specified subscription ID.") + public boolean telephonySetIccLockEnabledForSubscription( + @RpcParameter(name = "subId") Integer subId, + @RpcParameter(name = "enabled") Boolean enabled, + @RpcParameter(name = "pin") String pin) { + if (!telephonyIsSubscriptionIdValid(subId)) { + return false; + } + PinResult result= mTelephonyManager.createForSubscriptionId(subId).setIccLockEnabled(enabled,pin); + if(result != null) { + return (result.getResult() == PinResult.PIN_RESULT_TYPE_SUCCESS); + } + return false; + } + + /** + * Check whether ICC PIN lock is enabled. + * @param subId the subscription ID + * @return true or false for changing the ICC lock PIN successfully or unsuccessfully. + */ + @Rpc(description = "Check whether ICC PIN lock is enabled " + + "for specified subscription ID.") + public boolean telephonyIsIccLockEnabled( + @RpcParameter(name = "subId") Integer subId) { + if (!telephonyIsSubscriptionIdValid(subId)) { + return false; + } + return mTelephonyManager.createForSubscriptionId(subId).isIccLockEnabled(); + } + + /** + * Change the ICC lock PIN of specified subscription ID. + * @param subId the subscription ID + * @param oldPin is the old PIN. + * @param newPin is the new PIN. + * @return true or false for changing the ICC lock PIN successfully or unsuccessfully. + */ + @Rpc(description = "Change the ICC lock PIN " + + "for specified subscription ID.") + public boolean telephonyChangeIccLockPinForSubscription( + @RpcParameter(name = "subId") Integer subId, + @RpcParameter(name = "oldPin") String oldPin, + @RpcParameter(name = "newPin") String newPin) { + if (!telephonyIsSubscriptionIdValid(subId)) { + return false; + } + PinResult result= mTelephonyManager.createForSubscriptionId(subId).changeIccLockPin(oldPin,newPin); + if(result != null) { + return (result.getResult() == PinResult.PIN_RESULT_TYPE_SUCCESS); + } + return false; + } + @Rpc(description = "Returns the unique subscriber ID (such as IMSI) " + "for default subscription ID, or null if unavailable") public String telephonyGetSubscriberId() { @@ -1172,7 +1289,7 @@ public class TelephonyManagerFacade extends RpcReceiver { public void telephonySetCellInfoListRate( @RpcParameter(name = "rate") Integer rate ) { - mTelephonyManager.setCellInfoListRate(rate); + mTelephonyManager.setCellInfoListRate(rate, SubscriptionManager.getDefaultSubscriptionId()); } /** @@ -1513,6 +1630,53 @@ public class TelephonyManagerFacade extends RpcReceiver { } /** + * Plays an audio file specified by {@code audioFileName} during a phone call. + * + * @return {@code true} if the audio file is successfully played. + */ + @Rpc(description = "Plays the specified audio file during a phone call") + public boolean telephonyPlayAudioFile( + @RpcParameter(name = "audioFileName", description = "the audio file in the app's files folder") + String audioFileName) { + Log.d(String.format("Playing audio file \"%s\"...", audioFileName)); + InCallServiceImpl.setEventFacade(mEventFacade); + return InCallServiceImpl.playAudioFile(audioFileName); + } + + /** Stops playing an audio file during a call. */ + @Rpc(description = "Stops playing audio file during a phone call") + public void telephonyStopPlayingAudioFile() { + InCallServiceImpl.stopPlayAudioFile(); + } + + /** + * Records voice and writes to a wav file specified by {@code recordFileName} + * during a phone call. + * + * @return {@code true} if voice is successfully recorded + */ + @Rpc(description = "Records voice and writes to a wav file during a phone call") + public boolean telephonyRecordVoice( + @RpcParameter(name = "recordFileName", description = "The recorded voice file name") + String recordFileName, + @RpcParameter(name = "sampleRate", description = "sampling rate of voice data") + @RpcDefault("16000") Integer sampleRate, + @RpcParameter(name = "channelCount", description = "channel number of voice to record") + @RpcDefault("1") Integer channelCount, + @RpcParameter(name = "cancelNoiseEcho", description = "enable echo canceler and noise suppressor") + @RpcDefault("false") Boolean cancelNoiseEcho) { + Log.d(String.format("Recording voice to the \"%s\" file...", recordFileName)); + InCallServiceImpl.setEventFacade(mEventFacade); + return InCallServiceImpl.recordVoice(recordFileName, sampleRate, channelCount, cancelNoiseEcho); + } + + /** Stops recording voice during a phone call.*/ + @Rpc(description = "Stops recording voice during a call.") + public void telephonyStopRecordVoice() { + InCallServiceImpl.stopRecordVoice(); + } + + /** * Get the list of Forbidden PLMNs stored on the USIM * profile of the SIM for the default subscription. */ diff --git a/Common/src/com/googlecode/android_scripting/facade/uwb/UwbManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/uwb/UwbManagerFacade.java index 5ca7ff83..03a9a29c 100644 --- a/Common/src/com/googlecode/android_scripting/facade/uwb/UwbManagerFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/uwb/UwbManagerFacade.java @@ -26,6 +26,10 @@ import android.uwb.RangingSession; import android.uwb.UwbAddress; import android.uwb.UwbManager; +import com.google.uwb.support.ccc.CccOpenRangingParams; +import com.google.uwb.support.ccc.CccParams; +import com.google.uwb.support.ccc.CccPulseShapeCombo; +import com.google.uwb.support.ccc.CccRangingStartedParams; import com.google.uwb.support.fira.FiraOpenSessionParams; import com.google.uwb.support.fira.FiraParams; import com.google.uwb.support.fira.FiraRangingReconfigureParams; @@ -318,6 +322,77 @@ public class UwbManagerFacade extends RpcReceiver { return builder.build(); } + private CccRangingStartedParams generateCccRangingStartedParams(JSONObject j) + throws JSONException { + if (j == null) { + return null; + } + CccRangingStartedParams.Builder builder = new CccRangingStartedParams.Builder(); + if (j.has("stsIndex")) { + builder.setStartingStsIndex(j.getInt("stsIndex")); + } + if (j.has("uwbTime")) { + builder.setUwbTime0(j.getInt("uwbTime")); + } + if (j.has("hopModeKey")) { + builder.setHopModeKey(j.getInt("hopModeKey")); + } + if (j.has("syncCodeIndex")) { + builder.setSyncCodeIndex(j.getInt("syncCodeIndex")); + } + if (j.has("ranMultiplier")) { + builder.setRanMultiplier(j.getInt("ranMultiplier")); + } + + return builder.build(); + } + + private CccOpenRangingParams generateCccOpenRangingParams(JSONObject j) throws JSONException { + if (j == null) { + return null; + } + CccOpenRangingParams.Builder builder = new CccOpenRangingParams.Builder(); + builder.setProtocolVersion(CccParams.PROTOCOL_VERSION_1_0); + if (j.has("sessionId")) { + builder.setSessionId(j.getInt("sessionId")); + } + if (j.has("uwbConfig")) { + builder.setUwbConfig(j.getInt("uwbConfig")); + } + if (j.has("ranMultiplier")) { + builder.setRanMultiplier(j.getInt("ranMultiplier")); + } + if (j.has("channel")) { + builder.setChannel(j.getInt("channel")); + } + if (j.has("chapsPerSlot")) { + builder.setNumChapsPerSlot(j.getInt("chapsPerSlot")); + } + if (j.has("responderNodes")) { + builder.setNumResponderNodes(j.getInt("responderNodes")); + } + if (j.has("slotsPerRound")) { + builder.setNumSlotsPerRound(j.getInt("slotsPerRound")); + } + if (j.has("hoppingMode")) { + builder.setHoppingConfigMode(j.getInt("hoppingMode")); + } + if (j.has("hoppingSequence")) { + builder.setHoppingSequence(j.getInt("hoppingSequence")); + } + if (j.has("syncCodeIndex")) { + builder.setSyncCodeIndex(j.getInt("syncCodeIndex")); + } + if (j.has("pulseShapeCombo")) { + JSONObject pulseShapeCombo = j.getJSONObject("pulseShapeCombo"); + builder.setPulseShapeCombo(new CccPulseShapeCombo( + pulseShapeCombo.getInt("pulseShapeComboTx"), + pulseShapeCombo.getInt("pulseShapeComboRx"))); + } + + return builder.build(); + } + private FiraOpenSessionParams generateFiraOpenSessionParams(JSONObject j) throws JSONException { if (j == null) { return null; @@ -355,7 +430,7 @@ public class UwbManagerFacade extends RpcReceiver { builder.setDestAddressList(Arrays.asList(destinationUwbAddresses)); } if (j.has("initiationTimeMs")) { - builder.setInitiationTimeMs(j.getInt("initiationTimeMs")); + builder.setInitiationTime(j.getInt("initiationTimeMs")); } if (j.has("slotDurationRstu")) { builder.setSlotDurationRstu(j.getInt("slotDurationRstu")); @@ -424,6 +499,22 @@ public class UwbManagerFacade extends RpcReceiver { } /** + * Open CCC UWB ranging session. + */ + @Rpc(description = "Open CCC UWB ranging session") + public String openCccRangingSession(@RpcParameter(name = "config") JSONObject config) + throws JSONException { + RangingSessionCallback rangingSessionCallback = new RangingSessionCallback( + Event.EventAll.getType()); + CccOpenRangingParams params = generateCccOpenRangingParams(config); + CancellationSignal cancellationSignal = mUwbManager.openRangingSession( + params.toBundle(), mExecutor, rangingSessionCallback); + String key = rangingSessionCallback.mId; + sRangingSessionCallbackMap.put(key, rangingSessionCallback); + return key; + } + + /** * Start UWB ranging. */ @Rpc(description = "Start UWB ranging") @@ -433,6 +524,16 @@ public class UwbManagerFacade extends RpcReceiver { } /** + * Start CCC UWB ranging. + */ + @Rpc(description = "Start CCC UWB ranging") + public void startCccRangingSession(String key, JSONObject config) throws JSONException { + RangingSessionCallback rangingSessionCallback = sRangingSessionCallbackMap.get(key); + CccRangingStartedParams params = generateCccRangingStartedParams(config); + rangingSessionCallback.rangingSession.start(params.toBundle()); + } + + /** * Reconfigures UWB ranging session. */ @Rpc(description = "Reconfigure UWB ranging session") diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiAwareManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiAwareManagerFacade.java index 7586039a..85a72a76 100644 --- a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiAwareManagerFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiAwareManagerFacade.java @@ -413,7 +413,7 @@ public class WifiAwareManagerFacade extends RpcReceiver { new AwareAttachCallbackPostsEvents(sessionId, useIdInCallbackEventName), (identityCb != null && identityCb.booleanValue()) ? new AwareIdentityChangeListenerPostsEvents(sessionId, - useIdInCallbackEventName) : null); + useIdInCallbackEventName) : null, false, null); return sessionId; } } diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java index 0b1318f1..2bc1f228 100755 --- a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java @@ -2154,7 +2154,7 @@ public class WifiManagerFacade extends RpcReceiver { if (bandList != null) { // Build a JSON array of bands represented as operating classes - Log.d("onFailure list of supported bands: " + bandList); + Log.d("onFailure list of supported bands: " + Arrays.toString(bandList)); JSONArray formattedBandList = new JSONArray(); for (int i = 0; i < bandList.length; i++) { formattedBandList.put(bandList[i]); diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java index bea4a824..300c98ce 100644 --- a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java +++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java @@ -27,7 +27,6 @@ import android.net.wifi.WifiScanner.ScanData; import android.net.wifi.WifiScanner.ScanSettings; import android.os.Bundle; import android.os.SystemClock; -import android.provider.Settings.Global; import android.provider.Settings.SettingNotFoundException; import com.googlecode.android_scripting.Log; @@ -400,7 +399,7 @@ public class WifiScannerFacade extends RpcReceiver { @RpcParameter(name = "scanSettings") JSONObject scanSettings) throws JSONException { ScanSettings ss = parseScanSettings(scanSettings); - Log.d("startWifiScannerScan with " + ss.channels); + Log.d("startWifiScannerScan with " + Arrays.toString(ss.channels)); WifiScanListener listener = genBackgroundWifiScanListener(); mScan.startBackgroundScan(ss, listener); return listener.mIndex; @@ -450,7 +449,7 @@ public class WifiScannerFacade extends RpcReceiver { @RpcParameter(name = "scanSettings") JSONObject scanSettings) throws JSONException { ScanSettings ss = parseScanSettings(scanSettings); - Log.d("startWifiScannerScan with " + ss.channels); + Log.d("startWifiScannerScan with " + Arrays.toString(ss.channels)); WifiScanListener listener = genWifiScanListener(); mScan.startScan(ss, listener); return listener.mIndex; diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java index bb90c69a..aead5979 100644 --- a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java +++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java @@ -1085,7 +1085,7 @@ public class JsonBuilder { msg.put("iccId", data.getIccId()); msg.put("simSlotIndex", data.getSimSlotIndex()); msg.put("displayName", data.getDisplayName()); - msg.put("nameSource", data.getNameSource()); + msg.put("nameSource", data.getDisplayNameSource()); msg.put("iconTint", data.getIconTint()); msg.put("number", data.getNumber()); msg.put("dataRoaming", data.getDataRoaming()); @@ -7,6 +7,7 @@ gmoturu@google.com jaineelm@google.com jpawlowski@google.com krisr@google.com +rahulsabnis@google.com siyuanh@google.com tturney@google.com xianyuanjia@google.com diff --git a/ScriptingLayerForAndroid/AndroidManifest.xml b/ScriptingLayerForAndroid/AndroidManifest.xml index de7442df..e937bcff 100644 --- a/ScriptingLayerForAndroid/AndroidManifest.xml +++ b/ScriptingLayerForAndroid/AndroidManifest.xml @@ -132,6 +132,7 @@ android:icon="@drawable/sl4a_logo_48" android:label="@string/application_title" android:name=".Sl4aApplication" + android:testOnly="true" android:theme="@android:style/Theme.DeviceDefault" android:usesCleartextTraffic="true"> <activity android:name=".activity.ScriptManager" android:configChanges="keyboardHidden|orientation" android:windowSoftInputMode="adjustResize" android:launchMode="singleTop" android:exported="true"> |