Merge "Change the impl lib name of java_sdk_library"
diff --git a/api/system-current.txt b/api/system-current.txt
index 276be9d..37b5e1e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4445,6 +4445,10 @@
method public abstract java.lang.Object onTransactStarted(android.os.IBinder, int);
}
+ public static class Build.VERSION {
+ field public static final java.lang.String PREVIEW_SDK_FINGERPRINT;
+ }
+
public final class ConfigUpdate {
field public static final java.lang.String ACTION_UPDATE_CARRIER_ID_DB = "android.os.action.UPDATE_CARRIER_ID_DB";
field public static final java.lang.String ACTION_UPDATE_CARRIER_PROVISIONING_URLS = "android.intent.action.UPDATE_CARRIER_PROVISIONING_URLS";
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index f4a1715..453a0c0 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -25,6 +25,7 @@
import "frameworks/base/core/proto/android/app/settings_enums.proto";
import "frameworks/base/core/proto/android/app/job/enums.proto";
import "frameworks/base/core/proto/android/bluetooth/enums.proto";
+import "frameworks/base/core/proto/android/bluetooth/hci/enums.proto";
import "frameworks/base/core/proto/android/net/networkcapabilities.proto";
import "frameworks/base/core/proto/android/os/enums.proto";
import "frameworks/base/core/proto/android/server/connectivity/data_stall_event.proto";
@@ -183,6 +184,7 @@
RescuePartyResetReported rescue_party_reset_reported = 122;
SignedConfigReported signed_config_reported = 123;
GnssNiEventReported gnss_ni_event_reported = 124;
+ BluetoothLinkLayerConnectionEvent bluetooth_link_layer_connection_event = 125;
}
// Pulled events will start at field 10000.
@@ -1316,6 +1318,90 @@
optional int32 bt_profile = 3;
}
+// Logs when there is an event affecting Bluetooth device's link layer connection.
+// - This event is triggered when there is a related HCI command or event
+// - Users of this metrics can deduce Bluetooth device's connection state from these events
+// - HCI commands are logged before the command is sent, after receiving command status, and after
+// receiving command complete
+// - HCI events are logged when they arrive
+//
+// Low level log from system/bt
+//
+// Bluetooth classic commands:
+// - CMD_CREATE_CONNECTION
+// - CMD_DISCONNECT
+// - CMD_CREATE_CONNECTION_CANCEL
+// - CMD_ACCEPT_CONNECTION_REQUEST
+// - CMD_REJECT_CONNECTION_REQUEST
+// - CMD_SETUP_ESCO_CONNECTION
+// - CMD_ACCEPT_ESCO_CONNECTION
+// - CMD_REJECT_ESCO_CONNECTION
+// - CMD_ENH_SETUP_ESCO_CONNECTION
+// - CMD_ENH_ACCEPT_ESCO_CONNECTION
+//
+// Bluetooth low energy commands:
+// - CMD_BLE_CREATE_LL_CONN [Only logged on error or when initiator filter policy is 0x00]
+// - CMD_BLE_CREATE_CONN_CANCEL [Only logged when there is an error]
+// - CMD_BLE_EXTENDED_CREATE_CONNECTION [Only logged on error or when initiator filter policy is 0x00]
+// - CMD_BLE_CLEAR_WHITE_LIST
+// - CMD_BLE_ADD_WHITE_LIST
+// - CMD_BLE_REMOVE_WHITE_LIST
+//
+// Bluetooth classic events:
+// - EVT_CONNECTION_COMP
+// - EVT_CONNECTION_REQUEST
+// - EVT_DISCONNECTION_COMP
+// - EVT_ESCO_CONNECTION_COMP
+// - EVT_ESCO_CONNECTION_CHANGED
+//
+// Bluetooth low energy meta events:
+// - BLE_EVT_CONN_COMPLETE_EVT
+// - BLE_EVT_ENHANCED_CONN_COMPLETE_EVT
+//
+// Next tag: 10
+message BluetoothLinkLayerConnectionEvent {
+ // An identifier that can be used to match events for this device.
+ // Currently, this is a salted hash of the MAC address of this Bluetooth device.
+ // Salt: Randomly generated 256 bit value
+ // Hash algorithm: HMAC-SHA256
+ // Size: 32 byte
+ // Default: null or empty if the device identifier is not known
+ optional bytes obfuscated_id = 1 [(android.os.statsd.log_mode) = MODE_BYTES];
+ // Connection handle of this connection if available
+ // Range: 0x0000 - 0x0EFF (12 bits)
+ // Default: 0xFFFF if the handle is unknown
+ optional int32 connection_handle = 2;
+ // Direction of the link
+ // Default: DIRECTION_UNKNOWN
+ optional android.bluetooth.DirectionEnum direction = 3;
+ // Type of this link
+ // Default: LINK_TYPE_UNKNOWN
+ optional android.bluetooth.LinkTypeEnum type = 4;
+
+ // Reason metadata for this link layer connection event, rules for interpretation:
+ // 1. If hci_cmd is set and valid, hci_event can be either EVT_COMMAND_STATUS or
+ // EVT_COMMAND_COMPLETE, ignore hci_ble_event in this case
+ // 2. If hci_event is set to EVT_BLE_META, look at hci_ble_event; otherwise, if hci_event is
+ // set and valid, ignore hci_ble_event
+
+ // HCI command associated with this event
+ // Default: CMD_UNKNOWN
+ optional android.bluetooth.hci.CommandEnum hci_cmd = 5;
+ // HCI event associated with this event
+ // Default: EVT_UNKNOWN
+ optional android.bluetooth.hci.EventEnum hci_event = 6;
+ // HCI BLE meta event associated with this event
+ // Default: BLE_EVT_UNKNOWN
+ optional android.bluetooth.hci.BleMetaEventEnum hci_ble_event = 7;
+ // HCI command status code if this is triggerred by hci_cmd
+ // Default: STATUS_UNKNOWN
+ optional android.bluetooth.hci.StatusEnum cmd_status = 8;
+ // HCI reason code associated with this event
+ // Default: STATUS_UNKNOWN
+ optional android.bluetooth.hci.StatusEnum reason_code = 9;
+}
+
+
/**
* Logs when something is plugged into or removed from the USB-C connector.
*
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 98c5a0fb..d374f1c 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -17,6 +17,7 @@
package android.app;
import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+import static android.os.Process.myUid;
import static java.lang.Character.MIN_VALUE;
@@ -1025,7 +1026,7 @@
*/
@Nullable private ContentCaptureManager getContentCaptureManager() {
// ContextCapture disabled for system apps
- if (getApplicationInfo().isSystemApp()) return null;
+ if (!UserHandle.isApp(myUid())) return null;
if (mContentCaptureManager == null) {
mContentCaptureManager = getSystemService(ContentCaptureManager.class);
}
@@ -1048,9 +1049,8 @@
private void notifyContentCaptureManagerIfNeeded(@ContentCaptureNotificationType int type) {
final ContentCaptureManager cm = getContentCaptureManager();
- if (cm == null) {
- return;
- }
+ if (cm == null) return;
+
switch (type) {
case CONTENT_CAPTURE_START:
//TODO(b/111276913): decide whether the InteractionSessionId should be
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index be8009e..a7734f5 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -33,6 +33,8 @@
import android.util.ArrayMap;
import android.util.Log;
+import java.util.List;
+
/**
* The {@link HdmiControlManager} class is used to send HDMI control messages
* to attached CEC devices.
@@ -404,6 +406,72 @@
}
/**
+ * Get a snapshot of the real-time status of the remote devices.
+ *
+ * @return a list of {@link HdmiDeviceInfo} of the devices connected to the current device.
+ *
+ * TODO(b/110094868): unhide for Q
+ * @hide
+ */
+ public List<HdmiDeviceInfo> getConnectedDevicesList() {
+ try {
+ return mService.getDeviceList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Power off the target device.
+ *
+ * @param deviceInfo HdmiDeviceInfo of the device to be powered off
+ *
+ * TODO(b/110094868): unhide for Q
+ * @hide
+ */
+ public void powerOffRemoteDevice(HdmiDeviceInfo deviceInfo) {
+ try {
+ mService.powerOffRemoteDevice(
+ deviceInfo.getLogicalAddress(), deviceInfo.getDevicePowerStatus());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Power on the target device.
+ *
+ * @param deviceInfo HdmiDeviceInfo of the device to be powered on
+ *
+ * TODO(b/110094868): unhide for Q
+ * @hide
+ */
+ public void powerOnRemoteDevice(HdmiDeviceInfo deviceInfo) {
+ try {
+ mService.powerOnRemoteDevice(
+ deviceInfo.getLogicalAddress(), deviceInfo.getDevicePowerStatus());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Ask the target device to be the new Active Source.
+ *
+ * @param deviceInfo HdmiDeviceInfo of the target device
+ *
+ * TODO(b/110094868): unhide for Q
+ * @hide
+ */
+ public void askRemoteDeviceToBecomeActiveSource(HdmiDeviceInfo deviceInfo) {
+ try {
+ mService.askRemoteDeviceToBecomeActiveSource(deviceInfo.getPhysicalAddress());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Controls standby mode of the system. It will also try to turn on/off the connected devices if
* necessary.
*
diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
index 66bb084..1cd9920 100644
--- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl
+++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
@@ -61,6 +61,9 @@
void setInputChangeListener(IHdmiInputChangeListener listener);
List<HdmiDeviceInfo> getInputDevices();
List<HdmiDeviceInfo> getDeviceList();
+ void powerOffRemoteDevice(int logicalAddress, int powerStatus);
+ void powerOnRemoteDevice(int logicalAddress, int powerStatus);
+ void askRemoteDeviceToBecomeActiveSource(int physicalAddress);
void sendVendorCommand(int deviceType, int targetAddress, in byte[] params,
boolean hasVendorId);
void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType);
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
index 617125b3..c2963fd 100644
--- a/core/java/android/net/LinkProperties.java
+++ b/core/java/android/net/LinkProperties.java
@@ -191,6 +191,7 @@
}
setMtu(source.mMtu);
mTcpBufferSizes = source.mTcpBufferSizes;
+ mNat64Prefix = source.mNat64Prefix;
}
}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 9fea873..2d61a4e 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -289,6 +289,26 @@
"ro.build.version.preview_sdk", 0);
/**
+ * The SDK fingerprint for a given prerelease SDK. This value will always be
+ * {@code REL} on production platform builds/devices.
+ *
+ * <p>When this value is not {@code REL}, it contains a string fingerprint of the API
+ * surface exposed by the preview SDK. Preview platforms with different API surfaces
+ * will have different {@code PREVIEW_SDK_FINGERPRINT}.
+ *
+ * <p>This attribute is intended for use by installers for finer grained targeting of
+ * packages. Applications targeting preview APIs should not use this field and should
+ * instead use {@code PREVIEW_SDK_INT} or use reflection or other runtime checks to
+ * detect the presence of an API or guard themselves against unexpected runtime
+ * behavior.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String PREVIEW_SDK_FINGERPRINT = SystemProperties.get(
+ "ro.build.version.preview_sdk_fingerprint", "REL");
+
+ /**
* The current development codename, or the string "REL" if this is
* a release build.
*/
diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl
index fdd7488..8ced722 100644
--- a/core/java/android/os/INetworkManagementService.aidl
+++ b/core/java/android/os/INetworkManagementService.aidl
@@ -356,16 +356,6 @@
void removeVpnUidRanges(int netId, in UidRange[] ranges);
/**
- * Start the clatd (464xlat) service on the given interface.
- */
- void startClatd(String interfaceName);
-
- /**
- * Stop the clatd (464xlat) service on the given interface.
- */
- void stopClatd(String interfaceName);
-
- /**
* Start listening for mobile activity state changes.
*/
void registerNetworkActivityListener(INetworkActivityListener listener);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 10555fa..39c4266 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11091,6 +11091,31 @@
/** {@hide} */
public static final String
BLUETOOTH_HEARING_AID_PRIORITY_PREFIX = "bluetooth_hearing_aid_priority_";
+ /**
+ * Enable/disable radio bug detection
+ *
+ * {@hide}
+ */
+ public static final String
+ ENABLE_RADIO_BUG_DETECTION = "enable_radio_bug_detection";
+
+ /**
+ * Count threshold of RIL wakelock timeout for radio bug detection
+ *
+ * {@hide}
+ */
+ public static final String
+ RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD =
+ "radio_bug_wakelock_timeout_count_threshold";
+
+ /**
+ * Count threshold of RIL system error for radio bug detection
+ *
+ * {@hide}
+ */
+ public static final String
+ RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD =
+ "radio_bug_system_error_count_threshold";
/**
* Activity manager specific settings.
diff --git a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
index 0116961..aaba85b 100644
--- a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
+++ b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java
@@ -42,6 +42,7 @@
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAugmentedAutofillManagerClient;
+import android.view.autofill.IAutofillWindowPresenter;
import com.android.internal.annotations.GuardedBy;
@@ -95,10 +96,10 @@
}
@Override
- public void onDestroyFillWindowRequest(int sessionId) {
+ public void onDestroyAllFillWindowsRequest() {
mHandler.sendMessage(
- obtainMessage(AugmentedAutofillService::handleOnDestroyFillWindowRequest,
- AugmentedAutofillService.this, sessionId));
+ obtainMessage(AugmentedAutofillService::handleOnDestroyAllFillWindowsRequest,
+ AugmentedAutofillService.this));
}
};
@@ -185,18 +186,21 @@
new FillCallback(proxy));
}
- private void handleOnDestroyFillWindowRequest(@NonNull int sessionId) {
- AutofillProxy proxy = null;
+ private void handleOnDestroyAllFillWindowsRequest() {
if (mAutofillProxies != null) {
- proxy = mAutofillProxies.get(sessionId);
+ final int size = mAutofillProxies.size();
+ for (int i = 0; i < size; i++) {
+ final int sessionId = mAutofillProxies.keyAt(i);
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ if (proxy == null) {
+ // TODO(b/111330312): this might be fine, in which case we should logv it
+ Log.w(TAG, "No proxy for session " + sessionId);
+ return;
+ }
+ proxy.destroy();
+ }
+ mAutofillProxies.clear();
}
- if (proxy == null) {
- // TODO(b/111330312): this might be fine, in which case we should logv it
- Log.w(TAG, "No proxy for session " + sessionId);
- return;
- }
- proxy.destroy();
- mAutofillProxies.remove(sessionId);
}
private void handleOnUnbind() {
@@ -350,6 +354,16 @@
}
}
+ public void requestShowFillUi(int width, int height, Rect anchorBounds,
+ IAutofillWindowPresenter presenter) throws RemoteException {
+ mClient.requestShowFillUi(mSessionId, mFocusedId, width, height, anchorBounds,
+ presenter);
+ }
+
+ public void requestHideFillUi() throws RemoteException {
+ mClient.requestHideFillUi(mSessionId, mFocusedId);
+ }
+
private void update(@NonNull AutofillId focusedId, @NonNull AutofillValue focusedValue) {
synchronized (mLock) {
// TODO(b/111330312): should we close the popupwindow if the focused id changed?
diff --git a/core/java/android/service/autofill/augmented/FillWindow.java b/core/java/android/service/autofill/augmented/FillWindow.java
index 33b88e42..51b0f01 100644
--- a/core/java/android/service/autofill/augmented/FillWindow.java
+++ b/core/java/android/service/autofill/augmented/FillWindow.java
@@ -16,22 +16,25 @@
package android.service.autofill.augmented;
import static android.service.autofill.augmented.AugmentedAutofillService.DEBUG;
+import static android.service.autofill.augmented.AugmentedAutofillService.VERBOSE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.TestApi;
-import android.app.Dialog;
import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
import android.service.autofill.augmented.PresentationParams.Area;
import android.util.Log;
-import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
import android.view.WindowManager;
+import android.view.autofill.IAutofillWindowPresenter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
@@ -71,7 +74,7 @@
/** Indicates the data being shown is a physical address */
public static final long FLAG_METADATA_ADDRESS = 0x1;
- // TODO(b/111330312): add moar flags
+ // TODO(b/111330312): add more flags
/** @hide */
@LongDef(prefix = { "FLAG" }, value = {
@@ -83,8 +86,17 @@
private final Object mLock = new Object();
private final CloseGuard mCloseGuard = CloseGuard.get();
+ private final @NonNull Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
+ private final @NonNull FillWindowPresenter mFillWindowPresenter = new FillWindowPresenter();
+
@GuardedBy("mLock")
- private Dialog mDialog;
+ private WindowManager mWm;
+ @GuardedBy("mLock")
+ private View mFillView;
+ @GuardedBy("mLock")
+ private boolean mShowing;
+ @GuardedBy("mLock")
+ private Rect mBounds;
@GuardedBy("mLock")
private boolean mDestroyed;
@@ -140,51 +152,28 @@
// window instead of destroying. In fact, it might be better to allocate a full window
// initially, which is transparent (and let touches get through) everywhere but in the
// rect boundaries.
- destroy();
// TODO(b/111330312): make sure all touch events are handled, window is always closed,
// etc.
- mDialog = new Dialog(rootView.getContext()) {
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
- FillWindow.this.destroy();
+ mWm = rootView.getContext().getSystemService(WindowManager.class);
+ mFillView = rootView;
+ // Listen to the touch outside to destroy the window when typing is detected.
+ mFillView.setOnTouchListener(
+ (view, motionEvent) -> {
+ if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ if (VERBOSE) Log.v(TAG, "Outside touch detected, hiding the window");
+ hide();
+ }
+ return false;
}
- return false;
- }
- };
- mCloseGuard.open("destroy");
- final Window window = mDialog.getWindow();
- window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
- // Makes sure touch outside the dialog is received by the window behind the dialog.
- window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
- // Makes sure the touch outside the dialog is received by the dialog to dismiss it.
- window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
- // Makes sure keyboard shows up.
- window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
-
- final int height = rect.bottom - rect.top;
- final int width = rect.right - rect.left;
- final WindowManager.LayoutParams windowParams = window.getAttributes();
- windowParams.gravity = Gravity.TOP | Gravity.LEFT;
- windowParams.y = rect.top + height;
- windowParams.height = height;
- windowParams.x = rect.left;
- windowParams.width = width;
-
- window.setAttributes(windowParams);
- window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
- window.setBackgroundDrawableResource(android.R.color.transparent);
-
- mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
- final ViewGroup.LayoutParams diagParams = new ViewGroup.LayoutParams(width, height);
- mDialog.setContentView(rootView, diagParams);
-
+ );
+ mShowing = false;
+ mBounds = new Rect(area.getBounds());
if (DEBUG) {
Log.d(TAG, "Created FillWindow: params= " + smartSuggestion + " view=" + rootView);
}
-
+ mDestroyed = false;
mProxy.setFillWindow(this);
return true;
}
@@ -194,36 +183,87 @@
void show() {
// TODO(b/111330312): check if updated first / throw exception
if (DEBUG) Log.d(TAG, "show()");
-
synchronized (mLock) {
checkNotDestroyedLocked();
- if (mDialog == null) {
+ if (mWm == null || mFillView == null) {
throw new IllegalStateException("update() not called yet, or already destroyed()");
}
-
- mDialog.show();
if (mProxy != null) {
+ try {
+ mProxy.requestShowFillUi(mBounds.right - mBounds.left,
+ mBounds.bottom - mBounds.top,
+ /*anchorBounds=*/ null, mFillWindowPresenter);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to show fill window", e);
+ }
mProxy.report(AutofillProxy.REPORT_EVENT_UI_SHOWN);
}
}
}
/**
+ * Hides the window.
+ *
+ * <p>The window is not destroyed and can be shown again
+ */
+ private void hide() {
+ if (DEBUG) Log.d(TAG, "hide()");
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+ if (mWm == null || mFillView == null) {
+ throw new IllegalStateException("update() not called yet, or already destroyed()");
+ }
+ if (mProxy != null && mShowing) {
+ try {
+ mProxy.requestHideFillUi();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to hide fill window", e);
+ }
+ }
+ }
+ }
+
+ private void handleShow(WindowManager.LayoutParams p) {
+ if (DEBUG) Log.d(TAG, "handleShow()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null) {
+ p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ if (!mShowing) {
+ mWm.addView(mFillView, p);
+ mShowing = true;
+ } else {
+ mWm.updateViewLayout(mFillView, p);
+ }
+ }
+ }
+ }
+
+ private void handleHide() {
+ if (DEBUG) Log.d(TAG, "handleHide()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null && mShowing) {
+ mWm.removeView(mFillView);
+ mShowing = false;
+ }
+ }
+ }
+
+ /**
* Destroys the window.
*
* <p>Once destroyed, this window cannot be used anymore
*/
public void destroy() {
- if (DEBUG) Log.d(TAG, "destroy(): mDestroyed=" + mDestroyed + " mDialog=" + mDialog);
-
- synchronized (this) {
- if (mDestroyed || mDialog == null) return;
-
- mDialog.dismiss();
- mDialog = null;
- if (mProxy != null) {
- mProxy.report(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
- }
+ if (DEBUG) {
+ Log.d(TAG,
+ "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView="
+ + mFillView);
+ }
+ synchronized (mLock) {
+ if (mDestroyed) return;
+ hide();
+ mProxy.report(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
+ mDestroyed = true;
mCloseGuard.close();
}
}
@@ -250,11 +290,15 @@
public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
synchronized (this) {
pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
- if (mDialog != null) {
- pw.print(prefix); pw.print("dialog: ");
- pw.println(mDialog.isShowing() ? "shown" : "hidden");
- pw.print(prefix); pw.print("window: ");
- pw.println(mDialog.getWindow().getAttributes());
+ if (mFillView != null) {
+ pw.print(prefix); pw.print("fill window: ");
+ pw.println(mShowing ? "shown" : "hidden");
+ pw.print(prefix); pw.print("fill view: ");
+ pw.println(mFillView);
+ pw.print(prefix); pw.print("mBounds: ");
+ pw.println(mBounds);
+ pw.print(prefix); pw.print("mWm: ");
+ pw.println(mWm);
}
}
}
@@ -264,4 +308,19 @@
public void close() throws Exception {
destroy();
}
+
+ private final class FillWindowPresenter extends IAutofillWindowPresenter.Stub {
+ @Override
+ public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
+ boolean fitsSystemWindows, int layoutDirection) {
+ if (DEBUG) Log.d(TAG, "FillWindowPresenter.show()");
+ mUiThreadHandler.sendMessage(obtainMessage(FillWindow::handleShow, FillWindow.this, p));
+ }
+
+ @Override
+ public void hide(Rect transitionEpicenter) {
+ if (DEBUG) Log.d(TAG, "FillWindowPresenter.hide()");
+ mUiThreadHandler.sendMessage(obtainMessage(FillWindow::handleHide, FillWindow.this));
+ }
+ }
}
diff --git a/core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl b/core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl
index b3ac2da..fb6912a 100644
--- a/core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl
+++ b/core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl
@@ -36,5 +36,5 @@
in ComponentName activityComponent, in AutofillId focusedId,
in AutofillValue focusedValue, long requestTime, in IFillCallback callback);
- void onDestroyFillWindowRequest(int sessionId);
+ void onDestroyAllFillWindowsRequest();
}
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index e5e028d..1eaa3c5 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -323,15 +323,21 @@
mSessionUids.put(sessionId, uid);
onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
- final int flags = context.getFlags();
- if ((flags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
- setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED_BY_FLAG_SECURE,
- mClientInterface.asBinder());
- return;
+ final int clientFlags = context.getFlags();
+ int stateFlags = 0;
+ if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
+ stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE;
}
+ if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) {
+ stateFlags |= ContentCaptureSession.STATE_BY_APP;
+ }
+ if (stateFlags == 0) {
+ stateFlags = ContentCaptureSession.STATE_ACTIVE;
+ } else {
+ stateFlags |= ContentCaptureSession.STATE_DISABLED;
- setClientState(clientReceiver, ContentCaptureSession.STATE_ACTIVE,
- mClientInterface.asBinder());
+ }
+ setClientState(clientReceiver, stateFlags, mClientInterface.asBinder());
}
private void handleSendEvents(int uid,
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 93941d0..888a4c5 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -2997,5 +2997,23 @@
afm.post(() -> afm.autofill(sessionId, ids, values));
}
}
+
+ @Override
+ public void requestShowFillUi(int sessionId, AutofillId id, int width, int height,
+ Rect anchorBounds, IAutofillWindowPresenter presenter) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestShowFillUi(sessionId, id, width, height, anchorBounds,
+ presenter));
+ }
+ }
+
+ @Override
+ public void requestHideFillUi(int sessionId, AutofillId id) {
+ final AutofillManager afm = mAfm.get();
+ if (afm != null) {
+ afm.post(() -> afm.requestHideFillUi(id, false));
+ }
+ }
}
}
diff --git a/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl b/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl
index 67cd0bf..140507c 100644
--- a/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl
@@ -21,6 +21,7 @@
import android.graphics.Rect;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
+import android.view.autofill.IAutofillWindowPresenter;
/**
* Object running in the application process and responsible to provide the functionalities
@@ -29,6 +30,24 @@
* @hide
*/
interface IAugmentedAutofillManagerClient {
- Rect getViewCoordinates(in AutofillId id);
- void autofill(int sessionId, in List<AutofillId> ids, in List<AutofillValue> values);
+ /**
+ * Gets the coordinates of the input field view.
+ */
+ Rect getViewCoordinates(in AutofillId id);
+
+ /**
+ * Autofills the activity with the contents of the values.
+ */
+ void autofill(int sessionId, in List<AutofillId> ids, in List<AutofillValue> values);
+
+ /**
+ * Requests showing the fill UI.
+ */
+ void requestShowFillUi(int sessionId, in AutofillId id, int width, int height,
+ in Rect anchorBounds, in IAutofillWindowPresenter presenter);
+
+ /**
+ * Requests hiding the fill UI.
+ */
+ void requestHideFillUi(int sessionId, in AutofillId id);
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index ff45efd..81b2e01 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -29,12 +29,12 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.Preconditions;
import com.android.internal.util.SyncResultReceiver;
import java.io.PrintWriter;
-import java.util.concurrent.atomic.AtomicBoolean;
/*
* NOTE: all methods in this class should return right away, or do the real work in a handler
@@ -62,8 +62,10 @@
static final boolean VERBOSE = false;
static final boolean DEBUG = true; // STOPSHIP if not set to false
- @NonNull
- private final AtomicBoolean mDisabled = new AtomicBoolean();
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private boolean mDisabled;
@NonNull
private final Context mContext;
@@ -71,11 +73,16 @@
@Nullable
private final IContentCaptureManager mService;
+ // Flags used for starting session.
+ @GuardedBy("mLock")
+ private int mFlags;
+
// TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
// held at the Application level
@NonNull
private final Handler mHandler;
+ @GuardedBy("mLock")
private MainContentCaptureSession mMainSession;
/** @hide */
@@ -114,20 +121,25 @@
@NonNull
@UiThread
public MainContentCaptureSession getMainContentCaptureSession() {
- if (mMainSession == null) {
- mMainSession = new MainContentCaptureSession(mContext, mHandler, mService,
- mDisabled);
- if (VERBOSE) {
- Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
+ synchronized (mLock) {
+ if (mMainSession == null) {
+ mMainSession = new MainContentCaptureSession(mContext, mHandler, mService,
+ mDisabled);
+ if (VERBOSE) {
+ Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
+ }
}
+ return mMainSession;
}
- return mMainSession;
}
/** @hide */
public void onActivityStarted(@NonNull IBinder applicationToken,
@NonNull ComponentName activityComponent, int flags) {
- getMainContentCaptureSession().start(applicationToken, activityComponent, flags);
+ synchronized (mLock) {
+ mFlags |= flags;
+ getMainContentCaptureSession().start(applicationToken, activityComponent, mFlags);
+ }
}
/** @hide */
@@ -173,7 +185,9 @@
* Checks whether content capture is enabled for this activity.
*/
public boolean isContentCaptureEnabled() {
- return mService != null && !mDisabled.get();
+ synchronized (mLock) {
+ return mService != null && !mDisabled;
+ }
}
/**
@@ -183,7 +197,9 @@
* it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
*/
public void setContentCaptureEnabled(boolean enabled) {
- //TODO(b/111276913): implement (need to finish / disable all sessions)
+ synchronized (mLock) {
+ mFlags |= enabled ? 0 : ContentCaptureContext.FLAG_DISABLED_BY_APP;
+ }
}
/**
@@ -198,20 +214,22 @@
/** @hide */
public void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.println("ContentCaptureManager");
-
- pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled.get());
- pw.print(prefix); pw.print("Context: "); pw.println(mContext);
- pw.print(prefix); pw.print("User: "); pw.println(mContext.getUserId());
- if (mService != null) {
- pw.print(prefix); pw.print("Service: "); pw.println(mService);
- }
- if (mMainSession != null) {
- final String prefix2 = prefix + " ";
- pw.print(prefix); pw.println("Main session:");
- mMainSession.dump(prefix2, pw);
- } else {
- pw.print(prefix); pw.println("No sessions");
+ synchronized (mLock) {
+ pw.print(prefix); pw.println("ContentCaptureManager");
+ pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled);
+ pw.print(prefix); pw.print("Context: "); pw.println(mContext);
+ pw.print(prefix); pw.print("User: "); pw.println(mContext.getUserId());
+ if (mService != null) {
+ pw.print(prefix); pw.print("Service: "); pw.println(mService);
+ }
+ pw.print(prefix); pw.print("Flags: "); pw.println(mFlags);
+ if (mMainSession != null) {
+ final String prefix2 = prefix + " ";
+ pw.print(prefix); pw.println("Main session:");
+ mMainSession.dump(prefix2, pw);
+ } else {
+ pw.print(prefix); pw.println("No sessions");
+ }
}
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index d9a8416..2123308 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -21,6 +21,7 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.util.DebugUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewStructure;
@@ -56,42 +57,58 @@
*
* @hide
*/
- public static final int STATE_UNKNOWN = 0;
+ // NOTE: not prefixed by STATE_ so it's not printed on getStateAsString()
+ public static final int UNKNWON_STATE = 0x0;
/**
* Service's startSession() was called, but server didn't confirm it was created yet.
*
* @hide
*/
- public static final int STATE_WAITING_FOR_SERVER = 1;
+ public static final int STATE_WAITING_FOR_SERVER = 0x1;
/**
* Session is active.
*
* @hide
*/
- public static final int STATE_ACTIVE = 2;
+ public static final int STATE_ACTIVE = 0x2;
/**
* Session is disabled because there is no service for this user.
*
* @hide
*/
- public static final int STATE_DISABLED_NO_SERVICE = 3;
+ public static final int STATE_DISABLED = 0x4;
/**
* Session is disabled because its id already existed on server.
*
* @hide
*/
- public static final int STATE_DISABLED_DUPLICATED_ID = 4;
+ public static final int STATE_DUPLICATED_ID = 0x8;
+
+ /**
+ * Session is disabled because service is not set for user.
+ *
+ * @hide
+ */
+ public static final int STATE_NO_SERVICE = 0x10;
/**
* Session is disabled by FLAG_SECURE
*
* @hide
*/
- public static final int STATE_DISABLED_BY_FLAG_SECURE = 5;
+ public static final int STATE_FLAG_SECURE = 0x20;
+
+ /**
+ * Session is disabled manually by the specific app.
+ *
+ * @hide
+ */
+ public static final int STATE_BY_APP = 0x40;
+
private static final int INITIAL_CHILDREN_CAPACITY = 5;
@@ -110,7 +127,7 @@
@Nullable
protected final String mId;
- private int mState = STATE_UNKNOWN;
+ private int mState = UNKNWON_STATE;
// Lazily created on demand.
private ContentCaptureSessionId mContentCaptureSessionId;
@@ -382,21 +399,7 @@
*/
@NonNull
protected static String getStateAsString(int state) {
- switch (state) {
- case STATE_UNKNOWN:
- return "UNKNOWN";
- case STATE_WAITING_FOR_SERVER:
- return "WAITING_FOR_SERVER";
- case STATE_ACTIVE:
- return "ACTIVE";
- case STATE_DISABLED_NO_SERVICE:
- return "DISABLED_NO_SERVICE";
- case STATE_DISABLED_DUPLICATED_ID:
- return "DISABLED_DUPLICATED_ID";
- case STATE_DISABLED_BY_FLAG_SECURE:
- return "DISABLED_FLAG_SECURE";
- default:
- return "INVALID:" + state;
- }
+ return state + " (" + (state == UNKNWON_STATE ? "UNKNOWN"
+ : DebugUtils.flagsToString(ContentCaptureSession.class, "STATE_", state)) + ")";
}
}
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index a29aaf0..1d9018c 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -88,6 +88,7 @@
*/
public static final String EXTRA_BINDER = "binder";
+ // TODO(b/111276913): make sure disabled state is in sync with manager's disabled
@NonNull
private final AtomicBoolean mDisabled;
@@ -113,7 +114,7 @@
@Nullable
private DeathRecipient mDirectServiceVulture;
- private int mState = STATE_UNKNOWN;
+ private int mState = UNKNWON_STATE;
@Nullable
private IBinder mApplicationToken;
@@ -133,11 +134,11 @@
/** @hide */
protected MainContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
@Nullable IContentCaptureManager systemServerInterface,
- @NonNull AtomicBoolean disabled) {
+ @NonNull boolean disabled) {
mContext = context;
mHandler = handler;
mSystemServerInterface = systemServerInterface;
- mDisabled = disabled;
+ mDisabled = new AtomicBoolean(disabled);
}
@Override
@@ -184,7 +185,7 @@
private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName,
int flags) {
- if (mState != STATE_UNKNOWN) {
+ if (mState != UNKNWON_STATE) {
// TODO(b/111276913): revisit this scenario
Log.w(TAG, "ignoring handleStartSession(" + token + ") while on state "
+ getStateAsString(mState));
@@ -247,17 +248,14 @@
}
}
- // TODO(b/111276913): change the resultCode to use flags so there's just one flag for
- // disabled stuff
- if (resultCode == STATE_DISABLED_NO_SERVICE || resultCode == STATE_DISABLED_DUPLICATED_ID
- || resultCode == STATE_DISABLED_BY_FLAG_SECURE) {
+ if ((mState & STATE_DISABLED) != 0) {
mDisabled.set(true);
handleResetSession(/* resetState= */ false);
} else {
mDisabled.set(false);
}
if (VERBOSE) {
- Log.v(TAG, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
+ Log.v(TAG, "handleSessionStarted() result: id=" + mId
+ ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
+ ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
}
@@ -407,7 +405,7 @@
// clearings out.
private void handleResetSession(boolean resetState) {
if (resetState) {
- mState = STATE_UNKNOWN;
+ mState = UNKNWON_STATE;
}
// TODO(b/122454205): must reset children (which currently is owned by superclass)
@@ -496,8 +494,7 @@
}
pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
- pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
- pw.print(getStateAsString(mState)); pw.println(")");
+ pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
if (mApplicationToken != null) {
pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
}
diff --git a/core/proto/Android.bp b/core/proto/Android.bp
new file mode 100644
index 0000000..80cc2d4
--- /dev/null
+++ b/core/proto/Android.bp
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 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.
+
+// C++ library for Bluetooth platform wide protobuf definitions
+cc_library_static {
+ name: "libbt-platform-protos-lite",
+ host_supported: true,
+ proto: {
+ export_proto_headers: true,
+ type: "lite",
+ },
+ srcs: [
+ "android/bluetooth/enums.proto",
+ "android/bluetooth/hci/enums.proto",
+ ],
+}
diff --git a/core/proto/android/bluetooth/enums.proto b/core/proto/android/bluetooth/enums.proto
index d0c9226..76c240e 100644
--- a/core/proto/android/bluetooth/enums.proto
+++ b/core/proto/android/bluetooth/enums.proto
@@ -41,3 +41,18 @@
ENABLE_DISABLE_REASON_USER_SWITCH = 8;
ENABLE_DISABLE_REASON_RESTORE_USER_SETTING = 9;
}
+
+enum DirectionEnum {
+ DIRECTION_UNKNOWN = 0;
+ DIRECTION_OUTGOING = 1;
+ DIRECTION_INCOMING = 2;
+}
+
+// First item is the default value, other values follow Bluetooth spec definition
+enum LinkTypeEnum {
+ // Link type is at most 1 byte (0xFF), thus 0xFFF must not be a valid value
+ LINK_TYPE_UNKNOWN = 0xFFF;
+ LINK_TYPE_SCO = 0x00;
+ LINK_TYPE_ACL = 0x01;
+ LINK_TYPE_ESCO = 0x02;
+}
diff --git a/core/proto/android/bluetooth/hci/enums.proto b/core/proto/android/bluetooth/hci/enums.proto
new file mode 100644
index 0000000..e1d96bb
--- /dev/null
+++ b/core/proto/android/bluetooth/hci/enums.proto
@@ -0,0 +1,519 @@
+/*
+ * Copyright 2018 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.
+ */
+
+syntax = "proto2";
+package android.bluetooth.hci;
+
+option java_outer_classname = "BluetoothHciProtoEnums";
+option java_multiple_files = true;
+
+// HCI command opcodes (OCF+OGF) from Bluetooth 5.0 specification Vol 2, Part E, Section 7
+// Original definition: system/bt/stack/include/hcidefs.h
+enum CommandEnum {
+ // Opcode is at most 2 bytes (0xFFFF), thus 0xFFFFF must not be a valid value
+ CMD_UNKNOWN = 0xFFFFF;
+ // Link control commands 0x0400
+ CMD_INQUIRY = 0x0401;
+ CMD_INQUIRY_CANCEL = 0x0402;
+ CMD_PERIODIC_INQUIRY_MODE = 0x0403;
+ CMD_EXIT_PERIODIC_INQUIRY_MODE = 0x0404;
+ CMD_CREATE_CONNECTION = 0x0405;
+ CMD_DISCONNECT = 0x0406;
+ CMD_ADD_SCO_CONNECTION = 0x0407; // Deprecated since Bluetooth 1.2
+ CMD_CREATE_CONNECTION_CANCEL = 0x0408;
+ CMD_ACCEPT_CONNECTION_REQUEST = 0x0409;
+ CMD_REJECT_CONNECTION_REQUEST = 0x040A;
+ CMD_LINK_KEY_REQUEST_REPLY = 0x040B;
+ CMD_LINK_KEY_REQUEST_NEG_REPLY = 0x040C;
+ CMD_PIN_CODE_REQUEST_REPLY = 0x040D;
+ CMD_PIN_CODE_REQUEST_NEG_REPLY = 0x040E;
+ CMD_CHANGE_CONN_PACKET_TYPE = 0x040F;
+ CMD_AUTHENTICATION_REQUESTED = 0x0411;
+ CMD_SET_CONN_ENCRYPTION = 0x0413;
+ CMD_CHANGE_CONN_LINK_KEY = 0x0415;
+ CMD_MASTER_LINK_KEY = 0x0417;
+ CMD_RMT_NAME_REQUEST = 0x0419;
+ CMD_RMT_NAME_REQUEST_CANCEL = 0x041A;
+ CMD_READ_RMT_FEATURES = 0x041B;
+ CMD_READ_RMT_EXT_FEATURES = 0x041C;
+ CMD_READ_RMT_VERSION_INFO = 0x041D;
+ CMD_READ_RMT_CLOCK_OFFSET = 0x041F;
+ CMD_READ_LMP_HANDLE = 0x0420;
+ CMD_SETUP_ESCO_CONNECTION = 0x0428;
+ CMD_ACCEPT_ESCO_CONNECTION = 0x0429;
+ CMD_REJECT_ESCO_CONNECTION = 0x042A;
+ CMD_IO_CAPABILITY_REQUEST_REPLY = 0x042B;
+ CMD_USER_CONF_REQUEST_REPLY = 0x042C;
+ CMD_USER_CONF_VALUE_NEG_REPLY = 0x042D;
+ CMD_USER_PASSKEY_REQ_REPLY = 0x042E;
+ CMD_USER_PASSKEY_REQ_NEG_REPLY = 0x042F;
+ CMD_REM_OOB_DATA_REQ_REPLY = 0x0430;
+ CMD_REM_OOB_DATA_REQ_NEG_REPLY = 0x0433;
+ CMD_IO_CAP_REQ_NEG_REPLY = 0x0434;
+ // BEGIN: AMP commands (not used in system/bt)
+ CMD_CREATE_PHYSICAL_LINK = 0x0435;
+ CMD_ACCEPT_PHYSICAL_LINK = 0x0436;
+ CMD_DISCONNECT_PHYSICAL_LINK = 0x0437;
+ CMD_CREATE_LOGICAL_LINK = 0x0438;
+ CMD_ACCEPT_LOGICAL_LINK = 0x0439;
+ CMD_DISCONNECT_LOGICAL_LINK = 0x043A;
+ CMD_LOGICAL_LINK_CANCEL = 0x043B;
+ CMD_FLOW_SPEC_MODIFY = 0x043C;
+ // END: AMP commands
+ CMD_ENH_SETUP_ESCO_CONNECTION = 0x043D;
+ CMD_ENH_ACCEPT_ESCO_CONNECTION = 0x043E;
+ CMD_TRUNCATED_PAGE = 0x043F;
+ CMD_TRUNCATED_PAGE_CANCEL = 0x0440;
+ CMD_SET_CLB = 0x0441;
+ CMD_RECEIVE_CLB = 0x0442;
+ CMD_START_SYNC_TRAIN = 0x0443;
+ CMD_RECEIVE_SYNC_TRAIN = 0x0444;
+ CMD_REM_OOB_EXTENDED_DATA_REQ_REPLY = 0x0445; // Not currently used in system/bt
+ // Link policy commands 0x0800
+ CMD_HOLD_MODE = 0x0801;
+ CMD_SNIFF_MODE = 0x0803;
+ CMD_EXIT_SNIFF_MODE = 0x0804;
+ CMD_PARK_MODE = 0x0805;
+ CMD_EXIT_PARK_MODE = 0x0806;
+ CMD_QOS_SETUP = 0x0807;
+ CMD_ROLE_DISCOVERY = 0x0809;
+ CMD_SWITCH_ROLE = 0x080B;
+ CMD_READ_POLICY_SETTINGS = 0x080C;
+ CMD_WRITE_POLICY_SETTINGS = 0x080D;
+ CMD_READ_DEF_POLICY_SETTINGS = 0x080E;
+ CMD_WRITE_DEF_POLICY_SETTINGS = 0x080F;
+ CMD_FLOW_SPECIFICATION = 0x0810;
+ CMD_SNIFF_SUB_RATE = 0x0811;
+ // Host controller baseband commands 0x0C00
+ CMD_SET_EVENT_MASK = 0x0C01;
+ CMD_RESET = 0x0C03;
+ CMD_SET_EVENT_FILTER = 0x0C05;
+ CMD_FLUSH = 0x0C08;
+ CMD_READ_PIN_TYPE = 0x0C09;
+ CMD_WRITE_PIN_TYPE = 0x0C0A;
+ CMD_CREATE_NEW_UNIT_KEY = 0x0C0B;
+ CMD_GET_MWS_TRANS_LAYER_CFG = 0x0C0C; // Deprecated (not used in spec)
+ CMD_READ_STORED_LINK_KEY = 0x0C0D;
+ CMD_WRITE_STORED_LINK_KEY = 0x0C11;
+ CMD_DELETE_STORED_LINK_KEY = 0x0C12;
+ CMD_CHANGE_LOCAL_NAME = 0x0C13;
+ CMD_READ_LOCAL_NAME = 0x0C14;
+ CMD_READ_CONN_ACCEPT_TOUT = 0x0C15;
+ CMD_WRITE_CONN_ACCEPT_TOUT = 0x0C16;
+ CMD_READ_PAGE_TOUT = 0x0C17;
+ CMD_WRITE_PAGE_TOUT = 0x0C18;
+ CMD_READ_SCAN_ENABLE = 0x0C19;
+ CMD_WRITE_SCAN_ENABLE = 0x0C1A;
+ CMD_READ_PAGESCAN_CFG = 0x0C1B;
+ CMD_WRITE_PAGESCAN_CFG = 0x0C1C;
+ CMD_READ_INQUIRYSCAN_CFG = 0x0C1D;
+ CMD_WRITE_INQUIRYSCAN_CFG = 0x0C1E;
+ CMD_READ_AUTHENTICATION_ENABLE = 0x0C1F;
+ CMD_WRITE_AUTHENTICATION_ENABLE = 0x0C20;
+ CMD_READ_ENCRYPTION_MODE = 0x0C21; // Deprecated
+ CMD_WRITE_ENCRYPTION_MODE = 0x0C22; // Deprecated
+ CMD_READ_CLASS_OF_DEVICE = 0x0C23;
+ CMD_WRITE_CLASS_OF_DEVICE = 0x0C24;
+ CMD_READ_VOICE_SETTINGS = 0x0C25;
+ CMD_WRITE_VOICE_SETTINGS = 0x0C26;
+ CMD_READ_AUTOMATIC_FLUSH_TIMEOUT = 0x0C27;
+ CMD_WRITE_AUTOMATIC_FLUSH_TIMEOUT = 0x0C28;
+ CMD_READ_NUM_BCAST_REXMITS = 0x0C29;
+ CMD_WRITE_NUM_BCAST_REXMITS = 0x0C2A;
+ CMD_READ_HOLD_MODE_ACTIVITY = 0x0C2B;
+ CMD_WRITE_HOLD_MODE_ACTIVITY = 0x0C2C;
+ CMD_READ_TRANSMIT_POWER_LEVEL = 0x0C2D;
+ CMD_READ_SCO_FLOW_CTRL_ENABLE = 0x0C2E;
+ CMD_WRITE_SCO_FLOW_CTRL_ENABLE = 0x0C2F;
+ CMD_SET_HC_TO_HOST_FLOW_CTRL = 0x0C31;
+ CMD_HOST_BUFFER_SIZE = 0x0C33;
+ CMD_HOST_NUM_PACKETS_DONE = 0x0C35;
+ CMD_READ_LINK_SUPER_TOUT = 0x0C36;
+ CMD_WRITE_LINK_SUPER_TOUT = 0x0C37;
+ CMD_READ_NUM_SUPPORTED_IAC = 0x0C38;
+ CMD_READ_CURRENT_IAC_LAP = 0x0C39;
+ CMD_WRITE_CURRENT_IAC_LAP = 0x0C3A;
+ CMD_READ_PAGESCAN_PERIOD_MODE = 0x0C3B; // Deprecated
+ CMD_WRITE_PAGESCAN_PERIOD_MODE = 0x0C3C; // Deprecated
+ CMD_READ_PAGESCAN_MODE = 0x0C3D; // Deprecated
+ CMD_WRITE_PAGESCAN_MODE = 0x0C3E; // Deprecated
+ CMD_SET_AFH_CHANNELS = 0x0C3F;
+ CMD_READ_INQSCAN_TYPE = 0x0C42;
+ CMD_WRITE_INQSCAN_TYPE = 0x0C43;
+ CMD_READ_INQUIRY_MODE = 0x0C44;
+ CMD_WRITE_INQUIRY_MODE = 0x0C45;
+ CMD_READ_PAGESCAN_TYPE = 0x0C46;
+ CMD_WRITE_PAGESCAN_TYPE = 0x0C47;
+ CMD_READ_AFH_ASSESSMENT_MODE = 0x0C48;
+ CMD_WRITE_AFH_ASSESSMENT_MODE = 0x0C49;
+ CMD_READ_EXT_INQ_RESPONSE = 0x0C51;
+ CMD_WRITE_EXT_INQ_RESPONSE = 0x0C52;
+ CMD_REFRESH_ENCRYPTION_KEY = 0x0C53;
+ CMD_READ_SIMPLE_PAIRING_MODE = 0x0C55;
+ CMD_WRITE_SIMPLE_PAIRING_MODE = 0x0C56;
+ CMD_READ_LOCAL_OOB_DATA = 0x0C57;
+ CMD_READ_INQ_TX_POWER_LEVEL = 0x0C58;
+ CMD_WRITE_INQ_TX_POWER_LEVEL = 0x0C59;
+ CMD_READ_ERRONEOUS_DATA_RPT = 0x0C5A;
+ CMD_WRITE_ERRONEOUS_DATA_RPT = 0x0C5B;
+ CMD_ENHANCED_FLUSH = 0x0C5F;
+ CMD_SEND_KEYPRESS_NOTIF = 0x0C60;
+ CMD_READ_LOGICAL_LINK_ACCEPT_TIMEOUT = 0x0C61;
+ CMD_WRITE_LOGICAL_LINK_ACCEPT_TIMEOUT = 0x0C62;
+ CMD_SET_EVENT_MASK_PAGE_2 = 0x0C63;
+ CMD_READ_LOCATION_DATA = 0x0C64;
+ CMD_WRITE_LOCATION_DATA = 0x0C65;
+ CMD_READ_FLOW_CONTROL_MODE = 0x0C66;
+ CMD_WRITE_FLOW_CONTROL_MODE = 0x0C67;
+ CMD_READ_ENHANCED_TX_PWR_LEVEL = 0x0C68; // Not currently used in system/bt
+ CMD_READ_BE_FLUSH_TOUT = 0x0C69;
+ CMD_WRITE_BE_FLUSH_TOUT = 0x0C6A;
+ CMD_SHORT_RANGE_MODE = 0x0C6B;
+ CMD_READ_BLE_HOST_SUPPORT = 0x0C6C;
+ CMD_WRITE_BLE_HOST_SUPPORT = 0x0C6D;
+ CMD_SET_MWS_CHANNEL_PARAMETERS = 0x0C6E;
+ CMD_SET_EXTERNAL_FRAME_CONFIGURATION = 0x0C6F;
+ CMD_SET_MWS_SIGNALING = 0x0C70;
+ CMD_SET_MWS_TRANSPORT_LAYER = 0x0C71;
+ CMD_SET_MWS_SCAN_FREQUENCY_TABLE = 0x0C72;
+ CMD_SET_MWS_PATTERN_CONFIGURATION = 0x0C73;
+ CMD_SET_RESERVED_LT_ADDR = 0x0C74;
+ CMD_DELETE_RESERVED_LT_ADDR = 0x0C75;
+ CMD_WRITE_CLB_DATA = 0x0C76;
+ CMD_READ_SYNC_TRAIN_PARAM = 0x0C77;
+ CMD_WRITE_SYNC_TRAIN_PARAM = 0x0C78;
+ CMD_READ_SECURE_CONNS_SUPPORT = 0x0C79;
+ CMD_WRITE_SECURE_CONNS_SUPPORT = 0x0C7A;
+ CMD_READ_AUTHED_PAYLOAD_TIMEOUT = 0x0C7B; // Not currently used in system/bt
+ CMD_WRITE_AUTHED_PAYLOAD_TIMEOUT = 0x0C7C; // Not currently used in system/bt
+ CMD_READ_LOCAL_OOB_EXTENDED_DATA = 0x0C7D; // Not currently used in system/bt
+ CMD_READ_EXTENDED_PAGE_TIMEOUT = 0x0C7E; // Not currently used in system/bt
+ CMD_WRITE_EXTENDED_PAGE_TIMEOUT = 0x0C7F; // Not currently used in system/bt
+ CMD_READ_EXTENDED_INQUIRY_LENGTH = 0x0C80; // Not currently used in system/bt
+ CMD_WRITE_EXTENDED_INQUIRY_LENGTH = 0x0C81; // Not currently used in system/bt
+ // Informational parameter commands 0x1000
+ CMD_READ_LOCAL_VERSION_INFO = 0x1001;
+ CMD_READ_LOCAL_SUPPORTED_CMDS = 0x1002;
+ CMD_READ_LOCAL_FEATURES = 0x1003;
+ CMD_READ_LOCAL_EXT_FEATURES = 0x1004;
+ CMD_READ_BUFFER_SIZE = 0x1005;
+ CMD_READ_COUNTRY_CODE = 0x1007; // Deprecated
+ CMD_READ_BD_ADDR = 0x1009;
+ CMD_READ_DATA_BLOCK_SIZE = 0x100A;
+ CMD_READ_LOCAL_SUPPORTED_CODECS = 0x100B;
+ // Status parameter commands 0x1400
+ CMD_READ_FAILED_CONTACT_COUNTER = 0x1401;
+ CMD_RESET_FAILED_CONTACT_COUNTER = 0x1402;
+ CMD_GET_LINK_QUALITY = 0x1403;
+ CMD_READ_RSSI = 0x1405;
+ CMD_READ_AFH_CH_MAP = 0x1406;
+ CMD_READ_CLOCK = 0x1407;
+ CMD_READ_ENCR_KEY_SIZE = 0x1408;
+ CMD_READ_LOCAL_AMP_INFO = 0x1409;
+ CMD_READ_LOCAL_AMP_ASSOC = 0x140A;
+ CMD_WRITE_REMOTE_AMP_ASSOC = 0x140B;
+ CMD_GET_MWS_TRANSPORT_CFG = 0x140C; // Not currently used in system/bt
+ CMD_SET_TRIGGERED_CLK_CAPTURE = 0x140D; // Not currently used in system/bt
+ // Testing commands 0x1800
+ CMD_READ_LOOPBACK_MODE = 0x1801;
+ CMD_WRITE_LOOPBACK_MODE = 0x1802;
+ CMD_ENABLE_DEV_UNDER_TEST_MODE = 0x1803;
+ CMD_WRITE_SIMP_PAIR_DEBUG_MODE = 0x1804;
+ CMD_ENABLE_AMP_RCVR_REPORTS = 0x1807;
+ CMD_AMP_TEST_END = 0x1808;
+ CMD_AMP_TEST = 0x1809;
+ CMD_WRITE_SECURE_CONN_TEST_MODE = 0x180A; // Not currently used in system/bt
+ // BLE commands 0x2000
+ CMD_BLE_SET_EVENT_MASK = 0x2001;
+ CMD_BLE_READ_BUFFER_SIZE = 0x2002;
+ CMD_BLE_READ_LOCAL_SPT_FEAT = 0x2003;
+ CMD_BLE_WRITE_LOCAL_SPT_FEAT = 0x2004;
+ CMD_BLE_WRITE_RANDOM_ADDR = 0x2005;
+ CMD_BLE_WRITE_ADV_PARAMS = 0x2006;
+ CMD_BLE_READ_ADV_CHNL_TX_POWER = 0x2007;
+ CMD_BLE_WRITE_ADV_DATA = 0x2008;
+ CMD_BLE_WRITE_SCAN_RSP_DATA = 0x2009;
+ CMD_BLE_WRITE_ADV_ENABLE = 0x200A;
+ CMD_BLE_WRITE_SCAN_PARAMS = 0x200B;
+ CMD_BLE_WRITE_SCAN_ENABLE = 0x200C;
+ CMD_BLE_CREATE_LL_CONN = 0x200D;
+ CMD_BLE_CREATE_CONN_CANCEL = 0x200E;
+ CMD_BLE_READ_WHITE_LIST_SIZE = 0x200F;
+ CMD_BLE_CLEAR_WHITE_LIST = 0x2010;
+ CMD_BLE_ADD_WHITE_LIST = 0x2011;
+ CMD_BLE_REMOVE_WHITE_LIST = 0x2012;
+ CMD_BLE_UPD_LL_CONN_PARAMS = 0x2013;
+ CMD_BLE_SET_HOST_CHNL_CLASS = 0x2014;
+ CMD_BLE_READ_CHNL_MAP = 0x2015;
+ CMD_BLE_READ_REMOTE_FEAT = 0x2016;
+ CMD_BLE_ENCRYPT = 0x2017;
+ CMD_BLE_RAND = 0x2018;
+ CMD_BLE_START_ENC = 0x2019;
+ CMD_BLE_LTK_REQ_REPLY = 0x201A;
+ CMD_BLE_LTK_REQ_NEG_REPLY = 0x201B;
+ CMD_BLE_READ_SUPPORTED_STATES = 0x201C;
+ CMD_BLE_RECEIVER_TEST = 0x201D;
+ CMD_BLE_TRANSMITTER_TEST = 0x201E;
+ CMD_BLE_TEST_END = 0x201F;
+ CMD_BLE_RC_PARAM_REQ_REPLY = 0x2020;
+ CMD_BLE_RC_PARAM_REQ_NEG_REPLY = 0x2021;
+ CMD_BLE_SET_DATA_LENGTH = 0x2022;
+ CMD_BLE_READ_DEFAULT_DATA_LENGTH = 0x2023;
+ CMD_BLE_WRITE_DEFAULT_DATA_LENGTH = 0x2024;
+ CMD_BLE_GENERATE_DHKEY = 0x2026; // Not currently used in system/bt
+ CMD_BLE_ADD_DEV_RESOLVING_LIST = 0x2027;
+ CMD_BLE_RM_DEV_RESOLVING_LIST = 0x2028;
+ CMD_BLE_CLEAR_RESOLVING_LIST = 0x2029;
+ CMD_BLE_READ_RESOLVING_LIST_SIZE = 0x202A;
+ CMD_BLE_READ_RESOLVABLE_ADDR_PEER = 0x202B;
+ CMD_BLE_READ_RESOLVABLE_ADDR_LOCAL = 0x202C;
+ CMD_BLE_SET_ADDR_RESOLUTION_ENABLE = 0x202D;
+ CMD_BLE_SET_RAND_PRIV_ADDR_TIMOUT = 0x202E;
+ CMD_BLE_READ_MAXIMUM_DATA_LENGTH = 0x202F;
+ CMD_BLE_READ_PHY = 0x2030;
+ CMD_BLE_SET_DEFAULT_PHY = 0x2031;
+ CMD_BLE_SET_PHY = 0x2032;
+ CMD_BLE_ENH_RECEIVER_TEST = 0x2033;
+ CMD_BLE_ENH_TRANSMITTER_TEST = 0x2034;
+ CMD_BLE_SET_EXT_ADVERTISING_RANDOM_ADDRESS = 0x2035;
+ CMD_BLE_SET_EXT_ADVERTISING_PARAM = 0x2036;
+ CMD_BLE_SET_EXT_ADVERTISING_DATA = 0x2037;
+ CMD_BLE_SET_EXT_ADVERTISING_SCAN_RESP = 0x2038;
+ CMD_BLE_SET_EXT_ADVERTISING_ENABLE = 0x2039;
+ CMD_BLE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH = 0x203A;
+ CMD_BLE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS = 0x203B;
+ CMD_BLE_REMOVE_ADVERTISING_SET = 0x203C;
+ CMD_BLE_CLEAR_ADVERTISING_SETS = 0x203D;
+ CMD_BLE_SET_PERIODIC_ADVERTISING_PARAM = 0x203E;
+ CMD_BLE_SET_PERIODIC_ADVERTISING_DATA = 0x203F;
+ CMD_BLE_SET_PERIODIC_ADVERTISING_ENABLE = 0x2040;
+ CMD_BLE_SET_EXTENDED_SCAN_PARAMETERS = 0x2041;
+ CMD_BLE_SET_EXTENDED_SCAN_ENABLE = 0x2042;
+ CMD_BLE_EXTENDED_CREATE_CONNECTION = 0x2043;
+ CMD_BLE_PERIODIC_ADVERTISING_CREATE_SYNC = 0x2044;
+ CMD_BLE_PERIODIC_ADVERTISING_CREATE_SYNC_CANCEL = 0x2045;
+ CMD_BLE_PERIODIC_ADVERTISING_TERMINATE_SYNC = 0x2046;
+ CMD_BLE_ADD_DEVICE_TO_PERIODIC_ADVERTISING_LIST = 0x2047;
+ CMD_BLE_RM_DEVICE_FROM_PERIODIC_ADVERTISING_LIST = 0x2048;
+ CMD_BLE_CLEAR_PERIODIC_ADVERTISING_LIST = 0x2049;
+ CMD_BLE_READ_PERIODIC_ADVERTISING_LIST_SIZE = 0x204A;
+ CMD_BLE_READ_TRANSMIT_POWER = 0x204B;
+ CMD_BLE_READ_RF_COMPENS_POWER = 0x204C;
+ CMD_BLE_WRITE_RF_COMPENS_POWER = 0x204D;
+ CMD_BLE_SET_PRIVACY_MODE = 0x204E;
+ // Vendor specific commands 0xFC00 and above
+ // Android vendor specific commands defined in
+ // https://source.android.com/devices/bluetooth/hci_requirements#vendor-specific-capabilities
+ CMD_BLE_VENDOR_CAP = 0xFD53;
+ CMD_BLE_MULTI_ADV = 0xFD54;
+ CMD_BLE_BATCH_SCAN = 0xFD56;
+ CMD_BLE_ADV_FILTER = 0xFD57;
+ CMD_BLE_TRACK_ADV = 0xFD58;
+ CMD_BLE_ENERGY_INFO = 0xFD59;
+ CMD_BLE_EXTENDED_SCAN_PARAMS = 0xFD5A;
+ CMD_CONTROLLER_DEBUG_INFO = 0xFD5B;
+ CMD_CONTROLLER_A2DP_OPCODE = 0xFD5D;
+ CMD_BRCM_SET_ACL_PRIORITY = 0xFC57;
+ // Other vendor specific commands below here
+}
+
+// HCI event codes from the Bluetooth 5.0 specification Vol 2, Part 7, Section 7
+// Original definition: system/bt/stack/include/hcidefs.h
+enum EventEnum {
+ // Event is at most 1 byte (0xFF), thus 0xFFF must not be a valid value
+ EVT_UNKNOWN = 0xFFF;
+ EVT_INQUIRY_COMP = 0x01;
+ EVT_INQUIRY_RESULT = 0x02;
+ EVT_CONNECTION_COMP = 0x03;
+ EVT_CONNECTION_REQUEST = 0x04;
+ EVT_DISCONNECTION_COMP = 0x05;
+ EVT_AUTHENTICATION_COMP = 0x06;
+ EVT_RMT_NAME_REQUEST_COMP = 0x07;
+ EVT_ENCRYPTION_CHANGE = 0x08;
+ EVT_CHANGE_CONN_LINK_KEY = 0x09;
+ EVT_MASTER_LINK_KEY_COMP = 0x0A;
+ EVT_READ_RMT_FEATURES_COMP = 0x0B;
+ EVT_READ_RMT_VERSION_COMP = 0x0C;
+ EVT_QOS_SETUP_COMP = 0x0D;
+ EVT_COMMAND_COMPLETE = 0x0E;
+ EVT_COMMAND_STATUS = 0x0F;
+ EVT_HARDWARE_ERROR = 0x10;
+ EVT_FLUSH_OCCURED = 0x11;
+ EVT_ROLE_CHANGE = 0x12;
+ EVT_NUM_COMPL_DATA_PKTS = 0x13;
+ EVT_MODE_CHANGE = 0x14;
+ EVT_RETURN_LINK_KEYS = 0x15;
+ EVT_PIN_CODE_REQUEST = 0x16;
+ EVT_LINK_KEY_REQUEST = 0x17;
+ EVT_LINK_KEY_NOTIFICATION = 0x18;
+ EVT_LOOPBACK_COMMAND = 0x19;
+ EVT_DATA_BUF_OVERFLOW = 0x1A;
+ EVT_MAX_SLOTS_CHANGED = 0x1B;
+ EVT_READ_CLOCK_OFF_COMP = 0x1C;
+ EVT_CONN_PKT_TYPE_CHANGE = 0x1D;
+ EVT_QOS_VIOLATION = 0x1E;
+ EVT_PAGE_SCAN_MODE_CHANGE = 0x1F; // Deprecated
+ EVT_PAGE_SCAN_REP_MODE_CHNG = 0x20;
+ EVT_FLOW_SPECIFICATION_COMP = 0x21;
+ EVT_INQUIRY_RSSI_RESULT = 0x22;
+ EVT_READ_RMT_EXT_FEATURES_COMP = 0x23;
+ EVT_ESCO_CONNECTION_COMP = 0x2C;
+ EVT_ESCO_CONNECTION_CHANGED = 0x2D;
+ EVT_SNIFF_SUB_RATE = 0x2E;
+ EVT_EXTENDED_INQUIRY_RESULT = 0x2F;
+ EVT_ENCRYPTION_KEY_REFRESH_COMP = 0x30;
+ EVT_IO_CAPABILITY_REQUEST = 0x31;
+ EVT_IO_CAPABILITY_RESPONSE = 0x32;
+ EVT_USER_CONFIRMATION_REQUEST = 0x33;
+ EVT_USER_PASSKEY_REQUEST = 0x34;
+ EVT_REMOTE_OOB_DATA_REQUEST = 0x35;
+ EVT_SIMPLE_PAIRING_COMPLETE = 0x36;
+ EVT_LINK_SUPER_TOUT_CHANGED = 0x38;
+ EVT_ENHANCED_FLUSH_COMPLETE = 0x39;
+ EVT_USER_PASSKEY_NOTIFY = 0x3B;
+ EVT_KEYPRESS_NOTIFY = 0x3C;
+ EVT_RMT_HOST_SUP_FEAT_NOTIFY = 0x3D;
+ EVT_BLE_META = 0x3E;
+ EVT_PHYSICAL_LINK_COMP = 0x40;
+ EVT_CHANNEL_SELECTED = 0x41;
+ EVT_DISC_PHYSICAL_LINK_COMP = 0x42;
+ EVT_PHY_LINK_LOSS_EARLY_WARNING = 0x43;
+ EVT_PHY_LINK_RECOVERY = 0x44;
+ EVT_LOGICAL_LINK_COMP = 0x45;
+ EVT_DISC_LOGICAL_LINK_COMP = 0x46;
+ EVT_FLOW_SPEC_MODIFY_COMP = 0x47;
+ EVT_NUM_COMPL_DATA_BLOCKS = 0x48;
+ EVT_AMP_TEST_START = 0x49; // Not currently used in system/bt
+ EVT_AMP_TEST_END = 0x4A; // Not currently used in system/bt
+ EVT_AMP_RECEIVER_RPT = 0x4B; // Not currently used in system/bt
+ EVT_SHORT_RANGE_MODE_COMPLETE = 0x4C;
+ EVT_AMP_STATUS_CHANGE = 0x4D;
+ EVT_SET_TRIGGERED_CLOCK_CAPTURE = 0x4E;
+ EVT_SYNC_TRAIN_CMPL = 0x4F; // Not currently used in system/bt
+ EVT_SYNC_TRAIN_RCVD = 0x50; // Not currently used in system/bt
+ EVT_CONNLESS_SLAVE_BROADCAST_RCVD = 0x51; // Not currently used in system/bt
+ EVT_CONNLESS_SLAVE_BROADCAST_TIMEOUT = 0x52; // Not currently used in system/bt
+ EVT_TRUNCATED_PAGE_CMPL = 0x53; // Not currently used in system/bt
+ EVT_SLAVE_PAGE_RES_TIMEOUT = 0x54; // Not currently used in system/bt
+ EVT_CONNLESS_SLAVE_BROADCAST_CHNL_MAP_CHANGE = 0x55; // Not currently used in system/bt
+ EVT_INQUIRY_RES_NOTIFICATION = 0x56; // Not currently used in system/bt
+ EVT_AUTHED_PAYLOAD_TIMEOUT = 0x57; // Not currently used in system/bt
+ EVT_SAM_STATUS_CHANGE = 0x58; // Not currently used in system/bt
+}
+
+// Bluetooth low energy related meta event codes
+// from the Bluetooth 5.0 specification Vol 2, Part E, Section 7.7.65
+// Original definition: system/bt/stack/include/hcidefs.h
+enum BleMetaEventEnum {
+ // BLE meta event code is at most 1 byte (0xFF), thus 0xFFF must not be a valid value
+ BLE_EVT_UNKNOWN = 0xFFF;
+ BLE_EVT_CONN_COMPLETE_EVT = 0x01;
+ BLE_EVT_ADV_PKT_RPT_EVT = 0x02;
+ BLE_EVT_LL_CONN_PARAM_UPD_EVT = 0x03;
+ BLE_EVT_READ_REMOTE_FEAT_CMPL_EVT = 0x04;
+ BLE_EVT_LTK_REQ_EVT = 0x05;
+ BLE_EVT_RC_PARAM_REQ_EVT = 0x06;
+ BLE_EVT_DATA_LENGTH_CHANGE_EVT = 0x07;
+ BLE_EVT_READ_LOCAL_P256_PUB_KEY = 0x08; // Not currently used in system/bt
+ BLE_EVT_GEN_DHKEY_CMPL = 0x09; // Not currently used in system/bt
+ BLE_EVT_ENHANCED_CONN_COMPLETE_EVT = 0x0a;
+ BLE_EVT_DIRECT_ADV_EVT = 0x0b;
+ BLE_EVT_PHY_UPDATE_COMPLETE_EVT = 0x0c;
+ BLE_EVT_EXTENDED_ADVERTISING_REPORT_EVT = 0x0D;
+ BLE_EVT_PERIODIC_ADV_SYNC_EST_EVT = 0x0E;
+ BLE_EVT_PERIODIC_ADV_REPORT_EVT = 0x0F;
+ BLE_EVT_PERIODIC_ADV_SYNC_LOST_EVT = 0x10;
+ BLE_EVT_SCAN_TIMEOUT_EVT = 0x11;
+ BLE_EVT_ADVERTISING_SET_TERMINATED_EVT = 0x12;
+ BLE_EVT_SCAN_REQ_RX_EVT = 0x13;
+ BLE_EVT_CHNL_SELECTION_ALGORITHM = 0x14; // Not currently used in system/bt
+}
+
+// HCI status code from the Bluetooth 5.0 specification Vol 2, Part D.
+// Original definition: system/bt/stack/include/hcidefs.h
+enum StatusEnum {
+ // Status is at most 1 byte (0xFF), thus 0xFFF must not be a valid value
+ STATUS_UNKNOWN = 0xFFF;
+ STATUS_SUCCESS = 0x00;
+ STATUS_ILLEGAL_COMMAND = 0x01;
+ STATUS_NO_CONNECTION = 0x02;
+ STATUS_HW_FAILURE = 0x03;
+ STATUS_PAGE_TIMEOUT = 0x04;
+ STATUS_AUTH_FAILURE = 0x05;
+ STATUS_KEY_MISSING = 0x06;
+ STATUS_MEMORY_FULL = 0x07;
+ STATUS_CONNECTION_TOUT = 0x08;
+ STATUS_MAX_NUM_OF_CONNECTIONS = 0x09;
+ STATUS_MAX_NUM_OF_SCOS = 0x0A;
+ STATUS_CONNECTION_EXISTS = 0x0B;
+ STATUS_COMMAND_DISALLOWED = 0x0C;
+ STATUS_HOST_REJECT_RESOURCES = 0x0D;
+ STATUS_HOST_REJECT_SECURITY = 0x0E;
+ STATUS_HOST_REJECT_DEVICE = 0x0F;
+ STATUS_HOST_TIMEOUT = 0x10;
+ STATUS_UNSUPPORTED_VALUE = 0x11;
+ STATUS_ILLEGAL_PARAMETER_FMT = 0x12;
+ STATUS_PEER_USER = 0x13;
+ STATUS_PEER_LOW_RESOURCES = 0x14;
+ STATUS_PEER_POWER_OFF = 0x15;
+ STATUS_CONN_CAUSE_LOCAL_HOST = 0x16;
+ STATUS_REPEATED_ATTEMPTS = 0x17;
+ STATUS_PAIRING_NOT_ALLOWED = 0x18;
+ STATUS_UNKNOWN_LMP_PDU = 0x19;
+ STATUS_UNSUPPORTED_REM_FEATURE = 0x1A;
+ STATUS_SCO_OFFSET_REJECTED = 0x1B;
+ STATUS_SCO_INTERVAL_REJECTED = 0x1C;
+ STATUS_SCO_AIR_MODE = 0x1D;
+ STATUS_INVALID_LMP_PARAM = 0x1E;
+ STATUS_UNSPECIFIED = 0x1F;
+ STATUS_UNSUPPORTED_LMP_FEATURE = 0x20;
+ STATUS_ROLE_CHANGE_NOT_ALLOWED = 0x21;
+ STATUS_LMP_RESPONSE_TIMEOUT = 0x22;
+ STATUS_LMP_STATUS_TRANS_COLLISION = 0x23;
+ STATUS_LMP_PDU_NOT_ALLOWED = 0x24;
+ STATUS_ENCRY_MODE_NOT_ACCEPTABLE = 0x25;
+ STATUS_UNIT_KEY_USED = 0x26;
+ STATUS_QOS_NOT_SUPPORTED = 0x27;
+ STATUS_INSTANT_PASSED = 0x28;
+ STATUS_PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED = 0x29;
+ STATUS_DIFF_TRANSACTION_COLLISION = 0x2A;
+ STATUS_UNDEFINED_0x2B = 0x2B; // Not used
+ STATUS_QOS_UNACCEPTABLE_PARAM = 0x2C;
+ STATUS_QOS_REJECTED = 0x2D;
+ STATUS_CHAN_CLASSIF_NOT_SUPPORTED = 0x2E;
+ STATUS_INSUFFCIENT_SECURITY = 0x2F;
+ STATUS_PARAM_OUT_OF_RANGE = 0x30;
+ STATUS_UNDEFINED_0x31 = 0x31; // Not used
+ STATUS_ROLE_SWITCH_PENDING = 0x32;
+ STATUS_UNDEFINED_0x33 = 0x33;
+ STATUS_RESERVED_SLOT_VIOLATION = 0x34;
+ STATUS_ROLE_SWITCH_FAILED = 0x35;
+ STATUS_INQ_RSP_DATA_TOO_LARGE = 0x36;
+ STATUS_SIMPLE_PAIRING_NOT_SUPPORTED = 0x37;
+ STATUS_HOST_BUSY_PAIRING = 0x38;
+ STATUS_REJ_NO_SUITABLE_CHANNEL = 0x39;
+ STATUS_CONTROLLER_BUSY = 0x3A;
+ STATUS_UNACCEPT_CONN_INTERVAL = 0x3B;
+ STATUS_ADVERTISING_TIMEOUT = 0x3C;
+ STATUS_CONN_TOUT_DUE_TO_MIC_FAILURE = 0x3D;
+ STATUS_CONN_FAILED_ESTABLISHMENT = 0x3E;
+ STATUS_MAC_CONNECTION_FAILED = 0x3F;
+ STATUS_LT_ADDR_ALREADY_IN_USE = 0x40;
+ STATUS_LT_ADDR_NOT_ALLOCATED = 0x41;
+ STATUS_CLB_NOT_ENABLED = 0x42;
+ STATUS_CLB_DATA_TOO_BIG = 0x43;
+ STATUS_OPERATION_CANCELED_BY_HOST = 0x44; // Not currently used in system/bt
+}
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 0ec8c1a..7f3ea7a 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -403,18 +403,23 @@
optional bool is_charging = 1;
optional bool is_in_parole = 2;
+ // List of UIDs currently in the foreground.
+ repeated int32 foreground_uids = 3;
+
message TrackedJob {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
optional JobStatusShortInfoProto info = 1;
optional int32 source_uid = 2;
optional JobStatusDumpProto.Bucket effective_standby_bucket = 3;
- optional bool has_quota = 4;
+ // If the job started while the app was in the TOP state.
+ optional bool is_top_started_job = 4;
+ optional bool has_quota = 5;
// The amount of time that this job has remaining in its quota. This
// can be negative if the job is out of quota.
- optional int64 remaining_quota_ms = 5;
+ optional int64 remaining_quota_ms = 6;
}
- repeated TrackedJob tracked_jobs = 3;
+ repeated TrackedJob tracked_jobs = 4;
message Package {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -456,7 +461,7 @@
repeated TimingSession saved_sessions = 3;
}
- repeated PackageStats package_stats = 4;
+ repeated PackageStats package_stats = 5;
}
message StorageController {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 341f345..df4600e 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -27,7 +27,6 @@
import static java.lang.reflect.Modifier.isStatic;
import android.platform.test.annotations.Presubmit;
-import android.provider.Settings.Global;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -555,8 +554,10 @@
Settings.Global.APPOP_HISTORY_PARAMETERS,
Settings.Global.APPOP_HISTORY_MODE,
Settings.Global.APPOP_HISTORY_INTERVAL_MULTIPLIER,
- Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS);
-
+ Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS,
+ Settings.Global.ENABLE_RADIO_BUG_DETECTION,
+ Settings.Global.RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD,
+ Settings.Global.RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD);
private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS =
newHashSet(
Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
index 64b3ba0..28a8afe 100644
--- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
+++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
@@ -327,6 +327,18 @@
public int getPhysicalAddress() {
return 0x0000;
}
+
+ @Override
+ public void powerOffRemoteDevice(int logicalAddress, int powerStatus) {
+ }
+
+ @Override
+ public void powerOnRemoteDevice(int logicalAddress, int powerStatus) {
+ }
+
+ @Override
+ public void askRemoteDeviceToBecomeActiveSource(int physicalAddress) {
+ }
}
}
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index 6ce8148..035ee10 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -39,9 +39,6 @@
name: "privapp-permissions-platform.xml",
sub_dir: "permissions",
src: "privapp-permissions-platform.xml",
- required: [
- "privapp_whitelist_com.android.settings.intelligence",
- ],
}
prebuilt_etc {
@@ -86,6 +83,7 @@
prebuilt_etc {
name: "privapp_whitelist_com.android.settings.intelligence",
+ product_specific: true,
sub_dir: "permissions",
src: "com.android.settings.intelligence.xml",
filename_from_src: true,
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 595aeb3..c751c39 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -38,8 +38,8 @@
private static final String CURRENT_MODE_KEY = "CURRENT_MODE";
private static final String NEW_MODE_KEY = "NEW_MODE";
@VisibleForTesting
- static final String STORAGE_MANAGER_SHOW_OPT_IN_PROPERTY =
- "ro.storage_manager.show_opt_in";
+ static final String STORAGE_MANAGER_ENABLED_PROPERTY =
+ "ro.storage_manager.enabled";
private static Signature[] sSystemSignature;
private static String sPermissionControllerPackageName;
@@ -373,8 +373,7 @@
public static boolean isStorageManagerEnabled(Context context) {
boolean isDefaultOn;
try {
- // Turn off by default if the opt-in was shown.
- isDefaultOn = !SystemProperties.getBoolean(STORAGE_MANAGER_SHOW_OPT_IN_PROPERTY, true);
+ isDefaultOn = SystemProperties.getBoolean(STORAGE_MANAGER_ENABLED_PROPERTY, false);
} catch (Resources.NotFoundException e) {
isDefaultOn = false;
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 86f0438..92ebe44 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -17,7 +17,7 @@
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static com.android.settingslib.Utils.STORAGE_MANAGER_SHOW_OPT_IN_PROPERTY;
+import static com.android.settingslib.Utils.STORAGE_MANAGER_ENABLED_PROPERTY;
import static com.google.common.truth.Truth.assertThat;
@@ -159,7 +159,7 @@
@Test
public void testIsStorageManagerEnabled_UsesSystemProperties() {
- SystemProperties.set(STORAGE_MANAGER_SHOW_OPT_IN_PROPERTY, "false");
+ SystemProperties.set(STORAGE_MANAGER_ENABLED_PROPERTY, "true");
assertThat(Utils.isStorageManagerEnabled(mContext)).isTrue();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
index 41e9eba..a055950 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -46,6 +46,7 @@
protected View mEcaView;
protected boolean mEnableHaptics;
private boolean mDismissing;
+ protected boolean mResumed;
private CountDownTimer mCountdownTimer = null;
// To avoid accidental lockout due to events while the device in in the pocket, ignore
@@ -263,6 +264,8 @@
@Override
public void onPause() {
+ mResumed = false;
+
if (mCountdownTimer != null) {
mCountdownTimer.cancel();
mCountdownTimer = null;
@@ -276,6 +279,7 @@
@Override
public void onResume(int reason) {
+ mResumed = true;
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 41afa9a..3296c10 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -81,6 +81,11 @@
protected void resetState() {
mSecurityMessageDisplay.setMessage("");
final boolean wasDisabled = mPasswordEntry.isEnabled();
+ // Don't set enabled password entry & showSoftInput when PasswordEntry is invisible or in
+ // pausing stage.
+ if (!mResumed || !mPasswordEntry.isVisibleToUser()) {
+ return;
+ }
setPasswordEntryEnabled(true);
setPasswordEntryInputEnabled(true);
if (wasDisabled) {
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 24fd7b9..ec6d20d 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -701,6 +701,11 @@
public void onBackKeyPressed() {
if (sDebug) Slog.d(TAG, "onBackKeyPressed()");
mUi.hideAll(null);
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service =
+ getServiceForUserLocked(UserHandle.getCallingUserId());
+ service.onBackKeyPressed();
+ }
}
@Override
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index a6bb049..d037b08 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -187,6 +187,15 @@
}
@GuardedBy("mLock")
+ void onBackKeyPressed() {
+ final RemoteAugmentedAutofillService remoteService =
+ getRemoteAugmentedAutofillServiceLocked();
+ if (remoteService != null) {
+ remoteService.onDestroyAutofillWindowsRequest();
+ }
+ }
+
+ @GuardedBy("mLock")
@Override // from PerUserSystemService
protected boolean updateLocked(boolean disabled) {
destroySessionsLocked();
diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
index a8ff9b0..5d8d8fa 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
@@ -109,8 +109,8 @@
/**
* Called by {@link Session} when it's time to destroy all augmented autofill requests.
*/
- public void onDestroyAutofillWindowsRequest(int sessionId) {
- scheduleAsyncRequest((s) -> s.onDestroyFillWindowRequest(sessionId));
+ public void onDestroyAutofillWindowsRequest() {
+ scheduleAsyncRequest((s) -> s.onDestroyAllFillWindowsRequest());
}
// TODO(b/111330312): inline into PendingAutofillRequest if it doesn't have any other subclass
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index cf4963c..a5ef21a 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -2619,7 +2619,7 @@
currentValue);
if (mAugmentedAutofillDestroyer == null) {
- mAugmentedAutofillDestroyer = () -> remoteService.onDestroyAutofillWindowsRequest(id);
+ mAugmentedAutofillDestroyer = () -> remoteService.onDestroyAutofillWindowsRequest();
}
return mAugmentedAutofillDestroyer;
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 09aa421..01778cd 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -17,6 +17,9 @@
package com.android.server.contentcapture;
import static android.service.contentcapture.ContentCaptureService.setClientState;
+import static android.view.contentcapture.ContentCaptureSession.STATE_DISABLED;
+import static android.view.contentcapture.ContentCaptureSession.STATE_DUPLICATED_ID;
+import static android.view.contentcapture.ContentCaptureSession.STATE_NO_SERVICE;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA;
@@ -39,7 +42,6 @@
import android.service.contentcapture.SnapshotData;
import android.util.ArrayMap;
import android.util.Slog;
-import android.view.contentcapture.ContentCaptureSession;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
@@ -165,7 +167,7 @@
if (!isEnabledLocked()) {
// TODO: it would be better to split in differet reasons, like
// STATE_DISABLED_NO_SERVICE and STATE_DISABLED_BY_DEVICE_POLICY
- setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED_NO_SERVICE,
+ setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
/* binder= */ null);
return;
}
@@ -184,7 +186,7 @@
if (existingSession != null) {
Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
+ ": ignoring because it already exists for " + existingSession.mActivityToken);
- setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED_DUPLICATED_ID,
+ setClientState(clientReceiver, STATE_DISABLED | STATE_DUPLICATED_ID,
/* binder=*/ null);
return;
}
@@ -197,8 +199,7 @@
// TODO(b/119613670): log metrics
Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
+ ": ignoring because service is not set");
- // TODO(b/111276913): use a new disabled state?
- setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED_NO_SERVICE,
+ setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
/* binder= */ null);
return;
}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index d0666b9..d6f3e2b 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -4879,7 +4879,7 @@
final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
final NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(),
new Network(reserveNetId()), new NetworkInfo(networkInfo), lp, nc, currentScore,
- mContext, mTrackerHandler, new NetworkMisc(networkMisc), this);
+ mContext, mTrackerHandler, new NetworkMisc(networkMisc), this, mNetd, mNMS);
// Make sure the network capabilities reflect what the agent info says.
nai.networkCapabilities = mixInCapabilities(nai, nc);
final String extraInfo = networkInfo.getExtraInfo();
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index b0ca2df..aed0684 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -17,13 +17,11 @@
package com.android.server;
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
-import static android.Manifest.permission.DUMP;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.SHUTDOWN;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
-import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_NONE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_POWERSAVE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_STANDBY;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NONE;
@@ -40,6 +38,7 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.TrafficStats.UID_TETHERING;
+
import static com.android.server.NetworkManagementService.NetdResponseCode.ClatdStatusResult;
import static com.android.server.NetworkManagementService.NetdResponseCode.InterfaceGetCfgResult;
import static com.android.server.NetworkManagementService.NetdResponseCode.InterfaceListResult;
@@ -53,11 +52,9 @@
import android.annotation.NonNull;
import android.app.ActivityManager;
-import android.content.ContentResolver;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.INetd;
-import android.net.TetherStatsParcel;
import android.net.INetworkManagementEventObserver;
import android.net.ITetheringStatsProvider;
import android.net.InterfaceConfiguration;
@@ -69,18 +66,15 @@
import android.net.NetworkStats;
import android.net.NetworkUtils;
import android.net.RouteInfo;
+import android.net.TetherStatsParcel;
import android.net.UidRange;
-import android.net.UidRangeParcel;
import android.net.util.NetdService;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiConfiguration.KeyMgmt;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.INetworkActivityListener;
import android.os.INetworkManagementService;
-import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallbackList;
@@ -91,12 +85,7 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
-import android.provider.Settings;
import android.telephony.DataConnectionRealTimeInfo;
-import android.telephony.PhoneStateListener;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -110,13 +99,11 @@
import com.android.internal.util.DumpUtils;
import com.android.internal.util.HexDump;
import com.android.internal.util.Preconditions;
-import com.android.server.NativeDaemonConnector.Command;
-import com.android.server.NativeDaemonConnector.SensitiveArg;
+
import com.google.android.collect.Maps;
import java.io.BufferedReader;
import java.io.DataInputStream;
-import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
@@ -124,15 +111,11 @@
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InterfaceAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.StringTokenizer;
import java.util.concurrent.CountDownLatch;
/**
@@ -2153,28 +2136,6 @@
}
@Override
- public void startClatd(String interfaceName) throws IllegalStateException {
- mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
-
- try {
- mNetdService.clatdStart(interfaceName);
- } catch (RemoteException | ServiceSpecificException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public void stopClatd(String interfaceName) throws IllegalStateException {
- mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
-
- try {
- mNetdService.clatdStop(interfaceName);
- } catch (RemoteException | ServiceSpecificException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
public void registerNetworkActivityListener(INetworkActivityListener listener) {
mNetworkActivityListeners.register(listener);
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index bd6fa49..d292783 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -32,6 +32,7 @@
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
import static android.content.pm.PackageManager.GET_PROVIDERS;
+import static android.content.pm.PackageManager.MATCH_ALL;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
@@ -318,7 +319,6 @@
import com.android.internal.util.function.QuadFunction;
import com.android.internal.util.function.TriFunction;
import com.android.server.AlarmManagerInternal;
-import com.android.server.appop.AppOpsService;
import com.android.server.AttributeCache;
import com.android.server.DeviceIdleController;
import com.android.server.DisplayThread;
@@ -337,6 +337,7 @@
import com.android.server.Watchdog;
import com.android.server.am.ActivityManagerServiceDumpProcessesProto.UidObserverRegistrationProto;
import com.android.server.am.MemoryStatUtil.MemoryStat;
+import com.android.server.appop.AppOpsService;
import com.android.server.firewall.IntentFirewall;
import com.android.server.job.JobSchedulerInternal;
import com.android.server.pm.Installer;
@@ -658,9 +659,47 @@
/**
* When an app has restrictions on the other apps that can have associations with it,
- * it appears here with a set of the allowed apps.
+ * it appears here with a set of the allowed apps and also track debuggability of the app.
*/
- ArrayMap<String, ArraySet<String>> mAllowedAssociations;
+ ArrayMap<String, PackageAssociationInfo> mAllowedAssociations;
+
+ /**
+ * Tracks association information for a particular package along with debuggability.
+ * <p> Associations for a package A are allowed to package B if B is part of the
+ * allowed associations for A or if A is debuggable.
+ */
+ private final class PackageAssociationInfo {
+ private final String mSourcePackage;
+ private final ArraySet<String> mAllowedPackageAssociations;
+ private boolean mIsDebuggable;
+
+ PackageAssociationInfo(String sourcePackage, ArraySet<String> allowedPackages,
+ boolean isDebuggable) {
+ mSourcePackage = sourcePackage;
+ mAllowedPackageAssociations = allowedPackages;
+ mIsDebuggable = isDebuggable;
+ }
+
+ /**
+ * Returns true if {@code mSourcePackage} is allowed association with
+ * {@code targetPackage}.
+ */
+ boolean isPackageAssociationAllowed(String targetPackage) {
+ return mIsDebuggable || mAllowedPackageAssociations.contains(targetPackage);
+ }
+
+ boolean isDebuggable() {
+ return mIsDebuggable;
+ }
+
+ void setDebuggable(boolean isDebuggable) {
+ mIsDebuggable = isDebuggable;
+ }
+
+ ArraySet<String> getAllowedPackageAssociations() {
+ return mAllowedPackageAssociations;
+ }
+ }
/**
* All of the processes we currently have running organized by pid.
@@ -2392,12 +2431,10 @@
* If it does not, give it an empty set.
*/
void requireAllowedAssociationsLocked(String packageName) {
- if (mAllowedAssociations == null) {
- mAllowedAssociations = new ArrayMap<>(
- SystemConfig.getInstance().getAllowedAssociations());
- }
+ ensureAllowedAssociations();
if (mAllowedAssociations.get(packageName) == null) {
- mAllowedAssociations.put(packageName, new ArraySet<>());
+ mAllowedAssociations.put(packageName, new PackageAssociationInfo(packageName,
+ new ArraySet<>(), /* isDebuggable = */ false));
}
}
@@ -2408,10 +2445,7 @@
* association is implicitly allowed.
*/
boolean validateAssociationAllowedLocked(String pkg1, int uid1, String pkg2, int uid2) {
- if (mAllowedAssociations == null) {
- mAllowedAssociations = new ArrayMap<>(
- SystemConfig.getInstance().getAllowedAssociations());
- }
+ ensureAllowedAssociations();
// Interactions with the system uid are always allowed, since that is the core system
// that everyone needs to be able to interact with. Also allow reflexive associations
// within the same uid.
@@ -2419,24 +2453,57 @@
|| UserHandle.getAppId(uid2) == SYSTEM_UID) {
return true;
}
- // We won't allow this association if either pkg1 or pkg2 has a limit on the
- // associations that are allowed with it, and the other package is not explicitly
- // specified as one of those associations.
- ArraySet<String> pkgs = mAllowedAssociations.get(pkg1);
- if (pkgs != null) {
- if (!pkgs.contains(pkg2)) {
- return false;
- }
+
+ // Check for association on both source and target packages.
+ PackageAssociationInfo pai = mAllowedAssociations.get(pkg1);
+ if (pai != null && !pai.isPackageAssociationAllowed(pkg2)) {
+ return false;
}
- pkgs = mAllowedAssociations.get(pkg2);
- if (pkgs != null) {
- return pkgs.contains(pkg1);
+ pai = mAllowedAssociations.get(pkg2);
+ if (pai != null && !pai.isPackageAssociationAllowed(pkg1)) {
+ return false;
}
// If no explicit associations are provided in the manifest, then assume the app is
// allowed associations with any package.
return true;
}
+ /** Sets up allowed associations for system prebuilt packages from system config (if needed). */
+ private void ensureAllowedAssociations() {
+ if (mAllowedAssociations == null) {
+ ArrayMap<String, ArraySet<String>> allowedAssociations =
+ SystemConfig.getInstance().getAllowedAssociations();
+ mAllowedAssociations = new ArrayMap<>(allowedAssociations.size());
+ PackageManagerInternal pm = getPackageManagerInternalLocked();
+ for (int i = 0; i < allowedAssociations.size(); i++) {
+ final String pkg = allowedAssociations.keyAt(i);
+ final ArraySet<String> asc = allowedAssociations.valueAt(i);
+
+ // Query latest debuggable flag from package-manager.
+ boolean isDebuggable = false;
+ try {
+ ApplicationInfo ai = AppGlobals.getPackageManager()
+ .getApplicationInfo(pkg, MATCH_ALL, 0);
+ if (ai != null) {
+ isDebuggable = (ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+ }
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ mAllowedAssociations.put(pkg, new PackageAssociationInfo(pkg, asc, isDebuggable));
+ }
+ }
+ }
+
+ /** Updates allowed associations for app info (specifically, based on debuggability). */
+ private void updateAssociationForApp(ApplicationInfo appInfo) {
+ ensureAllowedAssociations();
+ PackageAssociationInfo pai = mAllowedAssociations.get(appInfo.packageName);
+ if (pai != null) {
+ pai.setDebuggable((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
+ }
+ }
+
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
@@ -10910,14 +10977,14 @@
void dumpAllowedAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
int opti, boolean dumpAll, String dumpPackage) {
boolean needSep = false;
- boolean printedAnything = false;
pw.println("ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity allowed-associations)");
boolean printed = false;
if (mAllowedAssociations != null) {
for (int i = 0; i < mAllowedAssociations.size(); i++) {
final String pkg = mAllowedAssociations.keyAt(i);
- final ArraySet<String> asc = mAllowedAssociations.valueAt(i);
+ final ArraySet<String> asc =
+ mAllowedAssociations.valueAt(i).getAllowedPackageAssociations();
boolean printedHeader = false;
for (int j = 0; j < asc.size(); j++) {
if (dumpPackage == null || pkg.equals(dumpPackage)
@@ -10926,7 +10993,6 @@
pw.println(" Allowed associations (by restricted package):");
printed = true;
needSep = true;
- printedAnything = true;
}
if (!printedHeader) {
pw.print(" * ");
@@ -10938,6 +11004,9 @@
pw.println(asc.valueAt(j));
}
}
+ if (mAllowedAssociations.valueAt(i).isDebuggable()) {
+ pw.println(" (debuggable)");
+ }
}
}
if (!printed) {
@@ -14519,6 +14588,7 @@
+ " ssp=" + ssp + " data=" + data);
return ActivityManager.BROADCAST_SUCCESS;
}
+ updateAssociationForApp(aInfo);
mAtmInternal.onPackageReplaced(aInfo);
mServices.updateServiceApplicationInfoLocked(aInfo);
sendPackageBroadcastLocked(ApplicationThreadConstants.PACKAGE_REPLACED,
diff --git a/services/core/java/com/android/server/connectivity/Nat464Xlat.java b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
index 6596d27..9d9b1cf 100644
--- a/services/core/java/com/android/server/connectivity/Nat464Xlat.java
+++ b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
@@ -16,8 +16,9 @@
package com.android.server.connectivity;
-import android.net.InterfaceConfiguration;
import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.InterfaceConfiguration;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkInfo;
@@ -59,6 +60,7 @@
NetworkInfo.State.SUSPENDED,
};
+ private final INetd mNetd;
private final INetworkManagementService mNMService;
// The network we're running on, and its type.
@@ -76,7 +78,8 @@
private String mIface;
private State mState = State.IDLE;
- public Nat464Xlat(INetworkManagementService nmService, NetworkAgentInfo nai) {
+ public Nat464Xlat(NetworkAgentInfo nai, INetd netd, INetworkManagementService nmService) {
+ mNetd = netd;
mNMService = nmService;
mNetwork = nai;
}
@@ -140,7 +143,7 @@
return;
}
try {
- mNMService.startClatd(baseIface);
+ mNetd.clatdStart(baseIface);
} catch(RemoteException|IllegalStateException e) {
Slog.e(TAG, "Error starting clatd on " + baseIface, e);
}
@@ -162,7 +165,7 @@
*/
private void enterStoppingState() {
try {
- mNMService.stopClatd(mBaseIface);
+ mNetd.clatdStop(mBaseIface);
} catch(RemoteException|IllegalStateException e) {
Slog.e(TAG, "Error stopping clatd on " + mBaseIface, e);
}
@@ -204,7 +207,7 @@
Slog.e(TAG, "startClat: Can't start clat on null interface");
return;
}
- // TODO: should we only do this if mNMService.startClatd() succeeds?
+ // TODO: should we only do this if mNetd.clatdStart() succeeds?
Slog.i(TAG, "Starting clatd on " + baseIface);
enterStartingState(baseIface);
}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 54c89aa..9ea73fb 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,6 +17,7 @@
package com.android.server.connectivity;
import android.content.Context;
+import android.net.INetd;
import android.net.INetworkMonitor;
import android.net.LinkProperties;
import android.net.Network;
@@ -239,12 +240,15 @@
private static final String TAG = ConnectivityService.class.getSimpleName();
private static final boolean VDBG = false;
private final ConnectivityService mConnService;
+ private final INetd mNetd;
+ private final INetworkManagementService mNMS;
private final Context mContext;
private final Handler mHandler;
public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
- NetworkMisc misc, ConnectivityService connService) {
+ NetworkMisc misc, ConnectivityService connService, INetd netd,
+ INetworkManagementService nms) {
this.messenger = messenger;
asyncChannel = ac;
network = net;
@@ -253,6 +257,8 @@
networkCapabilities = nc;
currentScore = score;
mConnService = connService;
+ mNetd = netd;
+ mNMS = nms;
mContext = context;
mHandler = handler;
networkMisc = misc;
@@ -587,18 +593,18 @@
public void updateClat(INetworkManagementService netd) {
if (Nat464Xlat.requiresClat(this)) {
- maybeStartClat(netd);
+ maybeStartClat();
} else {
maybeStopClat();
}
}
/** Ensure clat has started for this network. */
- public void maybeStartClat(INetworkManagementService netd) {
+ public void maybeStartClat() {
if (clatd != null && clatd.isStarted()) {
return;
}
- clatd = new Nat464Xlat(netd, this);
+ clatd = new Nat464Xlat(this, mNetd, mNMS);
clatd.start();
}
diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
index b640e7f..d137580 100755
--- a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
+++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
@@ -16,6 +16,7 @@
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.util.Slog;
@@ -51,8 +52,10 @@
private static final int STATE_WAITING_FOR_OSD_NAME = 3;
// State in which the action is waiting for gathering vendor id of non-local devices.
private static final int STATE_WAITING_FOR_VENDOR_ID = 4;
- // State in which the action is waiting for devices to be ready
+ // State in which the action is waiting for devices to be ready.
private static final int STATE_WAITING_FOR_DEVICES = 5;
+ // State in which the action is waiting for gathering power status of non-local devices.
+ private static final int STATE_WAITING_FOR_POWER = 6;
/**
* Interface used to report result of device discovery.
@@ -74,6 +77,7 @@
private int mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS;
private int mPortId = Constants.INVALID_PORT_ID;
private int mVendorId = Constants.UNKNOWN_VENDOR_ID;
+ private int mPowerStatus = HdmiControlManager.POWER_STATUS_UNKNOWN;
private String mDisplayName = "";
private int mDeviceType = HdmiDeviceInfo.DEVICE_INACTIVE;
@@ -83,7 +87,7 @@
private HdmiDeviceInfo toHdmiDeviceInfo() {
return new HdmiDeviceInfo(mLogicalAddress, mPhysicalAddress, mPortId, mDeviceType,
- mVendorId, mDisplayName);
+ mVendorId, mDisplayName, mPowerStatus);
}
}
@@ -91,7 +95,7 @@
private final DeviceDiscoveryCallback mCallback;
private int mProcessedDeviceCount = 0;
private int mTimeoutRetry = 0;
- private boolean mIsTvDevice = source().mService.isTvDevice();
+ private boolean mIsTvDevice = localDevice().mService.isTvDevice();
private final int mDelayPeriod;
/**
@@ -237,6 +241,29 @@
addTimer(mState, HdmiConfig.TIMEOUT_MS);
}
+ private void startPowerStatusStage() {
+ Slog.v(TAG, "Start [Power Status Stage]:" + mDevices.size());
+ mProcessedDeviceCount = 0;
+ mState = STATE_WAITING_FOR_POWER;
+
+ checkAndProceedStage();
+ }
+
+ private void queryPowerStatus(int address) {
+ if (!verifyValidLogicalAddress(address)) {
+ checkAndProceedStage();
+ return;
+ }
+
+ mActionTimer.clearTimerMessage();
+
+ if (mayProcessMessageIfCached(address, Constants.MESSAGE_REPORT_POWER_STATUS)) {
+ return;
+ }
+ sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), address));
+ addTimer(mState, HdmiConfig.TIMEOUT_MS);
+ }
+
private boolean mayProcessMessageIfCached(int address, int opcode) {
HdmiCecMessage message = getCecMessageCache().getMessage(address, opcode);
if (message != null) {
@@ -275,6 +302,16 @@
return true;
}
return false;
+ case STATE_WAITING_FOR_POWER:
+ if (cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS) {
+ handleReportPowerStatus(cmd);
+ return true;
+ } else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT)
+ && ((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_REPORT_POWER_STATUS)) {
+ handleReportPowerStatus(cmd);
+ return true;
+ }
+ return false;
case STATE_WAITING_FOR_DEVICE_POLLING:
// Fall through.
default:
@@ -359,6 +396,26 @@
checkAndProceedStage();
}
+ private void handleReportPowerStatus(HdmiCecMessage cmd) {
+ Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
+
+ DeviceInfo current = mDevices.get(mProcessedDeviceCount);
+ if (current.mLogicalAddress != cmd.getSource()) {
+ Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:"
+ + cmd.getSource());
+ return;
+ }
+
+ if (cmd.getOpcode() != Constants.MESSAGE_FEATURE_ABORT) {
+ byte[] params = cmd.getParams();
+ int powerStatus = params[0] & 0xFF;
+ current.mPowerStatus = powerStatus;
+ }
+
+ increaseProcessedDeviceCount();
+ checkAndProceedStage();
+ }
+
private void increaseProcessedDeviceCount() {
mProcessedDeviceCount++;
mTimeoutRetry = 0;
@@ -402,6 +459,9 @@
startVendorIdStage();
return;
case STATE_WAITING_FOR_VENDOR_ID:
+ startPowerStatusStage();
+ return;
+ case STATE_WAITING_FOR_POWER:
wrapUpAndFinish();
return;
default:
@@ -427,6 +487,9 @@
case STATE_WAITING_FOR_VENDOR_ID:
queryVendorId(address);
return;
+ case STATE_WAITING_FOR_POWER:
+ queryPowerStatus(address);
+ return;
default:
return;
}
@@ -448,7 +511,11 @@
}
mTimeoutRetry = 0;
Slog.v(TAG, "Timeout[State=" + mState + ", Processed=" + mProcessedDeviceCount);
- removeDevice(mProcessedDeviceCount);
+ if (mState != STATE_WAITING_FOR_POWER) {
+ removeDevice(mProcessedDeviceCount);
+ } else {
+ increaseProcessedDeviceCount();
+ }
checkAndProceedStage();
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index e777ce8..86be585 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -77,7 +77,7 @@
private static final int NUM_LOGICAL_ADDRESS = 16;
- private static final int MAX_CEC_MESSAGE_HISTORY = 20;
+ private static final int MAX_CEC_MESSAGE_HISTORY = 200;
// Predicate for whether the given logical address is remote device's one or not.
private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
@@ -682,7 +682,7 @@
void dump(final IndentingPrintWriter pw) {
for (int i = 0; i < mLocalDevices.size(); ++i) {
- pw.println("HdmiCecLocalDevice #" + i + ":");
+ pw.println("HdmiCecLocalDevice #" + mLocalDevices.keyAt(i) + ":");
pw.increaseIndent();
mLocalDevices.valueAt(i).dump(pw);
pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index cd0653f..1029a0d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.server.hdmi.Constants.AudioCodec;
import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
@@ -44,6 +45,7 @@
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -83,25 +85,24 @@
// processing.
private final HashMap<Integer, String> mTvInputs = new HashMap<>();
+ // Copy of mDeviceInfos to guarantee thread-safety.
+ @GuardedBy("mLock")
+ private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
+
// Map-like container of all cec devices.
// device id is used as key of container.
private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>();
protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) {
super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mSystemAudioControlFeatureEnabled = true;
- // TODO(amyjojo) make System Audio Control controllable by users
- /*mSystemAudioControlFeatureEnabled =
- mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED, true);*/
mRoutingControlFeatureEnabled =
- mService.readBooleanSetting(Global.HDMI_CEC_SWITCH_ENABLED, true);
- // TODO(amyjojo): make the map ro property.
- mTvInputs.put(Constants.CEC_SWITCH_HDMI1,
- "com.droidlogic.tvinput/.services.Hdmi1InputService/HW5");
- mTvInputs.put(Constants.CEC_SWITCH_HDMI2,
- "com.droidlogic.tvinput/.services.Hdmi2InputService/HW6");
- mTvInputs.put(Constants.CEC_SWITCH_HDMI3,
- "com.droidlogic.tvinput/.services.Hdmi3InputService/HW7");
+ mService.readBooleanSetting(Global.HDMI_CEC_SWITCH_ENABLED, false);
+ mSystemAudioControlFeatureEnabled =
+ mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED, true);
+ // TODO(amyjojo): Maintain a portId to TvinputId map.
+ mTvInputs.put(2, "com.droidlogic.tvinput/.services.Hdmi1InputService/HW5");
+ mTvInputs.put(4, "com.droidlogic.tvinput/.services.Hdmi2InputService/HW6");
+ mTvInputs.put(1, "com.droidlogic.tvinput/.services.Hdmi3InputService/HW7");
}
/**
@@ -175,6 +176,7 @@
removeDeviceInfo(deviceInfo.getId());
}
mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
+ updateSafeDeviceInfoList();
return oldDeviceInfo;
}
@@ -192,6 +194,7 @@
if (deviceInfo != null) {
mDeviceInfos.remove(id);
}
+ updateSafeDeviceInfoList();
return deviceInfo;
}
@@ -208,6 +211,24 @@
return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
}
+ @ServiceThreadOnly
+ private void updateSafeDeviceInfoList() {
+ assertRunOnServiceThread();
+ List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
+ synchronized (mLock) {
+ mSafeAllDeviceInfos = copiedDevices;
+ }
+ }
+
+ @GuardedBy("mLock")
+ List<HdmiDeviceInfo> getSafeCecDevicesLocked() {
+ ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
+ for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
+ infoList.add(info);
+ }
+ return infoList;
+ }
+
private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
mService.invokeDeviceEventListeners(info, status);
}
@@ -275,7 +296,7 @@
int systemAudioOnPowerOnProp, boolean lastSystemAudioControlStatus) {
if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
|| ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
- && lastSystemAudioControlStatus)) {
+ && lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) {
addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
}
}
@@ -408,8 +429,11 @@
@ServiceThreadOnly
protected boolean handleGiveAudioStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
-
- reportAudioStatus(message.getSource());
+ if (isSystemAudioControlFeatureEnabled()) {
+ reportAudioStatus(message.getSource());
+ } else {
+ mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
+ }
return true;
}
@@ -721,7 +745,7 @@
*/
private void setSystemAudioMode(boolean newSystemAudioMode) {
int targetPhysicalAddress = getActiveSource().physicalAddress;
- int port = getLocalPortFromPhysicalAddress(targetPhysicalAddress);
+ int port = mService.pathToPortId(targetPhysicalAddress);
if (newSystemAudioMode && port >= 0) {
switchToAudioInput();
}
@@ -729,16 +753,18 @@
// PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE is false when device never needs to be muted.
boolean currentMuteStatus =
mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC);
- if (SystemProperties.getBoolean(
- Constants.PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE, true)
- && currentMuteStatus == newSystemAudioMode) {
- mService.getAudioManager()
- .adjustStreamVolume(
- AudioManager.STREAM_MUSIC,
- newSystemAudioMode
- ? AudioManager.ADJUST_UNMUTE
- : AudioManager.ADJUST_MUTE,
- 0);
+ if (currentMuteStatus == newSystemAudioMode) {
+ if (SystemProperties.getBoolean(
+ Constants.PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE, true)
+ || newSystemAudioMode) {
+ mService.getAudioManager()
+ .adjustStreamVolume(
+ AudioManager.STREAM_MUSIC,
+ newSystemAudioMode
+ ? AudioManager.ADJUST_UNMUTE
+ : AudioManager.ADJUST_MUTE,
+ 0);
+ }
}
updateAudioManagerForSystemAudio(newSystemAudioMode);
synchronized (mLock) {
@@ -776,6 +802,13 @@
HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
}
+ void onSystemAduioControlFeatureSupportChanged(boolean enabled) {
+ setSystemAudioControlFeatureEnabled(enabled);
+ if (enabled) {
+ addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+ }
+ }
+
@ServiceThreadOnly
void setSystemAudioControlFeatureEnabled(boolean enabled) {
assertRunOnServiceThread();
@@ -913,7 +946,7 @@
@Override
protected void switchInputOnReceivingNewActivePath(int physicalAddress) {
- int port = getLocalPortFromPhysicalAddress(physicalAddress);
+ int port = mService.pathToPortId(physicalAddress);
if (isSystemAudioActivated() && port < 0) {
// If system audio mode is on and the new active source is not under the current device,
// Will switch to ARC input.
@@ -985,7 +1018,7 @@
@Override
protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
- int port = getLocalPortFromPhysicalAddress(physicalAddress);
+ int port = mService.pathToPortId(physicalAddress);
// Routing change or information sent from switches under the current device can be ignored.
if (port > 0) {
return;
@@ -1054,6 +1087,10 @@
@ServiceThreadOnly
private void launchDeviceDiscovery() {
assertRunOnServiceThread();
+ if (hasAction(DeviceDiscoveryAction.class)) {
+ Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
+ removeAction(DeviceDiscoveryAction.class);
+ }
DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
new DeviceDiscoveryCallback() {
@Override
@@ -1077,5 +1114,25 @@
invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
}
mDeviceInfos.clear();
+ updateSafeDeviceInfoList();
}
+
+ @Override
+ protected void dump(IndentingPrintWriter pw) {
+ pw.println("HdmiCecLocalDeviceAudioSystem:");
+ pw.increaseIndent();
+ pw.println("mSystemAudioActivated: " + mSystemAudioActivated);
+ pw.println("isRoutingFeatureEnabled " + isRoutingControlFeatureEnabled());
+ pw.println("mSystemAudioControlFeatureEnabled: " + mSystemAudioControlFeatureEnabled);
+ pw.println("mTvSystemAudioModeSupport: " + mTvSystemAudioModeSupport);
+ pw.println("mArcEstablished: " + mArcEstablished);
+ pw.println("mArcIntentUsed: " + mArcIntentUsed);
+ pw.println("mRoutingPort: " + getRoutingPort());
+ pw.println("mLocalActivePort: " + getLocalActivePort());
+ HdmiUtils.dumpMap(pw, "mTvInputs:", mTvInputs);
+ HdmiUtils.dumpSparseArray(pw, "mDeviceInfos:", mDeviceInfos);
+ pw.decreaseIndent();
+ super.dump(pw);
+ }
+
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
index cbddaf5..83c2fe1 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
@@ -66,7 +66,7 @@
@LocalActivePort
protected int mLocalActivePort = Constants.CEC_SWITCH_HOME;
- // Whether the Routing Coutrol feature is enabled or not. True by default.
+ // Whether the Routing Coutrol feature is enabled or not. False by default.
@GuardedBy("mLock")
protected boolean mRoutingControlFeatureEnabled;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
index 4052c8a..f8b3962 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
@@ -112,12 +112,11 @@
@Override
public String toString() {
StringBuffer s = new StringBuffer();
- s.append(String.format("<%s> src: %d, dst: %d",
- opcodeToString(mOpcode), mSource, mDestination));
+ s.append(String.format("<%s> %X%X:%02X",
+ opcodeToString(mOpcode), mSource, mDestination, mOpcode));
if (mParams.length > 0) {
- s.append(", params:");
for (byte data : mParams) {
- s.append(String.format(" %02X", data));
+ s.append(String.format(":%02X", data));
}
}
return s.toString();
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index fcfb066..d390d86 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -20,12 +20,14 @@
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH;
+import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
import static com.android.server.hdmi.Constants.DISABLED;
import static com.android.server.hdmi.Constants.ENABLED;
import static com.android.server.hdmi.Constants.OPTION_MHL_ENABLE;
import static com.android.server.hdmi.Constants.OPTION_MHL_INPUT_SWITCHING;
import static com.android.server.hdmi.Constants.OPTION_MHL_POWER_CHARGE;
import static com.android.server.hdmi.Constants.OPTION_MHL_SERVICE_CONTROL;
+import static com.android.server.power.ShutdownThread.SHUTDOWN_ACTION_PROPERTY;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
@@ -184,9 +186,10 @@
@Override
public void onReceive(Context context, Intent intent) {
assertRunOnServiceThread();
+ boolean isReboot = SystemProperties.get(SHUTDOWN_ACTION_PROPERTY).contains("1");
switch (intent.getAction()) {
case Intent.ACTION_SCREEN_OFF:
- if (isPowerOnOrTransient()) {
+ if (isPowerOnOrTransient() && !isReboot) {
onStandby(STANDBY_SCREEN_OFF);
}
break;
@@ -202,7 +205,7 @@
}
break;
case Intent.ACTION_SHUTDOWN:
- if (isPowerOnOrTransient()) {
+ if (isPowerOnOrTransient() && !isReboot) {
onStandby(STANDBY_SHUTDOWN);
}
break;
@@ -610,6 +613,9 @@
if (isTvDeviceEnabled()) {
tv().setSystemAudioControlFeatureEnabled(enabled);
}
+ if (isAudioSystemDevice()) {
+ audioSystem().onSystemAduioControlFeatureSupportChanged(enabled);
+ }
break;
case Global.HDMI_CEC_SWITCH_ENABLED:
if (isAudioSystemDevice()) {
@@ -1610,14 +1616,65 @@
public List<HdmiDeviceInfo> getDeviceList() {
enforceAccessPermission();
HdmiCecLocalDeviceTv tv = tv();
- synchronized (mLock) {
- return (tv == null)
+ if (tv != null) {
+ synchronized (mLock) {
+ return tv.getSafeCecDevicesLocked();
+ }
+ } else {
+ HdmiCecLocalDeviceAudioSystem audioSystem = audioSystem();
+ synchronized (mLock) {
+ return (audioSystem == null)
? Collections.<HdmiDeviceInfo>emptyList()
- : tv.getSafeCecDevicesLocked();
+ : audioSystem.getSafeCecDevicesLocked();
+ }
}
}
@Override
+ public void powerOffRemoteDevice(int logicalAddress, int powerStatus) {
+ enforceAccessPermission();
+ runOnServiceThread(new Runnable() {
+ @Override
+ public void run() {
+ if (powerStatus == HdmiControlManager.POWER_STATUS_ON
+ || powerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) {
+ sendCecCommand(HdmiCecMessageBuilder.buildStandby(
+ getRemoteControlSourceAddress(), logicalAddress));
+ } else {
+ Slog.w(TAG, "Device " + logicalAddress + " is already off " + powerStatus);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void powerOnRemoteDevice(int logicalAddress, int powerStatus) {
+ // TODO(amyjojo): implement the method
+ }
+
+ @Override
+ // TODO(AMYJOJO): add a result callback
+ public void askRemoteDeviceToBecomeActiveSource(int physicalAddress) {
+ enforceAccessPermission();
+ runOnServiceThread(new Runnable() {
+ @Override
+ public void run() {
+ HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(
+ getRemoteControlSourceAddress(), physicalAddress);
+ if (pathToPortId(physicalAddress) != Constants.INVALID_PORT_ID) {
+ if (getSwitchDevice() != null) {
+ getSwitchDevice().handleSetStreamPath(setStreamPath);
+ } else {
+ Slog.e(TAG, "Can't get the correct local device to handle routing.");
+ }
+ } else {
+ sendCecCommand(setStreamPath);
+ }
+ }
+ });
+ }
+
+ @Override
public void setSystemAudioVolume(final int oldIndex, final int newIndex,
final int maxIndex) {
enforceAccessPermission();
@@ -1887,30 +1944,54 @@
if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) return;
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- pw.println("mHdmiControlEnabled: " + mHdmiControlEnabled);
pw.println("mProhibitMode: " + mProhibitMode);
- if (mCecController != null) {
- pw.println("mCecController: ");
- pw.increaseIndent();
- mCecController.dump(pw);
- pw.decreaseIndent();
- }
+ pw.println("mPowerStatus: " + mPowerStatus);
+
+ // System settings
+ pw.println("System_settings:");
+ pw.increaseIndent();
+ pw.println("mHdmiControlEnabled: " + mHdmiControlEnabled);
+ pw.println("mMhlInputChangeEnabled: " + mMhlInputChangeEnabled);
+ pw.decreaseIndent();
pw.println("mMhlController: ");
pw.increaseIndent();
mMhlController.dump(pw);
pw.decreaseIndent();
- pw.println("mPortInfo: ");
- pw.increaseIndent();
- for (HdmiPortInfo hdmiPortInfo : mPortInfo) {
- pw.println("- " + hdmiPortInfo);
+ HdmiUtils.dumpIterable(pw, "mPortInfo:", mPortInfo);
+ if (mCecController != null) {
+ pw.println("mCecController: ");
+ pw.increaseIndent();
+ mCecController.dump(pw);
+ pw.decreaseIndent();
}
- pw.decreaseIndent();
- pw.println("mPowerStatus: " + mPowerStatus);
}
}
+ // Get the source address to send out commands to devices connected to the current device
+ // when other services interact with HdmiControlService.
+ private int getRemoteControlSourceAddress() {
+ if (isAudioSystemDevice()) {
+ return audioSystem().getDeviceInfo().getLogicalAddress();
+ } else if (isPlaybackDevice()) {
+ return playback().getDeviceInfo().getLogicalAddress();
+ }
+ return ADDR_UNREGISTERED;
+ }
+
+ // Get the switch device to do CEC routing control
+ @Nullable
+ private HdmiCecLocalDeviceSource getSwitchDevice() {
+ if (isAudioSystemDevice()) {
+ return audioSystem();
+ }
+ if (isPlaybackDevice()) {
+ return playback();
+ }
+ return null;
+ }
+
@ServiceThreadOnly
private void oneTouchPlay(final IHdmiControlCallback callback) {
assertRunOnServiceThread();
diff --git a/services/core/java/com/android/server/hdmi/HdmiUtils.java b/services/core/java/com/android/server/hdmi/HdmiUtils.java
index 2a8117f..2110682 100644
--- a/services/core/java/com/android/server/hdmi/HdmiUtils.java
+++ b/services/core/java/com/android/server/hdmi/HdmiUtils.java
@@ -20,9 +20,13 @@
import android.util.Slog;
import android.util.SparseArray;
+import com.android.internal.util.IndentingPrintWriter;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+
/**
* Various utilities to handle HDMI CEC messages.
@@ -317,4 +321,74 @@
info.getPhysicalAddress(), info.getPortId(), info.getDeviceType(),
info.getVendorId(), info.getDisplayName(), newPowerStatus);
}
+
+ /**
+ * Dump a {@link SparseArray} to the print writer.
+ *
+ * <p>The dump is formatted:
+ * <pre>
+ * name:
+ * key = value
+ * key = value
+ * ...
+ * </pre>
+ */
+ static <T> void dumpSparseArray(IndentingPrintWriter pw, String name,
+ SparseArray<T> sparseArray) {
+ printWithTrailingColon(pw, name);
+ pw.increaseIndent();
+ int size = sparseArray.size();
+ for (int i = 0; i < size; i++) {
+ int key = sparseArray.keyAt(i);
+ T value = sparseArray.get(key);
+ pw.printPair(Integer.toString(key), value);
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+
+ private static void printWithTrailingColon(IndentingPrintWriter pw, String name) {
+ pw.println(name.endsWith(":") ? name : name.concat(":"));
+ }
+
+ /**
+ * Dump a {@link Map} to the print writer.
+ *
+ * <p>The dump is formatted:
+ * <pre>
+ * name:
+ * key = value
+ * key = value
+ * ...
+ * </pre>
+ */
+ static <K, V> void dumpMap(IndentingPrintWriter pw, String name, Map<K, V> map) {
+ printWithTrailingColon(pw, name);
+ pw.increaseIndent();
+ for (Map.Entry<K, V> entry: map.entrySet()) {
+ pw.printPair(entry.getKey().toString(), entry.getValue());
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Dump a {@link Map} to the print writer.
+ *
+ * <p>The dump is formatted:
+ * <pre>
+ * name:
+ * value
+ * value
+ * ...
+ * </pre>
+ */
+ static <T> void dumpIterable(IndentingPrintWriter pw, String name, Iterable<T> values) {
+ printWithTrailingColon(pw, name);
+ pw.increaseIndent();
+ for (T value : values) {
+ pw.println(value);
+ }
+ pw.decreaseIndent();
+ }
}
diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
index 41bf01f..d665efe 100644
--- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
+++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
@@ -92,6 +92,9 @@
if (source.mService.audioSystem() != null) {
source = source.mService.audioSystem();
}
+ if (source.getLocalActivePort() != Constants.CEC_SWITCH_HOME) {
+ source.switchInputOnReceivingNewActivePath(getSourceAddress());
+ }
source.setRoutingPort(Constants.CEC_SWITCH_HOME);
source.setLocalActivePort(Constants.CEC_SWITCH_HOME);
}
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index ac2dbdf..c16d1b4 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -26,7 +26,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
+import android.app.IUidObserver;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
import android.content.BroadcastReceiver;
@@ -38,12 +41,14 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
@@ -69,6 +74,11 @@
* bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately
* applied to it.
*
+ * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
+ * freely when an app enters the foreground state and are restricted when the app leaves the
+ * foreground state. However, jobs that are started while the app is in the TOP state are not
+ * restricted regardless of the app's state change.
+ *
* Test: atest com.android.server.job.controllers.QuotaControllerTest
*/
public final class QuotaController extends StateController {
@@ -97,6 +107,12 @@
data.put(packageName, obj);
}
+ public void clear() {
+ for (int i = 0; i < mData.size(); ++i) {
+ mData.valueAt(i).clear();
+ }
+ }
+
/** Removes all the data for the user, if there was any. */
public void delete(int userId) {
mData.delete(userId);
@@ -119,6 +135,11 @@
return null;
}
+ /** @see SparseArray#indexOfKey */
+ public int indexOfKey(int userId) {
+ return mData.indexOfKey(userId);
+ }
+
/** Returns the userId at the given index. */
public int keyAt(int index) {
return mData.keyAt(index);
@@ -294,6 +315,17 @@
/** Cached calculation results for each app, with the standby buckets as the array indices. */
private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+ /** List of UIDs currently in the foreground. */
+ private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+
+ /**
+ * List of jobs that started while the UID was in the TOP state. There will be no more than
+ * 16 ({@link JobSchedulerService.MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
+ * fine.
+ */
+ private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
+
+ private final ActivityManagerInternal mActivityManagerInternal;
private final AlarmManager mAlarmManager;
private final ChargingTracker mChargeTracker;
private final Handler mHandler;
@@ -343,6 +375,29 @@
}
};
+ private final IUidObserver mUidObserver = new IUidObserver.Stub() {
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget();
+ }
+
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
+ }
+
+ @Override
+ public void onUidActive(int uid) {
+ }
+
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {
+ }
+
+ @Override
+ public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ };
+
/**
* The rolling window size for each standby bucket. Within each window, an app will have 10
* minutes to run its jobs.
@@ -363,12 +418,15 @@
private static final int MSG_CLEAN_UP_SESSIONS = 1;
/** Check if a package is now within its quota. */
private static final int MSG_CHECK_PACKAGE = 2;
+ /** Process state for a UID has changed. */
+ private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
public QuotaController(JobSchedulerService service) {
super(service);
mHandler = new QcHandler(mContext.getMainLooper());
mChargeTracker = new ChargingTracker();
mChargeTracker.startTracking();
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
// Set up the app standby bucketing tracker
@@ -376,6 +434,14 @@
UsageStatsManagerInternal.class);
usageStats.addAppIdleStateChangeListener(new StandbyTracker());
+ try {
+ ActivityManager.getService().registerUidObserver(mUidObserver,
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
+ } catch (RemoteException e) {
+ // ignored; both services live in system_server
+ }
+
onConstantsUpdatedLocked();
}
@@ -399,11 +465,15 @@
if (DEBUG) Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
final int userId = jobStatus.getSourceUserId();
final String packageName = jobStatus.getSourcePackageName();
+ final int uid = jobStatus.getSourceUid();
Timer timer = mPkgTimers.get(userId, packageName);
if (timer == null) {
- timer = new Timer(userId, packageName);
+ timer = new Timer(uid, userId, packageName);
mPkgTimers.add(userId, packageName, timer);
}
+ if (mActivityManagerInternal.getUidProcessState(uid) == ActivityManager.PROCESS_STATE_TOP) {
+ mTopStartedJobs.add(jobStatus);
+ }
timer.startTrackingJob(jobStatus);
}
@@ -421,6 +491,7 @@
if (jobs != null) {
jobs.remove(jobStatus);
}
+ mTopStartedJobs.remove(jobStatus);
}
}
@@ -511,6 +582,7 @@
mInQuotaAlarmListeners.delete(userId, packageName);
}
mExecutionStatsCache.delete(userId, packageName);
+ mForegroundUids.delete(uid);
}
@Override
@@ -522,6 +594,20 @@
mExecutionStatsCache.delete(userId);
}
+ private boolean isUidInForeground(int uid) {
+ if (UserHandle.isCore(uid)) {
+ return true;
+ }
+ synchronized (mLock) {
+ return mForegroundUids.get(uid);
+ }
+ }
+
+ /** @return true if the job was started while the app was in the TOP state. */
+ private boolean isTopStartedJob(@NonNull final JobStatus jobStatus) {
+ return mTopStartedJobs.contains(jobStatus);
+ }
+
/**
* Returns an appropriate standby bucket for the job, taking into account any standby
* exemptions.
@@ -537,9 +623,15 @@
private boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
final int standbyBucket = getEffectiveStandbyBucket(jobStatus);
- // Jobs for the active app should always be able to run.
- return jobStatus.uidActive || isWithinQuotaLocked(
- jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
+ Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
+ // A job is within quota if one of the following is true:
+ // 1. it was started while the app was in the TOP state
+ // 2. the app is currently in the foreground
+ // 3. the app overall is within its quota
+ return isTopStartedJob(jobStatus)
+ || isUidInForeground(jobStatus.getSourceUid())
+ || isWithinQuotaLocked(
+ jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
}
private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
@@ -800,7 +892,7 @@
final boolean isCharging = mChargeTracker.isCharging();
if (DEBUG) Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging);
// Deal with Timers first.
- mPkgTimers.forEach((t) -> t.onChargingChanged(nowElapsed, isCharging));
+ mPkgTimers.forEach((t) -> t.onStateChanged(nowElapsed, isCharging));
// Now update jobs.
maybeUpdateAllConstraintsLocked();
}
@@ -837,10 +929,15 @@
boolean changed = false;
for (int i = jobs.size() - 1; i >= 0; --i) {
final JobStatus js = jobs.valueAt(i);
- if (js.uidActive) {
- // Jobs for the active app should always be able to run.
+ if (isTopStartedJob(js)) {
+ // Job was started while the app was in the TOP state so we should allow it to
+ // finish.
changed |= js.setQuotaConstraintSatisfied(true);
- } else if (realStandbyBucket == getEffectiveStandbyBucket(js)) {
+ } else if (realStandbyBucket != ACTIVE_INDEX
+ && realStandbyBucket == getEffectiveStandbyBucket(js)) {
+ // An app in the ACTIVE bucket may be out of quota while the job could be in quota
+ // for some reason. Therefore, avoid setting the real value here and check each job
+ // individually.
changed |= js.setQuotaConstraintSatisfied(realInQuota);
} else {
// This job is somehow exempted. Need to determine its own quota status.
@@ -854,7 +951,7 @@
maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
} else {
QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
- if (alarmListener != null) {
+ if (alarmListener != null && alarmListener.isWaiting()) {
mAlarmManager.cancel(alarmListener);
// Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
alarmListener.setTriggerTime(0);
@@ -863,6 +960,56 @@
return changed;
}
+ private class UidConstraintUpdater implements Consumer<JobStatus> {
+ private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>();
+ public boolean wasJobChanged;
+
+ @Override
+ public void accept(JobStatus jobStatus) {
+ wasJobChanged |= jobStatus.setQuotaConstraintSatisfied(isWithinQuotaLocked(jobStatus));
+ final int userId = jobStatus.getSourceUserId();
+ final String packageName = jobStatus.getSourcePackageName();
+ final int realStandbyBucket = jobStatus.getStandbyBucket();
+ if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (alarmListener != null && alarmListener.isWaiting()) {
+ mAlarmManager.cancel(alarmListener);
+ // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
+ alarmListener.setTriggerTime(0);
+ }
+ } else {
+ mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
+ }
+ }
+
+ void postProcess() {
+ for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) {
+ final int userId = mToScheduleStartAlarms.keyAt(u);
+ for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) {
+ final String packageName = mToScheduleStartAlarms.keyAt(u, p);
+ final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
+ maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
+ }
+ }
+ }
+
+ void reset() {
+ wasJobChanged = false;
+ mToScheduleStartAlarms.clear();
+ }
+ }
+
+ private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater();
+
+ private boolean maybeUpdateConstraintForUidLocked(final int uid) {
+ mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints);
+
+ mUpdateUidConstraints.postProcess();
+ boolean changed = mUpdateUidConstraints.wasJobChanged;
+ mUpdateUidConstraints.reset();
+ return changed;
+ }
+
/**
* Maybe schedule a non-wakeup alarm for the next time this package will have quota to run
* again. This should only be called if the package is already out of quota.
@@ -1052,6 +1199,7 @@
private final class Timer {
private final Package mPkg;
+ private final int mUid;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
@@ -1059,16 +1207,18 @@
private long mStartTimeElapsed;
private int mBgJobCount;
- Timer(int userId, String packageName) {
+ Timer(int uid, int userId, String packageName) {
mPkg = new Package(userId, packageName);
+ mUid = uid;
}
void startTrackingJob(@NonNull JobStatus jobStatus) {
- if (jobStatus.uidActive) {
- // We intentionally don't pay attention to fg state changes after a job has started.
+ if (isTopStartedJob(jobStatus)) {
+ // We intentionally don't pay attention to fg state changes after a TOP job has
+ // started.
if (DEBUG) {
Slog.v(TAG,
- "Timer ignoring " + jobStatus.toShortString() + " because uidActive");
+ "Timer ignoring " + jobStatus.toShortString() + " because isTop");
}
return;
}
@@ -1076,7 +1226,7 @@
synchronized (mLock) {
// Always track jobs, even when charging.
mRunningBgJobs.add(jobStatus);
- if (!mChargeTracker.isCharging()) {
+ if (shouldTrackLocked()) {
mBgJobCount++;
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
@@ -1142,6 +1292,10 @@
}
}
+ boolean isRunning(JobStatus jobStatus) {
+ return mRunningBgJobs.contains(jobStatus);
+ }
+
long getCurrentDuration(long nowElapsed) {
synchronized (mLock) {
return !isActive() ? 0 : nowElapsed - mStartTimeElapsed;
@@ -1154,17 +1308,21 @@
}
}
- void onChargingChanged(long nowElapsed, boolean isCharging) {
+ private boolean shouldTrackLocked() {
+ return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid);
+ }
+
+ void onStateChanged(long nowElapsed, boolean isQuotaFree) {
synchronized (mLock) {
- if (isCharging) {
+ if (isQuotaFree) {
emitSessionLocked(nowElapsed);
- } else {
+ } else if (shouldTrackLocked()) {
// Start timing from unplug.
if (mRunningBgJobs.size() > 0) {
mStartTimeElapsed = nowElapsed;
// NOTE: this does have the unfortunate consequence that if the device is
- // repeatedly plugged in and unplugged, the job count for a package may be
- // artificially high.
+ // repeatedly plugged in and unplugged, or an app changes foreground state
+ // very frequently, the job count for a package may be artificially high.
mBgJobCount = mRunningBgJobs.size();
// Starting the timer means that all cached execution stats are now
// incorrect.
@@ -1371,6 +1529,38 @@
}
break;
}
+ case MSG_UID_PROCESS_STATE_CHANGED: {
+ final int uid = msg.arg1;
+ final int procState = msg.arg2;
+ final int userId = UserHandle.getUserId(uid);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ synchronized (mLock) {
+ boolean isQuotaFree;
+ if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ mForegroundUids.put(uid, true);
+ isQuotaFree = true;
+ } else {
+ mForegroundUids.delete(uid);
+ isQuotaFree = false;
+ }
+ // Update Timers first.
+ final int userIndex = mPkgTimers.indexOfKey(userId);
+ if (userIndex != -1) {
+ final int numPkgs = mPkgTimers.numPackagesForUser(userId);
+ for (int p = 0; p < numPkgs; ++p) {
+ Timer t = mPkgTimers.valueAt(userIndex, p);
+ if (t != null) {
+ t.onStateChanged(nowElapsed, isQuotaFree);
+ }
+ }
+ }
+ if (maybeUpdateConstraintForUidLocked(uid)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ break;
+ }
}
}
}
@@ -1420,6 +1610,12 @@
@VisibleForTesting
@NonNull
+ SparseBooleanArray getForegroundUids() {
+ return mForegroundUids;
+ }
+
+ @VisibleForTesting
+ @NonNull
Handler getHandler() {
return mHandler;
}
@@ -1450,6 +1646,10 @@
pw.println("In parole: " + mInParole);
pw.println();
+ pw.print("Foreground UIDs: ");
+ pw.println(mForegroundUids.toString());
+ pw.println();
+
mTrackedJobs.forEach((jobs) -> {
for (int j = 0; j < jobs.size(); j++) {
final JobStatus js = jobs.valueAt(j);
@@ -1460,6 +1660,9 @@
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
+ if (mTopStartedJobs.contains(js)) {
+ pw.print(" (TOP)");
+ }
pw.println();
pw.increaseIndent();
@@ -1511,6 +1714,11 @@
proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging());
proto.write(StateControllerProto.QuotaController.IS_IN_PAROLE, mInParole);
+ for (int i = 0; i < mForegroundUids.size(); ++i) {
+ proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS,
+ mForegroundUids.keyAt(i));
+ }
+
mTrackedJobs.forEach((jobs) -> {
for (int j = 0; j < jobs.size(); j++) {
final JobStatus js = jobs.valueAt(j);
@@ -1526,6 +1734,8 @@
proto.write(
StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET,
getEffectiveStandbyBucket(js));
+ proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB,
+ mTopStartedJobs.contains(js));
proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA,
js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS,
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index d611a17..7fffe8e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -80,7 +80,7 @@
private final ControllerStub mController;
private final SessionStub mSession;
private final SessionCb mSessionCb;
- private final MediaSessionService mService;
+ private final MediaSessionService.ServiceImpl mService;
private final Context mContext;
private final Object mLock = new Object();
@@ -120,7 +120,8 @@
private String mMetadataDescription;
public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
- SessionCallbackLink cb, String tag, MediaSessionService service, Looper handlerLooper) {
+ SessionCallbackLink cb, String tag, MediaSessionService.ServiceImpl service,
+ Looper handlerLooper) {
mOwnerPid = ownerPid;
mOwnerUid = ownerUid;
mUserId = userId;
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index ba7b87e..d20ed8c 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -16,79 +16,15 @@
package com.android.server.media;
-import static android.os.UserHandle.USER_ALL;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.INotificationManager;
-import android.app.KeyguardManager;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ServiceInfo;
-import android.content.pm.UserInfo;
-import android.database.ContentObserver;
-import android.media.AudioManager;
-import android.media.AudioManagerInternal;
-import android.media.AudioPlaybackConfiguration;
-import android.media.AudioSystem;
-import android.media.IAudioService;
-import android.media.IRemoteVolumeController;
-import android.media.MediaController2;
-import android.media.Session2CommandGroup;
import android.media.Session2Token;
-import android.media.session.IActiveSessionsListener;
-import android.media.session.ICallback;
-import android.media.session.IOnMediaKeyListener;
-import android.media.session.IOnVolumeKeyLongPressListener;
-import android.media.session.ISession;
-import android.media.session.ISession2TokensListener;
-import android.media.session.ISessionManager;
-import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
-import android.media.session.SessionCallbackLink;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerExecutor;
import android.os.IBinder;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.speech.RecognizerIntent;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseIntArray;
-import android.view.KeyEvent;
-import android.view.ViewConfiguration;
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.DumpUtils;
-import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.Watchdog;
import com.android.server.Watchdog.Monitor;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -97,2040 +33,124 @@
public class MediaSessionService extends SystemService implements Monitor {
private static final String TAG = "MediaSessionService";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- // Leave log for key event always.
- private static final boolean DEBUG_KEY_EVENT = true;
- private static final int WAKELOCK_TIMEOUT = 5000;
- private static final int MEDIA_KEY_LISTENER_TIMEOUT = 1000;
-
- private final SessionManagerImpl mSessionManagerImpl;
- private final MessageHandler mHandler = new MessageHandler();
- private final PowerManager.WakeLock mMediaEventWakeLock;
- private final int mLongPressTimeout;
- private final INotificationManager mNotificationManager;
- private final Object mLock = new Object();
- // Keeps the full user id for each user.
- @GuardedBy("mLock")
- private final SparseIntArray mFullUserIds = new SparseIntArray();
- @GuardedBy("mLock")
- private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<FullUserRecord>();
- @GuardedBy("mLock")
- private final ArrayList<SessionsListenerRecord> mSessionsListeners
- = new ArrayList<SessionsListenerRecord>();
- // Map user id as index to list of Session2Tokens
- // TODO: Keep session2 info in MediaSessionStack for prioritizing both session1 and session2 in
- // one place.
- @GuardedBy("mLock")
- private final SparseArray<List<Session2Token>> mSession2TokensPerUser = new SparseArray<>();
- @GuardedBy("mLock")
- private final List<Session2TokensListenerRecord> mSession2TokensListenerRecords =
- new ArrayList<>();
-
- private KeyguardManager mKeyguardManager;
- private IAudioService mAudioService;
- private AudioManagerInternal mAudioManagerInternal;
- private ActivityManager mActivityManager;
- private ContentResolver mContentResolver;
- private SettingsObserver mSettingsObserver;
- private boolean mHasFeatureLeanback;
-
- // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
- // It's always not null after the MediaSessionService is started.
- private FullUserRecord mCurrentFullUserRecord;
- private MediaSessionRecord mGlobalPrioritySession;
- private AudioPlayerStateMonitor mAudioPlayerStateMonitor;
-
- // Used to notify system UI when remote volume was changed. TODO find a
- // better way to handle this.
- private IRemoteVolumeController mRvc;
+ private final ServiceImpl mImpl;
public MediaSessionService(Context context) {
super(context);
- mSessionManagerImpl = new SessionManagerImpl();
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
- mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
- mNotificationManager = INotificationManager.Stub.asInterface(
- ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ mImpl = new MediaSessionServiceImpl(context);
}
@Override
public void onStart() {
- publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
+ publishBinderService(Context.MEDIA_SESSION_SERVICE, mImpl.getServiceBinder());
Watchdog.getInstance().addMonitor(this);
- mKeyguardManager =
- (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
- mAudioService = getAudioService();
- mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
- mActivityManager =
- (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
- mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
- mAudioPlayerStateMonitor.registerListener(
- (config, isRemoved) -> {
- if (isRemoved || !config.isActive() || config.getPlayerType()
- == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
- return;
- }
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(
- UserHandle.getUserId(config.getClientUid()));
- if (user != null) {
- user.mPriorityStack.updateMediaButtonSessionIfNeeded();
- }
- }
- }, null /* handler */);
- mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
- mContentResolver = getContext().getContentResolver();
- mSettingsObserver = new SettingsObserver();
- mSettingsObserver.observe();
- mHasFeatureLeanback = getContext().getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_LEANBACK);
- updateUser();
+ mImpl.onStart();
}
- private IAudioService getAudioService() {
- IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
- return IAudioService.Stub.asInterface(b);
+ @Override
+ public void onStartUser(int userId) {
+ mImpl.onStartUser(userId);
}
- private boolean isGlobalPriorityActiveLocked() {
- return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive();
+ @Override
+ public void onSwitchUser(int userId) {
+ mImpl.onSwitchUser(userId);
}
+ // Called when the user with the userId is removed.
+ @Override
+ public void onStopUser(int userId) {
+ mImpl.onStopUser(userId);
+ }
+
+ @Override
+ public void monitor() {
+ mImpl.monitor();
+ }
+
+ /**
+ * Updates session.
+ */
public void updateSession(MediaSessionRecord record) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(record.getUserId());
- if (user == null) {
- Log.w(TAG, "Unknown session updated. Ignoring.");
- return;
- }
- if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Global priority session is updated, active=" + record.isActive());
- }
- user.pushAddressedPlayerChangedLocked();
- } else {
- if (!user.mPriorityStack.contains(record)) {
- Log.w(TAG, "Unknown session updated. Ignoring.");
- return;
- }
- user.mPriorityStack.onSessionStateChange(record);
- }
- mHandler.postSessionsChanged(record.getUserId());
- }
+ mImpl.updateSession(record);
}
+ /**
+ * Sets global priority session.
+ */
public void setGlobalPrioritySession(MediaSessionRecord record) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(record.getUserId());
- if (mGlobalPrioritySession != record) {
- Log.d(TAG, "Global priority session is changed from " + mGlobalPrioritySession
- + " to " + record);
- mGlobalPrioritySession = record;
- if (user != null && user.mPriorityStack.contains(record)) {
- // Handle the global priority session separately.
- // Otherwise, it can be the media button session regardless of the active state
- // because it or other system components might have been the lastly played media
- // app.
- user.mPriorityStack.removeSession(record);
- }
- }
- }
- }
-
- private List<MediaSessionRecord> getActiveSessionsLocked(int userId) {
- List<MediaSessionRecord> records = new ArrayList<>();
- if (userId == USER_ALL) {
- int size = mUserRecords.size();
- for (int i = 0; i < size; i++) {
- records.addAll(mUserRecords.valueAt(i).mPriorityStack.getActiveSessions(userId));
- }
- } else {
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null) {
- Log.w(TAG, "getSessions failed. Unknown user " + userId);
- return records;
- }
- records.addAll(user.mPriorityStack.getActiveSessions(userId));
- }
-
- // Return global priority session at the first whenever it's asked.
- if (isGlobalPriorityActiveLocked()
- && (userId == USER_ALL || userId == mGlobalPrioritySession.getUserId())) {
- records.add(0, mGlobalPrioritySession);
- }
- return records;
+ mImpl.setGlobalPrioritySession(record);
}
List<Session2Token> getSession2TokensLocked(int userId) {
- List<Session2Token> list = new ArrayList<>();
- if (userId == USER_ALL) {
- for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
- list.addAll(mSession2TokensPerUser.valueAt(i));
- }
- } else {
- list.addAll(mSession2TokensPerUser.get(userId));
- }
- return list;
+ return mImpl.getSession2TokensLocked(userId);
}
/**
* Tells the system UI that volume has changed on an active remote session.
*/
public void notifyRemoteVolumeChanged(int flags, MediaSessionRecord session) {
- if (mRvc == null || !session.isActive()) {
- return;
- }
- try {
- mRvc.remoteVolumeChanged(session.getControllerBinder(), flags);
- } catch (Exception e) {
- Log.wtf(TAG, "Error sending volume change to system UI.", e);
- }
+ mImpl.notifyRemoteVolumeChanged(flags, session);
}
+ /**
+ * Called when session playstate is changed.
+ */
public void onSessionPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(record.getUserId());
- if (user == null || !user.mPriorityStack.contains(record)) {
- Log.d(TAG, "Unknown session changed playback state. Ignoring.");
- return;
- }
- user.mPriorityStack.onPlaystateChanged(record, oldState, newState);
- }
+ mImpl.onSessionPlaystateChanged(record, oldState, newState);
}
+ /**
+ * Called when session playback type is changed.
+ */
public void onSessionPlaybackTypeChanged(MediaSessionRecord record) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(record.getUserId());
- if (user == null || !user.mPriorityStack.contains(record)) {
- Log.d(TAG, "Unknown session changed playback type. Ignoring.");
- return;
- }
- pushRemoteVolumeUpdateLocked(record.getUserId());
- }
- }
-
- @Override
- public void onStartUser(int userId) {
- if (DEBUG) Log.d(TAG, "onStartUser: " + userId);
- updateUser();
- }
-
- @Override
- public void onSwitchUser(int userId) {
- if (DEBUG) Log.d(TAG, "onSwitchUser: " + userId);
- updateUser();
- }
-
- // Called when the user with the userId is removed.
- @Override
- public void onStopUser(int userId) {
- if (DEBUG) Log.d(TAG, "onStopUser: " + userId);
- synchronized (mLock) {
- // TODO: Also handle removing user in updateUser() because adding/switching user is
- // handled in updateUser().
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user != null) {
- if (user.mFullUserId == userId) {
- user.destroySessionsForUserLocked(USER_ALL);
- mUserRecords.remove(userId);
- } else {
- user.destroySessionsForUserLocked(userId);
- }
- }
- mSession2TokensPerUser.remove(userId);
- updateUser();
- }
- }
-
- @Override
- public void monitor() {
- synchronized (mLock) {
- // Check for deadlock
- }
+ mImpl.onSessionPlaybackTypeChanged(record);
}
protected void enforcePhoneStatePermission(int pid, int uid) {
- if (getContext().checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
- }
+ mImpl.enforcePhoneStatePermission(pid, uid);
}
void sessionDied(MediaSessionRecord session) {
- synchronized (mLock) {
- destroySessionLocked(session);
- }
+ mImpl.sessionDied(session);
}
void destroySession(MediaSessionRecord session) {
- synchronized (mLock) {
- destroySessionLocked(session);
- }
- }
-
- private void updateUser() {
- synchronized (mLock) {
- UserManager manager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
- mFullUserIds.clear();
- List<UserInfo> allUsers = manager.getUsers();
- if (allUsers != null) {
- for (UserInfo userInfo : allUsers) {
- if (userInfo.isManagedProfile()) {
- mFullUserIds.put(userInfo.id, userInfo.profileGroupId);
- } else {
- mFullUserIds.put(userInfo.id, userInfo.id);
- if (mUserRecords.get(userInfo.id) == null) {
- mUserRecords.put(userInfo.id, new FullUserRecord(userInfo.id));
- }
- }
- if (mSession2TokensPerUser.get(userInfo.id) == null) {
- mSession2TokensPerUser.put(userInfo.id, new ArrayList<>());
- }
- }
- }
- // Ensure that the current full user exists.
- int currentFullUserId = ActivityManager.getCurrentUser();
- mCurrentFullUserRecord = mUserRecords.get(currentFullUserId);
- if (mCurrentFullUserRecord == null) {
- Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
- mCurrentFullUserRecord = new FullUserRecord(currentFullUserId);
- mUserRecords.put(currentFullUserId, mCurrentFullUserRecord);
- if (mSession2TokensPerUser.get(currentFullUserId) == null) {
- mSession2TokensPerUser.put(currentFullUserId, new ArrayList<>());
- }
- }
- mFullUserIds.put(currentFullUserId, currentFullUserId);
- }
- }
-
- private void updateActiveSessionListeners() {
- synchronized (mLock) {
- for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
- SessionsListenerRecord listener = mSessionsListeners.get(i);
- try {
- enforceMediaPermissions(listener.componentName, listener.pid, listener.uid,
- listener.userId);
- } catch (SecurityException e) {
- Log.i(TAG, "ActiveSessionsListener " + listener.componentName
- + " is no longer authorized. Disconnecting.");
- mSessionsListeners.remove(i);
- try {
- listener.listener
- .onActiveSessionsChanged(new ArrayList<MediaSession.Token>());
- } catch (Exception e1) {
- // ignore
- }
- }
- }
- }
- }
-
- /*
- * When a session is removed several things need to happen.
- * 1. We need to remove it from the relevant user.
- * 2. We need to remove it from the priority stack.
- * 3. We need to remove it from all sessions.
- * 4. If this is the system priority session we need to clear it.
- * 5. We need to unlink to death from the cb binder
- * 6. We need to tell the session to do any final cleanup (onDestroy)
- */
- private void destroySessionLocked(MediaSessionRecord session) {
- if (DEBUG) {
- Log.d(TAG, "Destroying " + session);
- }
- FullUserRecord user = getFullUserRecordLocked(session.getUserId());
- if (mGlobalPrioritySession == session) {
- mGlobalPrioritySession = null;
- if (session.isActive() && user != null) {
- user.pushAddressedPlayerChangedLocked();
- }
- } else {
- if (user != null) {
- user.mPriorityStack.removeSession(session);
- }
- }
-
- try {
- session.getCallback().getBinder().unlinkToDeath(session, 0);
- } catch (Exception e) {
- // ignore exceptions while destroying a session.
- }
- session.onDestroy();
- mHandler.postSessionsChanged(session.getUserId());
- }
-
- private void enforcePackageName(String packageName, int uid) {
- if (TextUtils.isEmpty(packageName)) {
- throw new IllegalArgumentException("packageName may not be empty");
- }
- String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
- final int packageCount = packages.length;
- for (int i = 0; i < packageCount; i++) {
- if (packageName.equals(packages[i])) {
- return;
- }
- }
- throw new IllegalArgumentException("packageName is not owned by the calling process");
- }
-
- /**
- * Checks a caller's authorization to register an IRemoteControlDisplay.
- * Authorization is granted if one of the following is true:
- * <ul>
- * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL
- * permission</li>
- * <li>the caller's listener is one of the enabled notification listeners
- * for the caller's user</li>
- * </ul>
- */
- private void enforceMediaPermissions(ComponentName compName, int pid, int uid,
- int resolvedUserId) {
- if (isCurrentVolumeController(pid, uid)) return;
- if (getContext()
- .checkPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
- != PackageManager.PERMISSION_GRANTED
- && !isEnabledNotificationListener(compName, UserHandle.getUserId(uid),
- resolvedUserId)) {
- throw new SecurityException("Missing permission to control media.");
- }
- }
-
- private boolean isCurrentVolumeController(int pid, int uid) {
- return getContext().checkPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
- pid, uid) == PackageManager.PERMISSION_GRANTED;
- }
-
- private void enforceSystemUiPermission(String action, int pid, int uid) {
- if (!isCurrentVolumeController(pid, uid)) {
- throw new SecurityException("Only system ui may " + action);
- }
- }
-
- /**
- * This checks if the component is an enabled notification listener for the
- * specified user. Enabled components may only operate on behalf of the user
- * they're running as.
- *
- * @param compName The component that is enabled.
- * @param userId The user id of the caller.
- * @param forUserId The user id they're making the request on behalf of.
- * @return True if the component is enabled, false otherwise
- */
- private boolean isEnabledNotificationListener(ComponentName compName, int userId,
- int forUserId) {
- if (userId != forUserId) {
- // You may not access another user's content as an enabled listener.
- return false;
- }
- if (DEBUG) {
- Log.d(TAG, "Checking if enabled notification listener " + compName);
- }
- if (compName != null) {
- try {
- return mNotificationManager.isNotificationListenerAccessGrantedForUser(
- compName, userId);
- } catch(RemoteException e) {
- Log.w(TAG, "Dead NotificationManager in isEnabledNotificationListener", e);
- }
- }
- return false;
- }
-
- private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
- String callerPackageName, SessionCallbackLink cb, String tag) throws RemoteException {
- synchronized (mLock) {
- return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb, tag);
- }
- }
-
- /*
- * When a session is created the following things need to happen.
- * 1. Its callback binder needs a link to death
- * 2. It needs to be added to all sessions.
- * 3. It needs to be added to the priority stack.
- * 4. It needs to be added to the relevant user record.
- */
- private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
- String callerPackageName, SessionCallbackLink cb, String tag) {
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null) {
- Log.wtf(TAG, "Request from invalid user: " + userId);
- throw new RuntimeException("Session request from invalid user.");
- }
-
- final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
- callerPackageName, cb, tag, this, mHandler.getLooper());
- try {
- cb.getBinder().linkToDeath(session, 0);
- } catch (RemoteException e) {
- throw new RuntimeException("Media Session owner died prematurely.", e);
- }
-
- user.mPriorityStack.addSession(session);
- mHandler.postSessionsChanged(userId);
-
- if (DEBUG) {
- Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
- }
- return session;
- }
-
- private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
- for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
- if (mSessionsListeners.get(i).listener.asBinder() == listener.asBinder()) {
- return i;
- }
- }
- return -1;
- }
-
- private int findIndexOfSession2TokensListenerLocked(ISession2TokensListener listener) {
- for (int i = mSession2TokensListenerRecords.size() - 1; i >= 0; i--) {
- if (mSession2TokensListenerRecords.get(i).listener.asBinder() == listener.asBinder()) {
- return i;
- }
- }
- return -1;
- }
-
-
- private void pushSessionsChanged(int userId) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null) {
- Log.w(TAG, "pushSessionsChanged failed. No user with id=" + userId);
- return;
- }
- List<MediaSessionRecord> records = getActiveSessionsLocked(userId);
- int size = records.size();
- ArrayList<MediaSession.Token> tokens = new ArrayList<MediaSession.Token>();
- for (int i = 0; i < size; i++) {
- tokens.add(new MediaSession.Token(records.get(i).getControllerBinder()));
- }
- pushRemoteVolumeUpdateLocked(userId);
- for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
- SessionsListenerRecord record = mSessionsListeners.get(i);
- if (record.userId == USER_ALL || record.userId == userId) {
- try {
- record.listener.onActiveSessionsChanged(tokens);
- } catch (RemoteException e) {
- Log.w(TAG, "Dead ActiveSessionsListener in pushSessionsChanged, removing",
- e);
- mSessionsListeners.remove(i);
- }
- }
- }
- }
- }
-
- private void pushRemoteVolumeUpdateLocked(int userId) {
- if (mRvc != null) {
- try {
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null) {
- Log.w(TAG, "pushRemoteVolumeUpdateLocked failed. No user with id=" + userId);
- return;
- }
- MediaSessionRecord record = user.mPriorityStack.getDefaultRemoteSession(userId);
- mRvc.updateRemoteController(record == null ? null : record.getControllerBinder());
- } catch (RemoteException e) {
- Log.wtf(TAG, "Error sending default remote volume to sys ui.", e);
- }
- }
+ mImpl.destroySession(session);
}
void pushSession2TokensChangedLocked(int userId) {
- List<Session2Token> allSession2Tokens = getSession2TokensLocked(USER_ALL);
- List<Session2Token> session2Tokens = getSession2TokensLocked(userId);
-
- for (int i = mSession2TokensListenerRecords.size() - 1; i >= 0; i--) {
- Session2TokensListenerRecord listenerRecord = mSession2TokensListenerRecords.get(i);
- try {
- if (listenerRecord.userId == USER_ALL) {
- listenerRecord.listener.onSession2TokensChanged(allSession2Tokens);
- } else if (listenerRecord.userId == userId) {
- listenerRecord.listener.onSession2TokensChanged(session2Tokens);
- }
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to notify Session2Token change. Removing listener.", e);
- mSession2TokensListenerRecords.remove(i);
- }
- }
+ mImpl.pushSession2TokensChangedLocked(userId);
}
/**
- * Called when the media button receiver for the {@param record} is changed.
- *
- * @param record the media session whose media button receiver is updated.
+ * Called when media button receiver changed.
*/
public void onMediaButtonReceiverChanged(MediaSessionRecord record) {
- synchronized (mLock) {
- FullUserRecord user = getFullUserRecordLocked(record.getUserId());
- MediaSessionRecord mediaButtonSession =
- user.mPriorityStack.getMediaButtonSession();
- if (record == mediaButtonSession) {
- user.rememberMediaButtonReceiverLocked(mediaButtonSession);
- }
- }
+ mImpl.onMediaButtonReceiverChanged(record);
}
- private String getCallingPackageName(int uid) {
- String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
- if (packages != null && packages.length > 0) {
- return packages[0];
- }
- return "";
- }
-
- private void dispatchVolumeKeyLongPressLocked(KeyEvent keyEvent) {
- if (mCurrentFullUserRecord.mOnVolumeKeyLongPressListener == null) {
- return;
- }
- try {
- mCurrentFullUserRecord.mOnVolumeKeyLongPressListener.onVolumeKeyLongPress(keyEvent);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to send " + keyEvent + " to volume key long-press listener");
- }
- }
-
- private FullUserRecord getFullUserRecordLocked(int userId) {
- int fullUserId = mFullUserIds.get(userId, -1);
- if (fullUserId < 0) {
- return null;
- }
- return mUserRecords.get(fullUserId);
- }
-
- /**
- * Information about a full user and its corresponding managed profiles.
- *
- * <p>Since the full user runs together with its managed profiles, a user wouldn't differentiate
- * them when he/she presses a media/volume button. So keeping media sessions for them in one
- * place makes more sense and increases the readability.</p>
- * <p>The contents of this object is guarded by {@link #mLock}.
- */
- final class FullUserRecord implements MediaSessionStack.OnMediaButtonSessionChangedListener {
- public static final int COMPONENT_TYPE_INVALID = 0;
- public static final int COMPONENT_TYPE_BROADCAST = 1;
- public static final int COMPONENT_TYPE_ACTIVITY = 2;
- public static final int COMPONENT_TYPE_SERVICE = 3;
- private static final String COMPONENT_NAME_USER_ID_DELIM = ",";
-
- private final int mFullUserId;
- private final MediaSessionStack mPriorityStack;
- private PendingIntent mLastMediaButtonReceiver;
- private ComponentName mRestoredMediaButtonReceiver;
- private int mRestoredMediaButtonReceiverComponentType;
- private int mRestoredMediaButtonReceiverUserId;
-
- private IOnVolumeKeyLongPressListener mOnVolumeKeyLongPressListener;
- private int mOnVolumeKeyLongPressListenerUid;
- private KeyEvent mInitialDownVolumeKeyEvent;
- private int mInitialDownVolumeStream;
- private boolean mInitialDownMusicOnly;
-
- private IOnMediaKeyListener mOnMediaKeyListener;
- private int mOnMediaKeyListenerUid;
- private ICallback mCallback;
-
- public FullUserRecord(int fullUserId) {
- mFullUserId = fullUserId;
- mPriorityStack = new MediaSessionStack(mAudioPlayerStateMonitor, this);
- // Restore the remembered media button receiver before the boot.
- String mediaButtonReceiverInfo = Settings.Secure.getStringForUser(mContentResolver,
- Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId);
- if (mediaButtonReceiverInfo == null) {
- return;
- }
- String[] tokens = mediaButtonReceiverInfo.split(COMPONENT_NAME_USER_ID_DELIM);
- if (tokens == null || (tokens.length != 2 && tokens.length != 3)) {
- return;
- }
- mRestoredMediaButtonReceiver = ComponentName.unflattenFromString(tokens[0]);
- mRestoredMediaButtonReceiverUserId = Integer.parseInt(tokens[1]);
- if (tokens.length == 3) {
- mRestoredMediaButtonReceiverComponentType = Integer.parseInt(tokens[2]);
- } else {
- mRestoredMediaButtonReceiverComponentType =
- getComponentType(mRestoredMediaButtonReceiver);
- }
- }
-
- public void destroySessionsForUserLocked(int userId) {
- List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, userId);
- for (MediaSessionRecord session : sessions) {
- MediaSessionService.this.destroySessionLocked(session);
- }
- }
-
- public void dumpLocked(PrintWriter pw, String prefix) {
- pw.print(prefix + "Record for full_user=" + mFullUserId);
- // Dump managed profile user ids associated with this user.
- int size = mFullUserIds.size();
- for (int i = 0; i < size; i++) {
- if (mFullUserIds.keyAt(i) != mFullUserIds.valueAt(i)
- && mFullUserIds.valueAt(i) == mFullUserId) {
- pw.print(", profile_user=" + mFullUserIds.keyAt(i));
- }
- }
- pw.println();
- String indent = prefix + " ";
- pw.println(indent + "Volume key long-press listener: " + mOnVolumeKeyLongPressListener);
- pw.println(indent + "Volume key long-press listener package: " +
- getCallingPackageName(mOnVolumeKeyLongPressListenerUid));
- pw.println(indent + "Media key listener: " + mOnMediaKeyListener);
- pw.println(indent + "Media key listener package: " +
- getCallingPackageName(mOnMediaKeyListenerUid));
- pw.println(indent + "Callback: " + mCallback);
- pw.println(indent + "Last MediaButtonReceiver: " + mLastMediaButtonReceiver);
- pw.println(indent + "Restored MediaButtonReceiver: " + mRestoredMediaButtonReceiver);
- pw.println(indent + "Restored MediaButtonReceiverComponentType: "
- + mRestoredMediaButtonReceiverComponentType);
- mPriorityStack.dump(pw, indent);
- pw.println(indent + "Session2Tokens:");
- for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
- List<Session2Token> list = mSession2TokensPerUser.valueAt(i);
- if (list == null || list.size() == 0) {
- continue;
- }
- for (Session2Token token : list) {
- pw.println(indent + " " + token);
- }
- }
- }
-
- @Override
- public void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
- MediaSessionRecord newMediaButtonSession) {
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Media button session is changed to " + newMediaButtonSession);
- }
- synchronized (mLock) {
- if (oldMediaButtonSession != null) {
- mHandler.postSessionsChanged(oldMediaButtonSession.getUserId());
- }
- if (newMediaButtonSession != null) {
- rememberMediaButtonReceiverLocked(newMediaButtonSession);
- mHandler.postSessionsChanged(newMediaButtonSession.getUserId());
- }
- pushAddressedPlayerChangedLocked();
- }
- }
-
- // Remember media button receiver and keep it in the persistent storage.
- public void rememberMediaButtonReceiverLocked(MediaSessionRecord record) {
- PendingIntent receiver = record.getMediaButtonReceiver();
- mLastMediaButtonReceiver = receiver;
- mRestoredMediaButtonReceiver = null;
- mRestoredMediaButtonReceiverComponentType = COMPONENT_TYPE_INVALID;
-
- String mediaButtonReceiverInfo = "";
- if (receiver != null) {
- ComponentName component = receiver.getIntent().getComponent();
- if (component != null
- && record.getPackageName().equals(component.getPackageName())) {
- String componentName = component.flattenToString();
- int componentType = getComponentType(component);
- mediaButtonReceiverInfo = String.join(COMPONENT_NAME_USER_ID_DELIM,
- componentName, String.valueOf(record.getUserId()),
- String.valueOf(componentType));
- }
- }
- Settings.Secure.putStringForUser(mContentResolver,
- Settings.System.MEDIA_BUTTON_RECEIVER, mediaButtonReceiverInfo,
- mFullUserId);
- }
-
- private void pushAddressedPlayerChangedLocked() {
- if (mCallback == null) {
- return;
- }
- try {
- MediaSessionRecord mediaButtonSession = getMediaButtonSessionLocked();
- if (mediaButtonSession != null) {
- mCallback.onAddressedPlayerChangedToMediaSession(
- new MediaSession.Token(mediaButtonSession.getControllerBinder()));
- } else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) {
- mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
- mCurrentFullUserRecord.mLastMediaButtonReceiver
- .getIntent().getComponent());
- } else if (mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
- mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
- mCurrentFullUserRecord.mRestoredMediaButtonReceiver);
- }
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to pushAddressedPlayerChangedLocked", e);
- }
- }
-
- private MediaSessionRecord getMediaButtonSessionLocked() {
- return isGlobalPriorityActiveLocked()
- ? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession();
- }
-
- private int getComponentType(@Nullable ComponentName componentName) {
- if (componentName == null) {
- return COMPONENT_TYPE_INVALID;
- }
- PackageManager pm = getContext().getPackageManager();
- try {
- ActivityInfo activityInfo = pm.getActivityInfo(componentName,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.GET_ACTIVITIES);
- if (activityInfo != null) {
- return COMPONENT_TYPE_ACTIVITY;
- }
- } catch (NameNotFoundException e) {
- }
- try {
- ServiceInfo serviceInfo = pm.getServiceInfo(componentName,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.GET_SERVICES);
- if (serviceInfo != null) {
- return COMPONENT_TYPE_SERVICE;
- }
- } catch (NameNotFoundException e) {
- }
- // Pick legacy behavior for BroadcastReceiver or unknown.
- return COMPONENT_TYPE_BROADCAST;
- }
- }
-
- final class SessionsListenerRecord implements IBinder.DeathRecipient {
- public final IActiveSessionsListener listener;
- public final ComponentName componentName;
- public final int userId;
- public final int pid;
- public final int uid;
-
- public SessionsListenerRecord(IActiveSessionsListener listener,
- ComponentName componentName,
- int userId, int pid, int uid) {
- this.listener = listener;
- this.componentName = componentName;
- this.userId = userId;
- this.pid = pid;
- this.uid = uid;
- }
-
- @Override
- public void binderDied() {
- synchronized (mLock) {
- mSessionsListeners.remove(this);
- }
- }
- }
-
- final class Session2TokensListenerRecord implements IBinder.DeathRecipient {
- public final ISession2TokensListener listener;
- public final int userId;
-
- Session2TokensListenerRecord(ISession2TokensListener listener,
- int userId) {
- this.listener = listener;
- this.userId = userId;
- }
-
- @Override
- public void binderDied() {
- synchronized (mLock) {
- mSession2TokensListenerRecords.remove(this);
- }
- }
- }
-
- final class SettingsObserver extends ContentObserver {
- private final Uri mSecureSettingsUri = Settings.Secure.getUriFor(
- Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
-
- private SettingsObserver() {
- super(null);
- }
-
- private void observe() {
- mContentResolver.registerContentObserver(mSecureSettingsUri,
- false, this, USER_ALL);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- updateActiveSessionListeners();
- }
- }
-
- class SessionManagerImpl extends ISessionManager.Stub {
- private static final String EXTRA_WAKELOCK_ACQUIRED =
- "android.media.AudioService.WAKELOCK_ACQUIRED";
- private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; // magic number
-
- private boolean mVoiceButtonDown = false;
- private boolean mVoiceButtonHandled = false;
-
- @Override
- public ISession createSession(String packageName, SessionCallbackLink cb, String tag,
- int userId) throws RemoteException {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- enforcePackageName(packageName, uid);
- int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
- false /* allowAll */, true /* requireFull */, "createSession", packageName);
- if (cb == null) {
- throw new IllegalArgumentException("Controller callback cannot be null");
- }
- return createSessionInternal(pid, uid, resolvedUserId, packageName, cb, tag)
- .getSessionBinder();
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void notifySession2Created(Session2Token sessionToken) throws RemoteException {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- if (DEBUG) {
- Log.d(TAG, "Session2 is created " + sessionToken);
- }
- if (uid != sessionToken.getUid()) {
- throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
- + " but actually=" + sessionToken.getUid());
- }
- Controller2Callback callback = new Controller2Callback(sessionToken);
- // Note: It's safe not to keep controller here because it wouldn't be GC'ed until
- // it's closed.
- // TODO: Keep controller as well for better readability
- // because the GC behavior isn't straightforward.
- MediaController2 controller = new MediaController2(getContext(), sessionToken,
- new HandlerExecutor(mHandler), callback);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public List<IBinder> getSessions(ComponentName componentName, int userId) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- try {
- int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
- ArrayList<IBinder> binders = new ArrayList<IBinder>();
- synchronized (mLock) {
- List<MediaSessionRecord> records = getActiveSessionsLocked(resolvedUserId);
- for (MediaSessionRecord record : records) {
- binders.add(record.getControllerBinder().asBinder());
- }
- }
- return binders;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public List<Session2Token> getSession2Tokens(int userId) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- try {
- // Check that they can make calls on behalf of the user and
- // get the final user id
- int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
- true /* allowAll */, true /* requireFull */, "getSession2Tokens",
- null /* optional packageName */);
- List<Session2Token> result;
- synchronized (mLock) {
- result = getSession2TokensLocked(resolvedUserId);
- }
- return result;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void addSessionsListener(IActiveSessionsListener listener,
- ComponentName componentName, int userId) throws RemoteException {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- try {
- int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
- synchronized (mLock) {
- int index = findIndexOfSessionsListenerLocked(listener);
- if (index != -1) {
- Log.w(TAG, "ActiveSessionsListener is already added, ignoring");
- return;
- }
- SessionsListenerRecord record = new SessionsListenerRecord(listener,
- componentName, resolvedUserId, pid, uid);
- try {
- listener.asBinder().linkToDeath(record, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "ActiveSessionsListener is dead, ignoring it", e);
- return;
- }
- mSessionsListeners.add(record);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void removeSessionsListener(IActiveSessionsListener listener)
- throws RemoteException {
- synchronized (mLock) {
- int index = findIndexOfSessionsListenerLocked(listener);
- if (index != -1) {
- SessionsListenerRecord record = mSessionsListeners.remove(index);
- try {
- record.listener.asBinder().unlinkToDeath(record, 0);
- } catch (Exception e) {
- // ignore exceptions, the record is being removed
- }
- }
- }
- }
-
- @Override
- public void addSession2TokensListener(ISession2TokensListener listener,
- int userId) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- try {
- // Check that they can make calls on behalf of the user and get the final user id.
- int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
- true /* allowAll */, true /* requireFull */, "addSession2TokensListener",
- null /* optional packageName */);
- synchronized (mLock) {
- int index = findIndexOfSession2TokensListenerLocked(listener);
- if (index >= 0) {
- Log.w(TAG, "addSession2TokensListener is already added, ignoring");
- return;
- }
- mSession2TokensListenerRecords.add(
- new Session2TokensListenerRecord(listener, resolvedUserId));
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void removeSession2TokensListener(ISession2TokensListener listener) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- try {
- synchronized (mLock) {
- int index = findIndexOfSession2TokensListenerLocked(listener);
- if (index >= 0) {
- Session2TokensListenerRecord listenerRecord =
- mSession2TokensListenerRecords.remove(index);
- try {
- listenerRecord.listener.asBinder().unlinkToDeath(listenerRecord, 0);
- } catch (Exception e) {
- // Ignore exception.
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- /**
- * Handles the dispatching of the media button events to one of the
- * registered listeners, or if there was none, broadcast an
- * ACTION_MEDIA_BUTTON intent to the rest of the system.
- *
- * @param packageName The caller package
- * @param asSystemService {@code true} if the event sent to the session as if it was come
- * from the system service instead of the app process. This helps sessions to
- * distinguish between the key injection by the app and key events from the
- * hardware devices. Should be used only when the volume key events aren't handled
- * by foreground activity. {@code false} otherwise to tell session about the real
- * caller.
- * @param keyEvent a non-null KeyEvent whose key code is one of the
- * supported media buttons
- * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held
- * while this key event is dispatched.
- */
- @Override
- public void dispatchMediaKeyEvent(String packageName, boolean asSystemService,
- KeyEvent keyEvent, boolean needWakeLock) {
- if (keyEvent == null || !KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) {
- Log.w(TAG, "Attempted to dispatch null or non-media key event.");
- return;
- }
-
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- if (DEBUG) {
- Log.d(TAG, "dispatchMediaKeyEvent, pkg=" + packageName + " pid=" + pid
- + ", uid=" + uid + ", asSystem=" + asSystemService + ", event="
- + keyEvent);
- }
- if (!isUserSetupComplete()) {
- // Global media key handling can have the side-effect of starting new
- // activities which is undesirable while setup is in progress.
- Slog.i(TAG, "Not dispatching media key event because user "
- + "setup is in progress.");
- return;
- }
-
- synchronized (mLock) {
- boolean isGlobalPriorityActive = isGlobalPriorityActiveLocked();
- if (isGlobalPriorityActive && uid != Process.SYSTEM_UID) {
- // Prevent dispatching key event through reflection while the global
- // priority session is active.
- Slog.i(TAG, "Only the system can dispatch media key event "
- + "to the global priority session.");
- return;
- }
- if (!isGlobalPriorityActive) {
- if (mCurrentFullUserRecord.mOnMediaKeyListener != null) {
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Send " + keyEvent + " to the media key listener");
- }
- try {
- mCurrentFullUserRecord.mOnMediaKeyListener.onMediaKey(keyEvent,
- new MediaKeyListenerResultReceiver(packageName, pid, uid,
- asSystemService, keyEvent, needWakeLock));
- return;
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to send " + keyEvent
- + " to the media key listener");
- }
- }
- }
- if (!isGlobalPriorityActive && isVoiceKey(keyEvent.getKeyCode())) {
- handleVoiceKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent,
- needWakeLock);
- } else {
- dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
- keyEvent, needWakeLock);
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setCallback(ICallback callback) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- if (!UserHandle.isSameApp(uid, Process.BLUETOOTH_UID)) {
- throw new SecurityException("Only Bluetooth service processes can set"
- + " Callback");
- }
- synchronized (mLock) {
- int userId = UserHandle.getUserId(uid);
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null || user.mFullUserId != userId) {
- Log.w(TAG, "Only the full user can set the callback"
- + ", userId=" + userId);
- return;
- }
- user.mCallback = callback;
- Log.d(TAG, "The callback " + user.mCallback
- + " is set by " + getCallingPackageName(uid));
- if (user.mCallback == null) {
- return;
- }
- try {
- user.mCallback.asBinder().linkToDeath(
- new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- synchronized (mLock) {
- user.mCallback = null;
- }
- }
- }, 0);
- user.pushAddressedPlayerChangedLocked();
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to set callback", e);
- user.mCallback = null;
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setOnVolumeKeyLongPressListener(IOnVolumeKeyLongPressListener listener) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- // Enforce SET_VOLUME_KEY_LONG_PRESS_LISTENER permission.
- if (getContext().checkPermission(
- android.Manifest.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER, pid, uid)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException("Must hold the SET_VOLUME_KEY_LONG_PRESS_LISTENER" +
- " permission.");
- }
-
- synchronized (mLock) {
- int userId = UserHandle.getUserId(uid);
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null || user.mFullUserId != userId) {
- Log.w(TAG, "Only the full user can set the volume key long-press listener"
- + ", userId=" + userId);
- return;
- }
- if (user.mOnVolumeKeyLongPressListener != null &&
- user.mOnVolumeKeyLongPressListenerUid != uid) {
- Log.w(TAG, "The volume key long-press listener cannot be reset"
- + " by another app , mOnVolumeKeyLongPressListener="
- + user.mOnVolumeKeyLongPressListenerUid
- + ", uid=" + uid);
- return;
- }
-
- user.mOnVolumeKeyLongPressListener = listener;
- user.mOnVolumeKeyLongPressListenerUid = uid;
-
- Log.d(TAG, "The volume key long-press listener "
- + listener + " is set by " + getCallingPackageName(uid));
-
- if (user.mOnVolumeKeyLongPressListener != null) {
- try {
- user.mOnVolumeKeyLongPressListener.asBinder().linkToDeath(
- new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- synchronized (mLock) {
- user.mOnVolumeKeyLongPressListener = null;
- }
- }
- }, 0);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to set death recipient "
- + user.mOnVolumeKeyLongPressListener);
- user.mOnVolumeKeyLongPressListener = null;
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setOnMediaKeyListener(IOnMediaKeyListener listener) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- // Enforce SET_MEDIA_KEY_LISTENER permission.
- if (getContext().checkPermission(
- android.Manifest.permission.SET_MEDIA_KEY_LISTENER, pid, uid)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException("Must hold the SET_MEDIA_KEY_LISTENER" +
- " permission.");
- }
-
- synchronized (mLock) {
- int userId = UserHandle.getUserId(uid);
- FullUserRecord user = getFullUserRecordLocked(userId);
- if (user == null || user.mFullUserId != userId) {
- Log.w(TAG, "Only the full user can set the media key listener"
- + ", userId=" + userId);
- return;
- }
- if (user.mOnMediaKeyListener != null && user.mOnMediaKeyListenerUid != uid) {
- Log.w(TAG, "The media key listener cannot be reset by another app. "
- + ", mOnMediaKeyListenerUid=" + user.mOnMediaKeyListenerUid
- + ", uid=" + uid);
- return;
- }
-
- user.mOnMediaKeyListener = listener;
- user.mOnMediaKeyListenerUid = uid;
-
- Log.d(TAG, "The media key listener " + user.mOnMediaKeyListener
- + " is set by " + getCallingPackageName(uid));
-
- if (user.mOnMediaKeyListener != null) {
- try {
- user.mOnMediaKeyListener.asBinder().linkToDeath(
- new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- synchronized (mLock) {
- user.mOnMediaKeyListener = null;
- }
- }
- }, 0);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to set death recipient " + user.mOnMediaKeyListener);
- user.mOnMediaKeyListener = null;
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- /**
- * Handles the dispatching of the volume button events to one of the
- * registered listeners. If there's a volume key long-press listener and
- * there's no active global priority session, long-pressess will be sent to the
- * long-press listener instead of adjusting volume.
- *
- * @param packageName The caller's package name, obtained by Context#getPackageName()
- * @param opPackageName The caller's op package name, obtained by Context#getOpPackageName()
- * @param asSystemService {@code true} if the event sent to the session as if it was come
- * from the system service instead of the app process. This helps sessions to
- * distinguish between the key injection by the app and key events from the
- * hardware devices. Should be used only when the volume key events aren't handled
- * by foreground activity. {@code false} otherwise to tell session about the real
- * caller.
- * @param keyEvent a non-null KeyEvent whose key code is one of the
- * {@link KeyEvent#KEYCODE_VOLUME_UP},
- * {@link KeyEvent#KEYCODE_VOLUME_DOWN},
- * or {@link KeyEvent#KEYCODE_VOLUME_MUTE}.
- * @param stream stream type to adjust volume.
- * @param musicOnly true if both UI nor haptic feedback aren't needed when adjust volume.
- */
- @Override
- public void dispatchVolumeKeyEvent(String packageName, String opPackageName,
- boolean asSystemService, KeyEvent keyEvent, int stream, boolean musicOnly) {
- if (keyEvent == null ||
- (keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP
- && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN
- && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_MUTE)) {
- Log.w(TAG, "Attempted to dispatch null or non-volume key event.");
- return;
- }
-
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
-
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "dispatchVolumeKeyEvent, pkg=" + packageName + ", pid=" + pid + ", uid="
- + uid + ", asSystem=" + asSystemService + ", event=" + keyEvent);
- }
-
- try {
- synchronized (mLock) {
- if (isGlobalPriorityActiveLocked()
- || mCurrentFullUserRecord.mOnVolumeKeyLongPressListener == null) {
- dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
- asSystemService, keyEvent, stream, musicOnly);
- } else {
- // TODO: Consider the case when both volume up and down keys are pressed
- // at the same time.
- if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
- if (keyEvent.getRepeatCount() == 0) {
- // Keeps the copy of the KeyEvent because it can be reused.
- mCurrentFullUserRecord.mInitialDownVolumeKeyEvent =
- KeyEvent.obtain(keyEvent);
- mCurrentFullUserRecord.mInitialDownVolumeStream = stream;
- mCurrentFullUserRecord.mInitialDownMusicOnly = musicOnly;
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MessageHandler.MSG_VOLUME_INITIAL_DOWN,
- mCurrentFullUserRecord.mFullUserId, 0),
- mLongPressTimeout);
- }
- if (keyEvent.getRepeatCount() > 0 || keyEvent.isLongPress()) {
- mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
- if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null) {
- dispatchVolumeKeyLongPressLocked(
- mCurrentFullUserRecord.mInitialDownVolumeKeyEvent);
- // Mark that the key is already handled.
- mCurrentFullUserRecord.mInitialDownVolumeKeyEvent = null;
- }
- dispatchVolumeKeyLongPressLocked(keyEvent);
- }
- } else { // if up
- mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
- if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null
- && mCurrentFullUserRecord.mInitialDownVolumeKeyEvent
- .getDownTime() == keyEvent.getDownTime()) {
- // Short-press. Should change volume.
- dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
- asSystemService,
- mCurrentFullUserRecord.mInitialDownVolumeKeyEvent,
- mCurrentFullUserRecord.mInitialDownVolumeStream,
- mCurrentFullUserRecord.mInitialDownMusicOnly);
- dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
- asSystemService, keyEvent, stream, musicOnly);
- } else {
- dispatchVolumeKeyLongPressLocked(keyEvent);
- }
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- private void dispatchVolumeKeyEventLocked(String packageName, String opPackageName, int pid,
- int uid, boolean asSystemService, KeyEvent keyEvent, int stream,
- boolean musicOnly) {
- boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
- boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
- int direction = 0;
- boolean isMute = false;
- switch (keyEvent.getKeyCode()) {
- case KeyEvent.KEYCODE_VOLUME_UP:
- direction = AudioManager.ADJUST_RAISE;
- break;
- case KeyEvent.KEYCODE_VOLUME_DOWN:
- direction = AudioManager.ADJUST_LOWER;
- break;
- case KeyEvent.KEYCODE_VOLUME_MUTE:
- isMute = true;
- break;
- }
- if (down || up) {
- int flags = AudioManager.FLAG_FROM_KEY;
- if (musicOnly) {
- // This flag is used when the screen is off to only affect active media.
- flags |= AudioManager.FLAG_ACTIVE_MEDIA_ONLY;
- } else {
- // These flags are consistent with the home screen
- if (up) {
- flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
- } else {
- flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
- }
- }
- if (direction != 0) {
- // If this is action up we want to send a beep for non-music events
- if (up) {
- direction = 0;
- }
- dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid,
- asSystemService, stream, direction, flags);
- } else if (isMute) {
- if (down && keyEvent.getRepeatCount() == 0) {
- dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid,
- asSystemService, stream, AudioManager.ADJUST_TOGGLE_MUTE, flags);
- }
- }
- }
- }
-
- @Override
- public void dispatchAdjustVolume(String packageName, String opPackageName,
- int suggestedStream, int delta, int flags) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid, false,
- suggestedStream, delta, flags);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public void setRemoteVolumeController(IRemoteVolumeController rvc) {
- final int pid = Binder.getCallingPid();
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- enforceSystemUiPermission("listen for volume changes", pid, uid);
- mRvc = rvc;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override
- public boolean isGlobalPriorityActive() {
- synchronized (mLock) {
- return isGlobalPriorityActiveLocked();
- }
- }
-
- @Override
- public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
- if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
-
- pw.println("MEDIA SESSION SERVICE (dumpsys media_session)");
- pw.println();
-
- synchronized (mLock) {
- pw.println(mSessionsListeners.size() + " sessions listeners.");
- pw.println("Global priority session is " + mGlobalPrioritySession);
- if (mGlobalPrioritySession != null) {
- mGlobalPrioritySession.dump(pw, " ");
- }
- pw.println("User Records:");
- int count = mUserRecords.size();
- for (int i = 0; i < count; i++) {
- mUserRecords.valueAt(i).dumpLocked(pw, "");
- }
- mAudioPlayerStateMonitor.dump(getContext(), pw, "");
- }
- }
-
- /**
- * Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL
- * permission or an enabled notification listener)
- *
- * @param controllerPackageName package name of the controller app
- * @param controllerPid pid of the controller app
- * @param controllerUid uid of the controller app
- */
- @Override
- public boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid)
- throws RemoteException {
- final int uid = Binder.getCallingUid();
- final long token = Binder.clearCallingIdentity();
- try {
- // Don't perform sanity check between controllerPackageName and controllerUid.
- // When an (activity|service) runs on the another apps process by specifying
- // android:process in the AndroidManifest.xml, then PID and UID would have the
- // running process' information instead of the (activity|service) that has created
- // MediaController.
- // Note that we can use Context#getOpPackageName() instead of
- // Context#getPackageName() for getting package name that matches with the PID/UID,
- // but it doesn't tell which package has created the MediaController, so useless.
- return hasMediaControlPermission(UserHandle.getUserId(uid), controllerPackageName,
- controllerPid, controllerUid);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- // For MediaSession
- private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
- final int uid) {
- String packageName = null;
- if (componentName != null) {
- // If they gave us a component name verify they own the
- // package
- packageName = componentName.getPackageName();
- enforcePackageName(packageName, uid);
- }
- // Check that they can make calls on behalf of the user and
- // get the final user id
- int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
- true /* allowAll */, true /* requireFull */, "getSessions", packageName);
- // Check if they have the permissions or their component is
- // enabled for the user they're calling from.
- enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
- return resolvedUserId;
- }
-
- private boolean hasMediaControlPermission(int resolvedUserId, String packageName,
- int pid, int uid) throws RemoteException {
- // Allow API calls from the System UI
- if (isCurrentVolumeController(pid, uid)) {
- return true;
- }
-
- // Check if it's system server or has MEDIA_CONTENT_CONTROL.
- // Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra
- // check here.
- if (uid == Process.SYSTEM_UID || getContext().checkPermission(
- android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
- == PackageManager.PERMISSION_GRANTED) {
- return true;
- } else if (DEBUG) {
- Log.d(TAG, packageName + " (uid=" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
- }
-
- // You may not access another user's content as an enabled listener.
- final int userId = UserHandle.getUserId(uid);
- if (resolvedUserId != userId) {
- return false;
- }
-
- // TODO(jaewan): (Post-P) Propose NotificationManager#hasEnabledNotificationListener(
- // String pkgName) to notification team for optimization
- final List<ComponentName> enabledNotificationListeners =
- mNotificationManager.getEnabledNotificationListeners(userId);
- if (enabledNotificationListeners != null) {
- for (int i = 0; i < enabledNotificationListeners.size(); i++) {
- if (TextUtils.equals(packageName,
- enabledNotificationListeners.get(i).getPackageName())) {
- return true;
- }
- }
- }
- if (DEBUG) {
- Log.d(TAG, packageName + " (uid=" + uid + ") doesn't have an enabled "
- + "notification listener");
- }
- return false;
- }
-
- private void dispatchAdjustVolumeLocked(String packageName, String opPackageName, int pid,
- int uid, boolean asSystemService, int suggestedStream, int direction, int flags) {
- MediaSessionRecord session = isGlobalPriorityActiveLocked() ? mGlobalPrioritySession
- : mCurrentFullUserRecord.mPriorityStack.getDefaultVolumeSession();
-
- boolean preferSuggestedStream = false;
- if (isValidLocalStreamType(suggestedStream)
- && AudioSystem.isStreamActive(suggestedStream, 0)) {
- preferSuggestedStream = true;
- }
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Adjusting " + session + " by " + direction + ". flags="
- + flags + ", suggestedStream=" + suggestedStream
- + ", preferSuggestedStream=" + preferSuggestedStream);
- }
- if (session == null || preferSuggestedStream) {
- if ((flags & AudioManager.FLAG_ACTIVE_MEDIA_ONLY) != 0
- && !AudioSystem.isStreamActive(AudioManager.STREAM_MUSIC, 0)) {
- if (DEBUG) {
- Log.d(TAG, "No active session to adjust, skipping media only volume event");
- }
- return;
- }
-
- // Execute mAudioService.adjustSuggestedStreamVolume() on
- // handler thread of MediaSessionService.
- // This will release the MediaSessionService.mLock sooner and avoid
- // a potential deadlock between MediaSessionService.mLock and
- // ActivityManagerService lock.
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- final String callingOpPackageName;
- final int callingUid;
- if (asSystemService) {
- callingOpPackageName = getContext().getOpPackageName();
- callingUid = Process.myUid();
- } else {
- callingOpPackageName = opPackageName;
- callingUid = uid;
- }
- try {
- mAudioManagerInternal.adjustSuggestedStreamVolumeForUid(suggestedStream,
- direction, flags, callingOpPackageName, callingUid);
- } catch (SecurityException | IllegalArgumentException e) {
- Log.e(TAG, "Cannot adjust volume: direction=" + direction
- + ", suggestedStream=" + suggestedStream + ", flags=" + flags
- + ", packageName=" + packageName + ", uid=" + uid
- + ", asSystemService=" + asSystemService, e);
- }
- }
- });
- } else {
- session.adjustVolume(packageName, opPackageName, pid, uid, null, asSystemService,
- direction, flags, true);
- }
- }
-
- private void handleVoiceKeyEventLocked(String packageName, int pid, int uid,
- boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
- int action = keyEvent.getAction();
- boolean isLongPress = (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
- if (action == KeyEvent.ACTION_DOWN) {
- if (keyEvent.getRepeatCount() == 0) {
- mVoiceButtonDown = true;
- mVoiceButtonHandled = false;
- } else if (mVoiceButtonDown && !mVoiceButtonHandled && isLongPress) {
- mVoiceButtonHandled = true;
- startVoiceInput(needWakeLock);
- }
- } else if (action == KeyEvent.ACTION_UP) {
- if (mVoiceButtonDown) {
- mVoiceButtonDown = false;
- if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
- // Resend the down then send this event through
- KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
- dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
- downEvent, needWakeLock);
- dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
- keyEvent, needWakeLock);
- }
- }
- }
- }
-
- private void dispatchMediaKeyEventLocked(String packageName, int pid, int uid,
- boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
- MediaSessionRecord session = mCurrentFullUserRecord.getMediaButtonSessionLocked();
- if (session != null) {
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Sending " + keyEvent + " to " + session);
- }
- if (needWakeLock) {
- mKeyEventReceiver.aquireWakeLockLocked();
- }
- // If we don't need a wakelock use -1 as the id so we won't release it later.
- session.sendMediaButton(packageName, pid, uid, asSystemService, keyEvent,
- needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
- mKeyEventReceiver);
- if (mCurrentFullUserRecord.mCallback != null) {
- try {
- mCurrentFullUserRecord.mCallback.onMediaKeyEventDispatchedToMediaSession(
- keyEvent, new MediaSession.Token(session.getControllerBinder()));
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to send callback", e);
- }
- }
- } else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null
- || mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
- if (needWakeLock) {
- mKeyEventReceiver.aquireWakeLockLocked();
- }
- Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- mediaButtonIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
- // TODO: Find a way to also send PID/UID in secure way.
- String callerPackageName =
- (asSystemService) ? getContext().getPackageName() : packageName;
- mediaButtonIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, callerPackageName);
- try {
- if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) {
- PendingIntent receiver = mCurrentFullUserRecord.mLastMediaButtonReceiver;
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Sending " + keyEvent
- + " to the last known PendingIntent " + receiver);
- }
- receiver.send(getContext(),
- needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
- mediaButtonIntent, mKeyEventReceiver, mHandler);
- if (mCurrentFullUserRecord.mCallback != null) {
- ComponentName componentName = mCurrentFullUserRecord
- .mLastMediaButtonReceiver.getIntent().getComponent();
- if (componentName != null) {
- mCurrentFullUserRecord.mCallback
- .onMediaKeyEventDispatchedToMediaButtonReceiver(
- keyEvent, componentName);
- }
- }
- } else {
- ComponentName receiver =
- mCurrentFullUserRecord.mRestoredMediaButtonReceiver;
- int componentType = mCurrentFullUserRecord
- .mRestoredMediaButtonReceiverComponentType;
- UserHandle userHandle = UserHandle.of(mCurrentFullUserRecord
- .mRestoredMediaButtonReceiverUserId);
- if (DEBUG_KEY_EVENT) {
- Log.d(TAG, "Sending " + keyEvent + " to the restored intent "
- + receiver + ", type=" + componentType);
- }
- mediaButtonIntent.setComponent(receiver);
- try {
- switch (componentType) {
- case FullUserRecord.COMPONENT_TYPE_ACTIVITY:
- getContext().startActivityAsUser(mediaButtonIntent, userHandle);
- break;
- case FullUserRecord.COMPONENT_TYPE_SERVICE:
- getContext().startForegroundServiceAsUser(mediaButtonIntent,
- userHandle);
- break;
- default:
- // Legacy behavior for other cases.
- getContext().sendBroadcastAsUser(mediaButtonIntent, userHandle);
- }
- } catch (Exception e) {
- Log.w(TAG, "Error sending media button to the restored intent "
- + receiver + ", type=" + componentType, e);
- }
- if (mCurrentFullUserRecord.mCallback != null) {
- mCurrentFullUserRecord.mCallback
- .onMediaKeyEventDispatchedToMediaButtonReceiver(
- keyEvent, receiver);
- }
- }
- } catch (CanceledException e) {
- Log.i(TAG, "Error sending key event to media button receiver "
- + mCurrentFullUserRecord.mLastMediaButtonReceiver, e);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to send callback", e);
- }
- }
- }
-
- private void startVoiceInput(boolean needWakeLock) {
- Intent voiceIntent = null;
- // select which type of search to launch:
- // - screen on and device unlocked: action is ACTION_WEB_SEARCH
- // - device locked or screen off: action is
- // ACTION_VOICE_SEARCH_HANDS_FREE
- // with EXTRA_SECURE set to true if the device is securely locked
- PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
- boolean isLocked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
- if (!isLocked && pm.isScreenOn()) {
- voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
- Log.i(TAG, "voice-based interactions: about to use ACTION_WEB_SEARCH");
- } else {
- voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
- voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE,
- isLocked && mKeyguardManager.isKeyguardSecure());
- Log.i(TAG, "voice-based interactions: about to use ACTION_VOICE_SEARCH_HANDS_FREE");
- }
- // start the search activity
- if (needWakeLock) {
- mMediaEventWakeLock.acquire();
- }
- try {
- if (voiceIntent != null) {
- voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- if (DEBUG) Log.d(TAG, "voiceIntent: " + voiceIntent);
- getContext().startActivityAsUser(voiceIntent, UserHandle.CURRENT);
- }
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, "No activity for search: " + e);
- } finally {
- if (needWakeLock) {
- mMediaEventWakeLock.release();
- }
- }
- }
-
- private boolean isVoiceKey(int keyCode) {
- return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
- || (!mHasFeatureLeanback && keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
- }
-
- private boolean isUserSetupComplete() {
- return Settings.Secure.getIntForUser(getContext().getContentResolver(),
- Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
- }
-
- // we only handle public stream types, which are 0-5
- private boolean isValidLocalStreamType(int streamType) {
- return streamType >= AudioManager.STREAM_VOICE_CALL
- && streamType <= AudioManager.STREAM_NOTIFICATION;
- }
-
- private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable {
- private final String mPackageName;
- private final int mPid;
- private final int mUid;
- private final boolean mAsSystemService;
- private final KeyEvent mKeyEvent;
- private final boolean mNeedWakeLock;
- private boolean mHandled;
-
- private MediaKeyListenerResultReceiver(String packageName, int pid, int uid,
- boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
- super(mHandler);
- mHandler.postDelayed(this, MEDIA_KEY_LISTENER_TIMEOUT);
- mPackageName = packageName;
- mPid = pid;
- mUid = uid;
- mAsSystemService = asSystemService;
- mKeyEvent = keyEvent;
- mNeedWakeLock = needWakeLock;
- }
-
- @Override
- public void run() {
- Log.d(TAG, "The media key listener is timed-out for " + mKeyEvent);
- dispatchMediaKeyEvent();
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultCode == MediaSessionManager.RESULT_MEDIA_KEY_HANDLED) {
- mHandled = true;
- mHandler.removeCallbacks(this);
- return;
- }
- dispatchMediaKeyEvent();
- }
-
- private void dispatchMediaKeyEvent() {
- if (mHandled) {
- return;
- }
- mHandled = true;
- mHandler.removeCallbacks(this);
- synchronized (mLock) {
- if (!isGlobalPriorityActiveLocked()
- && isVoiceKey(mKeyEvent.getKeyCode())) {
- handleVoiceKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
- mKeyEvent, mNeedWakeLock);
- } else {
- dispatchMediaKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
- mKeyEvent, mNeedWakeLock);
- }
- }
- }
- }
-
- private KeyEventWakeLockReceiver mKeyEventReceiver = new KeyEventWakeLockReceiver(mHandler);
-
- class KeyEventWakeLockReceiver extends ResultReceiver implements Runnable,
- PendingIntent.OnFinished {
- private final Handler mHandler;
- private int mRefCount = 0;
- private int mLastTimeoutId = 0;
-
- public KeyEventWakeLockReceiver(Handler handler) {
- super(handler);
- mHandler = handler;
- }
-
- public void onTimeout() {
- synchronized (mLock) {
- if (mRefCount == 0) {
- // We've already released it, so just return
- return;
- }
- mLastTimeoutId++;
- mRefCount = 0;
- releaseWakeLockLocked();
- }
- }
-
- public void aquireWakeLockLocked() {
- if (mRefCount == 0) {
- mMediaEventWakeLock.acquire();
- }
- mRefCount++;
- mHandler.removeCallbacks(this);
- mHandler.postDelayed(this, WAKELOCK_TIMEOUT);
-
- }
-
- @Override
- public void run() {
- onTimeout();
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultCode < mLastTimeoutId) {
- // Ignore results from calls that were before the last
- // timeout, just in case.
- return;
- } else {
- synchronized (mLock) {
- if (mRefCount > 0) {
- mRefCount--;
- if (mRefCount == 0) {
- releaseWakeLockLocked();
- }
- }
- }
- }
- }
-
- private void releaseWakeLockLocked() {
- mMediaEventWakeLock.release();
- mHandler.removeCallbacks(this);
- }
-
- @Override
- public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
- String resultData, Bundle resultExtras) {
- onReceiveResult(resultCode, null);
- }
- };
-
- BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent == null) {
- return;
- }
- Bundle extras = intent.getExtras();
- if (extras == null) {
- return;
- }
- synchronized (mLock) {
- if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)
- && mMediaEventWakeLock.isHeld()) {
- mMediaEventWakeLock.release();
- }
- }
- }
- };
- }
-
- final class MessageHandler extends Handler {
- private static final int MSG_SESSIONS_CHANGED = 1;
- private static final int MSG_VOLUME_INITIAL_DOWN = 2;
- private final SparseArray<Integer> mIntegerCache = new SparseArray<>();
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_SESSIONS_CHANGED:
- pushSessionsChanged((int) msg.obj);
- break;
- case MSG_VOLUME_INITIAL_DOWN:
- synchronized (mLock) {
- FullUserRecord user = mUserRecords.get((int) msg.arg1);
- if (user != null && user.mInitialDownVolumeKeyEvent != null) {
- dispatchVolumeKeyLongPressLocked(user.mInitialDownVolumeKeyEvent);
- // Mark that the key is already handled.
- user.mInitialDownVolumeKeyEvent = null;
- }
- }
- break;
- }
- }
-
- public void postSessionsChanged(int userId) {
- // Use object instead of the arguments when posting message to remove pending requests.
- Integer userIdInteger = mIntegerCache.get(userId);
- if (userIdInteger == null) {
- userIdInteger = Integer.valueOf(userId);
- mIntegerCache.put(userId, userIdInteger);
- }
- removeMessages(MSG_SESSIONS_CHANGED, userIdInteger);
- obtainMessage(MSG_SESSIONS_CHANGED, userIdInteger).sendToTarget();
- }
- }
-
- private class Controller2Callback extends MediaController2.ControllerCallback {
- private final Session2Token mToken;
-
- Controller2Callback(Session2Token token) {
- mToken = token;
- }
-
- @Override
- public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
- synchronized (mLock) {
- int userId = UserHandle.getUserId(mToken.getUid());
- mSession2TokensPerUser.get(userId).add(mToken);
- pushSession2TokensChangedLocked(userId);
- }
- }
-
- @Override
- public void onDisconnected(MediaController2 controller) {
- synchronized (mLock) {
- int userId = UserHandle.getUserId(mToken.getUid());
- mSession2TokensPerUser.get(userId).remove(mToken);
- pushSession2TokensChangedLocked(userId);
- }
- }
+ abstract static class ServiceImpl {
+ public abstract void onStart();
+ public abstract void notifyRemoteVolumeChanged(int flags, MediaSessionRecord session);
+ public abstract void onSessionPlaystateChanged(
+ MediaSessionRecord record, int oldState, int newState);
+ public abstract void onSessionPlaybackTypeChanged(MediaSessionRecord record);
+ public abstract void onStartUser(int userId);
+ public abstract void onSwitchUser(int userId);
+ public abstract void monitor();
+ public abstract void onMediaButtonReceiverChanged(MediaSessionRecord record);
+ protected abstract void enforcePhoneStatePermission(int pid, int uid);
+ abstract void updateSession(MediaSessionRecord record);
+ abstract void setGlobalPrioritySession(MediaSessionRecord record);
+ abstract List<Session2Token> getSession2TokensLocked(int userId);
+ abstract void onStopUser(int userId);
+ abstract void sessionDied(MediaSessionRecord session);
+ abstract void destroySession(MediaSessionRecord session);
+ abstract void pushSession2TokensChangedLocked(int userId);
+ abstract Context getContext();
+ abstract IBinder getServiceBinder();
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionServiceImpl.java b/services/core/java/com/android/server/media/MediaSessionServiceImpl.java
new file mode 100644
index 0000000..f374c6d
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaSessionServiceImpl.java
@@ -0,0 +1,2142 @@
+/*
+ * 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.media;
+
+import static android.os.UserHandle.USER_ALL;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.INotificationManager;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.AudioManagerInternal;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioSystem;
+import android.media.IAudioService;
+import android.media.IRemoteVolumeController;
+import android.media.MediaController2;
+import android.media.Session2CommandGroup;
+import android.media.Session2Token;
+import android.media.session.IActiveSessionsListener;
+import android.media.session.ICallback;
+import android.media.session.IOnMediaKeyListener;
+import android.media.session.IOnVolumeKeyLongPressListener;
+import android.media.session.ISession;
+import android.media.session.ISession2TokensListener;
+import android.media.session.ISessionManager;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.SessionCallbackLink;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.speech.RecognizerIntent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.KeyEvent;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * System implementation of MediaSessionManager
+ */
+public class MediaSessionServiceImpl extends MediaSessionService.ServiceImpl {
+ private static final String TAG = "MediaSessionService";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ // Leave log for key event always.
+ private static final boolean DEBUG_KEY_EVENT = true;
+
+ private static final int WAKELOCK_TIMEOUT = 5000;
+ private static final int MEDIA_KEY_LISTENER_TIMEOUT = 1000;
+
+ private final Context mContext;
+ private final SessionManagerImpl mSessionManagerImpl;
+ private final MessageHandler mHandler = new MessageHandler();
+ private final PowerManager.WakeLock mMediaEventWakeLock;
+ private final int mLongPressTimeout;
+ private final INotificationManager mNotificationManager;
+ private final Object mLock = new Object();
+ // Keeps the full user id for each user.
+ @GuardedBy("mLock")
+ private final SparseIntArray mFullUserIds = new SparseIntArray();
+ @GuardedBy("mLock")
+ private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<FullUserRecord>();
+ @GuardedBy("mLock")
+ private final ArrayList<SessionsListenerRecord> mSessionsListeners =
+ new ArrayList<SessionsListenerRecord>();
+ // Map user id as index to list of Session2Tokens
+ // TODO: Keep session2 info in MediaSessionStack for prioritizing both session1 and session2 in
+ // one place.
+ @GuardedBy("mLock")
+ private final SparseArray<List<Session2Token>> mSession2TokensPerUser = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final List<Session2TokensListenerRecord> mSession2TokensListenerRecords =
+ new ArrayList<>();
+
+ private KeyguardManager mKeyguardManager;
+ private IAudioService mAudioService;
+ private AudioManagerInternal mAudioManagerInternal;
+ private ActivityManager mActivityManager;
+ private ContentResolver mContentResolver;
+ private SettingsObserver mSettingsObserver;
+ private boolean mHasFeatureLeanback;
+
+ // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
+ // It's always not null after the MediaSessionService is started.
+ private FullUserRecord mCurrentFullUserRecord;
+ private MediaSessionRecord mGlobalPrioritySession;
+ private AudioPlayerStateMonitor mAudioPlayerStateMonitor;
+
+ // Used to notify system UI when remote volume was changed. TODO find a
+ // better way to handle this.
+ private IRemoteVolumeController mRvc;
+
+ public MediaSessionServiceImpl(Context context) {
+ mContext = context;
+ mSessionManagerImpl = new SessionManagerImpl();
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
+ mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
+ mNotificationManager = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ IBinder getServiceBinder() {
+ return mSessionManagerImpl;
+ }
+
+ @Override
+ public void onStart() {
+ mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ mAudioService = getAudioService();
+ mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
+ mActivityManager =
+ (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
+ mAudioPlayerStateMonitor.registerListener(
+ (config, isRemoved) -> {
+ if (isRemoved || !config.isActive() || config.getPlayerType()
+ == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+ return;
+ }
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(
+ UserHandle.getUserId(config.getClientUid()));
+ if (user != null) {
+ user.mPriorityStack.updateMediaButtonSessionIfNeeded();
+ }
+ }
+ }, null /* handler */);
+ mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
+ mContentResolver = mContext.getContentResolver();
+ mSettingsObserver = new SettingsObserver();
+ mSettingsObserver.observe();
+ mHasFeatureLeanback = mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_LEANBACK);
+
+ updateUser();
+ }
+
+ private IAudioService getAudioService() {
+ IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
+ return IAudioService.Stub.asInterface(b);
+ }
+
+ private boolean isGlobalPriorityActiveLocked() {
+ return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive();
+ }
+
+ @Override
+ public void updateSession(MediaSessionRecord record) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+ if (user == null) {
+ Log.w(TAG, "Unknown session updated. Ignoring.");
+ return;
+ }
+ if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Global priority session is updated, active=" + record.isActive());
+ }
+ user.pushAddressedPlayerChangedLocked();
+ } else {
+ if (!user.mPriorityStack.contains(record)) {
+ Log.w(TAG, "Unknown session updated. Ignoring.");
+ return;
+ }
+ user.mPriorityStack.onSessionStateChange(record);
+ }
+ mHandler.postSessionsChanged(record.getUserId());
+ }
+ }
+
+ @Override
+ public void setGlobalPrioritySession(MediaSessionRecord record) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+ if (mGlobalPrioritySession != record) {
+ Log.d(TAG, "Global priority session is changed from " + mGlobalPrioritySession
+ + " to " + record);
+ mGlobalPrioritySession = record;
+ if (user != null && user.mPriorityStack.contains(record)) {
+ // Handle the global priority session separately.
+ // Otherwise, it can be the media button session regardless of the active state
+ // because it or other system components might have been the lastly played media
+ // app.
+ user.mPriorityStack.removeSession(record);
+ }
+ }
+ }
+ }
+
+ private List<MediaSessionRecord> getActiveSessionsLocked(int userId) {
+ List<MediaSessionRecord> records = new ArrayList<>();
+ if (userId == USER_ALL) {
+ int size = mUserRecords.size();
+ for (int i = 0; i < size; i++) {
+ records.addAll(mUserRecords.valueAt(i).mPriorityStack.getActiveSessions(userId));
+ }
+ } else {
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null) {
+ Log.w(TAG, "getSessions failed. Unknown user " + userId);
+ return records;
+ }
+ records.addAll(user.mPriorityStack.getActiveSessions(userId));
+ }
+
+ // Return global priority session at the first whenever it's asked.
+ if (isGlobalPriorityActiveLocked()
+ && (userId == USER_ALL || userId == mGlobalPrioritySession.getUserId())) {
+ records.add(0, mGlobalPrioritySession);
+ }
+ return records;
+ }
+
+ List<Session2Token> getSession2TokensLocked(int userId) {
+ List<Session2Token> list = new ArrayList<>();
+ if (userId == USER_ALL) {
+ for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
+ list.addAll(mSession2TokensPerUser.valueAt(i));
+ }
+ } else {
+ list.addAll(mSession2TokensPerUser.get(userId));
+ }
+ return list;
+ }
+
+ /**
+ * Tells the system UI that volume has changed on an active remote session.
+ */
+ public void notifyRemoteVolumeChanged(int flags, MediaSessionRecord session) {
+ if (mRvc == null || !session.isActive()) {
+ return;
+ }
+ try {
+ mRvc.remoteVolumeChanged(session.getControllerBinder(), flags);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Error sending volume change to system UI.", e);
+ }
+ }
+
+ @Override
+ public void onSessionPlaystateChanged(MediaSessionRecord record, int oldState, int newState) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+ if (user == null || !user.mPriorityStack.contains(record)) {
+ Log.d(TAG, "Unknown session changed playback state. Ignoring.");
+ return;
+ }
+ user.mPriorityStack.onPlaystateChanged(record, oldState, newState);
+ }
+ }
+
+ @Override
+ public void onSessionPlaybackTypeChanged(MediaSessionRecord record) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+ if (user == null || !user.mPriorityStack.contains(record)) {
+ Log.d(TAG, "Unknown session changed playback type. Ignoring.");
+ return;
+ }
+ pushRemoteVolumeUpdateLocked(record.getUserId());
+ }
+ }
+
+ @Override
+ public void onStartUser(int userId) {
+ if (DEBUG) Log.d(TAG, "onStartUser: " + userId);
+ updateUser();
+ }
+
+ @Override
+ public void onSwitchUser(int userId) {
+ if (DEBUG) Log.d(TAG, "onSwitchUser: " + userId);
+ updateUser();
+ }
+
+ // Called when the user with the userId is removed.
+ @Override
+ public void onStopUser(int userId) {
+ if (DEBUG) Log.d(TAG, "onStopUser: " + userId);
+ synchronized (mLock) {
+ // TODO: Also handle removing user in updateUser() because adding/switching user is
+ // handled in updateUser().
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user != null) {
+ if (user.mFullUserId == userId) {
+ user.destroySessionsForUserLocked(USER_ALL);
+ mUserRecords.remove(userId);
+ } else {
+ user.destroySessionsForUserLocked(userId);
+ }
+ }
+ mSession2TokensPerUser.remove(userId);
+ updateUser();
+ }
+ }
+
+ @Override
+ public void monitor() {
+ synchronized (mLock) {
+ // Check for deadlock
+ }
+ }
+
+ protected void enforcePhoneStatePermission(int pid, int uid) {
+ if (mContext.checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
+ }
+ }
+
+ void sessionDied(MediaSessionRecord session) {
+ synchronized (mLock) {
+ destroySessionLocked(session);
+ }
+ }
+
+ void destroySession(MediaSessionRecord session) {
+ synchronized (mLock) {
+ destroySessionLocked(session);
+ }
+ }
+
+ private void updateUser() {
+ synchronized (mLock) {
+ UserManager manager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mFullUserIds.clear();
+ List<UserInfo> allUsers = manager.getUsers();
+ if (allUsers != null) {
+ for (UserInfo userInfo : allUsers) {
+ if (userInfo.isManagedProfile()) {
+ mFullUserIds.put(userInfo.id, userInfo.profileGroupId);
+ } else {
+ mFullUserIds.put(userInfo.id, userInfo.id);
+ if (mUserRecords.get(userInfo.id) == null) {
+ mUserRecords.put(userInfo.id, new FullUserRecord(userInfo.id));
+ }
+ }
+ if (mSession2TokensPerUser.get(userInfo.id) == null) {
+ mSession2TokensPerUser.put(userInfo.id, new ArrayList<>());
+ }
+ }
+ }
+ // Ensure that the current full user exists.
+ int currentFullUserId = ActivityManager.getCurrentUser();
+ mCurrentFullUserRecord = mUserRecords.get(currentFullUserId);
+ if (mCurrentFullUserRecord == null) {
+ Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
+ mCurrentFullUserRecord = new FullUserRecord(currentFullUserId);
+ mUserRecords.put(currentFullUserId, mCurrentFullUserRecord);
+ if (mSession2TokensPerUser.get(currentFullUserId) == null) {
+ mSession2TokensPerUser.put(currentFullUserId, new ArrayList<>());
+ }
+ }
+ mFullUserIds.put(currentFullUserId, currentFullUserId);
+ }
+ }
+
+ private void updateActiveSessionListeners() {
+ synchronized (mLock) {
+ for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+ SessionsListenerRecord listener = mSessionsListeners.get(i);
+ try {
+ enforceMediaPermissions(listener.componentName, listener.pid, listener.uid,
+ listener.userId);
+ } catch (SecurityException e) {
+ Log.i(TAG, "ActiveSessionsListener " + listener.componentName
+ + " is no longer authorized. Disconnecting.");
+ mSessionsListeners.remove(i);
+ try {
+ listener.listener
+ .onActiveSessionsChanged(new ArrayList<MediaSession.Token>());
+ } catch (Exception e1) {
+ // ignore
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * When a session is removed several things need to happen.
+ * 1. We need to remove it from the relevant user.
+ * 2. We need to remove it from the priority stack.
+ * 3. We need to remove it from all sessions.
+ * 4. If this is the system priority session we need to clear it.
+ * 5. We need to unlink to death from the cb binder
+ * 6. We need to tell the session to do any final cleanup (onDestroy)
+ */
+ private void destroySessionLocked(MediaSessionRecord session) {
+ if (DEBUG) {
+ Log.d(TAG, "Destroying " + session);
+ }
+ FullUserRecord user = getFullUserRecordLocked(session.getUserId());
+ if (mGlobalPrioritySession == session) {
+ mGlobalPrioritySession = null;
+ if (session.isActive() && user != null) {
+ user.pushAddressedPlayerChangedLocked();
+ }
+ } else {
+ if (user != null) {
+ user.mPriorityStack.removeSession(session);
+ }
+ }
+
+ try {
+ session.getCallback().getBinder().unlinkToDeath(session, 0);
+ } catch (Exception e) {
+ // ignore exceptions while destroying a session.
+ }
+ session.onDestroy();
+ mHandler.postSessionsChanged(session.getUserId());
+ }
+
+ private void enforcePackageName(String packageName, int uid) {
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("packageName may not be empty");
+ }
+ String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+ final int packageCount = packages.length;
+ for (int i = 0; i < packageCount; i++) {
+ if (packageName.equals(packages[i])) {
+ return;
+ }
+ }
+ throw new IllegalArgumentException("packageName is not owned by the calling process");
+ }
+
+ /**
+ * Checks a caller's authorization to register an IRemoteControlDisplay.
+ * Authorization is granted if one of the following is true:
+ * <ul>
+ * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL
+ * permission</li>
+ * <li>the caller's listener is one of the enabled notification listeners
+ * for the caller's user</li>
+ * </ul>
+ */
+ private void enforceMediaPermissions(ComponentName compName, int pid, int uid,
+ int resolvedUserId) {
+ if (isCurrentVolumeController(pid, uid)) return;
+ if (mContext
+ .checkPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
+ != PackageManager.PERMISSION_GRANTED
+ && !isEnabledNotificationListener(compName, UserHandle.getUserId(uid),
+ resolvedUserId)) {
+ throw new SecurityException("Missing permission to control media.");
+ }
+ }
+
+ private boolean isCurrentVolumeController(int pid, int uid) {
+ return mContext.checkPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
+ pid, uid) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void enforceSystemUiPermission(String action, int pid, int uid) {
+ if (!isCurrentVolumeController(pid, uid)) {
+ throw new SecurityException("Only system ui may " + action);
+ }
+ }
+
+ /**
+ * This checks if the component is an enabled notification listener for the
+ * specified user. Enabled components may only operate on behalf of the user
+ * they're running as.
+ *
+ * @param compName The component that is enabled.
+ * @param userId The user id of the caller.
+ * @param forUserId The user id they're making the request on behalf of.
+ * @return True if the component is enabled, false otherwise
+ */
+ private boolean isEnabledNotificationListener(ComponentName compName, int userId,
+ int forUserId) {
+ if (userId != forUserId) {
+ // You may not access another user's content as an enabled listener.
+ return false;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Checking if enabled notification listener " + compName);
+ }
+ if (compName != null) {
+ try {
+ return mNotificationManager.isNotificationListenerAccessGrantedForUser(
+ compName, userId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Dead NotificationManager in isEnabledNotificationListener", e);
+ }
+ }
+ return false;
+ }
+
+ private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
+ String callerPackageName, SessionCallbackLink cb, String tag) throws RemoteException {
+ synchronized (mLock) {
+ return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb, tag);
+ }
+ }
+
+ /*
+ * When a session is created the following things need to happen.
+ * 1. Its callback binder needs a link to death
+ * 2. It needs to be added to all sessions.
+ * 3. It needs to be added to the priority stack.
+ * 4. It needs to be added to the relevant user record.
+ */
+ private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
+ String callerPackageName, SessionCallbackLink cb, String tag) {
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null) {
+ Log.wtf(TAG, "Request from invalid user: " + userId);
+ throw new RuntimeException("Session request from invalid user.");
+ }
+
+ final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
+ callerPackageName, cb, tag, this, mHandler.getLooper());
+ try {
+ cb.getBinder().linkToDeath(session, 0);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Media Session owner died prematurely.", e);
+ }
+
+ user.mPriorityStack.addSession(session);
+ mHandler.postSessionsChanged(userId);
+
+ if (DEBUG) {
+ Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
+ }
+ return session;
+ }
+
+ private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
+ for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+ if (mSessionsListeners.get(i).listener.asBinder() == listener.asBinder()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int findIndexOfSession2TokensListenerLocked(ISession2TokensListener listener) {
+ for (int i = mSession2TokensListenerRecords.size() - 1; i >= 0; i--) {
+ if (mSession2TokensListenerRecords.get(i).listener.asBinder() == listener.asBinder()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+
+ private void pushSessionsChanged(int userId) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null) {
+ Log.w(TAG, "pushSessionsChanged failed. No user with id=" + userId);
+ return;
+ }
+ List<MediaSessionRecord> records = getActiveSessionsLocked(userId);
+ int size = records.size();
+ ArrayList<MediaSession.Token> tokens = new ArrayList<MediaSession.Token>();
+ for (int i = 0; i < size; i++) {
+ tokens.add(new MediaSession.Token(records.get(i).getControllerBinder()));
+ }
+ pushRemoteVolumeUpdateLocked(userId);
+ for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+ SessionsListenerRecord record = mSessionsListeners.get(i);
+ if (record.userId == USER_ALL || record.userId == userId) {
+ try {
+ record.listener.onActiveSessionsChanged(tokens);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Dead ActiveSessionsListener in pushSessionsChanged, removing",
+ e);
+ mSessionsListeners.remove(i);
+ }
+ }
+ }
+ }
+ }
+
+ private void pushRemoteVolumeUpdateLocked(int userId) {
+ if (mRvc != null) {
+ try {
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null) {
+ Log.w(TAG, "pushRemoteVolumeUpdateLocked failed. No user with id=" + userId);
+ return;
+ }
+ MediaSessionRecord record = user.mPriorityStack.getDefaultRemoteSession(userId);
+ mRvc.updateRemoteController(record == null ? null : record.getControllerBinder());
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error sending default remote volume to sys ui.", e);
+ }
+ }
+ }
+
+ void pushSession2TokensChangedLocked(int userId) {
+ List<Session2Token> allSession2Tokens = getSession2TokensLocked(USER_ALL);
+ List<Session2Token> session2Tokens = getSession2TokensLocked(userId);
+
+ for (int i = mSession2TokensListenerRecords.size() - 1; i >= 0; i--) {
+ Session2TokensListenerRecord listenerRecord = mSession2TokensListenerRecords.get(i);
+ try {
+ if (listenerRecord.userId == USER_ALL) {
+ listenerRecord.listener.onSession2TokensChanged(allSession2Tokens);
+ } else if (listenerRecord.userId == userId) {
+ listenerRecord.listener.onSession2TokensChanged(session2Tokens);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to notify Session2Token change. Removing listener.", e);
+ mSession2TokensListenerRecords.remove(i);
+ }
+ }
+ }
+
+ /**
+ * Called when the media button receiver for the {@code record} is changed.
+ *
+ * @param record the media session whose media button receiver is updated.
+ */
+ public void onMediaButtonReceiverChanged(MediaSessionRecord record) {
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+ MediaSessionRecord mediaButtonSession =
+ user.mPriorityStack.getMediaButtonSession();
+ if (record == mediaButtonSession) {
+ user.rememberMediaButtonReceiverLocked(mediaButtonSession);
+ }
+ }
+ }
+
+ private String getCallingPackageName(int uid) {
+ String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+ if (packages != null && packages.length > 0) {
+ return packages[0];
+ }
+ return "";
+ }
+
+ private void dispatchVolumeKeyLongPressLocked(KeyEvent keyEvent) {
+ if (mCurrentFullUserRecord.mOnVolumeKeyLongPressListener == null) {
+ return;
+ }
+ try {
+ mCurrentFullUserRecord.mOnVolumeKeyLongPressListener.onVolumeKeyLongPress(keyEvent);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send " + keyEvent + " to volume key long-press listener");
+ }
+ }
+
+ private FullUserRecord getFullUserRecordLocked(int userId) {
+ int fullUserId = mFullUserIds.get(userId, -1);
+ if (fullUserId < 0) {
+ return null;
+ }
+ return mUserRecords.get(fullUserId);
+ }
+
+ /**
+ * Information about a full user and its corresponding managed profiles.
+ *
+ * <p>Since the full user runs together with its managed profiles, a user wouldn't differentiate
+ * them when he/she presses a media/volume button. So keeping media sessions for them in one
+ * place makes more sense and increases the readability.</p>
+ * <p>The contents of this object is guarded by {@link #mLock}.
+ */
+ final class FullUserRecord implements MediaSessionStack.OnMediaButtonSessionChangedListener {
+ public static final int COMPONENT_TYPE_INVALID = 0;
+ public static final int COMPONENT_TYPE_BROADCAST = 1;
+ public static final int COMPONENT_TYPE_ACTIVITY = 2;
+ public static final int COMPONENT_TYPE_SERVICE = 3;
+ private static final String COMPONENT_NAME_USER_ID_DELIM = ",";
+
+ private final int mFullUserId;
+ private final MediaSessionStack mPriorityStack;
+ private PendingIntent mLastMediaButtonReceiver;
+ private ComponentName mRestoredMediaButtonReceiver;
+ private int mRestoredMediaButtonReceiverComponentType;
+ private int mRestoredMediaButtonReceiverUserId;
+
+ private IOnVolumeKeyLongPressListener mOnVolumeKeyLongPressListener;
+ private int mOnVolumeKeyLongPressListenerUid;
+ private KeyEvent mInitialDownVolumeKeyEvent;
+ private int mInitialDownVolumeStream;
+ private boolean mInitialDownMusicOnly;
+
+ private IOnMediaKeyListener mOnMediaKeyListener;
+ private int mOnMediaKeyListenerUid;
+ private ICallback mCallback;
+
+ FullUserRecord(int fullUserId) {
+ mFullUserId = fullUserId;
+ mPriorityStack = new MediaSessionStack(mAudioPlayerStateMonitor, this);
+ // Restore the remembered media button receiver before the boot.
+ String mediaButtonReceiverInfo = Settings.Secure.getStringForUser(mContentResolver,
+ Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId);
+ if (mediaButtonReceiverInfo == null) {
+ return;
+ }
+ String[] tokens = mediaButtonReceiverInfo.split(COMPONENT_NAME_USER_ID_DELIM);
+ if (tokens == null || (tokens.length != 2 && tokens.length != 3)) {
+ return;
+ }
+ mRestoredMediaButtonReceiver = ComponentName.unflattenFromString(tokens[0]);
+ mRestoredMediaButtonReceiverUserId = Integer.parseInt(tokens[1]);
+ if (tokens.length == 3) {
+ mRestoredMediaButtonReceiverComponentType = Integer.parseInt(tokens[2]);
+ } else {
+ mRestoredMediaButtonReceiverComponentType =
+ getComponentType(mRestoredMediaButtonReceiver);
+ }
+ }
+
+ public void destroySessionsForUserLocked(int userId) {
+ List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, userId);
+ for (MediaSessionRecord session : sessions) {
+ MediaSessionServiceImpl.this.destroySessionLocked(session);
+ }
+ }
+
+ public void dumpLocked(PrintWriter pw, String prefix) {
+ pw.print(prefix + "Record for full_user=" + mFullUserId);
+ // Dump managed profile user ids associated with this user.
+ int size = mFullUserIds.size();
+ for (int i = 0; i < size; i++) {
+ if (mFullUserIds.keyAt(i) != mFullUserIds.valueAt(i)
+ && mFullUserIds.valueAt(i) == mFullUserId) {
+ pw.print(", profile_user=" + mFullUserIds.keyAt(i));
+ }
+ }
+ pw.println();
+ String indent = prefix + " ";
+ pw.println(indent + "Volume key long-press listener: " + mOnVolumeKeyLongPressListener);
+ pw.println(indent + "Volume key long-press listener package: "
+ + getCallingPackageName(mOnVolumeKeyLongPressListenerUid));
+ pw.println(indent + "Media key listener: " + mOnMediaKeyListener);
+ pw.println(indent + "Media key listener package: "
+ + getCallingPackageName(mOnMediaKeyListenerUid));
+ pw.println(indent + "Callback: " + mCallback);
+ pw.println(indent + "Last MediaButtonReceiver: " + mLastMediaButtonReceiver);
+ pw.println(indent + "Restored MediaButtonReceiver: " + mRestoredMediaButtonReceiver);
+ pw.println(indent + "Restored MediaButtonReceiverComponentType: "
+ + mRestoredMediaButtonReceiverComponentType);
+ mPriorityStack.dump(pw, indent);
+ pw.println(indent + "Session2Tokens:");
+ for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
+ List<Session2Token> list = mSession2TokensPerUser.valueAt(i);
+ if (list == null || list.size() == 0) {
+ continue;
+ }
+ for (Session2Token token : list) {
+ pw.println(indent + " " + token);
+ }
+ }
+ }
+
+ @Override
+ public void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession,
+ MediaSessionRecord newMediaButtonSession) {
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Media button session is changed to " + newMediaButtonSession);
+ }
+ synchronized (mLock) {
+ if (oldMediaButtonSession != null) {
+ mHandler.postSessionsChanged(oldMediaButtonSession.getUserId());
+ }
+ if (newMediaButtonSession != null) {
+ rememberMediaButtonReceiverLocked(newMediaButtonSession);
+ mHandler.postSessionsChanged(newMediaButtonSession.getUserId());
+ }
+ pushAddressedPlayerChangedLocked();
+ }
+ }
+
+ // Remember media button receiver and keep it in the persistent storage.
+ public void rememberMediaButtonReceiverLocked(MediaSessionRecord record) {
+ PendingIntent receiver = record.getMediaButtonReceiver();
+ mLastMediaButtonReceiver = receiver;
+ mRestoredMediaButtonReceiver = null;
+ mRestoredMediaButtonReceiverComponentType = COMPONENT_TYPE_INVALID;
+
+ String mediaButtonReceiverInfo = "";
+ if (receiver != null) {
+ ComponentName component = receiver.getIntent().getComponent();
+ if (component != null
+ && record.getPackageName().equals(component.getPackageName())) {
+ String componentName = component.flattenToString();
+ int componentType = getComponentType(component);
+ mediaButtonReceiverInfo = String.join(COMPONENT_NAME_USER_ID_DELIM,
+ componentName, String.valueOf(record.getUserId()),
+ String.valueOf(componentType));
+ }
+ }
+ Settings.Secure.putStringForUser(mContentResolver,
+ Settings.System.MEDIA_BUTTON_RECEIVER, mediaButtonReceiverInfo,
+ mFullUserId);
+ }
+
+ private void pushAddressedPlayerChangedLocked() {
+ if (mCallback == null) {
+ return;
+ }
+ try {
+ MediaSessionRecord mediaButtonSession = getMediaButtonSessionLocked();
+ if (mediaButtonSession != null) {
+ mCallback.onAddressedPlayerChangedToMediaSession(
+ new MediaSession.Token(mediaButtonSession.getControllerBinder()));
+ } else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) {
+ mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
+ mCurrentFullUserRecord.mLastMediaButtonReceiver
+ .getIntent().getComponent());
+ } else if (mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
+ mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
+ mCurrentFullUserRecord.mRestoredMediaButtonReceiver);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to pushAddressedPlayerChangedLocked", e);
+ }
+ }
+
+ private MediaSessionRecord getMediaButtonSessionLocked() {
+ return isGlobalPriorityActiveLocked()
+ ? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession();
+ }
+
+ private int getComponentType(@Nullable ComponentName componentName) {
+ if (componentName == null) {
+ return COMPONENT_TYPE_INVALID;
+ }
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ActivityInfo activityInfo = pm.getActivityInfo(componentName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.GET_ACTIVITIES);
+ if (activityInfo != null) {
+ return COMPONENT_TYPE_ACTIVITY;
+ }
+ } catch (NameNotFoundException e) {
+ }
+ try {
+ ServiceInfo serviceInfo = pm.getServiceInfo(componentName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.GET_SERVICES);
+ if (serviceInfo != null) {
+ return COMPONENT_TYPE_SERVICE;
+ }
+ } catch (NameNotFoundException e) {
+ }
+ // Pick legacy behavior for BroadcastReceiver or unknown.
+ return COMPONENT_TYPE_BROADCAST;
+ }
+ }
+
+ final class SessionsListenerRecord implements IBinder.DeathRecipient {
+ public final IActiveSessionsListener listener;
+ public final ComponentName componentName;
+ public final int userId;
+ public final int pid;
+ public final int uid;
+
+ SessionsListenerRecord(IActiveSessionsListener listener,
+ ComponentName componentName,
+ int userId, int pid, int uid) {
+ this.listener = listener;
+ this.componentName = componentName;
+ this.userId = userId;
+ this.pid = pid;
+ this.uid = uid;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ mSessionsListeners.remove(this);
+ }
+ }
+ }
+
+ final class Session2TokensListenerRecord implements IBinder.DeathRecipient {
+ public final ISession2TokensListener listener;
+ public final int userId;
+
+ Session2TokensListenerRecord(ISession2TokensListener listener,
+ int userId) {
+ this.listener = listener;
+ this.userId = userId;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ mSession2TokensListenerRecords.remove(this);
+ }
+ }
+ }
+
+ final class SettingsObserver extends ContentObserver {
+ private final Uri mSecureSettingsUri = Settings.Secure.getUriFor(
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
+
+ private SettingsObserver() {
+ super(null);
+ }
+
+ private void observe() {
+ mContentResolver.registerContentObserver(mSecureSettingsUri,
+ false, this, USER_ALL);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateActiveSessionListeners();
+ }
+ }
+
+ class SessionManagerImpl extends ISessionManager.Stub {
+ private static final String EXTRA_WAKELOCK_ACQUIRED =
+ "android.media.AudioService.WAKELOCK_ACQUIRED";
+ private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; // magic number
+
+ private boolean mVoiceButtonDown = false;
+ private boolean mVoiceButtonHandled = false;
+
+ @Override
+ public ISession createSession(String packageName, SessionCallbackLink cb, String tag,
+ int userId) throws RemoteException {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ enforcePackageName(packageName, uid);
+ int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ false /* allowAll */, true /* requireFull */, "createSession", packageName);
+ if (cb == null) {
+ throw new IllegalArgumentException("Controller callback cannot be null");
+ }
+ return createSessionInternal(pid, uid, resolvedUserId, packageName, cb, tag)
+ .getSessionBinder();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void notifySession2Created(Session2Token sessionToken) throws RemoteException {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "Session2 is created " + sessionToken);
+ }
+ if (uid != sessionToken.getUid()) {
+ throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+ + " but actually=" + sessionToken.getUid());
+ }
+ Controller2Callback callback = new Controller2Callback(sessionToken);
+ // Note: It's safe not to keep controller here because it wouldn't be GC'ed until
+ // it's closed.
+ // TODO: Keep controller as well for better readability
+ // because the GC behavior isn't straightforward.
+ MediaController2 controller = new MediaController2(mContext, sessionToken,
+ new HandlerExecutor(mHandler), callback);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public List<IBinder> getSessions(ComponentName componentName, int userId) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
+ ArrayList<IBinder> binders = new ArrayList<IBinder>();
+ synchronized (mLock) {
+ List<MediaSessionRecord> records = getActiveSessionsLocked(resolvedUserId);
+ for (MediaSessionRecord record : records) {
+ binders.add(record.getControllerBinder().asBinder());
+ }
+ }
+ return binders;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public List<Session2Token> getSession2Tokens(int userId) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ // Check that they can make calls on behalf of the user and
+ // get the final user id
+ int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ true /* allowAll */, true /* requireFull */, "getSession2Tokens",
+ null /* optional packageName */);
+ List<Session2Token> result;
+ synchronized (mLock) {
+ result = getSession2TokensLocked(resolvedUserId);
+ }
+ return result;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void addSessionsListener(IActiveSessionsListener listener,
+ ComponentName componentName, int userId) throws RemoteException {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
+ synchronized (mLock) {
+ int index = findIndexOfSessionsListenerLocked(listener);
+ if (index != -1) {
+ Log.w(TAG, "ActiveSessionsListener is already added, ignoring");
+ return;
+ }
+ SessionsListenerRecord record = new SessionsListenerRecord(listener,
+ componentName, resolvedUserId, pid, uid);
+ try {
+ listener.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "ActiveSessionsListener is dead, ignoring it", e);
+ return;
+ }
+ mSessionsListeners.add(record);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void removeSessionsListener(IActiveSessionsListener listener)
+ throws RemoteException {
+ synchronized (mLock) {
+ int index = findIndexOfSessionsListenerLocked(listener);
+ if (index != -1) {
+ SessionsListenerRecord record = mSessionsListeners.remove(index);
+ try {
+ record.listener.asBinder().unlinkToDeath(record, 0);
+ } catch (Exception e) {
+ // ignore exceptions, the record is being removed
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addSession2TokensListener(ISession2TokensListener listener,
+ int userId) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ // Check that they can make calls on behalf of the user and get the final user id.
+ int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ true /* allowAll */, true /* requireFull */, "addSession2TokensListener",
+ null /* optional packageName */);
+ synchronized (mLock) {
+ int index = findIndexOfSession2TokensListenerLocked(listener);
+ if (index >= 0) {
+ Log.w(TAG, "addSession2TokensListener is already added, ignoring");
+ return;
+ }
+ mSession2TokensListenerRecords.add(
+ new Session2TokensListenerRecord(listener, resolvedUserId));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void removeSession2TokensListener(ISession2TokensListener listener) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ synchronized (mLock) {
+ int index = findIndexOfSession2TokensListenerLocked(listener);
+ if (index >= 0) {
+ Session2TokensListenerRecord listenerRecord =
+ mSession2TokensListenerRecords.remove(index);
+ try {
+ listenerRecord.listener.asBinder().unlinkToDeath(listenerRecord, 0);
+ } catch (Exception e) {
+ // Ignore exception.
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Handles the dispatching of the media button events to one of the
+ * registered listeners, or if there was none, broadcast an
+ * ACTION_MEDIA_BUTTON intent to the rest of the system.
+ *
+ * @param packageName The caller package
+ * @param asSystemService {@code true} if the event sent to the session as if it was come
+ * from the system service instead of the app process. This helps sessions to
+ * distinguish between the key injection by the app and key events from the
+ * hardware devices. Should be used only when the volume key events aren't handled
+ * by foreground activity. {@code false} otherwise to tell session about the real
+ * caller.
+ * @param keyEvent a non-null KeyEvent whose key code is one of the
+ * supported media buttons
+ * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held
+ * while this key event is dispatched.
+ */
+ @Override
+ public void dispatchMediaKeyEvent(String packageName, boolean asSystemService,
+ KeyEvent keyEvent, boolean needWakeLock) {
+ if (keyEvent == null || !KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) {
+ Log.w(TAG, "Attempted to dispatch null or non-media key event.");
+ return;
+ }
+
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "dispatchMediaKeyEvent, pkg=" + packageName + " pid=" + pid
+ + ", uid=" + uid + ", asSystem=" + asSystemService + ", event="
+ + keyEvent);
+ }
+ if (!isUserSetupComplete()) {
+ // Global media key handling can have the side-effect of starting new
+ // activities which is undesirable while setup is in progress.
+ Slog.i(TAG, "Not dispatching media key event because user "
+ + "setup is in progress.");
+ return;
+ }
+
+ synchronized (mLock) {
+ boolean isGlobalPriorityActive = isGlobalPriorityActiveLocked();
+ if (isGlobalPriorityActive && uid != Process.SYSTEM_UID) {
+ // Prevent dispatching key event through reflection while the global
+ // priority session is active.
+ Slog.i(TAG, "Only the system can dispatch media key event "
+ + "to the global priority session.");
+ return;
+ }
+ if (!isGlobalPriorityActive) {
+ if (mCurrentFullUserRecord.mOnMediaKeyListener != null) {
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Send " + keyEvent + " to the media key listener");
+ }
+ try {
+ mCurrentFullUserRecord.mOnMediaKeyListener.onMediaKey(keyEvent,
+ new MediaKeyListenerResultReceiver(packageName, pid, uid,
+ asSystemService, keyEvent, needWakeLock));
+ return;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send " + keyEvent
+ + " to the media key listener");
+ }
+ }
+ }
+ if (!isGlobalPriorityActive && isVoiceKey(keyEvent.getKeyCode())) {
+ handleVoiceKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent,
+ needWakeLock);
+ } else {
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+ keyEvent, needWakeLock);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setCallback(ICallback callback) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (!UserHandle.isSameApp(uid, Process.BLUETOOTH_UID)) {
+ throw new SecurityException("Only Bluetooth service processes can set"
+ + " Callback");
+ }
+ synchronized (mLock) {
+ int userId = UserHandle.getUserId(uid);
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null || user.mFullUserId != userId) {
+ Log.w(TAG, "Only the full user can set the callback"
+ + ", userId=" + userId);
+ return;
+ }
+ user.mCallback = callback;
+ Log.d(TAG, "The callback " + user.mCallback
+ + " is set by " + getCallingPackageName(uid));
+ if (user.mCallback == null) {
+ return;
+ }
+ try {
+ user.mCallback.asBinder().linkToDeath(
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ user.mCallback = null;
+ }
+ }
+ }, 0);
+ user.pushAddressedPlayerChangedLocked();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to set callback", e);
+ user.mCallback = null;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setOnVolumeKeyLongPressListener(IOnVolumeKeyLongPressListener listener) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Enforce SET_VOLUME_KEY_LONG_PRESS_LISTENER permission.
+ if (mContext.checkPermission(
+ android.Manifest.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER, pid, uid)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Must hold the SET_VOLUME_KEY_LONG_PRESS_LISTENER"
+ + " permission.");
+ }
+
+ synchronized (mLock) {
+ int userId = UserHandle.getUserId(uid);
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null || user.mFullUserId != userId) {
+ Log.w(TAG, "Only the full user can set the volume key long-press listener"
+ + ", userId=" + userId);
+ return;
+ }
+ if (user.mOnVolumeKeyLongPressListener != null
+ && user.mOnVolumeKeyLongPressListenerUid != uid) {
+ Log.w(TAG, "The volume key long-press listener cannot be reset"
+ + " by another app , mOnVolumeKeyLongPressListener="
+ + user.mOnVolumeKeyLongPressListenerUid
+ + ", uid=" + uid);
+ return;
+ }
+
+ user.mOnVolumeKeyLongPressListener = listener;
+ user.mOnVolumeKeyLongPressListenerUid = uid;
+
+ Log.d(TAG, "The volume key long-press listener "
+ + listener + " is set by " + getCallingPackageName(uid));
+
+ if (user.mOnVolumeKeyLongPressListener != null) {
+ try {
+ user.mOnVolumeKeyLongPressListener.asBinder().linkToDeath(
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ user.mOnVolumeKeyLongPressListener = null;
+ }
+ }
+ }, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to set death recipient "
+ + user.mOnVolumeKeyLongPressListener);
+ user.mOnVolumeKeyLongPressListener = null;
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setOnMediaKeyListener(IOnMediaKeyListener listener) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Enforce SET_MEDIA_KEY_LISTENER permission.
+ if (mContext.checkPermission(
+ android.Manifest.permission.SET_MEDIA_KEY_LISTENER, pid, uid)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Must hold the SET_MEDIA_KEY_LISTENER permission.");
+ }
+
+ synchronized (mLock) {
+ int userId = UserHandle.getUserId(uid);
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null || user.mFullUserId != userId) {
+ Log.w(TAG, "Only the full user can set the media key listener"
+ + ", userId=" + userId);
+ return;
+ }
+ if (user.mOnMediaKeyListener != null && user.mOnMediaKeyListenerUid != uid) {
+ Log.w(TAG, "The media key listener cannot be reset by another app. "
+ + ", mOnMediaKeyListenerUid=" + user.mOnMediaKeyListenerUid
+ + ", uid=" + uid);
+ return;
+ }
+
+ user.mOnMediaKeyListener = listener;
+ user.mOnMediaKeyListenerUid = uid;
+
+ Log.d(TAG, "The media key listener " + user.mOnMediaKeyListener
+ + " is set by " + getCallingPackageName(uid));
+
+ if (user.mOnMediaKeyListener != null) {
+ try {
+ user.mOnMediaKeyListener.asBinder().linkToDeath(
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ user.mOnMediaKeyListener = null;
+ }
+ }
+ }, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to set death recipient " + user.mOnMediaKeyListener);
+ user.mOnMediaKeyListener = null;
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Handles the dispatching of the volume button events to one of the
+ * registered listeners. If there's a volume key long-press listener and
+ * there's no active global priority session, long-pressess will be sent to the
+ * long-press listener instead of adjusting volume.
+ *
+ * @param packageName The caller's package name, obtained by Context#getPackageName()
+ * @param opPackageName The caller's op package name, obtained by Context#getOpPackageName()
+ * @param asSystemService {@code true} if the event sent to the session as if it was come
+ * from the system service instead of the app process. This helps sessions to
+ * distinguish between the key injection by the app and key events from the
+ * hardware devices. Should be used only when the volume key events aren't handled
+ * by foreground activity. {@code false} otherwise to tell session about the real
+ * caller.
+ * @param keyEvent a non-null KeyEvent whose key code is one of the
+ * {@link KeyEvent#KEYCODE_VOLUME_UP},
+ * {@link KeyEvent#KEYCODE_VOLUME_DOWN},
+ * or {@link KeyEvent#KEYCODE_VOLUME_MUTE}.
+ * @param stream stream type to adjust volume.
+ * @param musicOnly true if both UI nor haptic feedback aren't needed when adjust volume.
+ */
+ @Override
+ public void dispatchVolumeKeyEvent(String packageName, String opPackageName,
+ boolean asSystemService, KeyEvent keyEvent, int stream, boolean musicOnly) {
+ if (keyEvent == null
+ || (keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP
+ && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN
+ && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_MUTE)) {
+ Log.w(TAG, "Attempted to dispatch null or non-volume key event.");
+ return;
+ }
+
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "dispatchVolumeKeyEvent, pkg=" + packageName + ", pid=" + pid + ", uid="
+ + uid + ", asSystem=" + asSystemService + ", event=" + keyEvent);
+ }
+
+ try {
+ synchronized (mLock) {
+ if (isGlobalPriorityActiveLocked()
+ || mCurrentFullUserRecord.mOnVolumeKeyLongPressListener == null) {
+ dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
+ asSystemService, keyEvent, stream, musicOnly);
+ } else {
+ // TODO: Consider the case when both volume up and down keys are pressed
+ // at the same time.
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ if (keyEvent.getRepeatCount() == 0) {
+ // Keeps the copy of the KeyEvent because it can be reused.
+ mCurrentFullUserRecord.mInitialDownVolumeKeyEvent =
+ KeyEvent.obtain(keyEvent);
+ mCurrentFullUserRecord.mInitialDownVolumeStream = stream;
+ mCurrentFullUserRecord.mInitialDownMusicOnly = musicOnly;
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MessageHandler.MSG_VOLUME_INITIAL_DOWN,
+ mCurrentFullUserRecord.mFullUserId, 0),
+ mLongPressTimeout);
+ }
+ if (keyEvent.getRepeatCount() > 0 || keyEvent.isLongPress()) {
+ mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
+ if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null) {
+ dispatchVolumeKeyLongPressLocked(
+ mCurrentFullUserRecord.mInitialDownVolumeKeyEvent);
+ // Mark that the key is already handled.
+ mCurrentFullUserRecord.mInitialDownVolumeKeyEvent = null;
+ }
+ dispatchVolumeKeyLongPressLocked(keyEvent);
+ }
+ } else { // if up
+ mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
+ if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null
+ && mCurrentFullUserRecord.mInitialDownVolumeKeyEvent
+ .getDownTime() == keyEvent.getDownTime()) {
+ // Short-press. Should change volume.
+ dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
+ asSystemService,
+ mCurrentFullUserRecord.mInitialDownVolumeKeyEvent,
+ mCurrentFullUserRecord.mInitialDownVolumeStream,
+ mCurrentFullUserRecord.mInitialDownMusicOnly);
+ dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
+ asSystemService, keyEvent, stream, musicOnly);
+ } else {
+ dispatchVolumeKeyLongPressLocked(keyEvent);
+ }
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private void dispatchVolumeKeyEventLocked(String packageName, String opPackageName, int pid,
+ int uid, boolean asSystemService, KeyEvent keyEvent, int stream,
+ boolean musicOnly) {
+ boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
+ boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
+ int direction = 0;
+ boolean isMute = false;
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ direction = AudioManager.ADJUST_RAISE;
+ break;
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ direction = AudioManager.ADJUST_LOWER;
+ break;
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ isMute = true;
+ break;
+ }
+ if (down || up) {
+ int flags = AudioManager.FLAG_FROM_KEY;
+ if (musicOnly) {
+ // This flag is used when the screen is off to only affect active media.
+ flags |= AudioManager.FLAG_ACTIVE_MEDIA_ONLY;
+ } else {
+ // These flags are consistent with the home screen
+ if (up) {
+ flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
+ } else {
+ flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
+ }
+ }
+ if (direction != 0) {
+ // If this is action up we want to send a beep for non-music events
+ if (up) {
+ direction = 0;
+ }
+ dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid,
+ asSystemService, stream, direction, flags);
+ } else if (isMute) {
+ if (down && keyEvent.getRepeatCount() == 0) {
+ dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid,
+ asSystemService, stream, AudioManager.ADJUST_TOGGLE_MUTE, flags);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dispatchAdjustVolume(String packageName, String opPackageName,
+ int suggestedStream, int delta, int flags) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ dispatchAdjustVolumeLocked(packageName, opPackageName, pid, uid, false,
+ suggestedStream, delta, flags);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setRemoteVolumeController(IRemoteVolumeController rvc) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ enforceSystemUiPermission("listen for volume changes", pid, uid);
+ mRvc = rvc;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public boolean isGlobalPriorityActive() {
+ synchronized (mLock) {
+ return isGlobalPriorityActiveLocked();
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ pw.println("MEDIA SESSION SERVICE (dumpsys media_session)");
+ pw.println();
+
+ synchronized (mLock) {
+ pw.println(mSessionsListeners.size() + " sessions listeners.");
+ pw.println("Global priority session is " + mGlobalPrioritySession);
+ if (mGlobalPrioritySession != null) {
+ mGlobalPrioritySession.dump(pw, " ");
+ }
+ pw.println("User Records:");
+ int count = mUserRecords.size();
+ for (int i = 0; i < count; i++) {
+ mUserRecords.valueAt(i).dumpLocked(pw, "");
+ }
+ mAudioPlayerStateMonitor.dump(mContext, pw, "");
+ }
+ }
+
+ /**
+ * Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL
+ * permission or an enabled notification listener)
+ *
+ * @param controllerPackageName package name of the controller app
+ * @param controllerPid pid of the controller app
+ * @param controllerUid uid of the controller app
+ */
+ @Override
+ public boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid)
+ throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Don't perform sanity check between controllerPackageName and controllerUid.
+ // When an (activity|service) runs on the another apps process by specifying
+ // android:process in the AndroidManifest.xml, then PID and UID would have the
+ // running process' information instead of the (activity|service) that has created
+ // MediaController.
+ // Note that we can use Context#getOpPackageName() instead of
+ // Context#getPackageName() for getting package name that matches with the PID/UID,
+ // but it doesn't tell which package has created the MediaController, so useless.
+ return hasMediaControlPermission(UserHandle.getUserId(uid), controllerPackageName,
+ controllerPid, controllerUid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // For MediaSession
+ private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
+ final int uid) {
+ String packageName = null;
+ if (componentName != null) {
+ // If they gave us a component name verify they own the
+ // package
+ packageName = componentName.getPackageName();
+ enforcePackageName(packageName, uid);
+ }
+ // Check that they can make calls on behalf of the user and
+ // get the final user id
+ int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ true /* allowAll */, true /* requireFull */, "getSessions", packageName);
+ // Check if they have the permissions or their component is
+ // enabled for the user they're calling from.
+ enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
+ return resolvedUserId;
+ }
+
+ private boolean hasMediaControlPermission(int resolvedUserId, String packageName,
+ int pid, int uid) throws RemoteException {
+ // Allow API calls from the System UI
+ if (isCurrentVolumeController(pid, uid)) {
+ return true;
+ }
+
+ // Check if it's system server or has MEDIA_CONTENT_CONTROL.
+ // Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra
+ // check here.
+ if (uid == Process.SYSTEM_UID || mContext.checkPermission(
+ android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ } else if (DEBUG) {
+ Log.d(TAG, packageName + " (uid=" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
+ }
+
+ // You may not access another user's content as an enabled listener.
+ final int userId = UserHandle.getUserId(uid);
+ if (resolvedUserId != userId) {
+ return false;
+ }
+
+ // TODO(jaewan): (Post-P) Propose NotificationManager#hasEnabledNotificationListener(
+ // String pkgName) to notification team for optimization
+ final List<ComponentName> enabledNotificationListeners =
+ mNotificationManager.getEnabledNotificationListeners(userId);
+ if (enabledNotificationListeners != null) {
+ for (int i = 0; i < enabledNotificationListeners.size(); i++) {
+ if (TextUtils.equals(packageName,
+ enabledNotificationListeners.get(i).getPackageName())) {
+ return true;
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, packageName + " (uid=" + uid + ") doesn't have an enabled "
+ + "notification listener");
+ }
+ return false;
+ }
+
+ private void dispatchAdjustVolumeLocked(String packageName, String opPackageName, int pid,
+ int uid, boolean asSystemService, int suggestedStream, int direction, int flags) {
+ MediaSessionRecord session = isGlobalPriorityActiveLocked() ? mGlobalPrioritySession
+ : mCurrentFullUserRecord.mPriorityStack.getDefaultVolumeSession();
+
+ boolean preferSuggestedStream = false;
+ if (isValidLocalStreamType(suggestedStream)
+ && AudioSystem.isStreamActive(suggestedStream, 0)) {
+ preferSuggestedStream = true;
+ }
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Adjusting " + session + " by " + direction + ". flags="
+ + flags + ", suggestedStream=" + suggestedStream
+ + ", preferSuggestedStream=" + preferSuggestedStream);
+ }
+ if (session == null || preferSuggestedStream) {
+ if ((flags & AudioManager.FLAG_ACTIVE_MEDIA_ONLY) != 0
+ && !AudioSystem.isStreamActive(AudioManager.STREAM_MUSIC, 0)) {
+ if (DEBUG) {
+ Log.d(TAG, "No active session to adjust, skipping media only volume event");
+ }
+ return;
+ }
+
+ // Execute mAudioService.adjustSuggestedStreamVolume() on
+ // handler thread of MediaSessionService.
+ // This will release the MediaSessionService.mLock sooner and avoid
+ // a potential deadlock between MediaSessionService.mLock and
+ // ActivityManagerService lock.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final String callingOpPackageName;
+ final int callingUid;
+ if (asSystemService) {
+ callingOpPackageName = mContext.getOpPackageName();
+ callingUid = Process.myUid();
+ } else {
+ callingOpPackageName = opPackageName;
+ callingUid = uid;
+ }
+ try {
+ mAudioManagerInternal.adjustSuggestedStreamVolumeForUid(suggestedStream,
+ direction, flags, callingOpPackageName, callingUid);
+ } catch (SecurityException | IllegalArgumentException e) {
+ Log.e(TAG, "Cannot adjust volume: direction=" + direction
+ + ", suggestedStream=" + suggestedStream + ", flags=" + flags
+ + ", packageName=" + packageName + ", uid=" + uid
+ + ", asSystemService=" + asSystemService, e);
+ }
+ }
+ });
+ } else {
+ session.adjustVolume(packageName, opPackageName, pid, uid, null, asSystemService,
+ direction, flags, true);
+ }
+ }
+
+ private void handleVoiceKeyEventLocked(String packageName, int pid, int uid,
+ boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
+ int action = keyEvent.getAction();
+ boolean isLongPress = (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (keyEvent.getRepeatCount() == 0) {
+ mVoiceButtonDown = true;
+ mVoiceButtonHandled = false;
+ } else if (mVoiceButtonDown && !mVoiceButtonHandled && isLongPress) {
+ mVoiceButtonHandled = true;
+ startVoiceInput(needWakeLock);
+ }
+ } else if (action == KeyEvent.ACTION_UP) {
+ if (mVoiceButtonDown) {
+ mVoiceButtonDown = false;
+ if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
+ // Resend the down then send this event through
+ KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+ downEvent, needWakeLock);
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+ keyEvent, needWakeLock);
+ }
+ }
+ }
+ }
+
+ private void dispatchMediaKeyEventLocked(String packageName, int pid, int uid,
+ boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
+ MediaSessionRecord session = mCurrentFullUserRecord.getMediaButtonSessionLocked();
+ if (session != null) {
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Sending " + keyEvent + " to " + session);
+ }
+ if (needWakeLock) {
+ mKeyEventReceiver.aquireWakeLockLocked();
+ }
+ // If we don't need a wakelock use -1 as the id so we won't release it later.
+ session.sendMediaButton(packageName, pid, uid, asSystemService, keyEvent,
+ needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
+ mKeyEventReceiver);
+ if (mCurrentFullUserRecord.mCallback != null) {
+ try {
+ mCurrentFullUserRecord.mCallback.onMediaKeyEventDispatchedToMediaSession(
+ keyEvent, new MediaSession.Token(session.getControllerBinder()));
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send callback", e);
+ }
+ }
+ } else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null
+ || mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
+ if (needWakeLock) {
+ mKeyEventReceiver.aquireWakeLockLocked();
+ }
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+ // TODO: Find a way to also send PID/UID in secure way.
+ String callerPackageName =
+ (asSystemService) ? mContext.getPackageName() : packageName;
+ mediaButtonIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, callerPackageName);
+ try {
+ if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) {
+ PendingIntent receiver = mCurrentFullUserRecord.mLastMediaButtonReceiver;
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Sending " + keyEvent
+ + " to the last known PendingIntent " + receiver);
+ }
+ receiver.send(mContext,
+ needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
+ mediaButtonIntent, mKeyEventReceiver, mHandler);
+ if (mCurrentFullUserRecord.mCallback != null) {
+ ComponentName componentName = mCurrentFullUserRecord
+ .mLastMediaButtonReceiver.getIntent().getComponent();
+ if (componentName != null) {
+ mCurrentFullUserRecord.mCallback
+ .onMediaKeyEventDispatchedToMediaButtonReceiver(
+ keyEvent, componentName);
+ }
+ }
+ } else {
+ ComponentName receiver =
+ mCurrentFullUserRecord.mRestoredMediaButtonReceiver;
+ int componentType = mCurrentFullUserRecord
+ .mRestoredMediaButtonReceiverComponentType;
+ UserHandle userHandle = UserHandle.of(mCurrentFullUserRecord
+ .mRestoredMediaButtonReceiverUserId);
+ if (DEBUG_KEY_EVENT) {
+ Log.d(TAG, "Sending " + keyEvent + " to the restored intent "
+ + receiver + ", type=" + componentType);
+ }
+ mediaButtonIntent.setComponent(receiver);
+ try {
+ switch (componentType) {
+ case FullUserRecord.COMPONENT_TYPE_ACTIVITY:
+ mContext.startActivityAsUser(mediaButtonIntent, userHandle);
+ break;
+ case FullUserRecord.COMPONENT_TYPE_SERVICE:
+ mContext.startForegroundServiceAsUser(mediaButtonIntent,
+ userHandle);
+ break;
+ default:
+ // Legacy behavior for other cases.
+ mContext.sendBroadcastAsUser(mediaButtonIntent, userHandle);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Error sending media button to the restored intent "
+ + receiver + ", type=" + componentType, e);
+ }
+ if (mCurrentFullUserRecord.mCallback != null) {
+ mCurrentFullUserRecord.mCallback
+ .onMediaKeyEventDispatchedToMediaButtonReceiver(
+ keyEvent, receiver);
+ }
+ }
+ } catch (CanceledException e) {
+ Log.i(TAG, "Error sending key event to media button receiver "
+ + mCurrentFullUserRecord.mLastMediaButtonReceiver, e);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send callback", e);
+ }
+ }
+ }
+
+ private void startVoiceInput(boolean needWakeLock) {
+ Intent voiceIntent = null;
+ // select which type of search to launch:
+ // - screen on and device unlocked: action is ACTION_WEB_SEARCH
+ // - device locked or screen off: action is
+ // ACTION_VOICE_SEARCH_HANDS_FREE
+ // with EXTRA_SECURE set to true if the device is securely locked
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ boolean isLocked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
+ if (!isLocked && pm.isScreenOn()) {
+ voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
+ Log.i(TAG, "voice-based interactions: about to use ACTION_WEB_SEARCH");
+ } else {
+ voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE);
+ voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE,
+ isLocked && mKeyguardManager.isKeyguardSecure());
+ Log.i(TAG, "voice-based interactions: about to use ACTION_VOICE_SEARCH_HANDS_FREE");
+ }
+ // start the search activity
+ if (needWakeLock) {
+ mMediaEventWakeLock.acquire();
+ }
+ try {
+ if (voiceIntent != null) {
+ voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ if (DEBUG) Log.d(TAG, "voiceIntent: " + voiceIntent);
+ mContext.startActivityAsUser(voiceIntent, UserHandle.CURRENT);
+ }
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "No activity for search: " + e);
+ } finally {
+ if (needWakeLock) {
+ mMediaEventWakeLock.release();
+ }
+ }
+ }
+
+ private boolean isVoiceKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || (!mHasFeatureLeanback && keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ }
+
+ private boolean isUserSetupComplete() {
+ return Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
+ }
+
+ // we only handle public stream types, which are 0-5
+ private boolean isValidLocalStreamType(int streamType) {
+ return streamType >= AudioManager.STREAM_VOICE_CALL
+ && streamType <= AudioManager.STREAM_NOTIFICATION;
+ }
+
+ private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable {
+ private final String mPackageName;
+ private final int mPid;
+ private final int mUid;
+ private final boolean mAsSystemService;
+ private final KeyEvent mKeyEvent;
+ private final boolean mNeedWakeLock;
+ private boolean mHandled;
+
+ private MediaKeyListenerResultReceiver(String packageName, int pid, int uid,
+ boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
+ super(mHandler);
+ mHandler.postDelayed(this, MEDIA_KEY_LISTENER_TIMEOUT);
+ mPackageName = packageName;
+ mPid = pid;
+ mUid = uid;
+ mAsSystemService = asSystemService;
+ mKeyEvent = keyEvent;
+ mNeedWakeLock = needWakeLock;
+ }
+
+ @Override
+ public void run() {
+ Log.d(TAG, "The media key listener is timed-out for " + mKeyEvent);
+ dispatchMediaKeyEvent();
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == MediaSessionManager.RESULT_MEDIA_KEY_HANDLED) {
+ mHandled = true;
+ mHandler.removeCallbacks(this);
+ return;
+ }
+ dispatchMediaKeyEvent();
+ }
+
+ private void dispatchMediaKeyEvent() {
+ if (mHandled) {
+ return;
+ }
+ mHandled = true;
+ mHandler.removeCallbacks(this);
+ synchronized (mLock) {
+ if (!isGlobalPriorityActiveLocked()
+ && isVoiceKey(mKeyEvent.getKeyCode())) {
+ handleVoiceKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
+ mKeyEvent, mNeedWakeLock);
+ } else {
+ dispatchMediaKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
+ mKeyEvent, mNeedWakeLock);
+ }
+ }
+ }
+ }
+
+ private KeyEventWakeLockReceiver mKeyEventReceiver = new KeyEventWakeLockReceiver(mHandler);
+
+ class KeyEventWakeLockReceiver extends ResultReceiver implements Runnable,
+ PendingIntent.OnFinished {
+ private final Handler mHandler;
+ private int mRefCount = 0;
+ private int mLastTimeoutId = 0;
+
+ KeyEventWakeLockReceiver(Handler handler) {
+ super(handler);
+ mHandler = handler;
+ }
+
+ public void onTimeout() {
+ synchronized (mLock) {
+ if (mRefCount == 0) {
+ // We've already released it, so just return
+ return;
+ }
+ mLastTimeoutId++;
+ mRefCount = 0;
+ releaseWakeLockLocked();
+ }
+ }
+
+ public void aquireWakeLockLocked() {
+ if (mRefCount == 0) {
+ mMediaEventWakeLock.acquire();
+ }
+ mRefCount++;
+ mHandler.removeCallbacks(this);
+ mHandler.postDelayed(this, WAKELOCK_TIMEOUT);
+
+ }
+
+ @Override
+ public void run() {
+ onTimeout();
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode < mLastTimeoutId) {
+ // Ignore results from calls that were before the last
+ // timeout, just in case.
+ return;
+ } else {
+ synchronized (mLock) {
+ if (mRefCount > 0) {
+ mRefCount--;
+ if (mRefCount == 0) {
+ releaseWakeLockLocked();
+ }
+ }
+ }
+ }
+ }
+
+ private void releaseWakeLockLocked() {
+ mMediaEventWakeLock.release();
+ mHandler.removeCallbacks(this);
+ }
+
+ @Override
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ onReceiveResult(resultCode, null);
+ }
+ };
+
+ BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ return;
+ }
+ synchronized (mLock) {
+ if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)
+ && mMediaEventWakeLock.isHeld()) {
+ mMediaEventWakeLock.release();
+ }
+ }
+ }
+ };
+ }
+
+ final class MessageHandler extends Handler {
+ private static final int MSG_SESSIONS_CHANGED = 1;
+ private static final int MSG_VOLUME_INITIAL_DOWN = 2;
+ private final SparseArray<Integer> mIntegerCache = new SparseArray<>();
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SESSIONS_CHANGED:
+ pushSessionsChanged((int) msg.obj);
+ break;
+ case MSG_VOLUME_INITIAL_DOWN:
+ synchronized (mLock) {
+ FullUserRecord user = mUserRecords.get((int) msg.arg1);
+ if (user != null && user.mInitialDownVolumeKeyEvent != null) {
+ dispatchVolumeKeyLongPressLocked(user.mInitialDownVolumeKeyEvent);
+ // Mark that the key is already handled.
+ user.mInitialDownVolumeKeyEvent = null;
+ }
+ }
+ break;
+ }
+ }
+
+ public void postSessionsChanged(int userId) {
+ // Use object instead of the arguments when posting message to remove pending requests.
+ Integer userIdInteger = mIntegerCache.get(userId);
+ if (userIdInteger == null) {
+ userIdInteger = Integer.valueOf(userId);
+ mIntegerCache.put(userId, userIdInteger);
+ }
+ removeMessages(MSG_SESSIONS_CHANGED, userIdInteger);
+ obtainMessage(MSG_SESSIONS_CHANGED, userIdInteger).sendToTarget();
+ }
+ }
+
+ private class Controller2Callback extends MediaController2.ControllerCallback {
+ private final Session2Token mToken;
+
+ Controller2Callback(Session2Token token) {
+ mToken = token;
+ }
+
+ @Override
+ public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
+ synchronized (mLock) {
+ int userId = UserHandle.getUserId(mToken.getUid());
+ mSession2TokensPerUser.get(userId).add(mToken);
+ pushSession2TokensChangedLocked(userId);
+ }
+ }
+
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ synchronized (mLock) {
+ int userId = UserHandle.getUserId(mToken.getUid());
+ mSession2TokensPerUser.get(userId).remove(mToken);
+ pushSession2TokensChangedLocked(userId);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/dex/DexLogger.java b/services/core/java/com/android/server/pm/dex/DexLogger.java
index 68a755b..78fa82c 100644
--- a/services/core/java/com/android/server/pm/dex/DexLogger.java
+++ b/services/core/java/com/android/server/pm/dex/DexLogger.java
@@ -28,7 +28,6 @@
import android.util.Slog;
import android.util.SparseArray;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
@@ -53,21 +52,18 @@
private final IPackageManager mPackageManager;
private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
- private final Object mInstallLock;
- @GuardedBy("mInstallLock")
private final Installer mInstaller;
- public DexLogger(IPackageManager pms, Installer installer, Object installLock) {
- this(pms, installer, installLock, new PackageDynamicCodeLoading());
+ public DexLogger(IPackageManager pms, Installer installer) {
+ this(pms, installer, new PackageDynamicCodeLoading());
}
@VisibleForTesting
- DexLogger(IPackageManager pms, Installer installer, Object installLock,
+ DexLogger(IPackageManager pms, Installer installer,
PackageDynamicCodeLoading packageDynamicCodeLoading) {
mPackageManager = pms;
mPackageDynamicCodeLoading = packageDynamicCodeLoading;
mInstaller = installer;
- mInstallLock = installLock;
}
public Set<String> getAllPackagesWithDynamicCodeLoading() {
@@ -131,14 +127,16 @@
}
byte[] hash = null;
- synchronized (mInstallLock) {
- try {
- hash = mInstaller.hashSecondaryDexFile(filePath, packageName, appInfo.uid,
- appInfo.volumeUuid, storageFlags);
- } catch (InstallerException e) {
- Slog.e(TAG, "Got InstallerException when hashing file " + filePath
- + ": " + e.getMessage());
- }
+ try {
+ // Note that we do not take the install lock here. Hashing should never interfere
+ // with app update/compilation/removal. We may get anomalous results if a file
+ // changes while we hash it, but that can happen anyway and is harmless for our
+ // purposes.
+ hash = mInstaller.hashSecondaryDexFile(filePath, packageName, appInfo.uid,
+ appInfo.volumeUuid, storageFlags);
+ } catch (InstallerException e) {
+ Slog.e(TAG, "Got InstallerException when hashing file " + filePath
+ + ": " + e.getMessage());
}
String fileName = new File(filePath).getName();
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index e57d9d7..b546836 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -129,7 +129,7 @@
mPackageDexOptimizer = pdo;
mInstaller = installer;
mInstallLock = installLock;
- mDexLogger = new DexLogger(pms, installer, installLock);
+ mDexLogger = new DexLogger(pms, installer);
}
public DexLogger getDexLogger() {
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 4a553cf..e944858 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -1278,28 +1278,28 @@
}
/**
- * Checks if the root activity requires a particular orientation (either by override or
+ * Checks if the top activity requires a particular orientation (either by override or
* activityInfo) and returns that. Otherwise, this returns ORIENTATION_UNDEFINED.
*/
- private int getRootActivityRequestedOrientation() {
- ActivityRecord root = getRootActivity();
+ private int getTopActivityRequestedOrientation() {
+ ActivityRecord top = getTopActivity();
if (getRequestedOverrideConfiguration().orientation != ORIENTATION_UNDEFINED
- || root == null) {
+ || top == null) {
return getRequestedOverrideConfiguration().orientation;
}
- int rootScreenOrientation = root.getOrientation();
- if (rootScreenOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
+ int screenOrientation = top.getOrientation();
+ if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
// NOSENSOR means the display's "natural" orientation, so return that.
ActivityDisplay display = mStack != null ? mStack.getDisplay() : null;
if (display != null && display.mDisplayContent != null) {
return mStack.getDisplay().mDisplayContent.getNaturalOrientation();
}
- } else if (rootScreenOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
+ } else if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
// LOCKED means the activity's orientation remains unchanged, so return existing value.
- return root.getConfiguration().orientation;
- } else if (ActivityInfo.isFixedOrientationLandscape(rootScreenOrientation)) {
+ return top.getConfiguration().orientation;
+ } else if (ActivityInfo.isFixedOrientationLandscape(screenOrientation)) {
return ORIENTATION_LANDSCAPE;
- } else if (ActivityInfo.isFixedOrientationPortrait(rootScreenOrientation)) {
+ } else if (ActivityInfo.isFixedOrientationPortrait(screenOrientation)) {
return ORIENTATION_PORTRAIT;
}
return ORIENTATION_UNDEFINED;
@@ -2196,9 +2196,9 @@
// In FULLSCREEN mode, always start with empty bounds to indicate "fill parent"
outOverrideBounds.setEmpty();
- // If the task or its root activity require a different orientation, make it fit the
+ // If the task or its top activity requires a different orientation, make it fit the
// available bounds by scaling down its bounds.
- int forcedOrientation = getRootActivityRequestedOrientation();
+ int forcedOrientation = getTopActivityRequestedOrientation();
if (forcedOrientation != ORIENTATION_UNDEFINED
&& forcedOrientation != newParentConfig.orientation) {
final Rect parentBounds = newParentConfig.windowConfiguration.getBounds();
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index f1cd0cd..57ee6dc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -33,6 +33,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -43,7 +44,12 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
+import android.app.AppGlobals;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
import android.app.job.JobInfo;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
@@ -56,13 +62,16 @@
import android.os.BatteryManagerInternal;
import android.os.Handler;
import android.os.Looper;
+import android.os.RemoteException;
import android.os.SystemClock;
+import android.util.SparseBooleanArray;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.JobStore;
import com.android.server.job.controllers.QuotaController.ExecutionStats;
import com.android.server.job.controllers.QuotaController.TimingSession;
@@ -96,9 +105,13 @@
private BroadcastReceiver mChargingReceiver;
private Constants mConstants;
private QuotaController mQuotaController;
+ private int mSourceUid;
+ private IUidObserver mUidObserver;
private MockitoSession mMockingSession;
@Mock
+ private ActivityManagerInternal mActivityMangerInternal;
+ @Mock
private AlarmManager mAlarmManager;
@Mock
private Context mContext;
@@ -107,6 +120,8 @@
@Mock
private UsageStatsManagerInternal mUsageStatsManager;
+ private JobStore mJobStore;
+
@Before
public void setUp() {
mMockingSession = mockitoSession()
@@ -123,8 +138,17 @@
when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
// Called in QuotaController constructor.
+ IActivityManager activityManager = ActivityManager.getService();
+ spyOn(activityManager);
+ try {
+ doNothing().when(activityManager).registerUidObserver(any(), anyInt(), anyInt(), any());
+ } catch (RemoteException e) {
+ fail("registerUidObserver threw exception: " + e.getMessage());
+ }
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+ doReturn(mActivityMangerInternal)
+ .when(() -> LocalServices.getService(ActivityManagerInternal.class));
doReturn(mock(BatteryManagerInternal.class))
.when(() -> LocalServices.getService(BatteryManagerInternal.class));
doReturn(mUsageStatsManager)
@@ -132,6 +156,9 @@
// Used in JobStatus.
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
+ // Used in QuotaController.Handler.
+ mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
+ when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore);
// Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
// in the past, and QuotaController sometimes floors values at 0, so if the test time
@@ -150,10 +177,23 @@
// Capture the listeners.
ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
+ ArgumentCaptor<IUidObserver> uidObserverCaptor =
+ ArgumentCaptor.forClass(IUidObserver.class);
mQuotaController = new QuotaController(mJobSchedulerService);
verify(mContext).registerReceiver(receiverCaptor.capture(), any());
mChargingReceiver = receiverCaptor.getValue();
+ try {
+ verify(activityManager).registerUidObserver(
+ uidObserverCaptor.capture(),
+ eq(ActivityManager.UID_OBSERVER_PROCSTATE),
+ eq(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE),
+ any());
+ mUidObserver = uidObserverCaptor.getValue();
+ mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
}
@After
@@ -182,6 +222,25 @@
mChargingReceiver.onReceive(mContext, intent);
}
+ private void setProcessState(int procState) {
+ try {
+ doReturn(procState).when(mActivityMangerInternal).getUidProcessState(mSourceUid);
+ SparseBooleanArray foregroundUids = mQuotaController.getForegroundUids();
+ spyOn(foregroundUids);
+ mUidObserver.onUidStateChanged(mSourceUid, procState, 0);
+ if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ verify(foregroundUids, timeout(SECOND_IN_MILLIS).times(1))
+ .put(eq(mSourceUid), eq(true));
+ assertTrue(foregroundUids.get(mSourceUid));
+ } else {
+ verify(foregroundUids, timeout(SECOND_IN_MILLIS).times(1)).delete(eq(mSourceUid));
+ assertFalse(foregroundUids.get(mSourceUid));
+ }
+ } catch (RemoteException e) {
+ fail("registerUidObserver threw exception: " + e.getMessage());
+ }
+ }
+
private void setStandbyBucket(int bucketIndex) {
int bucket;
switch (bucketIndex) {
@@ -204,9 +263,18 @@
anyLong())).thenReturn(bucket);
}
- private void setStandbyBucket(int bucketIndex, JobStatus job) {
+ private void setStandbyBucket(int bucketIndex, JobStatus... jobs) {
setStandbyBucket(bucketIndex);
- job.setStandbyBucket(bucketIndex);
+ for (JobStatus job : jobs) {
+ job.setStandbyBucket(bucketIndex);
+ }
+ }
+
+ private void trackJobs(JobStatus... jobs) {
+ for (JobStatus job : jobs) {
+ mJobStore.add(job);
+ mQuotaController.maybeStartTrackingJobLocked(job, null);
+ }
}
private JobStatus createJobStatus(String testTag, int jobId) {
@@ -214,8 +282,11 @@
new ComponentName(mContext, "TestQuotaJobService"))
.setMinimumLatency(Math.abs(jobId) + 1)
.build();
- return JobStatus.createFromJobInfo(
+ JobStatus js = JobStatus.createFromJobInfo(
jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ // Make sure tests aren't passing just because the default bucket is likely ACTIVE.
+ js.setStandbyBucket(FREQUENT_INDEX);
+ return js;
}
private TimingSession createTimingSession(long start, long duration, int count) {
@@ -709,6 +780,7 @@
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1);
+ setStandbyBucket(standbyBucket, jobStatus);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
mQuotaController.prepareForExecutionLocked(jobStatus);
advanceElapsedClock(5 * MINUTE_IN_MILLIS);
@@ -1339,19 +1411,23 @@
setDischarging();
JobStatus jobStatus = createJobStatus("testTimerTracking_AllForeground", 1);
- jobStatus.uidActive = true;
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
mQuotaController.prepareForExecutionLocked(jobStatus);
advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ // Change to a state that should still be considered foreground.
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
}
/**
- * Tests that Timers properly track overlapping foreground and background jobs.
+ * Tests that Timers properly track sessions when switching between foreground and background
+ * states.
*/
@Test
public void testTimerTracking_ForegroundAndBackground() {
@@ -1360,7 +1436,6 @@
JobStatus jobBg1 = createJobStatus("testTimerTracking_ForegroundAndBackground", 1);
JobStatus jobBg2 = createJobStatus("testTimerTracking_ForegroundAndBackground", 2);
JobStatus jobFg3 = createJobStatus("testTimerTracking_ForegroundAndBackground", 3);
- jobFg3.uidActive = true;
mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
@@ -1368,6 +1443,7 @@
List<TimingSession> expected = new ArrayList<>();
// UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
long start = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.prepareForExecutionLocked(jobBg1);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
@@ -1379,48 +1455,223 @@
// Bg job starts while inactive, spans an entire active session, and ends after the
// active session.
- // Fg job starts after the bg job and ends before the bg job.
- // Entire bg job duration should be counted since it started before active session. However,
- // count should only be 1 since Timer shouldn't count fg jobs.
+ // App switching to foreground state then fg job starts.
+ // App remains in foreground state after coming to foreground, so there should only be one
+ // session.
start = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
mQuotaController.prepareForExecutionLocked(jobBg2);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
mQuotaController.prepareForExecutionLocked(jobFg3);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
- expected.add(createTimingSession(start, 30 * SECOND_IN_MILLIS, 1));
assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
advanceElapsedClock(SECOND_IN_MILLIS);
// Bg job 1 starts, then fg job starts. Bg job 1 job ends. Shortly after, uid goes
// "inactive" and then bg job 2 starts. Then fg job ends.
- // This should result in two TimingSessions with a count of one each.
+ // This should result in two TimingSessions:
+ // * The first should have a count of 1
+ // * The second should have a count of 2 since it will include both jobs
start = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
mQuotaController.maybeStartTrackingJobLocked(jobFg3, null);
+ setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY);
mQuotaController.prepareForExecutionLocked(jobBg1);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
mQuotaController.prepareForExecutionLocked(jobFg3);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
- expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
mQuotaController.prepareForExecutionLocked(jobBg2);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
- expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
}
/**
+ * Tests that Timers properly track overlapping top and background jobs.
+ */
+ @Test
+ public void testTimerTracking_TopAndNonTop() {
+ setDischarging();
+
+ JobStatus jobBg1 = createJobStatus("testTimerTracking_TopAndNonTop", 1);
+ JobStatus jobBg2 = createJobStatus("testTimerTracking_TopAndNonTop", 2);
+ JobStatus jobFg1 = createJobStatus("testTimerTracking_TopAndNonTop", 3);
+ JobStatus jobTop = createJobStatus("testTimerTracking_TopAndNonTop", 4);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobFg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobTop, null);
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ List<TimingSession> expected = new ArrayList<>();
+
+ // UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job starts while inactive, spans an entire active session, and ends after the
+ // active session.
+ // App switching to top state then fg job starts.
+ // App remains in top state after coming to top, so there should only be one
+ // session.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ advanceElapsedClock(SECOND_IN_MILLIS);
+
+ // Bg job 1 starts, then top job starts. Bg job 1 job ends. Then app goes to
+ // foreground_service and a new job starts. Shortly after, uid goes
+ // "inactive" and then bg job 2 starts. Then top job ends, followed by bg and fg jobs.
+ // This should result in two TimingSessions:
+ // * The first should have a count of 1
+ // * The second should have a count of 2, which accounts for the bg2 and fg, but not top
+ // jobs.
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.maybeStartTrackingJobLocked(jobBg1, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobBg2, null);
+ mQuotaController.maybeStartTrackingJobLocked(jobTop, null);
+ setProcessState(ActivityManager.PROCESS_STATE_LAST_ACTIVITY);
+ mQuotaController.prepareForExecutionLocked(jobBg1);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ mQuotaController.prepareForExecutionLocked(jobFg1);
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
+ start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ mQuotaController.prepareForExecutionLocked(jobBg2);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+ advanceElapsedClock(10 * SECOND_IN_MILLIS);
+ mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+ mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+ expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
+ assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ /**
+ * Tests that TOP jobs aren't stopped when an app runs out of quota.
+ */
+ @Test
+ public void testTracking_OutOfQuota_ForegroundAndBackground() {
+ setDischarging();
+
+ JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1);
+ JobStatus jobTop = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 2);
+ trackJobs(jobBg, jobTop);
+ setStandbyBucket(WORKING_INDEX, jobTop, jobBg); // 2 hour window
+ // Now the package only has 20 seconds to run.
+ final long remainingTimeMs = 20 * SECOND_IN_MILLIS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
+ 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
+
+ InOrder inOrder = inOrder(mJobSchedulerService);
+
+ // UID starts out inactive.
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+ // Start the job.
+ mQuotaController.prepareForExecutionLocked(jobBg);
+ advanceElapsedClock(remainingTimeMs / 2);
+ // New job starts after UID is in the foreground. Since the app is now in the foreground, it
+ // should continue to have remainingTimeMs / 2 time remaining.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ advanceElapsedClock(remainingTimeMs);
+
+ // Wait for some extra time to allow for job processing.
+ inOrder.verify(mJobSchedulerService,
+ timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
+ .onControllerStateChanged();
+ assertEquals(remainingTimeMs / 2, mQuotaController.getRemainingExecutionTimeLocked(jobBg));
+ assertEquals(remainingTimeMs / 2, mQuotaController.getRemainingExecutionTimeLocked(jobTop));
+ // Go to a background state.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
+ advanceElapsedClock(remainingTimeMs / 2 + 1);
+ inOrder.verify(mJobSchedulerService,
+ timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ // Top job should still be allowed to run.
+ assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+ // New jobs to run.
+ JobStatus jobBg2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 3);
+ JobStatus jobTop2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 4);
+ JobStatus jobFg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 5);
+ setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg);
+
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ trackJobs(jobFg, jobTop);
+ mQuotaController.prepareForExecutionLocked(jobTop);
+ assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+ // App still in foreground so everything should be in quota.
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+
+ advanceElapsedClock(20 * SECOND_IN_MILLIS);
+ setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
+ inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
+ .onControllerStateChanged();
+ // App is now in background and out of quota. Fg should now change to out of quota since it
+ // wasn't started. Top should remain in quota since it started when the app was in TOP.
+ assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ trackJobs(jobBg2);
+ assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ }
+
+ /**
* Tests that a job is properly updated and JobSchedulerService is notified when a job reaches
* its quota.
*/
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
index 3b6b48b..f817e8e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
@@ -77,7 +77,6 @@
@Mock IPackageManager mPM;
@Mock Installer mInstaller;
- private final Object mInstallLock = new Object();
private PackageDynamicCodeLoading mPackageDynamicCodeLoading;
private DexLogger mDexLogger;
@@ -103,7 +102,7 @@
};
// For test purposes capture log messages as well as sending to the event log.
- mDexLogger = new DexLogger(mPM, mInstaller, mInstallLock, mPackageDynamicCodeLoading) {
+ mDexLogger = new DexLogger(mPM, mInstaller, mPackageDynamicCodeLoading) {
@Override
void writeDclEvent(int uid, String message) {
super.writeDclEvent(uid, message);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index fa42289..51bebbb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -39,7 +39,9 @@
import androidx.test.filters.SmallTest;
+import org.junit.AfterClass;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
/**
@@ -52,15 +54,34 @@
@Presubmit
public class AppTransitionTests extends WindowTestsBase {
+ private static RootWindowContainer sOriginalRootWindowContainer;
+
private DisplayContent mDc;
+ @BeforeClass
+ public static void setUpRootWindowContainerMock() {
+ final WindowManagerService wm = WmServiceUtils.getWindowManagerService();
+ // For unit test, we don't need to test performSurfacePlacement to prevent some abnormal
+ // interaction with surfaceflinger native side.
+ sOriginalRootWindowContainer = wm.mRoot;
+ // Creating spied mock of RootWindowContainer shouldn't be done in @Before, since it will
+ // create unnecessary nested spied objects chain, because WindowManagerService object under
+ // test is a single instance shared among all tests that extend WindowTestsBase class.
+ // Instead it should be done once before running all tests in this test class.
+ wm.mRoot = spy(wm.mRoot);
+ doNothing().when(wm.mRoot).performSurfacePlacement(anyBoolean());
+ }
+
+ @AfterClass
+ public static void tearDownRootWindowContainerMock() {
+ final WindowManagerService wm = WmServiceUtils.getWindowManagerService();
+ wm.mRoot = sOriginalRootWindowContainer;
+ sOriginalRootWindowContainer = null;
+ }
+
@Before
public void setUp() throws Exception {
mDc = mWm.getDefaultDisplayContentLocked();
- // For unit test, we don't need to test performSurfacePlacement to prevent some
- // abnormal interaction with surfaceflinger native side.
- mWm.mRoot = spy(mWm.mRoot);
- doNothing().when(mWm.mRoot).performSurfacePlacement(anyBoolean());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
index 8a98cbe..cdb578d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
@@ -217,12 +217,17 @@
info.logicalHeight = fullScreenBounds.height();
ActivityDisplay display = addNewActivityDisplayAt(info, POSITION_TOP);
assertTrue(mRootActivityContainer.getActivityDisplay(display.mDisplayId) != null);
+ // Override display orientation. Normally this is available via DisplayContent, but DC
+ // is mocked-out.
+ display.getRequestedOverrideConfiguration().orientation =
+ Configuration.ORIENTATION_LANDSCAPE;
+ display.onRequestedOverrideConfigurationChanged(
+ display.getRequestedOverrideConfiguration());
ActivityStack stack = new StackBuilder(mRootActivityContainer)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
TaskRecord task = stack.getChildAt(0);
- ActivityRecord root = task.getRootActivity();
- ActivityRecord top = new ActivityBuilder(mService).setTask(task).setStack(stack).build();
- assertEquals(root, task.getRootActivity());
+ ActivityRecord root = task.getTopActivity();
+ assertEquals(root, task.getTopActivity());
assertEquals(fullScreenBounds, task.getBounds());
@@ -233,16 +238,22 @@
assertTrue(task.getBounds().width() < task.getBounds().height());
assertEquals(fullScreenBounds.height(), task.getBounds().height());
- // Setting non-root app has no effect
- setActivityRequestedOrientation(root, SCREEN_ORIENTATION_LANDSCAPE);
- assertTrue(task.getBounds().width() < task.getBounds().height());
+ // Top activity gets used
+ ActivityRecord top = new ActivityBuilder(mService).setTask(task).setStack(stack).build();
+ assertEquals(top, task.getTopActivity());
+ setActivityRequestedOrientation(top, SCREEN_ORIENTATION_LANDSCAPE);
+ assertTrue(task.getBounds().width() > task.getBounds().height());
+ assertEquals(task.getBounds().width(), fullScreenBounds.width());
// Setting app to unspecified restores
- setActivityRequestedOrientation(root, SCREEN_ORIENTATION_UNSPECIFIED);
+ setActivityRequestedOrientation(top, SCREEN_ORIENTATION_UNSPECIFIED);
assertEquals(fullScreenBounds, task.getBounds());
// Setting app to fixed landscape and changing display
- setActivityRequestedOrientation(root, SCREEN_ORIENTATION_LANDSCAPE);
+ setActivityRequestedOrientation(top, SCREEN_ORIENTATION_LANDSCAPE);
+ // simulate display orientation changing (normally done via DisplayContent)
+ display.getRequestedOverrideConfiguration().orientation =
+ Configuration.ORIENTATION_PORTRAIT;
display.setBounds(fullScreenBoundsPort);
assertTrue(task.getBounds().width() > task.getBounds().height());
assertEquals(fullScreenBoundsPort.width(), task.getBounds().width());
diff --git a/telephony/java/android/telephony/AvailableNetworkInfo.java b/telephony/java/android/telephony/AvailableNetworkInfo.java
index fe07370..4da79b3 100644
--- a/telephony/java/android/telephony/AvailableNetworkInfo.java
+++ b/telephony/java/android/telephony/AvailableNetworkInfo.java
@@ -110,6 +110,7 @@
private AvailableNetworkInfo(Parcel in) {
mSubId = in.readInt();
mPriority = in.readInt();
+ mMccMncs = new ArrayList<>();
in.readStringList(mMccMncs);
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index babeb7b..3311218 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -9963,7 +9963,7 @@
boolean ret = false;
try {
IOns iOpportunisticNetworkService = getIOns();
- if (iOpportunisticNetworkService != null) {
+ if (iOpportunisticNetworkService != null && availableNetworks != null) {
ret = iOpportunisticNetworkService.updateAvailableNetworks(availableNetworks,
pkgForDebug);
}
diff --git a/telephony/java/com/android/internal/telephony/DctConstants.java b/telephony/java/com/android/internal/telephony/DctConstants.java
index 96f7a1b..3408291 100644
--- a/telephony/java/com/android/internal/telephony/DctConstants.java
+++ b/telephony/java/com/android/internal/telephony/DctConstants.java
@@ -92,6 +92,7 @@
public static final int EVENT_DATA_RECONNECT = BASE + 47;
public static final int EVENT_ROAMING_SETTING_CHANGE = BASE + 48;
public static final int EVENT_DATA_SERVICE_BINDING_CHANGED = BASE + 49;
+ public static final int EVENT_DEVICE_PROVISIONED_CHANGE = BASE + 50;
/***** Constants *****/
diff --git a/telephony/java/com/android/internal/telephony/TelephonyIntents.java b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
index 2a648bd..8523554 100644
--- a/telephony/java/com/android/internal/telephony/TelephonyIntents.java
+++ b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
@@ -501,4 +501,18 @@
*/
public static final String ACTION_LINE1_NUMBER_ERROR_DETECTED =
"com.android.internal.telephony.ACTION_LINE1_NUMBER_ERROR_DETECTED";
+
+ /**
+ * Broadcast action to notify radio bug.
+ *
+ * Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * @hide
+ */
+ public static final String ACTION_REPORT_RADIO_BUG =
+ "com.android.internal.telephony.ACTION_REPORT_RADIO_BUG";
+
+ // ACTION_REPORT_RADIO_BUG extra keys
+ public static final String EXTRA_SLOT_ID = "slotId";
+ public static final String EXTRA_RADIO_BUG_TYPE = "radioBugType";
}
diff --git a/tests/net/java/android/net/LinkPropertiesTest.java b/tests/net/java/android/net/LinkPropertiesTest.java
index 932fee0..299fbef 100644
--- a/tests/net/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/java/android/net/LinkPropertiesTest.java
@@ -849,6 +849,18 @@
assertEquals(new ArraySet<>(expectRemoved), (new ArraySet<>(result.removed)));
}
+ private void assertParcelingIsLossless(LinkProperties source) {
+ Parcel p = Parcel.obtain();
+ source.writeToParcel(p, /* flags */ 0);
+ p.setDataPosition(0);
+ final byte[] marshalled = p.marshall();
+ p = Parcel.obtain();
+ p.unmarshall(marshalled, 0, marshalled.length);
+ p.setDataPosition(0);
+ LinkProperties dest = LinkProperties.CREATOR.createFromParcel(p);
+ assertEquals(source, dest);
+ }
+
@Test
public void testLinkPropertiesParcelable() throws Exception {
LinkProperties source = new LinkProperties();
@@ -870,15 +882,12 @@
source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96"));
- Parcel p = Parcel.obtain();
- source.writeToParcel(p, /* flags */ 0);
- p.setDataPosition(0);
- final byte[] marshalled = p.marshall();
- p = Parcel.obtain();
- p.unmarshall(marshalled, 0, marshalled.length);
- p.setDataPosition(0);
- LinkProperties dest = LinkProperties.CREATOR.createFromParcel(p);
+ assertParcelingIsLossless(source);
+ }
- assertEquals(source, dest);
+ @Test
+ public void testParcelUninitialized() throws Exception {
+ LinkProperties empty = new LinkProperties();
+ assertParcelingIsLossless(empty);
}
}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index bf39644..2a92a7d 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -4683,7 +4683,7 @@
mCellNetworkAgent.sendLinkProperties(cellLp);
mCellNetworkAgent.connect(true);
networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
- verify(mNetworkManagementService, times(1)).startClatd(MOBILE_IFNAME);
+ verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME);
Nat464Xlat clat = mService.getNat464Xlat(mCellNetworkAgent);
// Clat iface up, expect stack link updated.
@@ -4710,7 +4710,7 @@
mCellNetworkAgent.sendLinkProperties(cellLp);
waitForIdle();
networkCallback.expectCallback(CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
- verify(mNetworkManagementService, times(1)).stopClatd(MOBILE_IFNAME);
+ verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
// Clat iface removed, expect linkproperties revert to original one
clat.interfaceRemoved(CLAT_PREFIX + MOBILE_IFNAME);
diff --git a/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
index 4c52d81..9578ded 100644
--- a/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -32,11 +32,13 @@
import android.content.Context;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.INetd;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkMisc;
import android.net.NetworkStack;
+import android.os.INetworkManagementService;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.text.format.DateUtils;
@@ -66,6 +68,8 @@
LingerMonitor mMonitor;
@Mock ConnectivityService mConnService;
+ @Mock INetd mNetd;
+ @Mock INetworkManagementService mNMS;
@Mock Context mCtx;
@Mock NetworkMisc mMisc;
@Mock NetworkNotificationManager mNotifier;
@@ -352,7 +356,7 @@
caps.addCapability(0);
caps.addTransportType(transport);
NetworkAgentInfo nai = new NetworkAgentInfo(null, null, new Network(netId), info, null,
- caps, 50, mCtx, null, mMisc, mConnService);
+ caps, 50, mCtx, null, mMisc, mConnService, mNetd, mNMS);
nai.everValidated = true;
return nai;
}
diff --git a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
index bf42412..07b1d05 100644
--- a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -17,9 +17,7 @@
package com.android.server.connectivity;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -27,6 +25,7 @@
import static org.mockito.Mockito.when;
import android.net.ConnectivityManager;
+import android.net.INetd;
import android.net.InterfaceConfiguration;
import android.net.LinkAddress;
import android.net.LinkProperties;
@@ -57,6 +56,7 @@
@Mock ConnectivityService mConnectivity;
@Mock NetworkMisc mMisc;
+ @Mock INetd mNetd;
@Mock INetworkManagementService mNms;
@Mock InterfaceConfiguration mConfig;
@Mock NetworkAgentInfo mNai;
@@ -65,7 +65,7 @@
Handler mHandler;
Nat464Xlat makeNat464Xlat() {
- return new Nat464Xlat(mNms, mNai);
+ return new Nat464Xlat(mNai, mNetd, mNms);
}
@Before
@@ -129,7 +129,7 @@
nat.start();
verify(mNms).registerObserver(eq(nat));
- verify(mNms).startClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStart(eq(BASE_IFACE));
// Stacked interface up notification arrives.
nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -144,7 +144,7 @@
// ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
nat.stop();
- verify(mNms).stopClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStop(eq(BASE_IFACE));
// Stacked interface removed notification arrives.
nat.interfaceRemoved(STACKED_IFACE);
@@ -156,7 +156,7 @@
assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
assertIdle(nat);
- verifyNoMoreInteractions(mNms, mConnectivity);
+ verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
@Test
@@ -168,7 +168,7 @@
nat.start();
verify(mNms).registerObserver(eq(nat));
- verify(mNms).startClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStart(eq(BASE_IFACE));
// Stacked interface up notification arrives.
nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -185,7 +185,7 @@
mLooper.dispatchNext();
verify(mNms).unregisterObserver(eq(nat));
- verify(mNms).stopClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStop(eq(BASE_IFACE));
verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
assertTrue(c.getValue().getStackedLinks().isEmpty());
assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
@@ -194,7 +194,7 @@
// ConnectivityService stops clat: no-op.
nat.stop();
- verifyNoMoreInteractions(mNms, mConnectivity);
+ verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
@Test
@@ -205,13 +205,13 @@
nat.start();
verify(mNms).registerObserver(eq(nat));
- verify(mNms).startClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStart(eq(BASE_IFACE));
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
nat.stop();
verify(mNms).unregisterObserver(eq(nat));
- verify(mNms).stopClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStop(eq(BASE_IFACE));
assertIdle(nat);
// In-flight interface up notification arrives: no-op
@@ -225,7 +225,7 @@
assertIdle(nat);
- verifyNoMoreInteractions(mNms, mConnectivity);
+ verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
@Test
@@ -236,16 +236,16 @@
nat.start();
verify(mNms).registerObserver(eq(nat));
- verify(mNms).startClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStart(eq(BASE_IFACE));
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
nat.stop();
verify(mNms).unregisterObserver(eq(nat));
- verify(mNms).stopClatd(eq(BASE_IFACE));
+ verify(mNetd).clatdStop(eq(BASE_IFACE));
assertIdle(nat);
- verifyNoMoreInteractions(mNms, mConnectivity);
+ verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
static void assertIdle(Nat464Xlat nat) {