Snap for 10447354 from e72d0b88c6b4dacf2789f39b418e95829a8f9145 to mainline-cellbroadcast-release

Change-Id: I212428646539ce28a90ad924a5b40f41d6d373e4
diff --git a/Common/Android.bp b/Common/Android.bp
index 16f0331..41154b2 100644
--- a/Common/Android.bp
+++ b/Common/Android.bp
@@ -34,10 +34,12 @@
         "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 @@
         "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 6ec89ed..3e9aa05 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 java.time.ZonedDateTime;
 import java.util.Locale;
+import java.util.Set;
 
 /**
  * DataUsageController.
@@ -87,7 +91,8 @@
         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 @@
         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 @@
      * @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 0a7223c..a7b514d 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 @@
                                   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 60e9797..33a667e 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 @@
 
     @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 @@
     }
 
     /**
-     * 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 @@
                 .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 @@
             @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 ba61ded..bc73e6a 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 @@
 
     // 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 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 24f0b08..6829c90 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.RpcDefault;
 import com.googlecode.android_scripting.rpc.RpcParameter;
 
+import java.util.Arrays;
 import java.util.List;
 
 /*
@@ -122,9 +123,9 @@
                 }
                 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 18ca858..49efe6c 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.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 @@
     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 @@
         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 @@
                 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 e228672..176dd30 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 @@
     }
 
     /**
+     * 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 0000000..bd5fd5f
--- /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 0e1e8ca..1629217 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.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 @@
 
     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 @@
         }
     }
 
+    /** 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 @@
         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 @@
          */
         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 @@
 
         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 @@
         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 0000000..f708f46
--- /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 0000000..3b4adb6
--- /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 1de1716..1c8724f 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 @@
     }
 
     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 d5e9633..591542f 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 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 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 e60e5f1..f68647a 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.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 @@
     @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 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 @@
         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 void telephonySetCellInfoListRate(
                 @RpcParameter(name = "rate") Integer rate
             ) {
-        mTelephonyManager.setCellInfoListRate(rate);
+        mTelephonyManager.setCellInfoListRate(rate, SubscriptionManager.getDefaultSubscriptionId());
     }
 
     /**
@@ -1513,6 +1630,53 @@
     }
 
     /**
+     * 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 5ca7ff8..03a9a29 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.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 @@
         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 @@
             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 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 7586039..85a72a7 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 @@
                     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 0b1318f..2bc1f22 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 @@
 
             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 bea4a82..300c98c 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.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 @@
             @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 @@
             @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 bb90c69..aead597 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 @@
         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());
diff --git a/OWNERS b/OWNERS
index f4dea96..367c156 100644
--- a/OWNERS
+++ b/OWNERS
@@ -7,6 +7,7 @@
 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 de7442d..e937bcf 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">