Merge "Remove libhwbinder/libhidltransport deps"
diff --git a/api/system-current.txt b/api/system-current.txt
index eb7ed6e..8c222b6 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8129,7 +8129,9 @@
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public long getPreferredNetworkTypeBitmask();
method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public int getRadioPowerState();
method public int getSimApplicationState();
+ method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimApplicationState(int);
method public int getSimCardState();
+ method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimCardState(int);
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Locale getSimLocale();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public long getSupportedRadioAccessFamily();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms();
@@ -9238,7 +9240,7 @@
field public static final int STATE_UNAVAILABLE = 0; // 0x0
}
- public static class ImsFeature.Capabilities {
+ @Deprecated public static class ImsFeature.Capabilities {
field @Deprecated protected int mCapabilities;
}
@@ -9272,7 +9274,7 @@
public static class MmTelFeature.MmTelCapabilities extends android.telephony.ims.feature.ImsFeature.Capabilities {
ctor public MmTelFeature.MmTelCapabilities();
ctor @Deprecated public MmTelFeature.MmTelCapabilities(android.telephony.ims.feature.ImsFeature.Capabilities);
- ctor public MmTelFeature.MmTelCapabilities(int);
+ ctor public MmTelFeature.MmTelCapabilities(@android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability int);
method public final void addCapabilities(@android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability int);
method public final boolean isCapable(@android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability int);
method public final void removeCapabilities(@android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.MmTelCapability int);
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 8987b23..f0b3546 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -883,48 +883,6 @@
}
}
- // /apex/com.android.art/lib, /vendor/lib, /odm/lib and /product/lib
- // are added to the native lib search paths of the classloader.
- // Note that this is done AFTER the classloader is
- // created by ApplicationLoaders.getDefault().getClassLoader(...). The
- // reason is because if we have added the paths when creating the classloader
- // above, the paths are also added to the search path of the linker namespace
- // 'classloader-namespace', which will allow ALL libs in the paths to apps.
- // Since only the libs listed in <partition>/etc/public.libraries.txt can be
- // available to apps, we shouldn't add the paths then.
- //
- // However, we need to add the paths to the classloader (Java) though. This
- // is because when a native lib is requested via System.loadLibrary(), the
- // classloader first tries to find the requested lib in its own native libs
- // search paths. If a lib is not found in one of the paths, dlopen() is not
- // called at all. This can cause a problem that a vendor public native lib
- // is accessible when directly opened via dlopen(), but inaccesible via
- // System.loadLibrary(). In order to prevent the problem, we explicitly
- // add the paths only to the classloader, and not to the native loader
- // (linker namespace).
- List<String> extraLibPaths = new ArrayList<>(4);
- String abiSuffix = VMRuntime.getRuntime().is64Bit() ? "64" : "";
- if (!defaultSearchPaths.contains("/apex/com.android.art/lib")) {
- extraLibPaths.add("/apex/com.android.art/lib" + abiSuffix);
- }
- if (!defaultSearchPaths.contains("/vendor/lib")) {
- extraLibPaths.add("/vendor/lib" + abiSuffix);
- }
- if (!defaultSearchPaths.contains("/odm/lib")) {
- extraLibPaths.add("/odm/lib" + abiSuffix);
- }
- if (!defaultSearchPaths.contains("/product/lib")) {
- extraLibPaths.add("/product/lib" + abiSuffix);
- }
- if (!extraLibPaths.isEmpty()) {
- StrictMode.ThreadPolicy oldPolicy = allowThreadDiskReads();
- try {
- ApplicationLoaders.getDefault().addNative(mDefaultClassLoader, extraLibPaths);
- } finally {
- setThreadPolicy(oldPolicy);
- }
- }
-
if (addedPaths != null && addedPaths.size() > 0) {
final String add = TextUtils.join(File.pathSeparator, addedPaths);
ApplicationLoaders.getDefault().addPath(mDefaultClassLoader, add);
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 8355e08..2a4576a 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -581,12 +581,16 @@
public static ParcelFileDescriptor fromData(byte[] data, String name) throws IOException {
if (data == null) return null;
MemoryFile file = new MemoryFile(name, data.length);
- if (data.length > 0) {
- file.writeBytes(data, 0, 0, data.length);
+ try {
+ if (data.length > 0) {
+ file.writeBytes(data, 0, 0, data.length);
+ }
+ file.deactivate();
+ FileDescriptor fd = file.getFileDescriptor();
+ return fd != null ? ParcelFileDescriptor.dup(fd) : null;
+ } finally {
+ file.close();
}
- file.deactivate();
- FileDescriptor fd = file.getFileDescriptor();
- return fd != null ? ParcelFileDescriptor.dup(fd) : null;
}
/**
diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java
index f8b38e9..07cecd3 100644
--- a/core/java/android/util/TimeUtils.java
+++ b/core/java/android/util/TimeUtils.java
@@ -62,9 +62,9 @@
}
/**
- * Tries to return a frozen ICU time zone that would have had the specified offset
- * and DST value at the specified moment in the specified country.
- * Returns null if no suitable zone could be found.
+ * Returns a frozen ICU time zone that has / would have had the specified offset and DST value
+ * at the specified moment in the specified country. Returns null if no suitable zone could be
+ * found.
*/
private static android.icu.util.TimeZone getIcuTimeZone(
int offset, boolean dst, long when, String country) {
@@ -73,8 +73,15 @@
}
android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault();
- return TimeZoneFinder.getInstance()
- .lookupTimeZoneByCountryAndOffset(country, offset, dst, when, bias);
+ CountryTimeZones countryTimeZones =
+ TimeZoneFinder.getInstance().lookupCountryTimeZones(country);
+ if (countryTimeZones == null) {
+ return null;
+ }
+
+ CountryTimeZones.OffsetResult offsetResult =
+ countryTimeZones.lookupByOffsetWithBias(offset, dst, when, bias);
+ return offsetResult != null ? offsetResult.mTimeZone : null;
}
/**
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index ad53eb9..c24ffb0 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -479,6 +479,10 @@
closeSocket();
+ if (parsedArgs.mNiceName != null) {
+ Process.setArgV0(parsedArgs.mNiceName);
+ }
+
// End of the postFork event.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
if (parsedArgs.mInvokeWith != null) {
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 2d7ce1a..9ca90e0 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -37,6 +37,7 @@
import android.os.UserHandle;
import android.os.ZygoteProcess;
import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
import android.security.keystore.AndroidKeyStoreProvider;
import android.system.ErrnoException;
import android.system.Os;
@@ -459,6 +460,16 @@
ZygoteHooks.gcAndFinalize();
}
+ private static boolean shouldProfileSystemServer() {
+ boolean defaultValue = SystemProperties.getBoolean("dalvik.vm.profilesystemserver",
+ /*default=*/ false);
+ // Can't use DeviceConfig since it's not initialized at this point.
+ return SystemProperties.getBoolean(
+ "persist.device_config." + DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT
+ + ".profilesystemserver",
+ defaultValue);
+ }
+
/**
* Finish remaining work for the newly forked system server process.
*/
@@ -481,10 +492,9 @@
}
// Capturing profiles is only supported for debug or eng builds since selinux normally
// prevents it.
- boolean profileSystemServer = SystemProperties.getBoolean(
- "dalvik.vm.profilesystemserver", false);
- if (profileSystemServer && (Build.IS_USERDEBUG || Build.IS_ENG)) {
+ if (shouldProfileSystemServer() && (Build.IS_USERDEBUG || Build.IS_ENG)) {
try {
+ Log.d(TAG, "Preparing system server profile");
prepareSystemServerProfile(systemServerClasspath);
} catch (Exception e) {
Log.wtf(TAG, "Failed to set up system server profile", e);
@@ -755,9 +765,7 @@
Zygote.applyDebuggerSystemProperty(parsedArgs);
Zygote.applyInvokeWithSystemProperty(parsedArgs);
- boolean profileSystemServer = SystemProperties.getBoolean(
- "dalvik.vm.profilesystemserver", false);
- if (profileSystemServer) {
+ if (shouldProfileSystemServer()) {
parsedArgs.mRuntimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;
}
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 2d7069c..f929dde 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -644,6 +644,12 @@
return si.totalram;
}
+/*
+ * The outFields array is initialized to -1 to allow the caller to identify
+ * when the status file (and therefore the process) they specified is invalid.
+ * This array should not be overwritten or cleared before we know that the
+ * status file can be read.
+ */
void android_os_Process_readProcLines(JNIEnv* env, jobject clazz, jstring fileStr,
jobjectArray reqFields, jlongArray outFields)
{
@@ -692,14 +698,14 @@
return;
}
- //ALOGI("Clearing %" PRId32 " sizes", count);
- for (i=0; i<count; i++) {
- sizesArray[i] = 0;
- }
-
int fd = open(file.string(), O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
+ //ALOGI("Clearing %" PRId32 " sizes", count);
+ for (i=0; i<count; i++) {
+ sizesArray[i] = 0;
+ }
+
const size_t BUFFER_SIZE = 4096;
char* buffer = (char*)malloc(BUFFER_SIZE);
int len = read(fd, buffer, BUFFER_SIZE-1);
diff --git a/core/tests/coretests/src/android/os/ProcessTest.java b/core/tests/coretests/src/android/os/ProcessTest.java
index b749e71..6b02764 100644
--- a/core/tests/coretests/src/android/os/ProcessTest.java
+++ b/core/tests/coretests/src/android/os/ProcessTest.java
@@ -17,13 +17,12 @@
package android.os;
-import androidx.test.filters.MediumTest;
-
import junit.framework.TestCase;
public class ProcessTest extends TestCase {
- @MediumTest
+ private static final int BAD_PID = 0;
+
public void testProcessGetUidFromName() throws Exception {
assertEquals(android.os.Process.SYSTEM_UID, Process.getUidForName("system"));
assertEquals(Process.BLUETOOTH_UID, Process.getUidForName("bluetooth"));
@@ -35,7 +34,6 @@
Process.getUidForName("u3_a100"));
}
- @MediumTest
public void testProcessGetUidFromNameFailure() throws Exception {
// Failure cases
assertEquals(-1, Process.getUidForName("u2a_foo"));
@@ -47,4 +45,29 @@
assertEquals(-1, Process.getUidForName("u2jhsajhfkjhsafkhskafhkashfkjashfkjhaskjfdhakj3"));
}
+ /**
+ * Tests getUidForPid() by ensuring that it returns the correct value when the process queried
+ * doesn't exist.
+ */
+ public void testGetUidForPidInvalidPid() {
+ assertEquals(-1, Process.getUidForPid(BAD_PID));
+ }
+
+ /**
+ * Tests getParentPid() by ensuring that it returns the correct value when the process queried
+ * doesn't exist.
+ */
+ public void testGetParentPidInvalidPid() {
+ assertEquals(-1, Process.getParentPid(BAD_PID));
+ }
+
+ /**
+ * Tests getThreadGroupLeader() by ensuring that it returns the correct value when the
+ * thread queried doesn't exist.
+ */
+ public void testGetThreadGroupLeaderInvalidTid() {
+ // This function takes a TID instead of a PID but BAD_PID should also be a bad TID.
+ assertEquals(-1, Process.getThreadGroupLeader(BAD_PID));
+ }
+
}
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 2c2013c..884e7a5 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -19,9 +19,6 @@
"exclude-filter": "android.app.cts.AlarmManagerTest#testSetRepeating"
},
{
- "exclude-filter": "android.app.cts.ServiceTest#testAppZygoteServices"
- },
- {
"exclude-filter": "android.app.cts.SystemFeaturesTest#testLocationFeatures"
},
{
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 884ecba..6010b1dc 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -15,9 +15,6 @@
*/
package com.android.server.audio;
-import static com.android.server.audio.AudioService.CONNECTION_STATE_CONNECTED;
-import static com.android.server.audio.AudioService.CONNECTION_STATE_DISCONNECTED;
-
import android.annotation.NonNull;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice;
@@ -40,9 +37,11 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
+import android.util.PrintWriterPrinter;
import com.android.internal.annotations.GuardedBy;
+import java.io.PrintWriter;
/** @hide */
/*package*/ final class AudioDeviceBroker {
@@ -93,13 +92,28 @@
/*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
mContext = context;
mAudioService = service;
- setupMessaging(context);
mBtHelper = new BtHelper(this);
mDeviceInventory = new AudioDeviceInventory(this);
+ init();
+ }
+
+ /** for test purposes only, inject AudioDeviceInventory */
+ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
+ @NonNull AudioDeviceInventory mockDeviceInventory) {
+ mContext = context;
+ mAudioService = service;
+ mBtHelper = new BtHelper(this);
+ mDeviceInventory = mockDeviceInventory;
+
+ init();
+ }
+
+ private void init() {
+ setupMessaging(mContext);
+
mForcedUseForComm = AudioSystem.FORCE_NONE;
mForcedUseForCommExt = mForcedUseForComm;
-
}
/*package*/ Context getContext() {
@@ -123,6 +137,8 @@
/*package*/ void onAudioServerDied() {
// Restore forced usage for communications and record
synchronized (mDeviceStateLock) {
+ AudioSystem.setParameters(
+ "BT_SCO=" + (mForcedUseForComm == AudioSystem.FORCE_BT_SCO ? "on" : "off"));
onSetForceUse(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, "onAudioServerDied");
onSetForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm, "onAudioServerDied");
}
@@ -228,17 +244,42 @@
mSupprNoisy = suppressNoisyIntent;
mVolume = vol;
}
+
+ // redefine equality op so we can match messages intended for this device
+ @Override
+ public boolean equals(Object o) {
+ return mDevice.equals(o);
+ }
}
+
/*package*/ void postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
@NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
int profile, boolean suppressNoisyIntent, int a2dpVolume) {
final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
suppressNoisyIntent, a2dpVolume);
- // TODO add a check to try to remove unprocessed messages for the same device (the old
- // check didn't work), and make sure it doesn't conflict with config change message
- sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
+ // when receiving a request to change the connection state of a device, this last request
+ // is the source of truth, so cancel all previous requests
+ removeAllA2dpConnectionEvents(device);
+
+ sendLMsgNoDelay(
+ state == BluetoothProfile.STATE_CONNECTED
+ ? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
+ : MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+ SENDMSG_QUEUE, info);
+ }
+
+ /** remove all previously scheduled connection and disconnection events for the given device */
+ private void removeAllA2dpConnectionEvents(@NonNull BluetoothDevice device) {
+ mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+ device);
+ mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
+ device);
+ mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+ device);
+ mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ device);
}
private static final class HearingAidDeviceConnectionInfo {
@@ -426,13 +467,16 @@
sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
}
- /*package*/ void postA2dpSinkConnection(int state,
+ /*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
- sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE,
+ sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
+ ? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
+ : MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ SENDMSG_QUEUE,
state, btDeviceInfo, delay);
}
- /*package*/ void postA2dpSourceConnection(int state,
+ /*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
state, btDeviceInfo, delay);
@@ -518,25 +562,6 @@
}
}
- @GuardedBy("mDeviceStateLock")
- /*package*/ void handleSetA2dpSinkConnectionState(@BluetoothProfile.BtProfileState int state,
- @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
- final int intState = (state == BluetoothA2dp.STATE_CONNECTED)
- ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;
- final int delay = mDeviceInventory.checkSendBecomingNoisyIntent(
- AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, intState,
- AudioSystem.DEVICE_NONE);
- final String addr = btDeviceInfo == null ? "null" : btDeviceInfo.getBtDevice().getAddress();
-
- if (AudioService.DEBUG_DEVICES) {
- Log.d(TAG, "handleSetA2dpSinkConnectionState btDevice= " + btDeviceInfo
- + " state= " + state
- + " is dock: " + btDeviceInfo.getBtDevice().isBluetoothDock());
- }
- sendILMsg(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE, SENDMSG_QUEUE,
- state, btDeviceInfo, delay);
- }
-
/*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
@@ -571,8 +596,10 @@
// must be called synchronized on mConnectedDevices
/*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
- return mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE,
- new BtHelper.BluetoothA2dpDeviceInfo(btDevice));
+ return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+ new BtHelper.BluetoothA2dpDeviceInfo(btDevice))
+ || mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ new BtHelper.BluetoothA2dpDeviceInfo(btDevice)));
}
/*package*/ void setA2dpDockTimeout(String address, int a2dpCodec, int delayMs) {
@@ -597,6 +624,15 @@
}
}
+ /*package*/ void dump(PrintWriter pw, String prefix) {
+ if (mBrokerHandler != null) {
+ pw.println(prefix + "Message handler (watch for unhandled messages):");
+ mBrokerHandler.dump(new PrintWriterPrinter(pw), prefix + " ");
+ } else {
+ pw.println("Message handler is null");
+ }
+ }
+
//---------------------------------------------------------------------
// Internal handling of messages
// These methods are ALL synchronous, in response to message handling in BrokerHandler
@@ -698,7 +734,8 @@
mDeviceInventory.onReportNewRoutes();
}
break;
- case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
synchronized (mDeviceStateLock) {
mDeviceInventory.onSetA2dpSinkConnectionState(
(BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
@@ -823,7 +860,8 @@
}
}
break;
- case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: {
+ case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
+ case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION: {
final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
"setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent "
@@ -874,7 +912,7 @@
private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3;
private static final int MSG_IIL_SET_FORCE_USE = 4;
private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
- private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE = 6;
+ private static final int MSG_TOGGLE_HDMI = 6;
private static final int MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE = 7;
private static final int MSG_IL_SET_HEARING_AID_CONNECTION_STATE = 8;
private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
@@ -885,7 +923,6 @@
private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
private static final int MSG_I_DISCONNECT_BT_SCO = 16;
- private static final int MSG_TOGGLE_HDMI = 17;
private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE = 18;
private static final int MSG_DISCONNECT_A2DP = 19;
private static final int MSG_DISCONNECT_A2DP_SINK = 20;
@@ -895,25 +932,30 @@
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK = 24;
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID = 25;
private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET = 26;
+ private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED = 27;
+ private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED = 28;
// process external command to (dis)connect an A2DP device
- private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27;
+ private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION = 29;
+ private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION = 30;
// process external command to (dis)connect a hearing aid device
- private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28;
+ private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
// a ScoClient died in BtHelper
- private static final int MSG_L_SCOCLIENT_DIED = 29;
+ private static final int MSG_L_SCOCLIENT_DIED = 32;
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
- case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
case MSG_IL_BTA2DP_DOCK_TIMEOUT:
case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
case MSG_TOGGLE_HDMI:
case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
- case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
+ case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
+ case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION:
case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
return true;
default:
@@ -994,7 +1036,8 @@
switch (msg) {
case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
- case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
case MSG_IL_BTA2DP_DOCK_TIMEOUT:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index a9a8ef2..90973a8 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -41,14 +41,16 @@
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
/**
* Class to manage the inventory of all connected devices.
* This class is thread-safe.
+ * (non final for mocking/spying)
*/
-public final class AudioDeviceInventory {
+public class AudioDeviceInventory {
private static final String TAG = "AS.AudioDeviceInventory";
@@ -56,11 +58,7 @@
// Key for map created from DeviceInfo.makeDeviceListKey()
private final ArrayMap<String, DeviceInfo> mConnectedDevices = new ArrayMap<>();
- private final @NonNull AudioDeviceBroker mDeviceBroker;
-
- AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
- mDeviceBroker = broker;
- }
+ private @NonNull AudioDeviceBroker mDeviceBroker;
// cache of the address of the last dock the device was connected to
private String mDockAddress;
@@ -70,6 +68,20 @@
final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
new RemoteCallbackList<IAudioRoutesObserver>();
+ /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
+ mDeviceBroker = broker;
+ }
+
+ //-----------------------------------------------------------
+ /** for mocking only */
+ /*package*/ AudioDeviceInventory() {
+ mDeviceBroker = null;
+ }
+
+ /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
+ mDeviceBroker = broker;
+ }
+
//------------------------------------------------------------
/**
* Class to store info about connected devices.
@@ -146,8 +158,10 @@
}
}
+ // only public for mocking/spying
@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
- /*package*/ void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
+ @VisibleForTesting
+ public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
@AudioService.BtProfileConnectionState int state) {
final BluetoothDevice btDevice = btInfo.getBtDevice();
int a2dpVolume = btInfo.getVolume();
@@ -159,30 +173,40 @@
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
address = "";
}
- AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
- "A2DP sink connected: device addr=" + address + " state=" + state
- + " vol=" + a2dpVolume));
final int a2dpCodec = btInfo.getCodec();
+ AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
+ "A2DP sink connected: device addr=" + address + " state=" + state
+ + " codec=" + a2dpCodec
+ + " vol=" + a2dpVolume));
+
synchronized (mConnectedDevices) {
final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
btDevice.getAddress());
final DeviceInfo di = mConnectedDevices.get(key);
boolean isConnected = di != null;
- if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
- if (btDevice.isBluetoothDock()) {
- if (state == BluetoothProfile.STATE_DISCONNECTED) {
- // introduction of a delay for transient disconnections of docks when
- // power is rapidly turned off/on, this message will be canceled if
- // we reconnect the dock under a preset delay
- makeA2dpDeviceUnavailableLater(address,
- AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
- // the next time isConnected is evaluated, it will be false for the dock
+ if (isConnected) {
+ if (state == BluetoothProfile.STATE_CONNECTED) {
+ // device is already connected, but we are receiving a connection again,
+ // it could be for a codec change
+ if (a2dpCodec != di.mDeviceCodecFormat) {
+ mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice);
}
} else {
- makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+ if (btDevice.isBluetoothDock()) {
+ if (state == BluetoothProfile.STATE_DISCONNECTED) {
+ // introduction of a delay for transient disconnections of docks when
+ // power is rapidly turned off/on, this message will be canceled if
+ // we reconnect the dock under a preset delay
+ makeA2dpDeviceUnavailableLater(address,
+ AudioDeviceBroker.BTA2DP_DOCK_TIMEOUT_MS);
+ // the next time isConnected is evaluated, it will be false for the dock
+ }
+ } else {
+ makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+ }
}
} else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
if (btDevice.isBluetoothDock()) {
@@ -282,11 +306,9 @@
+ " event=" + BtHelper.a2dpDeviceEventToString(event)));
synchronized (mConnectedDevices) {
- //TODO original CL is not consistent between BluetoothDevice and BluetoothA2dpDeviceInfo
- // for this type of message
if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
- "A2dp config change ignored"));
+ "A2dp config change ignored (scheduled connection change)"));
return;
}
final String key = DeviceInfo.makeDeviceListKey(
@@ -534,8 +556,10 @@
return mCurAudioRoutes;
}
+ // only public for mocking/spying
@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
- /*package*/ void setBluetoothA2dpDeviceConnectionState(
+ @VisibleForTesting
+ public void setBluetoothA2dpDeviceConnectionState(
@NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
int delay;
@@ -544,9 +568,12 @@
}
synchronized (mConnectedDevices) {
if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
- int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
+ @AudioService.ConnectionState int asState =
+ (state == BluetoothA2dp.STATE_CONNECTED)
+ ? AudioService.CONNECTION_STATE_CONNECTED
+ : AudioService.CONNECTION_STATE_DISCONNECTED;
delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
- intState, musicDevice);
+ asState, musicDevice);
} else {
delay = 0;
}
@@ -785,7 +812,7 @@
return 0;
}
mDeviceBroker.postBroadcastBecomingNoisy();
- delay = 1000;
+ delay = AudioService.BECOMING_NOISY_DELAY_MS;
}
return delay;
@@ -943,4 +970,21 @@
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels);
}
}
+
+ //----------------------------------------------------------
+ // For tests only
+
+ /**
+ * Check if device is in the list of connected devices
+ * @param device
+ * @return true if connected
+ */
+ @VisibleForTesting
+ public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) {
+ final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+ device.getAddress());
+ synchronized (mConnectedDevices) {
+ return (mConnectedDevices.get(key) != null);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 5628f94..5bc2261 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -54,8 +54,6 @@
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
import android.database.ContentObserver;
import android.hardware.hdmi.HdmiAudioSystemClient;
import android.hardware.hdmi.HdmiControlManager;
@@ -82,11 +80,7 @@
import android.media.IVolumeController;
import android.media.MediaExtractor;
import android.media.MediaFormat;
-import android.media.MediaPlayer;
-import android.media.MediaPlayer.OnCompletionListener;
-import android.media.MediaPlayer.OnErrorListener;
import android.media.PlayerBase;
-import android.media.SoundPool;
import android.media.VolumePolicy;
import android.media.audiofx.AudioEffect;
import android.media.audiopolicy.AudioMix;
@@ -102,7 +96,6 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -127,6 +120,7 @@
import android.util.IntArray;
import android.util.Log;
import android.util.MathUtils;
+import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseIntArray;
import android.view.KeyEvent;
@@ -134,9 +128,9 @@
import android.widget.Toast;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
-import com.android.internal.util.XmlUtils;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -145,15 +139,11 @@
import com.android.server.pm.UserManagerService;
import com.android.server.wm.ActivityTaskManagerInternal;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -201,6 +191,13 @@
private static final int UNMUTE_STREAM_DELAY = 350;
/**
+ * Delay before disconnecting a device that would cause BECOMING_NOISY intent to be sent,
+ * to give a chance to applications to pause.
+ */
+ @VisibleForTesting
+ public static final int BECOMING_NOISY_DELAY_MS = 1000;
+
+ /**
* Only used in the result from {@link #checkForRingerModeChange(int, int, int)}
*/
private static final int FLAG_ADJUST_VOLUME = 1;
@@ -293,19 +290,6 @@
// protects mRingerMode
private final Object mSettingsLock = new Object();
- private SoundPool mSoundPool;
- private final Object mSoundEffectsLock = new Object();
- private static final int NUM_SOUNDPOOL_CHANNELS = 4;
-
- /* Sound effect file names */
- private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/";
- private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
-
- /* Sound effect file name mapping sound effect id (AudioManager.FX_xxx) to
- * file index in SOUND_EFFECT_FILES[] (first column) and indicating if effect
- * uses soundpool (second column) */
- private final int[][] SOUND_EFFECT_FILES_MAP = new int[AudioManager.NUM_SOUND_EFFECTS][2];
-
/** Maximum volume index values for audio streams */
protected static int[] MAX_STREAM_VOLUME = new int[] {
5, // STREAM_VOICE_CALL
@@ -452,6 +436,9 @@
* @see System#MUTE_STREAMS_AFFECTED */
private int mMuteAffectedStreams;
+ @NonNull
+ private SoundEffectsHelper mSfxHelper;
+
/**
* NOTE: setVibrateSetting(), getVibrateSetting(), shouldVibrate() are deprecated.
* mVibrateSetting is just maintained during deprecation period but vibration policy is
@@ -492,14 +479,6 @@
private boolean mSystemReady;
// true if Intent.ACTION_USER_SWITCHED has ever been received
private boolean mUserSwitchedReceived;
- // listener for SoundPool sample load completion indication
- private SoundPoolCallback mSoundPoolCallBack;
- // thread for SoundPool listener
- private SoundPoolListenerThread mSoundPoolListenerThread;
- // message looper for SoundPool listener
- private Looper mSoundPoolLooper = null;
- // volume applied to sound played with playSoundEffect()
- private static int sSoundEffectVolumeDb;
// previous volume adjustment direction received by checkForRingerModeChange()
private int mPrevVolDirection = AudioManager.ADJUST_SAME;
// mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume
@@ -641,6 +620,8 @@
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
+ mSfxHelper = new SoundEffectsHelper(mContext);
+
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator();
@@ -731,9 +712,6 @@
MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM];
}
- sSoundEffectVolumeDb = context.getResources().getInteger(
- com.android.internal.R.integer.config_soundEffectVolumeDb);
-
createAudioSystemThread();
AudioSystem.setErrorCallback(mAudioSystemCallback);
@@ -1916,8 +1894,8 @@
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
final long ident = Binder.clearCallingIdentity();
try {
- mHdmiPlaybackClient.sendKeyEvent(keyCode, true);
- mHdmiPlaybackClient.sendKeyEvent(keyCode, false);
+ mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, true);
+ mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, false);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2542,15 +2520,11 @@
mVolumeController.postVolumeChanged(streamType, flags);
}
- // If Hdmi-CEC system audio mode is on, we show volume bar only when TV
- // receives volume notification from Audio Receiver.
+ // If Hdmi-CEC system audio mode is on and we are a TV panel, never show volume bar.
private int updateFlagsForTvPlatform(int flags) {
synchronized (mHdmiClientLock) {
- if (mHdmiTvClient != null) {
- if (mHdmiSystemAudioSupported &&
- ((flags & AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME) == 0)) {
- flags &= ~AudioManager.FLAG_SHOW_UI;
- }
+ if (mHdmiTvClient != null && mHdmiSystemAudioSupported) {
+ flags &= ~AudioManager.FLAG_SHOW_UI;
}
}
return flags;
@@ -3372,104 +3346,30 @@
//==========================================================================================
// Sound Effects
//==========================================================================================
+ private static final class LoadSoundEffectReply
+ implements SoundEffectsHelper.OnEffectsLoadCompleteHandler {
+ private static final int SOUND_EFFECTS_LOADING = 1;
+ private static final int SOUND_EFFECTS_LOADED = 0;
+ private static final int SOUND_EFFECTS_ERROR = -1;
+ private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000;
- private static final String TAG_AUDIO_ASSETS = "audio_assets";
- private static final String ATTR_VERSION = "version";
- private static final String TAG_GROUP = "group";
- private static final String ATTR_GROUP_NAME = "name";
- private static final String TAG_ASSET = "asset";
- private static final String ATTR_ASSET_ID = "id";
- private static final String ATTR_ASSET_FILE = "file";
+ private int mStatus = SOUND_EFFECTS_LOADING;
- private static final String ASSET_FILE_VERSION = "1.0";
- private static final String GROUP_TOUCH_SOUNDS = "touch_sounds";
-
- private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000;
-
- class LoadSoundEffectReply {
- public int mStatus = 1;
- };
-
- private void loadTouchSoundAssetDefaults() {
- SOUND_EFFECT_FILES.add("Effect_Tick.ogg");
- for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) {
- SOUND_EFFECT_FILES_MAP[i][0] = 0;
- SOUND_EFFECT_FILES_MAP[i][1] = -1;
- }
- }
-
- private void loadTouchSoundAssets() {
- XmlResourceParser parser = null;
-
- // only load assets once.
- if (!SOUND_EFFECT_FILES.isEmpty()) {
- return;
+ @Override
+ public synchronized void run(boolean success) {
+ mStatus = success ? SOUND_EFFECTS_LOADED : SOUND_EFFECTS_ERROR;
+ notify();
}
- loadTouchSoundAssetDefaults();
-
- try {
- parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets);
-
- XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS);
- String version = parser.getAttributeValue(null, ATTR_VERSION);
- boolean inTouchSoundsGroup = false;
-
- if (ASSET_FILE_VERSION.equals(version)) {
- while (true) {
- XmlUtils.nextElement(parser);
- String element = parser.getName();
- if (element == null) {
- break;
- }
- if (element.equals(TAG_GROUP)) {
- String name = parser.getAttributeValue(null, ATTR_GROUP_NAME);
- if (GROUP_TOUCH_SOUNDS.equals(name)) {
- inTouchSoundsGroup = true;
- break;
- }
- }
- }
- while (inTouchSoundsGroup) {
- XmlUtils.nextElement(parser);
- String element = parser.getName();
- if (element == null) {
- break;
- }
- if (element.equals(TAG_ASSET)) {
- String id = parser.getAttributeValue(null, ATTR_ASSET_ID);
- String file = parser.getAttributeValue(null, ATTR_ASSET_FILE);
- int fx;
-
- try {
- Field field = AudioManager.class.getField(id);
- fx = field.getInt(null);
- } catch (Exception e) {
- Log.w(TAG, "Invalid touch sound ID: "+id);
- continue;
- }
-
- int i = SOUND_EFFECT_FILES.indexOf(file);
- if (i == -1) {
- i = SOUND_EFFECT_FILES.size();
- SOUND_EFFECT_FILES.add(file);
- }
- SOUND_EFFECT_FILES_MAP[fx][0] = i;
- } else {
- break;
- }
+ public synchronized boolean waitForLoaded(int attempts) {
+ while ((mStatus == SOUND_EFFECTS_LOADING) && (attempts-- > 0)) {
+ try {
+ wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while waiting sound pool loaded.");
}
}
- } catch (Resources.NotFoundException e) {
- Log.w(TAG, "audio assets file not found", e);
- } catch (XmlPullParserException e) {
- Log.w(TAG, "XML parser exception reading touch sound assets", e);
- } catch (IOException e) {
- Log.w(TAG, "I/O exception reading touch sound assets", e);
- } finally {
- if (parser != null) {
- parser.close();
- }
+ return mStatus == SOUND_EFFECTS_LOADED;
}
}
@@ -3499,20 +3399,9 @@
* This method must be called at first when sound effects are enabled
*/
public boolean loadSoundEffects() {
- int attempts = 3;
LoadSoundEffectReply reply = new LoadSoundEffectReply();
-
- synchronized (reply) {
- sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);
- while ((reply.mStatus == 1) && (attempts-- > 0)) {
- try {
- reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
- } catch (InterruptedException e) {
- Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded.");
- }
- }
- }
- return (reply.mStatus == 0);
+ sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0);
+ return reply.waitForLoaded(3 /*attempts*/);
}
/**
@@ -3532,61 +3421,6 @@
sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);
}
- class SoundPoolListenerThread extends Thread {
- public SoundPoolListenerThread() {
- super("SoundPoolListenerThread");
- }
-
- @Override
- public void run() {
-
- Looper.prepare();
- mSoundPoolLooper = Looper.myLooper();
-
- synchronized (mSoundEffectsLock) {
- if (mSoundPool != null) {
- mSoundPoolCallBack = new SoundPoolCallback();
- mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack);
- }
- mSoundEffectsLock.notify();
- }
- Looper.loop();
- }
- }
-
- private final class SoundPoolCallback implements
- android.media.SoundPool.OnLoadCompleteListener {
-
- int mStatus = 1; // 1 means neither error nor last sample loaded yet
- List<Integer> mSamples = new ArrayList<Integer>();
-
- public int status() {
- return mStatus;
- }
-
- public void setSamples(int[] samples) {
- for (int i = 0; i < samples.length; i++) {
- // do not wait ack for samples rejected upfront by SoundPool
- if (samples[i] > 0) {
- mSamples.add(samples[i]);
- }
- }
- }
-
- public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
- synchronized (mSoundEffectsLock) {
- int i = mSamples.indexOf(sampleId);
- if (i >= 0) {
- mSamples.remove(i);
- }
- if ((status != 0) || mSamples. isEmpty()) {
- mStatus = status;
- mSoundEffectsLock.notify();
- }
- }
- }
- }
-
/** @see AudioManager#reloadAudioSettings() */
public void reloadAudioSettings() {
readAudioSettings(false /*userSwitch*/);
@@ -4124,7 +3958,9 @@
|| adjust == AudioManager.ADJUST_TOGGLE_MUTE;
}
- /*package*/ boolean isInCommunication() {
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public boolean isInCommunication() {
boolean IsInCall = false;
TelecomManager telecomManager =
@@ -4293,7 +4129,9 @@
return false;
}
- /*package*/ int getDeviceForStream(int stream) {
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public int getDeviceForStream(int stream) {
int device = getDevicesForStream(stream);
if ((device & (device - 1)) != 0) {
// Multiple device selection is either:
@@ -4338,7 +4176,9 @@
}
}
- /*package*/ void postObserveDevicesForAllStreams() {
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public void postObserveDevicesForAllStreams() {
sendMsg(mAudioHandler,
MSG_OBSERVE_DEVICES_FOR_ALL_STREAMS,
SENDMSG_QUEUE, 0 /*arg1*/, 0 /*arg2*/, null /*obj*/,
@@ -4449,7 +4289,9 @@
AudioSystem.DEVICE_OUT_ALL_USB |
AudioSystem.DEVICE_OUT_HDMI;
- /*package*/ void postAccessoryPlugMediaUnmute(int newDevice) {
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public void postAccessoryPlugMediaUnmute(int newDevice) {
sendMsg(mAudioHandler, MSG_ACCESSORY_PLUG_MEDIA_UNMUTE, SENDMSG_QUEUE,
newDevice, 0, null, 0);
}
@@ -4999,7 +4841,9 @@
}
}
- /*package*/ void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public void postSetVolumeIndexOnDevice(int streamType, int vssVolIndex, int device,
String caller) {
sendMsg(mAudioHandler,
MSG_SET_DEVICE_STREAM_VOLUME,
@@ -5107,230 +4951,6 @@
Settings.Global.putInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode);
}
- private String getSoundEffectFilePath(int effectType) {
- String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH
- + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
- if (!new File(filePath).isFile()) {
- filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH
- + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
- }
- return filePath;
- }
-
- private boolean onLoadSoundEffects() {
- int status;
-
- synchronized (mSoundEffectsLock) {
- if (!mSystemReady) {
- Log.w(TAG, "onLoadSoundEffects() called before boot complete");
- return false;
- }
-
- if (mSoundPool != null) {
- return true;
- }
-
- loadTouchSoundAssets();
-
- mSoundPool = new SoundPool.Builder()
- .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
- .setAudioAttributes(new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build())
- .build();
- mSoundPoolCallBack = null;
- mSoundPoolListenerThread = new SoundPoolListenerThread();
- mSoundPoolListenerThread.start();
- int attempts = 3;
- while ((mSoundPoolCallBack == null) && (attempts-- > 0)) {
- try {
- // Wait for mSoundPoolCallBack to be set by the other thread
- mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
- } catch (InterruptedException e) {
- Log.w(TAG, "Interrupted while waiting sound pool listener thread.");
- }
- }
-
- if (mSoundPoolCallBack == null) {
- Log.w(TAG, "onLoadSoundEffects() SoundPool listener or thread creation error");
- if (mSoundPoolLooper != null) {
- mSoundPoolLooper.quit();
- mSoundPoolLooper = null;
- }
- mSoundPoolListenerThread = null;
- mSoundPool.release();
- mSoundPool = null;
- return false;
- }
- /*
- * poolId table: The value -1 in this table indicates that corresponding
- * file (same index in SOUND_EFFECT_FILES[] has not been loaded.
- * Once loaded, the value in poolId is the sample ID and the same
- * sample can be reused for another effect using the same file.
- */
- int[] poolId = new int[SOUND_EFFECT_FILES.size()];
- for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) {
- poolId[fileIdx] = -1;
- }
- /*
- * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded.
- * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0:
- * this indicates we have a valid sample loaded for this effect.
- */
-
- int numSamples = 0;
- for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
- // Do not load sample if this effect uses the MediaPlayer
- if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {
- continue;
- }
- if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {
- String filePath = getSoundEffectFilePath(effect);
- int sampleId = mSoundPool.load(filePath, 0);
- if (sampleId <= 0) {
- Log.w(TAG, "Soundpool could not load file: "+filePath);
- } else {
- SOUND_EFFECT_FILES_MAP[effect][1] = sampleId;
- poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId;
- numSamples++;
- }
- } else {
- SOUND_EFFECT_FILES_MAP[effect][1] =
- poolId[SOUND_EFFECT_FILES_MAP[effect][0]];
- }
- }
- // wait for all samples to be loaded
- if (numSamples > 0) {
- mSoundPoolCallBack.setSamples(poolId);
-
- attempts = 3;
- status = 1;
- while ((status == 1) && (attempts-- > 0)) {
- try {
- mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
- status = mSoundPoolCallBack.status();
- } catch (InterruptedException e) {
- Log.w(TAG, "Interrupted while waiting sound pool callback.");
- }
- }
- } else {
- status = -1;
- }
-
- if (mSoundPoolLooper != null) {
- mSoundPoolLooper.quit();
- mSoundPoolLooper = null;
- }
- mSoundPoolListenerThread = null;
- if (status != 0) {
- Log.w(TAG,
- "onLoadSoundEffects(), Error "+status+ " while loading samples");
- for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
- if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) {
- SOUND_EFFECT_FILES_MAP[effect][1] = -1;
- }
- }
-
- mSoundPool.release();
- mSoundPool = null;
- }
- }
- return (status == 0);
- }
-
- /**
- * Unloads samples from the sound pool.
- * This method can be called to free some memory when
- * sound effects are disabled.
- */
- private void onUnloadSoundEffects() {
- synchronized (mSoundEffectsLock) {
- if (mSoundPool == null) {
- return;
- }
-
- int[] poolId = new int[SOUND_EFFECT_FILES.size()];
- for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) {
- poolId[fileIdx] = 0;
- }
-
- for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
- if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) {
- continue;
- }
- if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) {
- mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]);
- SOUND_EFFECT_FILES_MAP[effect][1] = -1;
- poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1;
- }
- }
- mSoundPool.release();
- mSoundPool = null;
- }
- }
-
- private void onPlaySoundEffect(int effectType, int volume) {
- synchronized (mSoundEffectsLock) {
-
- onLoadSoundEffects();
-
- if (mSoundPool == null) {
- return;
- }
- float volFloat;
- // use default if volume is not specified by caller
- if (volume < 0) {
- volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
- } else {
- volFloat = volume / 1000.0f;
- }
-
- if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
- mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
- volFloat, volFloat, 0, 0, 1.0f);
- } else {
- MediaPlayer mediaPlayer = new MediaPlayer();
- try {
- String filePath = getSoundEffectFilePath(effectType);
- mediaPlayer.setDataSource(filePath);
- mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
- mediaPlayer.prepare();
- mediaPlayer.setVolume(volFloat);
- mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
- public void onCompletion(MediaPlayer mp) {
- cleanupPlayer(mp);
- }
- });
- mediaPlayer.setOnErrorListener(new OnErrorListener() {
- public boolean onError(MediaPlayer mp, int what, int extra) {
- cleanupPlayer(mp);
- return true;
- }
- });
- mediaPlayer.start();
- } catch (IOException ex) {
- Log.w(TAG, "MediaPlayer IOException: "+ex);
- } catch (IllegalArgumentException ex) {
- Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
- } catch (IllegalStateException ex) {
- Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
- }
- }
- }
- }
-
- private void cleanupPlayer(MediaPlayer mp) {
- if (mp != null) {
- try {
- mp.stop();
- mp.release();
- } catch (IllegalStateException ex) {
- Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
- }
- }
- }
-
private void onPersistSafeVolumeState(int state) {
Settings.Global.putInt(mContentResolver,
Settings.Global.AUDIO_SAFE_VOLUME_STATE,
@@ -5377,24 +4997,25 @@
break;
case MSG_UNLOAD_SOUND_EFFECTS:
- onUnloadSoundEffects();
+ mSfxHelper.unloadSoundEffects();
break;
case MSG_LOAD_SOUND_EFFECTS:
- //FIXME: onLoadSoundEffects() should be executed in a separate thread as it
- // can take several dozens of milliseconds to complete
- boolean loaded = onLoadSoundEffects();
- if (msg.obj != null) {
- LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj;
- synchronized (reply) {
- reply.mStatus = loaded ? 0 : -1;
- reply.notify();
+ {
+ LoadSoundEffectReply reply = (LoadSoundEffectReply) msg.obj;
+ if (mSystemReady) {
+ mSfxHelper.loadSoundEffects(reply);
+ } else {
+ Log.w(TAG, "[schedule]loadSoundEffects() called before boot complete");
+ if (reply != null) {
+ reply.run(false);
}
}
+ }
break;
case MSG_PLAY_SOUND_EFFECT:
- onPlaySoundEffect(msg.arg1, msg.arg2);
+ mSfxHelper.playSoundEffect(msg.arg1, msg.arg2);
break;
case MSG_SET_FORCE_USE:
@@ -5578,15 +5199,19 @@
/**
* @return true if there is currently a registered dynamic mixing policy that affects media
+ * and is not a render + loopback policy
*/
- /*package*/ boolean hasMediaDynamicPolicy() {
+ // only public for mocking/spying
+ @VisibleForTesting
+ public boolean hasMediaDynamicPolicy() {
synchronized (mAudioPolicies) {
if (mAudioPolicies.isEmpty()) {
return false;
}
final Collection<AudioPolicyProxy> appColl = mAudioPolicies.values();
for (AudioPolicyProxy app : appColl) {
- if (app.hasMixAffectingUsage(AudioAttributes.USAGE_MEDIA)) {
+ if (app.hasMixAffectingUsage(AudioAttributes.USAGE_MEDIA,
+ AudioMix.ROUTE_FLAG_LOOP_BACK_RENDER)) {
return true;
}
}
@@ -5911,7 +5536,9 @@
return mMediaFocusControl.getFocusRampTimeMs(focusGain, attr);
}
- /*package*/ boolean hasAudioFocusUsers() {
+ /** only public for mocking/spying, do not call outside of AudioService */
+ @VisibleForTesting
+ public boolean hasAudioFocusUsers() {
return mMediaFocusControl.hasAudioFocusUsers();
}
@@ -6379,6 +6006,12 @@
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ if (mAudioHandler != null) {
+ pw.println("\nMessage handler (watch for unhandled messages):");
+ mAudioHandler.dump(new PrintWriterPrinter(pw), " ");
+ } else {
+ pw.println("\nMessage handler is null");
+ }
mMediaFocusControl.dump(pw);
dumpStreamStates(pw);
dumpRingerMode(pw);
@@ -6414,11 +6047,14 @@
dumpAudioPolicies(pw);
mDynPolicyLogger.dump(pw);
-
mPlaybackMonitor.dump(pw);
-
mRecordMonitor.dump(pw);
+ pw.println("\nAudioDeviceBroker:");
+ mDeviceBroker.dump(pw, " ");
+ pw.println("\nSoundEffects:");
+ mSfxHelper.dump(pw, " ");
+
pw.println("\n");
pw.println("\nEvent logs:");
mModeLogger.dump(pw);
@@ -7349,9 +6985,10 @@
Binder.restoreCallingIdentity(identity);
}
- boolean hasMixAffectingUsage(int usage) {
+ boolean hasMixAffectingUsage(int usage, int excludedFlags) {
for (AudioMix mix : mMixes) {
- if (mix.isAffectingUsage(usage)) {
+ if (mix.isAffectingUsage(usage)
+ && ((mix.getRouteFlags() & excludedFlags) != excludedFlags)) {
return true;
}
}
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 1a63f8f..9f1a6bd 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -139,6 +139,12 @@
public int getCodec() {
return mCodec;
}
+
+ // redefine equality op so we can match messages intended for this device
+ @Override
+ public boolean equals(Object o) {
+ return mBtDevice.equals(o);
+ }
}
// A2DP device events
@@ -441,9 +447,9 @@
return;
}
final BluetoothDevice btDevice = deviceList.get(0);
- final @BluetoothProfile.BtProfileState int state = mA2dp.getConnectionState(btDevice);
- mDeviceBroker.handleSetA2dpSinkConnectionState(
- state, new BluetoothA2dpDeviceInfo(btDevice));
+ // the device is guaranteed CONNECTED
+ mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice,
+ BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1);
}
/*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) {
diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java
index db55138..bd129f7 100644
--- a/services/core/java/com/android/server/audio/FocusRequester.java
+++ b/services/core/java/com/android/server/audio/FocusRequester.java
@@ -364,28 +364,8 @@
// check enforcement by the framework
boolean handled = false;
- if (focusLoss == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
- && MediaFocusControl.ENFORCE_DUCKING
- && frWinner != null) {
- // candidate for enforcement by the framework
- if (frWinner.mCallingUid != this.mCallingUid) {
- if (!forceDuck && ((mGrantFlags
- & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0)) {
- // the focus loser declared it would pause instead of duck, let it
- // handle it (the framework doesn't pause for apps)
- handled = false;
- Log.v(TAG, "not ducking uid " + this.mCallingUid + " - flags");
- } else if (!forceDuck && (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW &&
- this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL))
- {
- // legacy behavior, apps used to be notified when they should be ducking
- handled = false;
- Log.v(TAG, "not ducking uid " + this.mCallingUid + " - old SDK");
- } else {
- handled = mFocusController.duckPlayers(frWinner, this, forceDuck);
- }
- } // else: the focus change is within the same app, so let the dispatching
- // happen as if the framework was not involved.
+ if (frWinner != null) {
+ handled = frameworkHandleFocusLoss(focusLoss, frWinner, forceDuck);
}
if (handled) {
@@ -415,6 +395,47 @@
}
}
+ /**
+ * Let the framework handle the focus loss if possible
+ * @param focusLoss
+ * @param frWinner
+ * @param forceDuck
+ * @return true if the framework handled the focus loss
+ */
+ @GuardedBy("MediaFocusControl.mAudioFocusLock")
+ private boolean frameworkHandleFocusLoss(int focusLoss, @NonNull final FocusRequester frWinner,
+ boolean forceDuck) {
+ if (frWinner.mCallingUid == this.mCallingUid) {
+ // the focus change is within the same app, so let the dispatching
+ // happen as if the framework was not involved.
+ return false;
+ }
+
+ if (focusLoss == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ if (!MediaFocusControl.ENFORCE_DUCKING) {
+ return false;
+ }
+
+ // candidate for enforcement by the framework
+ if (!forceDuck && ((mGrantFlags
+ & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0)) {
+ // the focus loser declared it would pause instead of duck, let it
+ // handle it (the framework doesn't pause for apps)
+ Log.v(TAG, "not ducking uid " + this.mCallingUid + " - flags");
+ return false;
+ }
+ if (!forceDuck && (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW
+ && this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL)) {
+ // legacy behavior, apps used to be notified when they should be ducking
+ Log.v(TAG, "not ducking uid " + this.mCallingUid + " - old SDK");
+ return false;
+ }
+
+ return mFocusController.duckPlayers(frWinner, this, forceDuck);
+ }
+ return false;
+ }
+
int dispatchFocusChange(int focusChange) {
if (mFocusDispatcher == null) {
if (MediaFocusControl.DEBUG) { Log.e(TAG, "dispatchFocusChange: no focus dispatcher"); }
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 5c93071..c845981 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -105,12 +105,13 @@
//=================================================================
// PlayerFocusEnforcer implementation
@Override
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck) {
+ public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+ boolean forceDuck) {
return mFocusEnforcer.duckPlayers(winner, loser, forceDuck);
}
@Override
- public void unduckPlayers(FocusRequester winner) {
+ public void unduckPlayers(@NonNull FocusRequester winner) {
mFocusEnforcer.unduckPlayers(winner);
}
@@ -742,7 +743,20 @@
}
}
- /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
+ /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int)
+ * @param aa
+ * @param focusChangeHint
+ * @param cb
+ * @param fd
+ * @param clientId
+ * @param callingPackageName
+ * @param flags
+ * @param sdk
+ * @param forceDuck only true if
+ * {@link android.media.AudioFocusRequest.Builder#setFocusGain(int)} was set to true for
+ * accessibility.
+ * @return
+ */
protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb,
IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName,
int flags, int sdk, boolean forceDuck) {
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 3a25d98..f8ba55b 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -425,7 +425,8 @@
private final DuckingManager mDuckingManager = new DuckingManager();
@Override
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck) {
+ public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+ boolean forceDuck) {
if (DEBUG) {
Log.v(TAG, String.format("duckPlayers: uids winner=%d loser=%d",
winner.getClientUid(), loser.getClientUid()));
@@ -473,7 +474,7 @@
}
@Override
- public void unduckPlayers(FocusRequester winner) {
+ public void unduckPlayers(@NonNull FocusRequester winner) {
if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
synchronized (mPlayerLock) {
mDuckingManager.unduckUid(winner.getClientUid(), mPlayers);
diff --git a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
index 3c834da..89e7b782 100644
--- a/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
+++ b/services/core/java/com/android/server/audio/PlayerFocusEnforcer.java
@@ -16,6 +16,8 @@
package com.android.server.audio;
+import android.annotation.NonNull;
+
public interface PlayerFocusEnforcer {
/**
@@ -25,11 +27,24 @@
* @param loser
* @return
*/
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck);
+ boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser,
+ boolean forceDuck);
- public void unduckPlayers(FocusRequester winner);
+ /**
+ * Unduck the players that had been ducked with
+ * {@link #duckPlayers(FocusRequester, FocusRequester, boolean)}
+ * @param winner
+ */
+ void unduckPlayers(@NonNull FocusRequester winner);
- public void mutePlayersForCall(int[] usagesToMute);
+ /**
+ * Mute players at the beginning of a call
+ * @param usagesToMute array of {@link android.media.AudioAttributes} usages to mute
+ */
+ void mutePlayersForCall(int[] usagesToMute);
- public void unmutePlayersForCall();
+ /**
+ * Unmute players at the end of a call
+ */
+ void unmutePlayersForCall();
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
new file mode 100644
index 0000000..cf5bc8d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2019 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.android.server.audio;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.SoundPool;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.PrintWriterPrinter;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class for managing sound effects loading / unloading
+ * used by AudioService. As its methods are called on the message handler thread
+ * of AudioService, the actual work is offloaded to a dedicated thread.
+ * This helps keeping AudioService responsive.
+ * @hide
+ */
+class SoundEffectsHelper {
+ private static final String TAG = "AS.SfxHelper";
+
+ private static final int NUM_SOUNDPOOL_CHANNELS = 4;
+
+ /* Sound effect file names */
+ private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/";
+
+ private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0
+
+ private static final int MSG_LOAD_EFFECTS = 0;
+ private static final int MSG_UNLOAD_EFFECTS = 1;
+ private static final int MSG_PLAY_EFFECT = 2;
+ private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3;
+
+ interface OnEffectsLoadCompleteHandler {
+ void run(boolean success);
+ }
+
+ private final AudioEventLogger mSfxLogger = new AudioEventLogger(
+ AudioManager.NUM_SOUND_EFFECTS + 10, "Sound Effects Loading");
+
+ private final Context mContext;
+ // default attenuation applied to sound played with playSoundEffect()
+ private final int mSfxAttenuationDb;
+
+ // thread for doing all work
+ private SfxWorker mSfxWorker;
+ // thread's message handler
+ private SfxHandler mSfxHandler;
+
+ private static final class Resource {
+ final String mFileName;
+ int mSampleId;
+ boolean mLoaded; // for effects in SoundPool
+ Resource(String fileName) {
+ mFileName = fileName;
+ mSampleId = EFFECT_NOT_IN_SOUND_POOL;
+ }
+ }
+ // All the fields below are accessed by the worker thread exclusively
+ private final List<Resource> mResources = new ArrayList<Resource>();
+ private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources
+ private SoundPool mSoundPool;
+ private SoundPoolLoader mSoundPoolLoader;
+
+ SoundEffectsHelper(Context context) {
+ mContext = context;
+ mSfxAttenuationDb = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_soundEffectVolumeDb);
+ startWorker();
+ }
+
+ /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
+ sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0);
+ }
+
+ /**
+ * Unloads samples from the sound pool.
+ * This method can be called to free some memory when
+ * sound effects are disabled.
+ */
+ /*package*/ void unloadSoundEffects() {
+ sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0);
+ }
+
+ /*package*/ void playSoundEffect(int effect, int volume) {
+ sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0);
+ }
+
+ /*package*/ void dump(PrintWriter pw, String prefix) {
+ if (mSfxHandler != null) {
+ pw.println(prefix + "Message handler (watch for unhandled messages):");
+ mSfxHandler.dump(new PrintWriterPrinter(pw), " ");
+ } else {
+ pw.println(prefix + "Message handler is null");
+ }
+ pw.println(prefix + "Default attenuation (dB): " + mSfxAttenuationDb);
+ mSfxLogger.dump(pw);
+ }
+
+ private void startWorker() {
+ mSfxWorker = new SfxWorker();
+ mSfxWorker.start();
+ synchronized (this) {
+ while (mSfxHandler == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start");
+ }
+ }
+ }
+ }
+
+ private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) {
+ mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs);
+ }
+
+ private void logEvent(String msg) {
+ mSfxLogger.log(new AudioEventLogger.StringEvent(msg));
+ }
+
+ // All the methods below run on the worker thread
+ private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
+ if (mSoundPoolLoader != null) {
+ // Loading is ongoing.
+ mSoundPoolLoader.addHandler(onComplete);
+ return;
+ }
+ if (mSoundPool != null) {
+ if (onComplete != null) {
+ onComplete.run(true /*success*/);
+ }
+ return;
+ }
+
+ logEvent("effects loading started");
+ mSoundPool = new SoundPool.Builder()
+ .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
+ .setAudioAttributes(new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build())
+ .build();
+ loadTouchSoundAssets();
+
+ mSoundPoolLoader = new SoundPoolLoader();
+ mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
+ @Override
+ public void run(boolean success) {
+ mSoundPoolLoader = null;
+ if (!success) {
+ Log.w(TAG, "onLoadSoundEffects(), Error while loading samples");
+ onUnloadSoundEffects();
+ }
+ }
+ });
+ mSoundPoolLoader.addHandler(onComplete);
+
+ int resourcesToLoad = 0;
+ for (Resource res : mResources) {
+ String filePath = getResourceFilePath(res);
+ int sampleId = mSoundPool.load(filePath, 0);
+ if (sampleId > 0) {
+ res.mSampleId = sampleId;
+ res.mLoaded = false;
+ resourcesToLoad++;
+ } else {
+ logEvent("effect " + filePath + " rejected by SoundPool");
+ Log.w(TAG, "SoundPool could not load file: " + filePath);
+ }
+ }
+
+ if (resourcesToLoad > 0) {
+ sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS);
+ } else {
+ logEvent("effects loading completed, no effects to load");
+ mSoundPoolLoader.onComplete(true /*success*/);
+ }
+ }
+
+ void onUnloadSoundEffects() {
+ if (mSoundPool == null) {
+ return;
+ }
+ if (mSoundPoolLoader != null) {
+ mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
+ @Override
+ public void run(boolean success) {
+ onUnloadSoundEffects();
+ }
+ });
+ }
+
+ logEvent("effects unloading started");
+ for (Resource res : mResources) {
+ if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) {
+ mSoundPool.unload(res.mSampleId);
+ }
+ }
+ mSoundPool.release();
+ mSoundPool = null;
+ logEvent("effects unloading completed");
+ }
+
+ void onPlaySoundEffect(int effect, int volume) {
+ float volFloat;
+ // use default if volume is not specified by caller
+ if (volume < 0) {
+ volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20);
+ } else {
+ volFloat = volume / 1000.0f;
+ }
+
+ Resource res = mResources.get(mEffects[effect]);
+ if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) {
+ mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f);
+ } else {
+ MediaPlayer mediaPlayer = new MediaPlayer();
+ try {
+ String filePath = getResourceFilePath(res);
+ mediaPlayer.setDataSource(filePath);
+ mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
+ mediaPlayer.prepare();
+ mediaPlayer.setVolume(volFloat);
+ mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ cleanupPlayer(mp);
+ }
+ });
+ mediaPlayer.setOnErrorListener(new OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ cleanupPlayer(mp);
+ return true;
+ }
+ });
+ mediaPlayer.start();
+ } catch (IOException ex) {
+ Log.w(TAG, "MediaPlayer IOException: " + ex);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "MediaPlayer IllegalArgumentException: " + ex);
+ } catch (IllegalStateException ex) {
+ Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
+ }
+ }
+ }
+
+ private static void cleanupPlayer(MediaPlayer mp) {
+ if (mp != null) {
+ try {
+ mp.stop();
+ mp.release();
+ } catch (IllegalStateException ex) {
+ Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
+ }
+ }
+ }
+
+ private static final String TAG_AUDIO_ASSETS = "audio_assets";
+ private static final String ATTR_VERSION = "version";
+ private static final String TAG_GROUP = "group";
+ private static final String ATTR_GROUP_NAME = "name";
+ private static final String TAG_ASSET = "asset";
+ private static final String ATTR_ASSET_ID = "id";
+ private static final String ATTR_ASSET_FILE = "file";
+
+ private static final String ASSET_FILE_VERSION = "1.0";
+ private static final String GROUP_TOUCH_SOUNDS = "touch_sounds";
+
+ private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 15000;
+
+ private String getResourceFilePath(Resource res) {
+ String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+ if (!new File(filePath).isFile()) {
+ filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+ }
+ return filePath;
+ }
+
+ private void loadTouchSoundAssetDefaults() {
+ int defaultResourceIdx = mResources.size();
+ mResources.add(new Resource("Effect_Tick.ogg"));
+ for (int i = 0; i < mEffects.length; i++) {
+ mEffects[i] = defaultResourceIdx;
+ }
+ }
+
+ private void loadTouchSoundAssets() {
+ XmlResourceParser parser = null;
+
+ // only load assets once.
+ if (!mResources.isEmpty()) {
+ return;
+ }
+
+ loadTouchSoundAssetDefaults();
+
+ try {
+ parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets);
+
+ XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS);
+ String version = parser.getAttributeValue(null, ATTR_VERSION);
+ boolean inTouchSoundsGroup = false;
+
+ if (ASSET_FILE_VERSION.equals(version)) {
+ while (true) {
+ XmlUtils.nextElement(parser);
+ String element = parser.getName();
+ if (element == null) {
+ break;
+ }
+ if (element.equals(TAG_GROUP)) {
+ String name = parser.getAttributeValue(null, ATTR_GROUP_NAME);
+ if (GROUP_TOUCH_SOUNDS.equals(name)) {
+ inTouchSoundsGroup = true;
+ break;
+ }
+ }
+ }
+ while (inTouchSoundsGroup) {
+ XmlUtils.nextElement(parser);
+ String element = parser.getName();
+ if (element == null) {
+ break;
+ }
+ if (element.equals(TAG_ASSET)) {
+ String id = parser.getAttributeValue(null, ATTR_ASSET_ID);
+ String file = parser.getAttributeValue(null, ATTR_ASSET_FILE);
+ int fx;
+
+ try {
+ Field field = AudioManager.class.getField(id);
+ fx = field.getInt(null);
+ } catch (Exception e) {
+ Log.w(TAG, "Invalid touch sound ID: " + id);
+ continue;
+ }
+
+ mEffects[fx] = findOrAddResourceByFileName(file);
+ } else {
+ break;
+ }
+ }
+ }
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "audio assets file not found", e);
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "XML parser exception reading touch sound assets", e);
+ } catch (IOException e) {
+ Log.w(TAG, "I/O exception reading touch sound assets", e);
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ }
+
+ private int findOrAddResourceByFileName(String fileName) {
+ for (int i = 0; i < mResources.size(); i++) {
+ if (mResources.get(i).mFileName.equals(fileName)) {
+ return i;
+ }
+ }
+ int result = mResources.size();
+ mResources.add(new Resource(fileName));
+ return result;
+ }
+
+ private Resource findResourceBySampleId(int sampleId) {
+ for (Resource res : mResources) {
+ if (res.mSampleId == sampleId) {
+ return res;
+ }
+ }
+ return null;
+ }
+
+ private class SfxWorker extends Thread {
+ SfxWorker() {
+ super("AS.SfxWorker");
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (SoundEffectsHelper.this) {
+ mSfxHandler = new SfxHandler();
+ SoundEffectsHelper.this.notify();
+ }
+ Looper.loop();
+ }
+ }
+
+ private class SfxHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOAD_EFFECTS:
+ onLoadSoundEffects((OnEffectsLoadCompleteHandler) msg.obj);
+ break;
+ case MSG_UNLOAD_EFFECTS:
+ onUnloadSoundEffects();
+ break;
+ case MSG_PLAY_EFFECT:
+ onLoadSoundEffects(new OnEffectsLoadCompleteHandler() {
+ @Override
+ public void run(boolean success) {
+ if (success) {
+ onPlaySoundEffect(msg.arg1 /*effect*/, msg.arg2 /*volume*/);
+ }
+ }
+ });
+ break;
+ case MSG_LOAD_EFFECTS_TIMEOUT:
+ if (mSoundPoolLoader != null) {
+ mSoundPoolLoader.onTimeout();
+ }
+ break;
+ }
+ }
+ }
+
+ private class SoundPoolLoader implements
+ android.media.SoundPool.OnLoadCompleteListener {
+
+ private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers =
+ new ArrayList<OnEffectsLoadCompleteHandler>();
+
+ SoundPoolLoader() {
+ // SoundPool use the current Looper when creating its message handler.
+ // Since SoundPoolLoader is created on the SfxWorker thread, SoundPool's
+ // message handler ends up running on it (it's OK to have multiple
+ // handlers on the same Looper). Thus, onLoadComplete gets executed
+ // on the worker thread.
+ mSoundPool.setOnLoadCompleteListener(this);
+ }
+
+ void addHandler(OnEffectsLoadCompleteHandler handler) {
+ if (handler != null) {
+ mLoadCompleteHandlers.add(handler);
+ }
+ }
+
+ @Override
+ public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
+ if (status == 0) {
+ int remainingToLoad = 0;
+ for (Resource res : mResources) {
+ if (res.mSampleId == sampleId && !res.mLoaded) {
+ logEvent("effect " + res.mFileName + " loaded");
+ res.mLoaded = true;
+ }
+ if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) {
+ remainingToLoad++;
+ }
+ }
+ if (remainingToLoad == 0) {
+ onComplete(true);
+ }
+ } else {
+ Resource res = findResourceBySampleId(sampleId);
+ String filePath;
+ if (res != null) {
+ filePath = getResourceFilePath(res);
+ } else {
+ filePath = "with unknown sample ID " + sampleId;
+ }
+ logEvent("effect " + filePath + " loading failed, status " + status);
+ Log.w(TAG, "onLoadSoundEffects(), Error " + status + " while loading sample "
+ + filePath);
+ onComplete(false);
+ }
+ }
+
+ void onTimeout() {
+ onComplete(false);
+ }
+
+ void onComplete(boolean success) {
+ mSoundPool.setOnLoadCompleteListener(null);
+ for (OnEffectsLoadCompleteHandler handler : mLoadCompleteHandlers) {
+ handler.run(success);
+ }
+ logEvent("effects loading " + (success ? "completed" : "failed"));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index bd198dd..f12c689 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -892,8 +892,22 @@
@Override
public void onNotificationError(int callingUid, int callingPid, String pkg, String tag,
int id, int uid, int initialPid, String message, int userId) {
+ final boolean fgService;
+ synchronized (mNotificationLock) {
+ NotificationRecord r = findNotificationLocked(pkg, tag, id, userId);
+ fgService = r != null && (r.getNotification().flags & FLAG_FOREGROUND_SERVICE) != 0;
+ }
cancelNotification(callingUid, callingPid, pkg, tag, id, 0, 0, false, userId,
REASON_ERROR, null);
+ if (fgService) {
+ // Still crash for foreground services, preventing the not-crash behaviour abused
+ // by apps to give us a garbage notification and silently start a fg service.
+ Binder.withCleanCallingIdentity(
+ () -> mAm.crashApplication(uid, initialPid, pkg, -1,
+ "Bad notification(tag=" + tag + ", id=" + id + ") posted from package "
+ + pkg + ", crashing app(uid=" + uid + ", pid=" + initialPid + "): "
+ + message));
+ }
}
@Override
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 25bd4ec..c1bbb30 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -69,7 +69,7 @@
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.HARDWARE_TEST"/>
- <uses-permission android:name="android.permission.MANAGE_APPOPS"/>
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
new file mode 100644
index 0000000..5c2ad94
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2019 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.android.server.audio;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceBrokerTest {
+
+ private static final String TAG = "AudioDeviceBrokerTest";
+ private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
+
+ private Context mContext;
+ // the actual class under test
+ private AudioDeviceBroker mAudioDeviceBroker;
+
+ @Mock private AudioService mMockAudioService;
+ @Spy private AudioDeviceInventory mSpyDevInventory;
+
+ private BluetoothDevice mFakeBtDevice;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+
+ mMockAudioService = mock(AudioService.class);
+ mSpyDevInventory = spy(new AudioDeviceInventory());
+ mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory);
+ mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
+ Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
+ }
+
+ @After
+ public void tearDown() throws Exception { }
+
+ @Test
+ public void testSetUpAndTearDown() { }
+
+ /**
+ * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection
+ * calls into AudioDeviceInventory with the right params
+ * @throws Exception
+ */
+ @Test
+ public void testPostA2dpDeviceConnectionChange() throws Exception {
+ Log.i(TAG, "testPostA2dpDeviceConnectionChange");
+ Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
+
+ mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
+ Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ verify(mSpyDevInventory, times(1)).setBluetoothA2dpDeviceConnectionState(
+ any(BluetoothDevice.class),
+ ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED) /*state*/,
+ ArgumentMatchers.eq(BluetoothProfile.A2DP) /*profile*/,
+ ArgumentMatchers.eq(true) /*suppressNoisyIntent*/, anyInt() /*musicDevice*/,
+ ArgumentMatchers.eq(1) /*a2dpVolume*/
+ );
+ }
+
+ /**
+ * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for
+ * connection > pause > disconnection > connection
+ * keeps the device connected
+ * @throws Exception
+ */
+ @Test
+ public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
+ Log.i(TAG, "testA2dpDeviceConnectionDisconnectionConnectionChange");
+
+ doTestConnectionDisconnectionReconnection(0);
+ }
+
+ /**
+ * Verify device disconnection and reconnection within the BECOMING_NOISY window
+ * @throws Exception
+ */
+ @Test
+ public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
+ Log.i(TAG, "testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
+
+ doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2);
+ }
+
+ private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection)
+ throws Exception {
+ when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
+ .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
+ when(mMockAudioService.isInCommunication()).thenReturn(false);
+ when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
+ when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);
+
+ // first connection
+ mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
+ Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+
+ // disconnection
+ mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, false, -1);
+ if (delayAfterDisconnection > 0) {
+ Thread.sleep(delayAfterDisconnection);
+ }
+
+ // reconnection
+ mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 2);
+ Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
+
+ // Verify disconnection has been cancelled and we're seeing two connections attempts,
+ // with the device connected at the end of the test
+ verify(mSpyDevInventory, times(2)).onSetA2dpSinkConnectionState(
+ any(BtHelper.BluetoothA2dpDeviceInfo.class),
+ ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED));
+ Assert.assertTrue("Mock device not connected",
+ mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
+ }
+}
diff --git a/startop/view_compiler/dex_builder_test/Android.bp b/startop/view_compiler/dex_builder_test/Android.bp
index 1214538..f783aa6 100644
--- a/startop/view_compiler/dex_builder_test/Android.bp
+++ b/startop/view_compiler/dex_builder_test/Android.bp
@@ -37,15 +37,21 @@
android_test {
name: "dex-builder-test",
srcs: [
+ "src/android/startop/test/ApkLayoutCompilerTest.java",
"src/android/startop/test/DexBuilderTest.java",
"src/android/startop/test/LayoutCompilerTest.java",
"src/android/startop/test/TestClass.java",
],
sdk_version: "current",
- data: [":generate_dex_testcases", ":generate_compiled_layout1", ":generate_compiled_layout2"],
+ data: [
+ ":generate_dex_testcases",
+ ":generate_compiled_layout1",
+ ":generate_compiled_layout2",
+ ],
static_libs: [
- "androidx.test.rules",
- "guava",
+ "androidx.test.core",
+ "androidx.test.runner",
+ "junit",
],
manifest: "AndroidManifest.xml",
resource_dirs: ["res"],
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/ApkLayoutCompilerTest.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/ApkLayoutCompilerTest.java
new file mode 100644
index 0000000..230e8df
--- /dev/null
+++ b/startop/view_compiler/dex_builder_test/src/android/startop/test/ApkLayoutCompilerTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 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 android.startop.test;
+
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import android.view.View;
+import dalvik.system.PathClassLoader;
+import java.lang.reflect.Method;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ApkLayoutCompilerTest {
+ static ClassLoader loadDexFile() throws Exception {
+ Context context = InstrumentationRegistry.getTargetContext();
+ return new PathClassLoader(context.getCodeCacheDir() + "/compiled_view.dex",
+ ClassLoader.getSystemClassLoader());
+ }
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ // ensure PackageManager has compiled the layouts.
+ Process pm = Runtime.getRuntime().exec("pm compile --compile-layouts android.startop.test");
+ pm.waitFor();
+ }
+
+ @Test
+ public void loadAndInflateLayout1() throws Exception {
+ ClassLoader dex_file = loadDexFile();
+ Class compiled_view = dex_file.loadClass("android.startop.test.CompiledView");
+ Method layout1 = compiled_view.getMethod("layout1", Context.class, int.class);
+ Context context = InstrumentationRegistry.getTargetContext();
+ layout1.invoke(null, context, R.layout.layout1);
+ }
+
+ @Test
+ public void loadAndInflateLayout2() throws Exception {
+ ClassLoader dex_file = loadDexFile();
+ Class compiled_view = dex_file.loadClass("android.startop.test.CompiledView");
+ Method layout2 = compiled_view.getMethod("layout2", Context.class, int.class);
+ Context context = InstrumentationRegistry.getTargetContext();
+ layout2.invoke(null, context, R.layout.layout2);
+ }
+}
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java
index d1fe588..6af01f6f 100644
--- a/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java
+++ b/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java
@@ -14,8 +14,11 @@
package android.startop.test;
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
import dalvik.system.PathClassLoader;
-
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import org.junit.Assert;
import org.junit.Test;
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java
index 3dfb20c..b0cf91d 100644
--- a/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java
+++ b/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java
@@ -15,11 +15,11 @@
package android.startop.test;
import android.content.Context;
-
import androidx.test.InstrumentationRegistry;
-
+import android.view.View;
import dalvik.system.PathClassLoader;
-
+import java.lang.reflect.Method;
+import org.junit.Assert;
import org.junit.Test;
import java.lang.reflect.Method;
diff --git a/telephony/java/android/provider/Telephony.java b/telephony/java/android/provider/Telephony.java
index 3e4cd4d..ddda010 100644
--- a/telephony/java/android/provider/Telephony.java
+++ b/telephony/java/android/provider/Telephony.java
@@ -3963,6 +3963,13 @@
public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
/**
+ * The id of the subscription which received this cell broadcast message.
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String SUB_ID = "sub_id";
+
+ /**
* Message geographical scope. Valid values are:
* <ul>
* <li>{@link android.telephony.SmsCbMessage#GEOGRAPHICAL_SCOPE_CELL_WIDE}. meaning the
@@ -4184,6 +4191,18 @@
public static final String GEOMETRIES = "geometries";
/**
+ * Geo-Fencing Maximum Wait Time in second. The range of the time is [0, 255]. A device
+ * shall allow to determine its position meeting operator policy. If the device is unable to
+ * determine its position meeting operator policy within the GeoFencing Maximum Wait Time,
+ * it shall present the alert to the user and discontinue further positioning determination
+ * for the alert.
+ *
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String MAXIMUM_WAIT_TIME = "maximum_wait_time";
+
+ /**
* Query columns for instantiating {@link android.telephony.CellBroadcastMessage} objects.
* @hide
*/
@@ -4236,7 +4255,8 @@
CMAS_CERTAINTY,
RECEIVED_TIME,
MESSAGE_BROADCASTED,
- GEOMETRIES
+ GEOMETRIES,
+ MAXIMUM_WAIT_TIME
};
}
diff --git a/telephony/java/android/telephony/SmsCbMessage.java b/telephony/java/android/telephony/SmsCbMessage.java
index 77231d1..c078764 100644
--- a/telephony/java/android/telephony/SmsCbMessage.java
+++ b/telephony/java/android/telephony/SmsCbMessage.java
@@ -139,6 +139,12 @@
@Retention(RetentionPolicy.SOURCE)
public @interface MessagePriority {}
+ /**
+ * ATIS-0700041 Section 5.2.8 WAC Geo-Fencing Maximum Wait Time Table 12.
+ * @hide
+ */
+ public static final int MAXIMUM_WAIT_TIME_NOT_SET = 255;
+
/** Format of this message (for interpretation of service category values). */
private final int mMessageFormat;
@@ -187,6 +193,14 @@
@Nullable
private final SmsCbCmasInfo mCmasWarningInfo;
+ /**
+ * Geo-Fencing Maximum Wait Time in second, a device shall allow to determine its position
+ * meeting operator policy. If the device is unable to determine its position meeting operator
+ * policy within the GeoFencing Maximum Wait Time, it shall present the alert to the user and
+ * discontinue further positioning determination for the alert.
+ */
+ private final int mMaximumWaitTimeSec;
+
/** UNIX timestamp of when the message was received. */
private final long mReceivedTimeMillis;
@@ -202,8 +216,8 @@
@Nullable SmsCbCmasInfo cmasWarningInfo) {
this(messageFormat, geographicalScope, serialNumber, location, serviceCategory, language,
- body, priority, etwsWarningInfo, cmasWarningInfo, null /* geometries */,
- System.currentTimeMillis());
+ body, priority, etwsWarningInfo, cmasWarningInfo, 0 /* maximumWaitingTime */,
+ null /* geometries */, System.currentTimeMillis());
}
/**
@@ -213,7 +227,7 @@
public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber,
SmsCbLocation location, int serviceCategory, String language, String body,
int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo,
- List<Geometry> geometries, long receivedTimeMillis) {
+ int maximumWaitTimeSec, List<Geometry> geometries, long receivedTimeMillis) {
mMessageFormat = messageFormat;
mGeographicalScope = geographicalScope;
mSerialNumber = serialNumber;
@@ -226,6 +240,7 @@
mCmasWarningInfo = cmasWarningInfo;
mReceivedTimeMillis = receivedTimeMillis;
mGeometries = geometries;
+ mMaximumWaitTimeSec = maximumWaitTimeSec;
}
/**
@@ -262,6 +277,7 @@
mReceivedTimeMillis = in.readLong();
String geoStr = in.readString();
mGeometries = geoStr != null ? CbGeoUtils.parseGeometriesFromString(geoStr) : null;
+ mMaximumWaitTimeSec = in.readInt();
}
/**
@@ -295,6 +311,7 @@
dest.writeLong(mReceivedTimeMillis);
dest.writeString(
mGeometries != null ? CbGeoUtils.encodeGeometriesToString(mGeometries) : null);
+ dest.writeInt(mMaximumWaitTimeSec);
}
@NonNull
@@ -389,6 +406,15 @@
}
/**
+ * Get the Geo-Fencing Maximum Wait Time.
+ * @return the time in second.
+ * @hide
+ */
+ public int getMaximumWaitingTime() {
+ return mMaximumWaitTimeSec;
+ }
+
+ /**
* Get the time when this message was received.
* @return the time in millisecond
*/
@@ -475,6 +501,7 @@
+ ", priority=" + mPriority
+ (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "")
+ (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "")
+ + ", maximumWaitingTime = " + mMaximumWaitTimeSec
+ ", geo=" + (mGeometries != null
? CbGeoUtils.encodeGeometriesToString(mGeometries) : "null")
+ '}';
@@ -535,6 +562,8 @@
cv.put(CellBroadcasts.GEOMETRIES, (String) null);
}
+ cv.put(CellBroadcasts.MAXIMUM_WAIT_TIME, mMaximumWaitTimeSec);
+
return cv;
}
@@ -644,17 +673,21 @@
List<Geometry> geometries =
geoStr != null ? CbGeoUtils.parseGeometriesFromString(geoStr) : null;
- long receivedTimeSec = cursor.getLong(
+ long receivedTimeMillis = cursor.getLong(
cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME));
+ int maximumWaitTimeSec = cursor.getInt(
+ cursor.getColumnIndexOrThrow(CellBroadcasts.MAXIMUM_WAIT_TIME));
+
return new SmsCbMessage(format, geoScope, serialNum, location, category,
- language, body, priority, etwsInfo, cmasInfo, geometries, receivedTimeSec);
+ language, body, priority, etwsInfo, cmasInfo, maximumWaitTimeSec, geometries,
+ receivedTimeMillis);
}
/**
* @return {@code True} if this message needs geo-fencing check.
*/
public boolean needGeoFencingCheck() {
- return mGeometries != null;
+ return mMaximumWaitTimeSec > 0 && mGeometries != null;
}
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index b830860..74c6d5d 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3083,19 +3083,62 @@
*/
@SystemApi
public int getSimCardState() {
- int simCardState = getSimState();
- switch (simCardState) {
+ int simState = getSimState();
+ return getSimCardStateFromSimState(simState);
+ }
+
+ /**
+ * Returns a constant indicating the state of the device SIM card in a physical slot.
+ *
+ * @param physicalSlotIndex physical slot index
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_ABSENT
+ * @see #SIM_STATE_CARD_IO_ERROR
+ * @see #SIM_STATE_CARD_RESTRICTED
+ * @see #SIM_STATE_PRESENT
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public int getSimCardState(int physicalSlotIndex) {
+ int simState = getSimState(getLogicalSlotIndex(physicalSlotIndex));
+ return getSimCardStateFromSimState(simState);
+ }
+
+ /**
+ * Converts SIM state to SIM card state.
+ * @param simState
+ * @return SIM card state
+ */
+ private int getSimCardStateFromSimState(int simState) {
+ switch (simState) {
case SIM_STATE_UNKNOWN:
case SIM_STATE_ABSENT:
case SIM_STATE_CARD_IO_ERROR:
case SIM_STATE_CARD_RESTRICTED:
- return simCardState;
+ return simState;
default:
return SIM_STATE_PRESENT;
}
}
/**
+ * Converts a physical slot index to logical slot index.
+ * @param physicalSlotIndex physical slot index
+ * @return logical slot index
+ */
+ private int getLogicalSlotIndex(int physicalSlotIndex) {
+ UiccSlotInfo[] slotInfos = getUiccSlotsInfo();
+ if (slotInfos != null && physicalSlotIndex >= 0 && physicalSlotIndex < slotInfos.length) {
+ return slotInfos[physicalSlotIndex].getLogicalSlotIdx();
+ }
+
+ return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+ }
+
+ /**
* Returns a constant indicating the state of the card applications on the default SIM card.
*
* @see #SIM_STATE_UNKNOWN
@@ -3110,8 +3153,41 @@
*/
@SystemApi
public int getSimApplicationState() {
- int simApplicationState = getSimStateIncludingLoaded();
- switch (simApplicationState) {
+ int simState = getSimStateIncludingLoaded();
+ return getSimApplicationStateFromSimState(simState);
+ }
+
+ /**
+ * Returns a constant indicating the state of the card applications on the device SIM card in
+ * a physical slot.
+ *
+ * @param physicalSlotIndex physical slot index
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_PIN_REQUIRED
+ * @see #SIM_STATE_PUK_REQUIRED
+ * @see #SIM_STATE_NETWORK_LOCKED
+ * @see #SIM_STATE_NOT_READY
+ * @see #SIM_STATE_PERM_DISABLED
+ * @see #SIM_STATE_LOADED
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public int getSimApplicationState(int physicalSlotIndex) {
+ int simState =
+ SubscriptionManager.getSimStateForSlotIndex(getLogicalSlotIndex(physicalSlotIndex));
+ return getSimApplicationStateFromSimState(simState);
+ }
+
+ /**
+ * Converts SIM state to SIM application state.
+ * @param simState
+ * @return SIM application state
+ */
+ private int getSimApplicationStateFromSimState(int simState) {
+ switch (simState) {
case SIM_STATE_UNKNOWN:
case SIM_STATE_ABSENT:
case SIM_STATE_CARD_IO_ERROR:
@@ -3122,14 +3198,14 @@
// NOT_READY to either LOCKED or LOADED.
return SIM_STATE_NOT_READY;
default:
- return simApplicationState;
+ return simState;
}
}
/**
- * Returns a constant indicating the state of the device SIM card in a slot.
+ * Returns a constant indicating the state of the device SIM card in a logical slot.
*
- * @param slotIndex
+ * @param slotIndex logical slot index
*
* @see #SIM_STATE_UNKNOWN
* @see #SIM_STATE_ABSENT
diff --git a/telephony/java/android/telephony/ims/feature/ImsFeature.java b/telephony/java/android/telephony/ims/feature/ImsFeature.java
index 3a9979d..3562880 100644
--- a/telephony/java/android/telephony/ims/feature/ImsFeature.java
+++ b/telephony/java/android/telephony/ims/feature/ImsFeature.java
@@ -201,15 +201,20 @@
}
/**
- * Contains the capabilities defined and supported by an ImsFeature in the form of a bit mask.
- * <p>
- * Typically this class is not used directly, but rather extended in subclasses of
+ * Contains the IMS capabilities defined and supported by an ImsFeature in the form of a
+ * bit-mask.
+ *
+ * @deprecated This class is not used directly, but rather extended in subclasses of
* {@link ImsFeature} to provide service specific capabilities.
+ * @see MmTelFeature.MmTelCapabilities
* @hide
*/
- @SystemApi
+ // Not Actually deprecated, but we need to remove it from the @SystemApi surface.
+ @Deprecated
+ @SystemApi // SystemApi only because it was leaked through type usage in a previous release.
public static class Capabilities {
/** @deprecated Use getters and accessors instead. */
+ // Not actually deprecated, but we need to remove it from the @SystemApi surface eventually.
protected int mCapabilities = 0;
/**
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index 20c191d..ceb4704 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -242,8 +242,8 @@
* @param capabilities The capabilities that are supported for MmTel in the form of a
* bitfield.
*/
- public MmTelCapabilities(int capabilities) {
- mCapabilities = capabilities;
+ public MmTelCapabilities(@MmTelCapability int capabilities) {
+ super(capabilities);
}
@IntDef(flag = true,
diff --git a/telephony/java/com/android/internal/telephony/CbGeoUtils.java b/telephony/java/com/android/internal/telephony/CbGeoUtils.java
index 73dd822..0b73252 100644
--- a/telephony/java/com/android/internal/telephony/CbGeoUtils.java
+++ b/telephony/java/com/android/internal/telephony/CbGeoUtils.java
@@ -299,7 +299,8 @@
* @return the encoded string.
*/
@NonNull
- public static String encodeGeometriesToString(@NonNull List<Geometry> geometries) {
+ public static String encodeGeometriesToString(List<Geometry> geometries) {
+ if (geometries == null || geometries.isEmpty()) return "";
return geometries.stream()
.map(geometry -> encodeGeometryToString(geometry))
.filter(encodedStr -> !TextUtils.isEmpty(encodedStr))
diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
index dca4e6b..6eea118 100644
--- a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
+++ b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
@@ -104,7 +104,7 @@
header.getSerialNumber(), location, header.getServiceCategory(), null,
getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
- header.getCmasInfo(), null /* geometries */, receivedTimeMillis);
+ header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis);
} else if (header.isUmtsFormat()) {
// UMTS format has only 1 PDU
byte[] pdu = pdus[0];
@@ -120,9 +120,13 @@
// Has Warning Area Coordinates information
List<Geometry> geometries = null;
+ int maximumWaitingTimeSec = 255;
if (pdu.length > wacDataOffset) {
try {
- geometries = parseWarningAreaCoordinates(pdu, wacDataOffset);
+ Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu,
+ wacDataOffset);
+ maximumWaitingTimeSec = wac.first;
+ geometries = wac.second;
} catch (Exception ex) {
// Catch the exception here, the message will be considered as having no WAC
// information which means the message will be broadcasted directly.
@@ -133,7 +137,8 @@
return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
header.getGeographicalScope(), header.getSerialNumber(), location,
header.getServiceCategory(), language, body, priority,
- header.getEtwsInfo(), header.getCmasInfo(), geometries, receivedTimeMillis);
+ header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, geometries,
+ receivedTimeMillis);
} else {
String language = null;
StringBuilder sb = new StringBuilder();
@@ -148,7 +153,7 @@
return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
header.getGeographicalScope(), header.getSerialNumber(), location,
header.getServiceCategory(), language, sb.toString(), priority,
- header.getEtwsInfo(), header.getCmasInfo(), null /* geometries */,
+ header.getEtwsInfo(), header.getCmasInfo(), 0, null /* geometries */,
receivedTimeMillis);
}
}
@@ -197,7 +202,17 @@
}
}
- private static List<Geometry> parseWarningAreaCoordinates(byte[] pdu, int wacOffset) {
+ /**
+ * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV.
+ *
+ * @param pdu Warning Area Coordinates TLV.
+ * @param wacOffset the offset of Warning Area Coordinates TLV.
+ * @return a pair with the first element is maximum wait time and the second is the broadcast
+ * area. The default value of the maximum wait time is 255 which means use the device default
+ * value.
+ */
+ private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates(
+ byte[] pdu, int wacOffset) {
// little-endian
int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset];
int offset = wacOffset + 2;
@@ -209,6 +224,8 @@
BitStreamReader bitReader = new BitStreamReader(pdu, offset);
+ int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET;
+
List<Geometry> geo = new ArrayList<>();
int remainedBytes = wacDataLength;
while (remainedBytes > 0) {
@@ -220,8 +237,7 @@
switch (type) {
case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME:
- // TODO: handle the maximum wait time in cell broadcast provider.
- int maximumWaitTimeSec = bitReader.read(8);
+ maximumWaitTimeSec = bitReader.read(8);
break;
case CbGeoUtils.GEOMETRY_TYPE_POLYGON:
List<LatLng> latLngs = new ArrayList<>();
@@ -247,7 +263,7 @@
throw new IllegalArgumentException("Unsupported geoType = " + type);
}
}
- return geo;
+ return new Pair(maximumWaitTimeSec, geo);
}
/**