diff options
100 files changed, 5392 insertions, 2438 deletions
diff --git a/Android.bp b/Android.bp index c2024084d95d..b4d5e58b570b 100644 --- a/Android.bp +++ b/Android.bp @@ -305,6 +305,7 @@ java_defaults { "core/java/android/service/euicc/IUpdateSubscriptionNicknameCallback.aidl", "core/java/android/service/gatekeeper/IGateKeeperService.aidl", "core/java/android/service/contentcapture/IContentCaptureService.aidl", + "core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl", "core/java/android/service/notification/INotificationListener.aidl", "core/java/android/service/notification/IStatusBarNotificationHolder.aidl", "core/java/android/service/notification/IConditionListener.aidl", @@ -1317,7 +1318,6 @@ stubs_defaults { "ext", "framework", "voip-common", - "android.test.mock.impl", ], local_sourcepaths: frameworks_base_subdirs, installable: false, diff --git a/api/system-current.txt b/api/system-current.txt index 276be9dd746d..83d1da7619fc 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -94,7 +94,6 @@ package android { field public static final java.lang.String MANAGE_CONTENT_CAPTURE = "android.permission.MANAGE_CONTENT_CAPTURE"; field public static final java.lang.String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS"; field public static final java.lang.String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING"; - field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final java.lang.String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS"; field public static final java.lang.String MANAGE_ROLE_HOLDERS = "android.permission.MANAGE_ROLE_HOLDERS"; field public static final java.lang.String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS"; @@ -151,6 +150,7 @@ package android { field public static final java.lang.String REGISTER_CALL_PROVIDER = "android.permission.REGISTER_CALL_PROVIDER"; field public static final java.lang.String REGISTER_CONNECTION_MANAGER = "android.permission.REGISTER_CONNECTION_MANAGER"; field public static final java.lang.String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION"; + field public static final java.lang.String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER"; field public static final java.lang.String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES"; field public static final java.lang.String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; field public static final java.lang.String RESET_PASSWORD = "android.permission.RESET_PASSWORD"; @@ -4445,6 +4445,10 @@ package android.os { 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/api/system-removed.txt b/api/system-removed.txt index 4beb69920e28..5a7ec8bd09bd 100644 --- a/api/system-removed.txt +++ b/api/system-removed.txt @@ -1,3 +1,11 @@ +package android { + + public static final class Manifest.permission { + field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; + } + +} + package android.app { public class Notification implements android.os.Parcelable { diff --git a/api/test-current.txt b/api/test-current.txt index 71f02b9bdc9a..f8f49f832b13 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -509,6 +509,10 @@ package android.graphics { method public static android.graphics.ImageDecoder.Source createSource(android.content.res.Resources, java.io.InputStream, int); } + public class Paint { + method public void setColor(long); + } + } package android.graphics.drawable { diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index f4a17159b722..453a0c033a18 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -25,6 +25,7 @@ import "frameworks/base/core/proto/android/app/enums.proto"; 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 @@ message Atom { 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 @@ message BluetoothConnectionStateChanged { 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/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt index 4d3c5d195090..5392a3c428c1 100644 --- a/config/hiddenapi-greylist.txt +++ b/config/hiddenapi-greylist.txt @@ -1492,7 +1492,6 @@ Landroid/telephony/TelephonyManager$MultiSimVariants;->values()[Landroid/telepho Landroid/test/AndroidTestCase;->getTestContext()Landroid/content/Context; Landroid/test/AndroidTestCase;->setTestContext(Landroid/content/Context;)V Landroid/test/InstrumentationTestCase;->runMethod(Ljava/lang/reflect/Method;I)V -Landroid/test/RepetitiveTest;->numIterations()I Landroid/util/Singleton;-><init>()V Landroid/util/XmlPullAttributes;-><init>(Lorg/xmlpull/v1/XmlPullParser;)V Landroid/util/XmlPullAttributes;->mParser:Lorg/xmlpull/v1/XmlPullParser; diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 98c5a0fb59be..d374f1c0acdf 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 @@ public class Activity extends ContextThemeWrapper */ @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 @@ public class Activity extends ContextThemeWrapper 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/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 617125b3f904..c2963fd605c0 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -191,6 +191,7 @@ public final class LinkProperties implements Parcelable { } setMtu(source.mMtu); mTcpBufferSizes = source.mTcpBufferSizes; + mNat64Prefix = source.mNat64Prefix; } } diff --git a/core/java/android/net/ipmemorystore/NetworkAttributes.java b/core/java/android/net/ipmemorystore/NetworkAttributes.java index d7e5b2761e58..b932d2197f85 100644 --- a/core/java/android/net/ipmemorystore/NetworkAttributes.java +++ b/core/java/android/net/ipmemorystore/NetworkAttributes.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.StringJoiner; /** * A POD object to represent attributes of a single L2 network entry. @@ -207,4 +208,52 @@ public class NetworkAttributes { public int hashCode() { return Objects.hash(assignedV4Address, groupHint, dnsAddresses, mtu); } + + /** Pretty print */ + @Override + public String toString() { + final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}"); + final ArrayList<String> nullFields = new ArrayList<>(); + + if (null != assignedV4Address) { + resultJoiner.add("assignedV4Addr :"); + resultJoiner.add(assignedV4Address.toString()); + } else { + nullFields.add("assignedV4Addr"); + } + + if (null != groupHint) { + resultJoiner.add("groupHint :"); + resultJoiner.add(groupHint); + } else { + nullFields.add("groupHint"); + } + + if (null != dnsAddresses) { + resultJoiner.add("dnsAddr : ["); + for (final InetAddress addr : dnsAddresses) { + resultJoiner.add(addr.getHostAddress()); + } + resultJoiner.add("]"); + } else { + nullFields.add("dnsAddr"); + } + + if (null != mtu) { + resultJoiner.add("mtu :"); + resultJoiner.add(mtu.toString()); + } else { + nullFields.add("mtu"); + } + + if (!nullFields.isEmpty()) { + resultJoiner.add("; Null fields : ["); + for (final String field : nullFields) { + resultJoiner.add(field); + } + resultJoiner.add("]"); + } + + return resultJoiner.toString(); + } } diff --git a/core/java/android/net/ipmemorystore/SameL3NetworkResponse.java b/core/java/android/net/ipmemorystore/SameL3NetworkResponse.java index 0cb37e9f75b4..d040dcc3d608 100644 --- a/core/java/android/net/ipmemorystore/SameL3NetworkResponse.java +++ b/core/java/android/net/ipmemorystore/SameL3NetworkResponse.java @@ -128,4 +128,19 @@ public class SameL3NetworkResponse { public int hashCode() { return Objects.hash(l2Key1, l2Key2, confidence); } + + @Override + /** Pretty print */ + public String toString() { + switch (getNetworkSameness()) { + case NETWORK_SAME: + return "\"" + l2Key1 + "\" same L3 network as \"" + l2Key2 + "\""; + case NETWORK_DIFFERENT: + return "\"" + l2Key1 + "\" different L3 network from \"" + l2Key2 + "\""; + case NETWORK_NEVER_CONNECTED: + return "\"" + l2Key1 + "\" can't be tested against \"" + l2Key2 + "\""; + default: + return "Buggy sameness value ? \"" + l2Key1 + "\", \"" + l2Key2 + "\""; + } + } } diff --git a/core/java/android/net/ipmemorystore/Status.java b/core/java/android/net/ipmemorystore/Status.java index 5b016ec55ae1..95e504224ac7 100644 --- a/core/java/android/net/ipmemorystore/Status.java +++ b/core/java/android/net/ipmemorystore/Status.java @@ -26,6 +26,8 @@ import android.annotation.NonNull; public class Status { public static final int SUCCESS = 0; + public static final int ERROR_DATABASE_CANNOT_BE_OPENED = -1; + public final int resultCode; public Status(final int resultCode) { @@ -47,4 +49,14 @@ public class Status { public boolean isSuccess() { return SUCCESS == resultCode; } + + /** Pretty print */ + @Override + public String toString() { + switch (resultCode) { + case SUCCESS: return "SUCCESS"; + case ERROR_DATABASE_CANNOT_BE_OPENED: return "DATABASE CANNOT BE OPENED"; + default: return "Unknown value ?!"; + } + } } diff --git a/core/java/android/net/ipmemorystore/Utils.java b/core/java/android/net/ipmemorystore/Utils.java new file mode 100644 index 000000000000..73d8c83acdd9 --- /dev/null +++ b/core/java/android/net/ipmemorystore/Utils.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package android.net.ipmemorystore; + +import android.annotation.NonNull; + +/** {@hide} */ +public class Utils { + /** Pretty print */ + public static String blobToString(final Blob blob) { + final StringBuilder sb = new StringBuilder("Blob : ["); + if (blob.data.length <= 24) { + appendByteArray(sb, blob.data, 0, blob.data.length); + } else { + appendByteArray(sb, blob.data, 0, 16); + sb.append("..."); + appendByteArray(sb, blob.data, blob.data.length - 8, blob.data.length); + } + sb.append("]"); + return sb.toString(); + } + + // Adds the hex representation of the array between the specified indices (inclusive, exclusive) + private static void appendByteArray(@NonNull final StringBuilder sb, @NonNull final byte[] ar, + final int from, final int to) { + for (int i = from; i < to; ++i) { + sb.append(String.format("%02X", ar[i])); + } + } +} diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java index b15a4d3170b3..cbb3909a5536 100644 --- a/core/java/android/os/BugreportManager.java +++ b/core/java/android/os/BugreportManager.java @@ -114,7 +114,6 @@ public class BugreportManager { } } - // TODO(b/111441001) Connect up with BugreportListener methods. private final class DumpstateListener extends IDumpstateListener.Stub implements DeathRecipient { private final BugreportListener mListener; @@ -130,35 +129,35 @@ public class BugreportManager { @Override public void onProgress(int progress) throws RemoteException { - // TODO(b/111441001): implement + mListener.onProgress(progress); } @Override public void onError(int errorCode) throws RemoteException { - // TODO(b/111441001): implement + mListener.onError(errorCode); } @Override public void onFinished(long durationMs, String title, String description) throws RemoteException { - // TODO(b/111441001): implement + mListener.onFinished(durationMs, title, description); } // Old methods; should go away @Override public void onProgressUpdated(int progress) throws RemoteException { - // TODO(b/111441001): implement + // TODO(b/111441001): remove from interface } @Override public void onMaxProgressUpdated(int maxProgress) throws RemoteException { - // TODO(b/111441001): implement + // TODO(b/111441001): remove from interface } @Override public void onSectionComplete(String title, int status, int size, int durationMs) throws RemoteException { - // TODO(b/111441001): implement + // TODO(b/111441001): remove from interface } } } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 9fea873d34a8..2d61a4ee95d9 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -289,6 +289,26 @@ public class Build { "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 fdd74882eb39..8ced7225b0d4 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -356,16 +356,6 @@ interface INetworkManagementService 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 10555fab0a8a..39c4266e4cf8 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11091,6 +11091,31 @@ public final class Settings { /** {@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/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java index e5e028d22d97..302e1a656833 100644 --- a/core/java/android/service/contentcapture/ContentCaptureService.java +++ b/core/java/android/service/contentcapture/ContentCaptureService.java @@ -48,6 +48,7 @@ import com.android.internal.os.IResultReceiver; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -77,6 +78,7 @@ public abstract class ContentCaptureService extends Service { "android.service.contentcapture.ContentCaptureService"; private Handler mHandler; + private IContentCaptureServiceCallback mCallback; /** * Binder that receives calls from the system server. @@ -84,9 +86,15 @@ public abstract class ContentCaptureService extends Service { private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() { @Override - public void onConnectedStateChanged(boolean state) { - mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnectedStateChanged, - ContentCaptureService.this, state)); + public void onConnected(IBinder callback) { + mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected, + ContentCaptureService.this, callback)); + } + + @Override + public void onDisconnected() { + mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected, + ContentCaptureService.this)); } @Override @@ -166,7 +174,16 @@ public abstract class ContentCaptureService extends Service { */ public final void setContentCaptureWhitelist(@Nullable List<String> packages, @Nullable List<ComponentName> activities) { - //TODO(b/122595322): implement + final IContentCaptureServiceCallback callback = mCallback; + if (callback == null) { + Log.w(TAG, "setContentCaptureWhitelist(): no server callback"); + return; + } + try { + callback.setContentCaptureWhitelist(packages, activities); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } /** @@ -177,7 +194,16 @@ public abstract class ContentCaptureService extends Service { */ public final void setActivityContentCaptureEnabled(@NonNull ComponentName activity, boolean enabled) { - //TODO(b/122595322): implement + final IContentCaptureServiceCallback callback = mCallback; + if (callback == null) { + Log.w(TAG, "setActivityContentCaptureEnabled(): no server callback"); + return; + } + try { + callback.setActivityContentCaptureEnabled(activity, enabled); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } /** @@ -188,7 +214,16 @@ public abstract class ContentCaptureService extends Service { */ public final void setPackageContentCaptureEnabled(@NonNull String packageName, boolean enabled) { - //TODO(b/122595322): implement + final IContentCaptureServiceCallback callback = mCallback; + if (callback == null) { + Log.w(TAG, "setPackageContentCaptureEnabled(): no server callback"); + return; + } + try { + callback.setPackageContentCaptureEnabled(packageName, enabled); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } /** @@ -197,7 +232,12 @@ public abstract class ContentCaptureService extends Service { */ @NonNull public final Set<ComponentName> getContentCaptureDisabledActivities() { - //TODO(b/122595322): implement + final IContentCaptureServiceCallback callback = mCallback; + if (callback == null) { + Log.w(TAG, "getContentCaptureDisabledActivities(): no server callback"); + return Collections.emptySet(); + } + //TODO(b/122595322): implement (using SyncResultReceiver) return null; } @@ -207,7 +247,12 @@ public abstract class ContentCaptureService extends Service { */ @NonNull public final Set<String> getContentCaptureDisabledPackages() { - //TODO(b/122595322): implement + final IContentCaptureServiceCallback callback = mCallback; + if (callback == null) { + Log.w(TAG, "getContentCaptureDisabledPackages(): no server callback"); + return Collections.emptySet(); + } + //TODO(b/122595322): implement (using SyncResultReceiver) return null; } @@ -307,12 +352,14 @@ public abstract class ContentCaptureService extends Service { } } - private void handleOnConnectedStateChanged(boolean state) { - if (state) { - onConnected(); - } else { - onDisconnected(); - } + private void handleOnConnected(@NonNull IBinder callback) { + mCallback = IContentCaptureServiceCallback.Stub.asInterface(callback); + onConnected(); + } + + private void handleOnDisconnected() { + onDisconnected(); + mCallback = null; } //TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session, @@ -323,15 +370,21 @@ public abstract class ContentCaptureService extends Service { 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/service/contentcapture/IContentCaptureService.aidl b/core/java/android/service/contentcapture/IContentCaptureService.aidl index 1b4cccf630a0..a8dd21392337 100644 --- a/core/java/android/service/contentcapture/IContentCaptureService.aidl +++ b/core/java/android/service/contentcapture/IContentCaptureService.aidl @@ -16,6 +16,7 @@ package android.service.contentcapture; +import android.os.IBinder; import android.service.contentcapture.SnapshotData; import android.view.contentcapture.ContentCaptureContext; @@ -29,7 +30,8 @@ import java.util.List; * @hide */ oneway interface IContentCaptureService { - void onConnectedStateChanged(boolean state); + void onConnected(IBinder callback); + void onDisconnected(); void onSessionStarted(in ContentCaptureContext context, String sessionId, int uid, in IResultReceiver clientReceiver); void onSessionFinished(String sessionId); diff --git a/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl new file mode 100644 index 000000000000..e84bd6fdf2b6 --- /dev/null +++ b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.contentcapture; + +import android.content.ComponentName; +import com.android.internal.os.IResultReceiver; + +import java.util.List; + +/** + * Interface from the Content Capture service to the system. + * + * @hide + */ +oneway interface IContentCaptureServiceCallback { + void setContentCaptureWhitelist(in List<String> packages, in List<ComponentName> activities); + void setActivityContentCaptureEnabled(in ComponentName activity, boolean enabled); + void setPackageContentCaptureEnabled(in String packageName, boolean enabled); + void getContentCaptureDisabledActivities(in IResultReceiver receiver); + void getContentCaptureDisabledPackages(in IResultReceiver receiver); +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 85b6b889be33..44353b1609b7 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -33,8 +33,7 @@ import android.icu.text.Edits; import android.icu.util.ULocale; import android.os.Parcel; import android.os.Parcelable; -import android.os.SystemProperties; -import android.provider.Settings; +import android.sysprop.DisplayProperties; import android.text.style.AbsoluteSizeSpan; import android.text.style.AccessibilityClickableSpan; import android.text.style.AccessibilityURLSpan; @@ -2059,7 +2058,7 @@ public class TextUtils { return ((locale != null && !locale.equals(Locale.ROOT) && ULocale.forLocale(locale).isRightToLeft()) // If forcing into RTL layout mode, return RTL as default - || SystemProperties.getBoolean(Settings.Global.DEVELOPMENT_FORCE_RTL, false)) + || DisplayProperties.debug_force_rtl().orElse(false)) ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR; } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index abefd551331e..32974acdcbbd 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -76,8 +76,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; -import android.os.SystemProperties; import android.os.Trace; +import android.sysprop.DisplayProperties; import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; @@ -812,14 +812,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final String CONTENT_CAPTURE_LOG_TAG = "View.ContentCapture"; /** - * When set to true, apps will draw debugging information about their layouts. - * - * @hide - */ - @UnsupportedAppUsage - public static final String DEBUG_LAYOUT_PROPERTY = "debug.layout"; - - /** * When set to true, this view will save its attribute data. * * @hide @@ -27833,7 +27825,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Show where the margins, bounds and layout bounds are for each view. */ - boolean mDebugLayout = SystemProperties.getBoolean(DEBUG_LAYOUT_PROPERTY, false); + boolean mDebugLayout = DisplayProperties.debug_layout().orElse(false); /** * Point used to compute visible regions. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8e4dc670a2d3..27d4ea4a777a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -75,6 +75,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; +import android.sysprop.DisplayProperties; import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.Log; @@ -7071,7 +7072,7 @@ public final class ViewRootImpl implements ViewParent, } // Layout debugging - boolean layout = SystemProperties.getBoolean(View.DEBUG_LAYOUT_PROPERTY, false); + boolean layout = DisplayProperties.debug_layout().orElse(false); if (layout != mAttachInfo.mDebugLayout) { mAttachInfo.mDebugLayout = layout; if (!mHandler.hasMessages(MSG_INVALIDATE_WORLD)) { diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index c630177e1bf2..dfd9a2e95cb7 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -187,6 +187,7 @@ public abstract class Animation implements Cloneable { /** * An animation listener to be notified when the animation starts, ends or repeats. */ + @UnsupportedAppUsage private AnimationListener mListener; /** diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index ff45efd944cd..81b2e01dfbc7 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -29,12 +29,12 @@ import android.os.IBinder; 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 @@ public final class ContentCaptureManager { 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 @@ public final class ContentCaptureManager { @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 @@ public final class ContentCaptureManager { @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 @@ public final class ContentCaptureManager { * 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 @@ public final class ContentCaptureManager { * 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 @@ public final class ContentCaptureManager { /** @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 d9a8416dcb4d..2123308dbdeb 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -21,6 +21,7 @@ import static android.view.contentcapture.ContentCaptureManager.VERBOSE; 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 @@ public abstract class ContentCaptureSession implements AutoCloseable { * * @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 @@ public abstract class ContentCaptureSession implements AutoCloseable { @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 @@ public abstract class ContentCaptureSession implements AutoCloseable { */ @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 a29aaf013d49..1d9018c1682c 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -88,6 +88,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { */ 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @Nullable private DeathRecipient mDirectServiceVulture; - private int mState = STATE_UNKNOWN; + private int mState = UNKNWON_STATE; @Nullable private IBinder mApplicationToken; @@ -133,11 +134,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } - // 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } 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/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp index e97c9bc911dd..b4174922aa6c 100644 --- a/core/jni/android/graphics/Paint.cpp +++ b/core/jni/android/graphics/Paint.cpp @@ -557,6 +557,18 @@ namespace PaintGlue { return result; } + // FIXME: Should this be FastNative? + static void setColorLong(JNIEnv* env, jobject clazz, jlong paintHandle, jobject jColorSpace, + jfloat r, jfloat g, jfloat b, jfloat a) { + sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(env, jColorSpace); + if (GraphicsJNI::hasException(env)) { + return; + } + + SkColor4f color = SkColor4f{r, g, b, a}; + reinterpret_cast<Paint*>(paintHandle)->setColor4f(color, cs.get()); + } + // ------------------ @FastNative --------------------------- static jint setTextLocales(JNIEnv* env, jobject clazz, jlong objHandle, jstring locales) { @@ -1075,6 +1087,7 @@ static const JNINativeMethod methods[] = { {"nGetRunAdvance", "(J[CIIIIZI)F", (void*) PaintGlue::getRunAdvance___CIIIIZI_F}, {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*) PaintGlue::getOffsetForAdvance___CIIIIZF_I}, + {"nSetColor","(JLandroid/graphics/ColorSpace;FFFF)V", (void*) PaintGlue::setColorLong}, // --------------- @FastNative ---------------------- diff --git a/core/jni/android_app_NativeActivity.cpp b/core/jni/android_app_NativeActivity.cpp index 49a24a30f77e..b2d3651079e1 100644 --- a/core/jni/android_app_NativeActivity.cpp +++ b/core/jni/android_app_NativeActivity.cpp @@ -281,15 +281,18 @@ loadNativeCode_native(JNIEnv* env, jobject clazz, jstring path, jstring funcName std::unique_ptr<NativeCode> code; bool needs_native_bridge = false; + char* nativeloader_error_msg = nullptr; void* handle = OpenNativeLibrary(env, sdkVersion, pathStr.c_str(), classLoader, libraryPath, &needs_native_bridge, - &g_error_msg); + &nativeloader_error_msg); if (handle == nullptr) { + g_error_msg = nativeloader_error_msg; + NativeLoaderFreeErrorMessage(nativeloader_error_msg); ALOGW("NativeActivity LoadNativeLibrary(\"%s\") failed: %s", pathStr.c_str(), g_error_msg.c_str()); diff --git a/core/proto/Android.bp b/core/proto/Android.bp new file mode 100644 index 000000000000..80cc2d4cab94 --- /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 d0c922664351..76c240ecff4d 100644 --- a/core/proto/android/bluetooth/enums.proto +++ b/core/proto/android/bluetooth/enums.proto @@ -41,3 +41,18 @@ enum EnableDisableReasonEnum { 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 000000000000..e1d96bbce68a --- /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 0ec8c1ada47e..7f3ea7a249ba 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -403,18 +403,23 @@ message StateControllerProto { 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 @@ message StateControllerProto { 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/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 2f3a4910bf83..7813128d46c7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3196,9 +3196,10 @@ <!-- @SystemApi Required to add or remove another application as a device admin. <p>Not for use by third-party applications. - @hide --> + @hide + @removed --> <permission android:name="android.permission.MANAGE_DEVICE_ADMINS" - android:protectionLevel="signature|privileged" /> + android:protectionLevel="signature" /> <!-- @SystemApi Allows an app to reset the device password. <p>Not for use by third-party applications. @@ -3520,19 +3521,25 @@ <permission android:name="android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS" android:protectionLevel="signature|privileged" /> + <!-- @SystemApi Allows an application to provide remote displays. + <p>Not for use by third-party applications.</p> + @hide --> + <permission android:name="android.permission.REMOTE_DISPLAY_PROVIDER" + android:protectionLevel="signature|privileged" /> + <!-- Allows an application to capture video output. <p>Not for use by third-party applications.</p> @hide @removed --> <permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" - android:protectionLevel="signature|privileged" /> + android:protectionLevel="signature" /> <!-- Allows an application to capture secure video output. <p>Not for use by third-party applications.</p> @hide @removed --> <permission android:name="android.permission.CAPTURE_SECURE_VIDEO_OUTPUT" - android:protectionLevel="signature|privileged" /> + android:protectionLevel="signature" /> <!-- Allows an application to know what content is playing and control its playback. <p>Not for use by third-party applications due to privacy of media consumption</p> --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2e3bd7cfded5..4d20fa465479 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3696,7 +3696,7 @@ <string name="config_inputEventCompatProcessorOverrideClassName" translatable="false"></string> <!-- Component name for the default module metadata provider on this device --> - <string name="config_defaultModuleMetadataProvider">com.android.modulemetadata</string> + <string name="config_defaultModuleMetadataProvider" translatable="false">com.android.modulemetadata</string> <!-- This is the default launcher component to use on secondary displays that support system decorations. diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index 341f345ce11f..df4600e07047 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.isPublic; 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 @@ public class SettingsBackupTest { 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/data/etc/Android.bp b/data/etc/Android.bp index 6ce81481c1d7..035ee108c6da 100644 --- a/data/etc/Android.bp +++ b/data/etc/Android.bp @@ -39,9 +39,6 @@ prebuilt_etc { 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 { 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/data/etc/platform.xml b/data/etc/platform.xml index 4a2db0a6bb54..fb43e41010b4 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -223,12 +223,12 @@ code to link against. --> <library name="android.test.base" - file="/system/framework/android.test.base.impl.jar" /> + file="/system/framework/android.test.base.jar" /> <library name="android.test.mock" - file="/system/framework/android.test.mock.impl.jar" + file="/system/framework/android.test.mock.jar" dependency="android.test.base" /> <library name="android.test.runner" - file="/system/framework/android.test.runner.impl.jar" + file="/system/framework/android.test.runner.jar" dependency="android.test.base:android.test.mock" /> <!-- In BOOT_JARS historically, and now added to legacy applications. --> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 3342fd22caab..154c1b833b7a 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -17,12 +17,14 @@ package android.graphics; import android.annotation.ColorInt; +import android.annotation.ColorLong; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; import android.annotation.Size; +import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.graphics.fonts.FontVariationAxis; import android.os.Build; @@ -972,6 +974,31 @@ public class Paint { } /** + * Set the paint's color with a {@link ColorLong}. Note that the color is + * a long with an encoded {@link ColorSpace} as well as alpha and r,g,b. + * These values are not premultiplied, meaning that alpha can be any value, + * regardless of the values of r,g,b. See the {@link Color} class for more + * details. + * + * @param color The new color (including alpha and {@link ColorSpace}) + * to set in the paint. + * @throws IllegalArgumentException if the color space encoded in the long + * is invalid or unknown. + * + * @hide pending API approval + */ + @TestApi + public void setColor(@ColorLong long color) { + ColorSpace cs = Color.colorSpace(color); + float r = Color.red(color); + float g = Color.green(color); + float b = Color.blue(color); + float a = Color.alpha(color); + + nSetColor(mNativePaint, cs, r, g, b, a); + } + + /** * Helper to getColor() that just returns the color's alpha value. This is * the same as calling getColor() >>> 24. It always returns a value between * 0 (completely transparent) and 255 (completely opaque). @@ -2906,6 +2933,8 @@ public class Paint { int contextStart, int contextEnd, boolean isRtl, int offset); private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance); + private static native void nSetColor(long paintPtr, ColorSpace cs, + float r, float g, float b, float a); // ---------------- @FastNative ------------------------ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 4338b1cc2a21..e6e6b0e62305 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -96,7 +96,7 @@ bool SkiaOpenGLPipeline::draw(const Frame& frame, const SkRect& screenDirty, con SkASSERT(mRenderThread.getGrContext() != nullptr); sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget( - mRenderThread.getGrContext(), backendRT, kBottomLeft_GrSurfaceOrigin, colorType, + mRenderThread.getGrContext(), backendRT, this->getSurfaceOrigin(), colorType, mSurfaceColorSpace, &props)); SkiaPipeline::updateLighting(lightGeometry, lightInfo); diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index 479910697871..66929226a5e2 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -39,6 +39,7 @@ public: const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler) override; + GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; DeferredLayerUpdater* createTextureLayer() override; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 2e7850d48e54..d7faaf707aa8 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -174,7 +174,8 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator SkSurfaceProps props(0, kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); node->setLayerSurface(SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, info, 0, &props)); + SkBudgeted::kYes, info, 0, + this->getSurfaceOrigin(), &props)); if (node->getLayerSurface()) { // update the transform in window of the layer to reset its origin wrt light source // position diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index ff873133c6fb..94a699bfee31 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -48,7 +48,7 @@ public: bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, ErrorHandler* errorHandler) override; - SkColorType getSurfaceColorType() const { return mSurfaceColorType; } + SkColorType getSurfaceColorType() const override { return mSurfaceColorType; } sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; } void renderFrame(const LayerUpdateQueue& layers, const SkRect& clip, diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index 53ffc4422fd7..934307636913 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -35,6 +35,7 @@ public: const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler) override; + GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) override; DeferredLayerUpdater* createTextureLayer() override; diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index 42e17b273bee..d4dd62941440 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -84,6 +84,7 @@ public: virtual void onPrepareTree() = 0; virtual SkColorType getSurfaceColorType() const = 0; virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0; + virtual GrSurfaceOrigin getSurfaceOrigin() = 0; virtual ~IRenderPipeline() {} }; diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 595aeb3ecc10..c751c39aebbd 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -38,8 +38,8 @@ public class Utils { 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 class Utils { 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 86f04389604c..92ebe446ee98 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 @@ package com.android.settingslib; 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 @@ public class UtilsTest { @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/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index afb978174784..2d7471d9aca5 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -1953,8 +1953,7 @@ public class BugreportProgressService extends Service { @Override public void onProgress(int progress) throws RemoteException { - // TODO(b/111441001): change max argument? - updateProgressInfo(progress, CAPPED_MAX); + updateProgressInfo(progress, 100 /* progress is already a percentage; so max = 100 */); } @Override diff --git a/packages/SystemUI/res-keyguard/layout/stretchanalog_clock.xml b/packages/SystemUI/res-keyguard/layout/stretchanalog_clock.xml new file mode 100644 index 000000000000..9033fce881c4 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/stretchanalog_clock.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<com.android.keyguard.clock.ClockLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + <TextClock + android:id="@+id/digital_clock" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:letterSpacing="0.03" + android:singleLine="true" + style="@style/widget_big" + android:format12Hour="@string/keyguard_widget_12_hours_format" + android:format24Hour="@string/keyguard_widget_24_hours_format" + /> + <com.android.keyguard.clock.StretchAnalogClock + android:id="@+id/analog_clock" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> +</com.android.keyguard.clock.ClockLayout> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java index 41e9ebaac4f6..a055950a5522 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -46,6 +46,7 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout 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 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout @Override public void onPause() { + mResumed = false; + if (mCountdownTimer != null) { mCountdownTimer.cancel(); mCountdownTimer = null; @@ -276,6 +279,7 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout @Override public void onResume(int reason) { + mResumed = true; } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 0ec0bf08c317..a8094d20d5a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -19,6 +19,7 @@ import android.widget.TextClock; import androidx.annotation.VisibleForTesting; import com.android.keyguard.clock.BubbleClockController; +import com.android.keyguard.clock.StretchAnalogClockController; import com.android.systemui.Dependency; import com.android.systemui.plugins.ClockPlugin; import com.android.systemui.statusbar.StatusBarState; @@ -146,6 +147,12 @@ public class KeyguardClockSwitch extends RelativeLayout { Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, BubbleClockController.class.getName(), () -> BubbleClockController.build(mLayoutInflater))) + .withDefault( + new SettingsGattedSupplier( + mContentResolver, + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, + StretchAnalogClockController.class.getName(), + () -> StretchAnalogClockController.build(mLayoutInflater))) .build(); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 41afa9a21128..3296c10f7a39 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -81,6 +81,11 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView 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/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClock.java b/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClock.java new file mode 100644 index 000000000000..91cec86392d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClock.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.keyguard.clock; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Analog clock where the minute hand extends off of the screen. + */ +public class StretchAnalogClock extends View { + + private static final int DEFAULT_COLOR = Color.parseColor("#F5C983"); + private static final float HOUR_STROKE_WIDTH = 60f; + private static final float MINUTE_STROKE_WIDTH = 20f; + private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 80f; + + private final Paint mHourPaint = new Paint(); + private final Paint mMinutePaint = new Paint(); + private Calendar mTime; + private TimeZone mTimeZone; + + public StretchAnalogClock(Context context) { + this(context, null); + } + + public StretchAnalogClock(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public StretchAnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public StretchAnalogClock(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + /** + * Call when the time changes to update the clock hands. + */ + public void onTimeChanged() { + mTime.setTimeInMillis(System.currentTimeMillis()); + invalidate(); + } + + /** + * Call when the time zone has changed to update clock hands. + * + * @param timeZone The updated time zone that will be used. + */ + public void onTimeZoneChanged(TimeZone timeZone) { + mTime.setTimeZone(timeZone); + } + + /** + * Set the color of the minute hand. + */ + public void setMinuteHandColor(int color) { + mMinutePaint.setColor(color); + } + + private void init() { + mHourPaint.setColor(DEFAULT_COLOR); + mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH); + mHourPaint.setAntiAlias(true); + mHourPaint.setStrokeCap(Paint.Cap.ROUND); + + mMinutePaint.setColor(Color.WHITE); + mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH); + mMinutePaint.setAntiAlias(true); + mMinutePaint.setStrokeCap(Paint.Cap.ROUND); + } + + @Override + protected void onDraw(Canvas canvas) { + final float centerX = getWidth() / 2f; + final float centerY = getHeight() / 2f; + + final float minutesRotation = mTime.get(Calendar.MINUTE) * 6f; + final float hoursRotation = (mTime.get(Calendar.HOUR) * 30); + + // Compute length of clock hands. Hour hand is 60% the length from center to edge + // and minute hand is twice the length to make sure it extends past screen edge. + double sMinuteHandLengthFactor = Math.sin(2d * Math.PI * minutesRotation / 360d); + float sMinuteHandLength = (float) (2d * (centerY + (centerX - centerY) + * sMinuteHandLengthFactor * sMinuteHandLengthFactor)); + double sHourHandLengthFactor = Math.sin(2d * Math.PI * hoursRotation / 360d); + float sHourHandLength = (float) (0.6d * (centerY + (centerX - centerY) + * sHourHandLengthFactor * sHourHandLengthFactor)); + + canvas.save(); + + canvas.rotate(minutesRotation, centerX, centerY); + canvas.drawLine( + centerX, + centerY + CENTER_GAP_AND_CIRCLE_RADIUS, + centerX, + centerY - sMinuteHandLength, + mMinutePaint); + + canvas.rotate(hoursRotation - minutesRotation, centerX, centerY); + canvas.drawLine( + centerX, + centerY + CENTER_GAP_AND_CIRCLE_RADIUS, + centerX, + centerY - sHourHandLength, + mHourPaint); + + canvas.restore(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mTime = Calendar.getInstance(mTimeZone != null ? mTimeZone : TimeZone.getDefault()); + onTimeChanged(); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClockController.java b/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClockController.java new file mode 100644 index 000000000000..0a39158cd4be --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/clock/StretchAnalogClockController.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.keyguard.clock; + +import android.graphics.Paint.Style; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextClock; + +import com.android.keyguard.R; +import com.android.systemui.plugins.ClockPlugin; + +import java.util.TimeZone; + +/** + * Controller for Stretch clock that can appear on lock screen and AOD. + */ +public class StretchAnalogClockController implements ClockPlugin { + + /** + * Custom clock shown on AOD screen and behind stack scroller on lock. + */ + private View mBigClockView; + private TextClock mDigitalClock; + private StretchAnalogClock mAnalogClock; + + /** + * Small clock shown on lock screen above stack scroller. + */ + private View mView; + private TextClock mLockClock; + + /** + * Controller for transition to dark state. + */ + private CrossFadeDarkController mDarkController; + + private StretchAnalogClockController() { } + + /** + * Create a BubbleClockController instance. + * + * @param layoutInflater Inflater used to inflate custom clock views. + */ + public static StretchAnalogClockController build(LayoutInflater layoutInflater) { + StretchAnalogClockController controller = new StretchAnalogClockController(); + controller.createViews(layoutInflater); + return controller; + } + + private void createViews(LayoutInflater layoutInflater) { + mBigClockView = layoutInflater.inflate(R.layout.stretchanalog_clock, null); + mAnalogClock = mBigClockView.findViewById(R.id.analog_clock); + mDigitalClock = mBigClockView.findViewById(R.id.digital_clock); + + mView = layoutInflater.inflate(R.layout.digital_clock, null); + mLockClock = mView.findViewById(R.id.lock_screen_clock); + mLockClock.setVisibility(View.GONE); + + mDarkController = new CrossFadeDarkController(mDigitalClock, mLockClock); + } + + @Override + public View getView() { + return mView; + } + + @Override + public View getBigClockView() { + return mBigClockView; + } + + @Override + public void setStyle(Style style) {} + + @Override + public void setTextColor(int color) { + mLockClock.setTextColor(color); + mDigitalClock.setTextColor(color); + mAnalogClock.setMinuteHandColor(color); + } + + @Override + public void dozeTimeTick() { + mAnalogClock.onTimeChanged(); + } + + @Override + public void setDarkAmount(float darkAmount) { + mDarkController.setDarkAmount(darkAmount); + } + + @Override + public void onTimeZoneChanged(TimeZone timeZone) { + mAnalogClock.onTimeZoneChanged(timeZone); + } + + @Override + public boolean shouldShowStatusArea() { + return false; + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/StretchAnalogClockControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/StretchAnalogClockControllerTest.java new file mode 100644 index 000000000000..8de8f3d0674d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/clock/StretchAnalogClockControllerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.keyguard.clock; + +import static com.google.common.truth.Truth.assertThat; + +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public final class StretchAnalogClockControllerTest extends SysuiTestCase { + + private StretchAnalogClockController mClockController; + + @Before + public void setUp() { + LayoutInflater layoutInflater = LayoutInflater.from(getContext()); + mClockController = StretchAnalogClockController.build(layoutInflater); + } + + @Test + public void setDarkAmount_fadeIn() { + ViewGroup smallClockFrame = (ViewGroup) mClockController.getView(); + View smallClock = smallClockFrame.getChildAt(0); + // WHEN dark amount is set to AOD + mClockController.setDarkAmount(1f); + // THEN small clock should not be shown. + assertThat(smallClock.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setTextColor_setDigitalClock() { + ViewGroup smallClock = (ViewGroup) mClockController.getView(); + // WHEN text color is set + mClockController.setTextColor(42); + // THEN child of small clock should have text color set. + TextView digitalClock = (TextView) smallClock.getChildAt(0); + assertThat(digitalClock.getCurrentTextColor()).isEqualTo(42); + } +} diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java index 529430ca4eaf..a7bada07ffa8 100644 --- a/services/backup/java/com/android/server/backup/TransportManager.java +++ b/services/backup/java/com/android/server/backup/TransportManager.java @@ -562,7 +562,7 @@ public class TransportManager { private void registerTransportsFromPackage( String packageName, Predicate<ComponentName> transportComponentFilter) { try { - mPackageManager.getPackageInfo(packageName, 0); + mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Trying to register transports from package not found " + packageName); return; @@ -599,7 +599,8 @@ public class TransportManager { return false; } try { - PackageInfo packInfo = mPackageManager.getPackageInfo(transport.getPackageName(), 0); + PackageInfo packInfo = + mPackageManager.getPackageInfoAsUser(transport.getPackageName(), 0, mUserId); if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) == 0) { Slog.w(TAG, "Transport package " + transport.getPackageName() + " not privileged"); diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index 3a8966a04055..e0e81ffb6b48 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -495,7 +495,7 @@ public class UserBackupManagerService { mBaseStateDir = checkNotNull(baseStateDir, "baseStateDir cannot be null"); mBaseStateDir.mkdirs(); - if (!SELinux.restorecon(mBaseStateDir)) { + if (!SELinux.restoreconRecursive(mBaseStateDir)) { Slog.w(TAG, "SELinux restorecon failed on " + mBaseStateDir); } @@ -504,7 +504,7 @@ public class UserBackupManagerService { // is managed by init.rc so we don't have to create it below. if (userId != UserHandle.USER_SYSTEM) { mDataDir.mkdirs(); - if (!SELinux.restorecon(mDataDir)) { + if (!SELinux.restoreconRecursive(mDataDir)) { Slog.w(TAG, "SELinux restorecon failed on " + mDataDir); } } @@ -584,7 +584,7 @@ public class UserBackupManagerService { mBackupHandler.postDelayed(this::parseLeftoverJournals, INITIALIZATION_DELAY_MILLIS); // Power management - mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"); + mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*-" + userId); } void initializeBackupEnableState() { @@ -1352,7 +1352,7 @@ public class UserBackupManagerService { private List<PackageInfo> allAgentPackages() { // !!! TODO: cache this and regenerate only when necessary int flags = PackageManager.GET_SIGNING_CERTIFICATES; - List<PackageInfo> packages = mPackageManager.getInstalledPackages(flags); + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(flags, mUserId); int numPackages = packages.size(); for (int a = numPackages - 1; a >= 0; a--) { PackageInfo pkg = packages.get(a); @@ -1366,8 +1366,8 @@ public class UserBackupManagerService { // we will need the shared library path, so look that up and store it here. // This is used implicitly when we pass the PackageInfo object off to // the Activity Manager to launch the app for backup/restore purposes. - app = mPackageManager.getApplicationInfo(pkg.packageName, - PackageManager.GET_SHARED_LIBRARY_FILES); + app = mPackageManager.getApplicationInfoAsUser(pkg.packageName, + PackageManager.GET_SHARED_LIBRARY_FILES, mUserId); pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles; pkg.applicationInfo.sharedLibraryInfos = app.sharedLibraryInfos; } @@ -1392,7 +1392,7 @@ public class UserBackupManagerService { notification.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES | Intent.FLAG_RECEIVER_FOREGROUND); notification.putExtra(BACKUP_FINISHED_PACKAGE_EXTRA, packageName); - mContext.sendBroadcastAsUser(notification, UserHandle.OWNER); + mContext.sendBroadcastAsUser(notification, UserHandle.of(mUserId)); } mProcessedPackagesJournal.addPackage(packageName); @@ -2208,11 +2208,10 @@ public class UserBackupManagerService { /** Used by both incremental and full restore to restore widget data. */ public void restoreWidgetData(String packageName, byte[] widgetData) { // Apply the restored widget state and generate the ID update for the app - // TODO: http://b/22388012 if (MORE_DEBUG) { Slog.i(TAG, "Incorporating restored widget data"); } - AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, UserHandle.USER_SYSTEM); + AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId); } // ***************************** @@ -2291,20 +2290,6 @@ public class UserBackupManagerService { /** Sent from an app's backup agent to let the service know that there's new data to backup. */ public void dataChanged(final String packageName) { - final int callingUserHandle = UserHandle.getCallingUserId(); - if (callingUserHandle != UserHandle.USER_SYSTEM) { - // TODO: http://b/22388012 - // App is running under a non-owner user profile. For now, we do not back - // up data from secondary user profiles. - // TODO: backups for all user profiles although don't add backup for profiles - // without adding admin control in DevicePolicyManager. - if (MORE_DEBUG) { - Slog.v(TAG, "dataChanged(" + packageName + ") ignored because it's user " - + callingUserHandle); - } - return; - } - final HashSet<String> targets = dataChangedTargets(packageName); if (targets == null) { Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" @@ -2921,7 +2906,7 @@ public class UserBackupManagerService { try { int transportUid = mContext.getPackageManager() - .getPackageUid(transportComponent.getPackageName(), 0); + .getPackageUidAsUser(transportComponent.getPackageName(), 0, mUserId); if (callingUid != transportUid) { throw new SecurityException("Only the transport can change its description"); } diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java index c7f3315493b4..b3d9fbcb88d3 100644 --- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java +++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java @@ -68,6 +68,8 @@ import java.util.List; public class FullRestoreEngine extends RestoreEngine { private final UserBackupManagerService mBackupManagerService; + private final int mUserId; + // Task in charge of monitoring timeouts private final BackupRestoreTask mMonitorTask; @@ -146,6 +148,7 @@ public class FullRestoreEngine extends RestoreEngine { backupManagerService.getAgentTimeoutParameters(), "Timeout parameters cannot be null"); mIsAdbRestore = isAdbRestore; + mUserId = backupManagerService.getUserId(); } public IBackupAgent getAgent() { @@ -272,7 +275,7 @@ public class FullRestoreEngine extends RestoreEngine { instream, mBackupManagerService.getContext(), mDeleteObserver, mManifestSignatures, mPackagePolicies, info, installerPackageName, - bytesReadListener, mBackupManagerService.getUserId()); + bytesReadListener, mUserId); // good to go; promote to ACCEPT mPackagePolicies.put(pkg, isSuccessfullyInstalled ? RestorePolicy.ACCEPT @@ -329,9 +332,8 @@ public class FullRestoreEngine extends RestoreEngine { } try { - mTargetApp = - mBackupManagerService.getPackageManager().getApplicationInfo( - pkg, 0); + mTargetApp = mBackupManagerService.getPackageManager() + .getApplicationInfoAsUser(pkg, 0, mUserId); // If we haven't sent any data to this app yet, we probably // need to clear it first. Check that. @@ -684,7 +686,7 @@ public class FullRestoreEngine extends RestoreEngine { String packageListString = Settings.Secure.getStringForUser( mBackupManagerService.getContext().getContentResolver(), Settings.Secure.PACKAGES_TO_CLEAR_DATA_BEFORE_FULL_RESTORE, - mBackupManagerService.getUserId()); + mUserId); if (TextUtils.isEmpty(packageListString)) { return false; } diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 5284d94c2aa7..d01f77bfd84c 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -360,7 +360,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // If we're starting a full-system restore, set up to begin widget ID remapping if (mIsSystemRestore) { - // TODO: http://b/22388012 AppWidgetBackupBridge.restoreStarting(mUserId); } @@ -1079,7 +1078,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } // Kick off any work that may be needed regarding app widget restores - // TODO: http://b/22388012 AppWidgetBackupBridge.restoreFinished(mUserId); // If this was a full-system restore, record the ancestral diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java index 09aa4212654e..1dae2ceae7d9 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; @@ -36,10 +39,11 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.service.contentcapture.ContentCaptureService; +import android.service.contentcapture.IContentCaptureServiceCallback; import android.service.contentcapture.SnapshotData; import android.util.ArrayMap; +import android.util.Log; import android.util.Slog; -import android.view.contentcapture.ContentCaptureSession; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; @@ -48,6 +52,7 @@ import com.android.server.infra.AbstractPerUserSystemService; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * Per-user instance of {@link ContentCaptureManagerService}. @@ -72,6 +77,9 @@ final class ContentCapturePerUserService @GuardedBy("mLock") private RemoteContentCaptureService mRemoteService; + private final ContentCaptureServiceRemoteCallback mRemoteServiceCallback = + new ContentCaptureServiceRemoteCallback(); + // TODO(b/111276913): add mechanism to prune stale sessions, similar to Autofill's ContentCapturePerUserService(@NonNull ContentCaptureManagerService master, @@ -100,10 +108,10 @@ final class ContentCapturePerUserService } if (!disabled) { - mRemoteService = new RemoteContentCaptureService( - mMaster.getContext(), - ContentCaptureService.SERVICE_INTERFACE, serviceComponentName, mUserId, this, - mMaster.isBindInstantServiceAllowed(), mMaster.verbose); + mRemoteService = new RemoteContentCaptureService(mMaster.getContext(), + ContentCaptureService.SERVICE_INTERFACE, serviceComponentName, + mRemoteServiceCallback, mUserId, this, mMaster.isBindInstantServiceAllowed(), + mMaster.verbose); } } @@ -165,7 +173,7 @@ final class ContentCapturePerUserService 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 +192,7 @@ final class ContentCapturePerUserService 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 +205,7 @@ final class ContentCapturePerUserService // 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; } @@ -338,4 +345,50 @@ final class ContentCapturePerUserService } return null; } + + private final class ContentCaptureServiceRemoteCallback extends + IContentCaptureServiceCallback.Stub { + + @Override + public void setContentCaptureWhitelist(List<String> packages, + List<ComponentName> activities) { + if (mMaster.verbose) { + Log.v(TAG, "setContentCaptureWhitelist(packages=" + packages + ", activities=" + + activities + ")"); + } + // TODO(b/122595322): implement + // TODO(b/119613670): log metrics + } + + @Override + public void setActivityContentCaptureEnabled(ComponentName activity, boolean enabled) { + if (mMaster.verbose) { + Log.v(TAG, "setActivityContentCaptureEnabled(activity=" + activity + ", enabled=" + + enabled + ")"); + } + // TODO(b/122595322): implement + // TODO(b/119613670): log metrics + } + + @Override + public void setPackageContentCaptureEnabled(String packageName, boolean enabled) { + if (mMaster.verbose) { + Log.v(TAG, + "setPackageContentCaptureEnabled(packageName=" + packageName + ", enabled=" + + enabled + ")"); + } + // TODO(b/122595322): implement + // TODO(b/119613670): log metrics + } + + @Override + public void getContentCaptureDisabledActivities(IResultReceiver receiver) { + // TODO(b/122595322): implement + } + + @Override + public void getContentCaptureDisabledPackages(IResultReceiver receiver) { + // TODO(b/122595322): implement + } + } } diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java index 824162868e19..12742ca0a46f 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.Context; import android.os.IBinder; import android.service.contentcapture.IContentCaptureService; +import android.service.contentcapture.IContentCaptureServiceCallback; import android.service.contentcapture.SnapshotData; import android.text.format.DateUtils; import android.util.Slog; @@ -35,12 +36,15 @@ final class RemoteContentCaptureService private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + private final IBinder mServerCallback; + RemoteContentCaptureService(Context context, String serviceInterface, - ComponentName componentName, int userId, + ComponentName serviceComponentName, IContentCaptureServiceCallback callback, int userId, ContentCaptureServiceCallbacks callbacks, boolean bindInstantServiceAllowed, boolean verbose) { - super(context, serviceInterface, componentName, userId, callbacks, + super(context, serviceInterface, serviceComponentName, userId, callbacks, bindInstantServiceAllowed, verbose, /* initialCapacity= */ 2); + mServerCallback = callback.asBinder(); // Bind right away, which will trigger a onConnected() on service's scheduleBind(); @@ -69,7 +73,11 @@ final class RemoteContentCaptureService scheduleUnbind(); } try { - mService.onConnectedStateChanged(state); + if (state) { + mService.onConnected(mServerCallback); + } else { + mService.onDisconnected(); + } } catch (Exception e) { Slog.w(mTag, "Exception calling onConnectedStateChanged(" + state + "): " + e); } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index d0666b98c0e0..d6f3e2ba4835 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -4879,7 +4879,7 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 b0ca2df20f1f..aed0684a92cb 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_ALL; 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 static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENAB 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.NetworkPolicyManager; 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.StrictMode; 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.net.NetworkStatsFactory; 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.InputStreamReader; 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 @@ public class NetworkManagementService extends INetworkManagementService.Stub } @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 bd6fa498934c..43deb1176fe6 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.app.AppOpsManager.OP_NONE; 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.Preconditions; 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.ThreadPriorityBooster; 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 @@ public class ActivityManagerService extends IActivityManager.Stub /** * 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, 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. */ - ArrayMap<String, ArraySet<String>> mAllowedAssociations; + 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 @@ public class ActivityManagerService extends IActivityManager.Stub * 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 @@ public class ActivityManagerService extends IActivityManager.Stub * 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 @@ public class ActivityManagerService extends IActivityManager.Stub || 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 { @@ -10628,7 +10695,6 @@ public class ActivityManagerService extends IActivityManager.Stub ProtoUtils.toDuration(proto, ActivityManagerServiceDumpProcessesProto.LAST_IDLE_TIME, mLastIdleTime, now); proto.write(ActivityManagerServiceDumpProcessesProto.LOW_RAM_SINCE_LAST_IDLE_MS, getLowRamTimeSinceIdle(now)); } - } void writeProcessesToGcToProto(ProtoOutputStream proto, long fieldId, String dumpPackage) { @@ -10910,14 +10976,14 @@ public class ActivityManagerService extends IActivityManager.Stub 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 +10992,6 @@ public class ActivityManagerService extends IActivityManager.Stub pw.println(" Allowed associations (by restricted package):"); printed = true; needSep = true; - printedAnything = true; } if (!printedHeader) { pw.print(" * "); @@ -10938,6 +11003,9 @@ public class ActivityManagerService extends IActivityManager.Stub pw.println(asc.valueAt(j)); } } + if (mAllowedAssociations.valueAt(i).isDebuggable()) { + pw.println(" (debuggable)"); + } } } if (!printed) { @@ -14519,6 +14587,7 @@ public class ActivityManagerService extends IActivityManager.Stub + " 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 6596d27d021e..9d9b1cfdf6e2 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 @@ public class Nat464Xlat extends BaseNetworkObserver { NetworkInfo.State.SUSPENDED, }; + private final INetd mNetd; private final INetworkManagementService mNMService; // The network we're running on, and its type. @@ -76,7 +78,8 @@ public class Nat464Xlat extends BaseNetworkObserver { 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 @@ public class Nat464Xlat extends BaseNetworkObserver { 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 @@ public class Nat464Xlat extends BaseNetworkObserver { */ 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 @@ public class Nat464Xlat extends BaseNetworkObserver { 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 54c89aa04111..9ea73fbb1882 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 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> { 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 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> { networkCapabilities = nc; currentScore = score; mConnService = connService; + mNetd = netd; + mNMS = nms; mContext = context; mHandler = handler; networkMisc = misc; @@ -587,18 +593,18 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> { 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/content/SyncJobService.java b/services/core/java/com/android/server/content/SyncJobService.java index bfcc629541f8..aaf9cbc168af 100644 --- a/services/core/java/com/android/server/content/SyncJobService.java +++ b/services/core/java/com/android/server/content/SyncJobService.java @@ -141,10 +141,7 @@ public class SyncJobService extends JobService { final long runtime = nowUptime - startUptime; - if (startUptime == 0) { - wtf("Job " + jobId + " start uptime not found: " - + " params=" + jobParametersToString(params)); - } else if (runtime > 60 * 1000) { + if (runtime > 60 * 1000) { // WTF if startSyncH() hasn't happened, *unless* onStopJob() was called too soon. // (1 minute threshold.) // Also don't wtf when it's not ready to sync. 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 ac2dbdf9450e..c16d1b4ecec5 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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 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.BatteryManagerInternal; 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 @@ import java.util.function.Predicate; * 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { /** 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 @@ public final class QuotaController extends StateController { } }; + 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { if (jobs != null) { jobs.remove(jobStatus); } + mTopStartedJobs.remove(jobStatus); } } @@ -511,6 +582,7 @@ public final class QuotaController extends StateController { mInQuotaAlarmListeners.delete(userId, packageName); } mExecutionStatsCache.delete(userId, packageName); + mForegroundUids.delete(uid); } @Override @@ -522,6 +594,20 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { } } + boolean isRunning(JobStatus jobStatus) { + return mRunningBgJobs.contains(jobStatus); + } + long getCurrentDuration(long nowElapsed) { synchronized (mLock) { return !isActive() ? 0 : nowElapsed - mStartTimeElapsed; @@ -1154,17 +1308,21 @@ public final class QuotaController extends StateController { } } - 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 @@ public final class QuotaController extends StateController { } 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 @@ public final class QuotaController extends StateController { @VisibleForTesting @NonNull + SparseBooleanArray getForegroundUids() { + return mForegroundUids; + } + + @VisibleForTesting + @NonNull Handler getHandler() { return mHandler; } @@ -1450,6 +1646,10 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 d611a172d225..7fffe8eb402a 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 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { 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 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { 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 ba7b87e6f029..d20ed8c11009 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 @@ import java.util.List; 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(); - } - - private IAudioService getAudioService() { - IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); - return IAudioService.Stub.asInterface(b); - } - - private boolean isGlobalPriorityActiveLocked() { - return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive(); - } - - 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()); - } - } - - 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); - } - } - - 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); - } - } - - 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()); - } + mImpl.onStart(); } @Override public void onStartUser(int userId) { - if (DEBUG) Log.d(TAG, "onStartUser: " + userId); - updateUser(); + mImpl.onStartUser(userId); } @Override public void onSwitchUser(int userId) { - if (DEBUG) Log.d(TAG, "onSwitchUser: " + userId); - updateUser(); + mImpl.onSwitchUser(userId); } // 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(); - } + mImpl.onStopUser(userId); } @Override public void monitor() { - synchronized (mLock) { - // Check for deadlock - } - } - - 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.monitor(); } - void sessionDied(MediaSessionRecord session) { - synchronized (mLock) { - destroySessionLocked(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) + /** + * Updates session. */ - 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"); + public void updateSession(MediaSessionRecord record) { + mImpl.updateSession(record); } /** - * 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> + * Sets global priority session. */ - 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; + public void setGlobalPrioritySession(MediaSessionRecord record) { + mImpl.setGlobalPrioritySession(record); } - private void enforceSystemUiPermission(String action, int pid, int uid) { - if (!isCurrentVolumeController(pid, uid)) { - throw new SecurityException("Only system ui may " + action); - } + List<Session2Token> getSession2TokensLocked(int userId) { + return mImpl.getSession2TokensLocked(userId); } /** - * 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 + * Tells the system UI that volume has changed on an active remote session. */ - 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); - } + public void notifyRemoteVolumeChanged(int flags, MediaSessionRecord session) { + mImpl.notifyRemoteVolumeChanged(flags, session); } - /* - * 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. + /** + * Called when session playstate is changed. */ - 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; + public void onSessionPlaystateChanged(MediaSessionRecord record, int oldState, int newState) { + mImpl.onSessionPlaystateChanged(record, oldState, newState); } - 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; + /** + * Called when session playback type is changed. + */ + public void onSessionPlaybackTypeChanged(MediaSessionRecord record) { + mImpl.onSessionPlaybackTypeChanged(record); } - 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; + protected void enforcePhoneStatePermission(int pid, int uid) { + mImpl.enforcePhoneStatePermission(pid, uid); } - - 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); - } - } - } - } + void sessionDied(MediaSessionRecord session) { + mImpl.sessionDied(session); } - 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 destroySession(MediaSessionRecord session) { + 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); - } - } - } - - 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); - } - } + mImpl.onMediaButtonReceiverChanged(record); + } + + 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 000000000000..f374c6d204fd --- /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/media/RemoteDisplayProviderWatcher.java b/services/core/java/com/android/server/media/RemoteDisplayProviderWatcher.java index 6a5f563683a3..64c451d03caa 100644 --- a/services/core/java/com/android/server/media/RemoteDisplayProviderWatcher.java +++ b/services/core/java/com/android/server/media/RemoteDisplayProviderWatcher.java @@ -159,12 +159,13 @@ public final class RemoteDisplayProviderWatcher { + serviceInfo.packageName + "/" + serviceInfo.name); return false; } - if (!hasCaptureVideoPermission(serviceInfo.packageName)) { - // If the service does not have permission to capture video then it - // isn't going to be terribly useful as a remote display, is it? - // Kind of makes you wonder what it's doing there in the first place. + if (mPackageManager.checkPermission(Manifest.permission.REMOTE_DISPLAY_PROVIDER, + serviceInfo.packageName) != PackageManager.PERMISSION_GRANTED) { + // If the service does not have this permission then the system will not bind to it. + // This is to prevent non privileged apps declaring themselves as remote display + // providers just to be bound to by the system and keep their process alive. Slog.w(TAG, "Ignoring remote display provider service because it does not " - + "have the CAPTURE_VIDEO_OUTPUT or CAPTURE_SECURE_VIDEO_OUTPUT " + + "have the REMOTE_DISPLAY_PROVIDER " + "permission: " + serviceInfo.packageName + "/" + serviceInfo.name); return false; } @@ -172,18 +173,6 @@ public final class RemoteDisplayProviderWatcher { return true; } - private boolean hasCaptureVideoPermission(String packageName) { - if (mPackageManager.checkPermission(Manifest.permission.CAPTURE_VIDEO_OUTPUT, - packageName) == PackageManager.PERMISSION_GRANTED) { - return true; - } - if (mPackageManager.checkPermission(Manifest.permission.CAPTURE_SECURE_VIDEO_OUTPUT, - packageName) == PackageManager.PERMISSION_GRANTED) { - return true; - } - return false; - } - private int findProvider(String packageName, String className) { int count = mProviders.size(); for (int i = 0; i < count; i++) { 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 68a755b382ca..78fa82c6bcdd 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.PackageUtils; 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 @@ public class DexLogger { 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 @@ public class DexLogger { } 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 e57d9d7ab61c..b54683673e7b 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 @@ public class DexManager { 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/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index e82e7480740c..2cd4921a6672 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -210,6 +210,7 @@ import android.os.storage.StorageManager; import android.provider.Settings; import android.service.voice.IVoiceInteractionSession; import android.service.voice.VoiceInteractionManagerInternal; +import android.sysprop.DisplayProperties; import android.telecom.TelecomManager; import android.text.TextUtils; import android.text.format.Time; @@ -245,7 +246,6 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.server.appop.AppOpsService; import com.android.server.AttributeCache; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -261,6 +261,7 @@ import com.android.server.am.EventLogTags; import com.android.server.am.PendingIntentController; import com.android.server.am.PendingIntentRecord; import com.android.server.am.UserState; +import com.android.server.appop.AppOpsService; import com.android.server.firewall.IntentFirewall; import com.android.server.pm.UserManagerService; import com.android.server.uri.UriGrantsManagerInternal; @@ -694,7 +695,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final boolean isPc = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); // Transfer any global setting for forcing RTL layout, into a System Property - SystemProperties.set(DEVELOPMENT_FORCE_RTL, forceRtl ? "1":"0"); + DisplayProperties.debug_force_rtl(forceRtl); final Configuration configuration = new Configuration(); Settings.System.getConfiguration(resolver, configuration); diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java new file mode 100644 index 000000000000..eaab6507e8d4 --- /dev/null +++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java @@ -0,0 +1,143 @@ +/* + * 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. + */ + +package com.android.server.net.ipmemorystore; + +import android.annotation.NonNull; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * Encapsulating class for using the SQLite database backing the memory store. + * + * This class groups together the contracts and the SQLite helper used to + * use the database. + * + * @hide + */ +public class IpMemoryStoreDatabase { + /** + * Contract class for the Network Attributes table. + */ + public static class NetworkAttributesContract { + public static final String TABLENAME = "NetworkAttributes"; + + public static final String COLNAME_L2KEY = "l2Key"; + public static final String COLTYPE_L2KEY = "TEXT NOT NULL"; + + public static final String COLNAME_EXPIRYDATE = "expiryDate"; + // Milliseconds since the Epoch, in true Java style + public static final String COLTYPE_EXPIRYDATE = "BIGINT"; + + public static final String COLNAME_ASSIGNEDV4ADDRESS = "assignedV4Address"; + public static final String COLTYPE_ASSIGNEDV4ADDRESS = "INTEGER"; + + // Please note that the group hint is only a *hint*, hence its name. The client can offer + // this information to nudge the grouping in the decision it thinks is right, but it can't + // decide for the memory store what is the same L3 network. + public static final String COLNAME_GROUPHINT = "groupHint"; + public static final String COLTYPE_GROUPHINT = "TEXT"; + + public static final String COLNAME_DNSADDRESSES = "dnsAddresses"; + // Stored in marshalled form as is + public static final String COLTYPE_DNSADDRESSES = "BLOB"; + + public static final String COLNAME_MTU = "mtu"; + public static final String COLTYPE_MTU = "INTEGER"; + + public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + + TABLENAME + " (" + + COLNAME_L2KEY + " " + COLTYPE_L2KEY + " PRIMARY KEY NOT NULL, " + + COLNAME_EXPIRYDATE + " " + COLTYPE_EXPIRYDATE + ", " + + COLNAME_ASSIGNEDV4ADDRESS + " " + COLTYPE_ASSIGNEDV4ADDRESS + ", " + + COLNAME_GROUPHINT + " " + COLTYPE_GROUPHINT + ", " + + COLNAME_DNSADDRESSES + " " + COLTYPE_DNSADDRESSES + ", " + + COLNAME_MTU + " " + COLTYPE_MTU + ")"; + public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME; + } + + /** + * Contract class for the Private Data table. + */ + public static class PrivateDataContract { + public static final String TABLENAME = "PrivateData"; + + public static final String COLNAME_L2KEY = "l2Key"; + public static final String COLTYPE_L2KEY = "TEXT NOT NULL"; + + public static final String COLNAME_CLIENT = "client"; + public static final String COLTYPE_CLIENT = "TEXT NOT NULL"; + + public static final String COLNAME_DATANAME = "dataName"; + public static final String COLTYPE_DATANAME = "TEXT NOT NULL"; + + public static final String COLNAME_DATA = "data"; + public static final String COLTYPE_DATA = "BLOB NOT NULL"; + + public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + + TABLENAME + " (" + + COLNAME_L2KEY + " " + COLTYPE_L2KEY + ", " + + COLNAME_CLIENT + " " + COLTYPE_CLIENT + ", " + + COLNAME_DATANAME + " " + COLTYPE_DATANAME + ", " + + COLNAME_DATA + " " + COLTYPE_DATA + ", " + + "PRIMARY KEY (" + + COLNAME_L2KEY + ", " + + COLNAME_CLIENT + ", " + + COLNAME_DATANAME + "))"; + public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME; + } + + // To save memory when the DB is not used, close it after 30s of inactivity. This is + // determined manually based on what feels right. + private static final long IDLE_CONNECTION_TIMEOUT_MS = 30_000; + + /** The SQLite DB helper */ + public static class DbHelper extends SQLiteOpenHelper { + // Update this whenever changing the schema. + private static final int SCHEMA_VERSION = 1; + private static final String DATABASE_FILENAME = "IpMemoryStore.db"; + + public DbHelper(@NonNull final Context context) { + super(context, DATABASE_FILENAME, null, SCHEMA_VERSION); + setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); + } + + /** Called when the database is created */ + public void onCreate(@NonNull final SQLiteDatabase db) { + db.execSQL(NetworkAttributesContract.CREATE_TABLE); + db.execSQL(PrivateDataContract.CREATE_TABLE); + } + + /** Called when the database is upgraded */ + public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, + final int newVersion) { + // No upgrade supported yet. + db.execSQL(NetworkAttributesContract.DROP_TABLE); + db.execSQL(PrivateDataContract.DROP_TABLE); + onCreate(db); + } + + /** Called when the database is downgraded */ + public void onDowngrade(@NonNull final SQLiteDatabase db, final int oldVersion, + final int newVersion) { + // Downgrades always nuke all data and recreate an empty table. + db.execSQL(NetworkAttributesContract.DROP_TABLE); + db.execSQL(PrivateDataContract.DROP_TABLE); + onCreate(db); + } + } +} diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java index c9759bf6170f..55a72190eff4 100644 --- a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java @@ -19,6 +19,8 @@ package com.android.server.net.ipmemorystore; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; import android.net.IIpMemoryStore; import android.net.ipmemorystore.Blob; import android.net.ipmemorystore.IOnBlobRetrievedListener; @@ -27,6 +29,10 @@ import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; import android.net.ipmemorystore.IOnSameNetworkResponseListener; import android.net.ipmemorystore.IOnStatusListener; import android.net.ipmemorystore.NetworkAttributesParcelable; +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Implementation for the IP memory store. @@ -37,10 +43,75 @@ import android.net.ipmemorystore.NetworkAttributesParcelable; * @hide */ public class IpMemoryStoreService extends IIpMemoryStore.Stub { + private static final String TAG = IpMemoryStoreService.class.getSimpleName(); + private static final int MAX_CONCURRENT_THREADS = 4; + + @NonNull final Context mContext; + @Nullable + final SQLiteDatabase mDb; + @NonNull + final ExecutorService mExecutor; + /** + * Construct an IpMemoryStoreService object. + * This constructor will block on disk access to open the database. + * @param context the context to access storage with. + */ public IpMemoryStoreService(@NonNull final Context context) { + // Note that constructing the service will access the disk and block + // for some time, but it should make no difference to the clients. Because + // the interface is one-way, clients fire and forget requests, and the callback + // will get called eventually in any case, and the framework will wait for the + // service to be created to deliver subsequent requests. + // Avoiding this would mean the mDb member can't be final, which means the service would + // have to test for nullity, care for failure, and allow for a wait at every single access, + // which would make the code a lot more complex and require all methods to possibly block. mContext = context; + SQLiteDatabase db; + final IpMemoryStoreDatabase.DbHelper helper = new IpMemoryStoreDatabase.DbHelper(context); + try { + db = helper.getWritableDatabase(); + if (null == db) Log.e(TAG, "Unexpected null return of getWriteableDatabase"); + } catch (final SQLException e) { + Log.e(TAG, "Can't open the Ip Memory Store database", e); + db = null; + } catch (final Exception e) { + Log.wtf(TAG, "Impossible exception Ip Memory Store database", e); + db = null; + } + mDb = db; + // The work-stealing thread pool executor will spawn threads as needed up to + // the max only when there is no free thread available. This generally behaves + // exactly like one would expect it intuitively : + // - When work arrives, it will spawn a new thread iff there are no available threads + // - When there is no work to do it will shutdown threads after a while (the while + // being equal to 2 seconds (not configurable) when max threads are spun up and + // twice as much for every one less thread) + // - When all threads are busy the work is enqueued and waits for any worker + // to become available. + // Because the stealing pool is made for very heavily parallel execution of + // small tasks that spawn others, it creates a queue per thread that in this + // case is overhead. However, the three behaviors above make it a superior + // choice to cached or fixedThreadPoolExecutor, neither of which can actually + // enqueue a task waiting for a thread to be free. This can probably be solved + // with judicious subclassing of ThreadPoolExecutor, but that's a lot of dangerous + // complexity for little benefit in this case. + mExecutor = Executors.newWorkStealingPool(MAX_CONCURRENT_THREADS); + } + + /** + * Shutdown the memory store service, cancelling running tasks and dropping queued tasks. + * + * This is provided to give a way to clean up, and is meant to be available in case of an + * emergency shutdown. + */ + public void shutdown() { + // By contrast with ExecutorService#shutdown, ExecutorService#shutdownNow tries + // to cancel the existing tasks, and does not wait for completion. It does not + // guarantee the threads can be terminated in any given amount of time. + mExecutor.shutdownNow(); + if (mDb != null) mDb.close(); } /** @@ -61,7 +132,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { public void storeNetworkAttributes(@NonNull final String l2Key, @NonNull final NetworkAttributesParcelable attributes, @Nullable final IOnStatusListener listener) { - // TODO : implement this + // TODO : implement this. } /** @@ -79,7 +150,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { public void storeBlob(@NonNull final String l2Key, @NonNull final String clientId, @NonNull final String name, @NonNull final Blob data, @Nullable final IOnStatusListener listener) { - // TODO : implement this + // TODO : implement this. } /** @@ -99,7 +170,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { @Override public void findL2Key(@NonNull final NetworkAttributesParcelable attributes, @NonNull final IOnL2KeyResponseListener listener) { - // TODO : implement this + // TODO : implement this. } /** @@ -114,7 +185,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { @Override public void isSameNetwork(@NonNull final String l2Key1, @NonNull final String l2Key2, @NonNull final IOnSameNetworkResponseListener listener) { - // TODO : implement this + // TODO : implement this. } /** diff --git a/services/ipmemorystore/java/com/android/server/net/ipmemorystore/RelevanceUtils.java b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/RelevanceUtils.java new file mode 100644 index 000000000000..aa454008958d --- /dev/null +++ b/services/ipmemorystore/java/com/android/server/net/ipmemorystore/RelevanceUtils.java @@ -0,0 +1,307 @@ +/* + * 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. + */ + +package com.android.server.net.ipmemorystore; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * A class containing the logic around the relevance value for + * IP Memory Store. + * + * @hide + */ +public class RelevanceUtils { + /** + * The relevance is a decaying value that gets lower and lower until it + * reaches 0 after some time passes. It follows an exponential decay law, + * dropping slowly at first then faster and faster, because a network is + * likely to be visited again if it was visited not long ago, and the longer + * it hasn't been visited the more likely it is that it won't be visited + * again. For example, a network visited on holiday should stay fresh for + * the duration of the holiday and persist for a while, but after the venue + * hasn't been visited for a while it should quickly be discarded. What + * should accelerate forgetting the network is extended periods without + * visits, so that occasional venues get discarded but regular visits keep + * the network relevant, even if the visits are infrequent. + * + * This function must be stable by iteration, meaning that adjusting the same value + * for different dates iteratively multiple times should give the same result. + * Formally, if f is the decay function that associates a relevance x at a date d1 + * to the value at ulterior date d3, then for any date d2 between d1 and d3 : + * f(x, d3 - d1) = f(f(x, d3 - d2), d2 - d1). Intuitively, this property simply + * means it should be the same to compute and store back the value after two months, + * or to do it once after one month, store it back, and do it again after another + * months has passed. + * The pair of the relevance and date define the entire curve, so any pair + * of values on the curve will define the same curve. Setting one of them to a + * constant, so as not to have to store it, means the other one will always suffice + * to describe the curve. For example, only storing the date for a known, constant + * value of the relevance is an efficient way of remembering this information (and + * to compare relevances together, as f is monotonically decreasing). + * + *** Choosing the function : + * Functions of the kind described above are standard exponential decay functions + * like the ones that govern atomic decay where the value at any given date can be + * computed uniformly from the value at a previous date and the time elapsed since + * that date. It is simple to picture this kind of function as one where after a + * given period of time called the half-life, the relevance value will have been + * halved. Decay of this kind is expressed in function of the previous value by + * functions like + * f(x, t) = x * F ^ (t / L) + * ...where x is the value, t is the elapsed time, L is the half-life (or more + * generally the F-th-life) and F the decay factor (typically 0.5, hence why L is + * usually called the half-life). The ^ symbol here is used for exponentiation. + * Or, starting at a given M for t = 0 : + * f(t) = M * F ^ (t / L) + * + * Because a line in the store needs to become irrelevant at some point but + * this class of functions never go to 0, a minimum cutoff has to be chosen to + * represent irrelevance. The simpler way of doing this is to simply add this + * minimum cutoff to the computation before and removing it after. + * Thus the function becomes : + * f(x, t) = ((x + K) * F ^ (t / L)) - K + * ...where K is the minimum cutoff, L the half-life, and F the factor between + * the original x and x after its half-life. Strictly speaking using the word + * "half-life" implies that F = 0.5, but the relation works for any value of F. + * + * It is easy enough to check that this function satisfies the stability + * relation that was given above for any value of F, L and K, which become + * parameters that can be defined at will. + * + * relevance + * 1.0 | + * |\ + * | \ + * | \ (this graph rendered with L = 75 days and K = 1/40) + * 0.75| ', + * | \ + * | '. + * | \. + * | \ + * 0.5 | '\ + * | ''. + * | ''. + * | ''. + * 0.25| '''.. + * | '''.. + * | ''''.... + * | '''''.......... + * 0 +-------------------------------------------------------''''''''''---- + * 0 50 100 150 200 250 300 350 400 days + * + *** Choosing the parameters + * The maximum M is an arbitrary parameter that simply scales the curve. + * The tradeoff for M is pretty simple : if the relevance is going to be an + * integer, the bigger M is the more precision there is in the relevance. + * However, values of M that are easy for humans to read are preferable to + * help debugging, and a suitably low value may be enough to ensure there + * won't be integer overflows in intermediate computations. + * A value of 1_000_000 probably is plenty for precision, while still in the + * low range of what ints can represent. + * + * F and L are parameters to be chosen arbitrarily and have an impact on how + * fast the relevance will be decaying at first, keeping in mind that + * the 400 days value and the cap stay the same. In simpler words, F and L + * define the steepness of the curve. + * To keep things simple (and familiar) F is arbitrarily chosen to be 0.5, and + * L is set to 200 days visually to achieve the desired effect. Refer to the + * illustration above to get a feel of how that feels. + * + * Moreover, the memory store works on an assumption that the relevance should + * be capped, and that an entry with capped relevance should decay in 400 days. + * This is on premises that the networks a device will need to remember the + * longest should be networks visited about once a year. + * For this reason, the relevance is at the maximum M 400 days before expiry : + * f(M, 400 days) = 0 + * From replacing this with the value of the function, K can then be derived + * from the values of M, F and L : + * (M + K) * F ^ (t / L) - K = 0 + * K = M * F ^ (400 days / L) / (1 - F ^ (400 days / L)) + * Replacing with actual values this gives : + * K = 1_000_000 * 0.5 ^ (400 / 200) / (1 - 0.5 ^ (400 / 200)) + * = 1_000_000 / 3 ≈ 333_333.3 + * This ensures the function has the desired profile, the desired value at + * cap, and the desired value at expiry. + * + *** Useful relations + * Let's define the expiry time for any given relevance x as the interval of + * time such as : + * f(x, expiry) = 0 + * which can be rewritten + * ((x + K) * F ^ (expiry / L)) = K + * ...giving an expression of the expiry in function of the relevance x as + * expiry = L * logF(K / (x + K)) + * Conversely the relevance x can be expressed in function of the expiry as + * x = K / F ^ (expiry / L) - K + * These relations are useful in utility functions. + * + *** Bumping things up + * The last issue therefore is to decide how to bump up the relevance. The + * simple approach is to simply lift up the curve a little bit by a constant + * normalized amount, delaying the time of expiry. For example increasing + * the relevance by an amount I gives : + * x2 = x1 + I + * x2 and x1 correspond to two different expiry times expiry2 and expiry1, + * and replacing x1 and x2 in the relation above with their expression in + * function of the expiry comes : + * K / F ^ (expiry2 / L) - K = K / F ^ (expiry1 / L) - K + I + * which resolves to : + * expiry2 = L * logF(K / (I + K / F ^ (expiry1 / L))) + * + * In this implementation, the bump is defined as 1/25th of the cap for + * the relevance. This means a network will be remembered for the maximum + * period of 400 days if connected 25 times in succession not accounting + * for decay. Of course decay actually happens so it will take more than 25 + * connections for any given network to actually reach the cap, but because + * decay is slow at first, it is a good estimate of how fast cap happens. + * + * Specifically, it gives the following four results : + * - A network that a device connects to once hits irrelevance about 32.7 days after + * it was first registered if never connected again. + * - A network that a device connects to once a day at a fixed hour will hit the cap + * on the 27th connection. + * - A network that a device connects to once a week at a fixed hour will hit the cap + * on the 57th connection. + * - A network that a device connects to every day for 7 straight days then never again + * expires 144 days after the last connection. + * These metrics tend to match pretty well the requirements. + */ + + // TODO : make these constants configurable at runtime. Don't forget to build it so that + // changes will wipe the database, migrate the values, or otherwise make sure the relevance + // values are still meaningful. + + // How long, in milliseconds, is a capped relevance valid for, or in other + // words how many milliseconds after its relevance was set to RELEVANCE_CAP does + // any given line expire. 400 days. + @VisibleForTesting + public static final long CAPPED_RELEVANCE_LIFETIME_MS = 400L * 24 * 60 * 60 * 1000; + + // The constant that represents a normalized 1.0 value for the relevance. In other words, + // the cap for the relevance. This is referred to as M in the explanation above. + @VisibleForTesting + public static final int CAPPED_RELEVANCE = 1_000_000; + + // The decay factor. After a half-life, the relevance will have decayed by this value. + // This is referred to as F in the explanation above. + private static final double DECAY_FACTOR = 0.5; + + // The half-life. After this time, the relevance will have decayed by a factor DECAY_FACTOR. + // This is referred to as L in the explanation above. + private static final long HALF_LIFE_MS = 200L * 24 * 60 * 60 * 1000; + + // The value of the frame change. This is referred to as K in the explanation above. + private static final double IRRELEVANCE_FLOOR = + CAPPED_RELEVANCE * powF((double) CAPPED_RELEVANCE_LIFETIME_MS / HALF_LIFE_MS) + / (1 - powF((double) CAPPED_RELEVANCE_LIFETIME_MS / HALF_LIFE_MS)); + + // How much to bump the relevance by every time a line is written to. + @VisibleForTesting + public static final int RELEVANCE_BUMP = CAPPED_RELEVANCE / 25; + + // Java doesn't include a function for the logarithm in an arbitrary base, so implement it + private static final double LOG_DECAY_FACTOR = Math.log(DECAY_FACTOR); + private static double logF(final double value) { + return Math.log(value) / LOG_DECAY_FACTOR; + } + + // Utility function to get a power of the decay factor, to simplify the code. + private static double powF(final double value) { + return Math.pow(DECAY_FACTOR, value); + } + + /** + * Compute the value of the relevance now given an expiry date. + * + * @param expiry the date at which the column in the database expires. + * @return the adjusted value of the relevance for this moment in time. + */ + public static int computeRelevanceForNow(final long expiry) { + return computeRelevanceForTargetDate(expiry, System.currentTimeMillis()); + } + + /** + * Compute the value of the relevance at a given date from an expiry date. + * + * Because relevance decays with time, a relevance in the past corresponds to + * a different relevance later. + * + * Relevance is always a positive value. 0 means not relevant at all. + * + * See the explanation at the top of this file to get the justification for this + * computation. + * + * @param expiry the date at which the column in the database expires. + * @param target the target date to adjust the relevance to. + * @return the adjusted value of the relevance for the target moment. + */ + public static int computeRelevanceForTargetDate(final long expiry, final long target) { + final long delay = expiry - target; + if (delay >= CAPPED_RELEVANCE_LIFETIME_MS) return CAPPED_RELEVANCE; + if (delay <= 0) return 0; + return (int) (IRRELEVANCE_FLOOR / powF((float) delay / HALF_LIFE_MS) - IRRELEVANCE_FLOOR); + } + + /** + * Compute the expiry duration adjusted up for a new fresh write. + * + * Every time data is written to the memory store for a given line, the + * relevance is bumped up by a certain amount, which will boost the priority + * of this line for computation of group attributes, and delay (possibly + * indefinitely, if the line is accessed regularly) forgetting the data stored + * in that line. + * As opposed to bumpExpiryDate, this function uses a duration from now to expiry. + * + * See the explanation at the top of this file for a justification of this computation. + * + * @param oldExpiryDuration the old expiry duration in milliseconds from now. + * @return the expiry duration representing a bumped up relevance value. + */ + public static long bumpExpiryDuration(final long oldExpiryDuration) { + // L * logF(K / (I + K / F ^ (expiry1 / L))), as documented above + final double divisionFactor = powF(((double) oldExpiryDuration) / HALF_LIFE_MS); + final double oldRelevance = IRRELEVANCE_FLOOR / divisionFactor; + final long newDuration = + (long) (HALF_LIFE_MS * logF(IRRELEVANCE_FLOOR / (RELEVANCE_BUMP + oldRelevance))); + return Math.min(newDuration, CAPPED_RELEVANCE_LIFETIME_MS); + } + + /** + * Compute the new expiry date adjusted up for a new fresh write. + * + * Every time data is written to the memory store for a given line, the + * relevance is bumped up by a certain amount, which will boost the priority + * of this line for computation of group attributes, and delay (possibly + * indefinitely, if the line is accessed regularly) forgetting the data stored + * in that line. + * As opposed to bumpExpiryDuration, this function takes the old timestamp and returns the + * new timestamp. + * + * {@see bumpExpiryDuration}, and keep in mind that the bump depends on when this is called, + * because the relevance decays exponentially, therefore bumping up a high relevance (for a + * date far in the future) is less potent than bumping up a low relevance (for a date in + * a close future). + * + * @param oldExpiryDate the old date of expiration. + * @return the new expiration date after the relevance bump. + */ + public static long bumpExpiryDate(final long oldExpiryDate) { + final long now = System.currentTimeMillis(); + final long newDuration = bumpExpiryDuration(oldExpiryDate - now); + return now + newDuration; + } +} diff --git a/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java b/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java index 7559560f4f6d..9a6e003c1317 100644 --- a/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java +++ b/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java @@ -58,7 +58,9 @@ import com.android.server.backup.transport.OnTransportRegisteredListener; import com.android.server.backup.transport.TransportClient; import com.android.server.backup.transport.TransportClientManager; import com.android.server.backup.transport.TransportNotRegisteredException; +import com.android.server.testing.shadows.ShadowApplicationPackageManager; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,6 +68,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayList; @@ -75,6 +78,7 @@ import java.util.Set; import java.util.stream.Stream; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowApplicationPackageManager.class}) @Presubmit public class TransportManagerTest { private static final String PACKAGE_A = "some.package.a"; @@ -102,6 +106,12 @@ public class TransportManagerTest { mTransportB1 = genericTransport(PACKAGE_B, "TransportBaz"); } + /** Reset shadow state. */ + @After + public void tearDown() throws Exception { + ShadowApplicationPackageManager.reset(); + } + @Test public void testRegisterTransports() throws Exception { setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED); @@ -666,7 +676,8 @@ public class TransportManagerTest { packageInfo.packageName = packageName; packageInfo.applicationInfo = new ApplicationInfo(); packageInfo.applicationInfo.privateFlags = flags; - mShadowPackageManager.addPackage(packageInfo); + mShadowPackageManager.installPackage(packageInfo); + ShadowApplicationPackageManager.addInstalledPackage(packageName, packageInfo); } private TransportManager createTransportManagerWithRegisteredTransports( diff --git a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java index 3b7fa3d6ed05..427aed7364d2 100644 --- a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java +++ b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java @@ -514,10 +514,14 @@ public class UserBackupManagerServiceTest { private void setUpForUpdateTransportAttributes() throws Exception { mTransportComponent = mTransport.getTransportComponent(); String transportPackage = mTransportComponent.getPackageName(); + PackageInfo packageInfo = getPackageInfo(transportPackage); ShadowPackageManager shadowPackageManager = shadowOf(mContext.getPackageManager()); - shadowPackageManager.addPackage(transportPackage); + shadowPackageManager.installPackage(packageInfo); shadowPackageManager.setPackagesForUid(PACKAGE_UID, transportPackage); + // Set up for user invocations on ApplicationPackageManager. + ShadowApplicationPackageManager.addInstalledPackage(transportPackage, packageInfo); + ShadowApplicationPackageManager.setPackageUid(transportPackage, PACKAGE_UID); mTransportUid = mContext.getPackageManager().getPackageUid(transportPackage, 0); } diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java index dc322094add8..ab121eddff06 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java @@ -38,6 +38,7 @@ public class ShadowApplicationPackageManager extends org.robolectric.shadows.ShadowApplicationPackageManager { private static final Map<String, PackageInfo> sPackageInfos = new ArrayMap<>(); private static final List<PackageInfo> sInstalledPackages = new ArrayList<>(); + private static final Map<String, Integer> sPackageUids = new ArrayMap<>(); /** * Registers the package {@code packageName} to be returned when invoking {@link @@ -49,6 +50,14 @@ public class ShadowApplicationPackageManager sInstalledPackages.add(packageInfo); } + /** + * Sets the package uid {@code packageUid} for the package {@code packageName} to be returned + * when invoking {@link ApplicationPackageManager#getPackageUidAsUser(String, int, int)}. + */ + public static void setPackageUid(String packageName, int packageUid) { + sPackageUids.put(packageName, packageUid); + } + @Override protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId) throws NameNotFoundException { @@ -63,6 +72,15 @@ public class ShadowApplicationPackageManager return sInstalledPackages; } + @Override + protected int getPackageUidAsUser(String packageName, int flags, int userId) + throws NameNotFoundException { + if (!sPackageUids.containsKey(packageName)) { + throw new NameNotFoundException(packageName); + } + return sPackageUids.get(packageName); + } + /** Clear package state. */ @Resetter public static void reset() { 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 f1cd0cd6d30c..57ee6dcad9f2 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.assertFalse; 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.timeout; 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.BatteryManager; 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { @Mock private UsageStatsManagerInternal mUsageStatsManager; + private JobStore mJobStore; + @Before public void setUp() { mMockingSession = mockitoSession() @@ -123,8 +138,17 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { // 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 @@ public class QuotaControllerTest { // 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { // 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 3b6b48b6aa3f..f817e8e33b31 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 @@ public class DexLoggerTests { @Mock IPackageManager mPM; @Mock Installer mInstaller; - private final Object mInstallLock = new Object(); private PackageDynamicCodeLoading mPackageDynamicCodeLoading; private DexLogger mDexLogger; @@ -103,7 +102,7 @@ public class DexLoggerTests { }; // 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/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 8f9d2badce30..d1609c3955bf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -41,9 +41,11 @@ import static org.junit.Assert.assertTrue; import android.app.ActivityOptions; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.PauseActivityItem; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.util.MergedConfiguration; import android.util.MutableBoolean; import androidx.test.filters.MediumTest; @@ -251,4 +253,72 @@ public class ActivityRecordTests extends ActivityTestsBase { assertEquals(prevSeq + 1, appWindowTokenRequestedOrientation.seq); verify(mActivity.mAppWindowToken).onMergedOverrideConfigurationChanged(); } + + @Test + public void testSetsRelaunchReason_NotDragResizing() { + mActivity.setState(ActivityStack.ActivityState.RESUMED, "Testing"); + + mTask.onRequestedOverrideConfigurationChanged(mTask.getConfiguration()); + mActivity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(), + mActivity.getConfiguration())); + + mActivity.info.configChanges &= ~ActivityInfo.CONFIG_ORIENTATION; + final Configuration newConfig = new Configuration(mTask.getConfiguration()); + newConfig.orientation = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT; + mTask.onRequestedOverrideConfigurationChanged(newConfig); + + mActivity.mRelaunchReason = ActivityTaskManagerService.RELAUNCH_REASON_NONE; + + mActivity.ensureActivityConfiguration(0, false, false); + + assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE, + mActivity.mRelaunchReason); + } + + @Test + public void testSetsRelaunchReason_DragResizing() { + mActivity.setState(ActivityStack.ActivityState.RESUMED, "Testing"); + + mTask.onRequestedOverrideConfigurationChanged(mTask.getConfiguration()); + mActivity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(), + mActivity.getConfiguration())); + + mActivity.info.configChanges &= ~ActivityInfo.CONFIG_ORIENTATION; + final Configuration newConfig = new Configuration(mTask.getConfiguration()); + newConfig.orientation = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT + ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT; + mTask.onRequestedOverrideConfigurationChanged(newConfig); + + doReturn(true).when(mTask.getTask()).isDragResizing(); + + mActivity.mRelaunchReason = ActivityTaskManagerService.RELAUNCH_REASON_NONE; + + mActivity.ensureActivityConfiguration(0, false, false); + + assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_FREE_RESIZE, + mActivity.mRelaunchReason); + } + + @Test + public void testSetsRelaunchReason_NonResizeConfigChanges() { + mActivity.setState(ActivityStack.ActivityState.RESUMED, "Testing"); + + mTask.onRequestedOverrideConfigurationChanged(mTask.getConfiguration()); + mActivity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(), + mActivity.getConfiguration())); + + mActivity.info.configChanges &= ~ActivityInfo.CONFIG_FONT_SCALE; + final Configuration newConfig = new Configuration(mTask.getConfiguration()); + newConfig.fontScale = 5; + mTask.onRequestedOverrideConfigurationChanged(newConfig); + + mActivity.mRelaunchReason = + ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE; + + mActivity.ensureActivityConfiguration(0, false, false); + + assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_NONE, + mActivity.mRelaunchReason); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java index b2a28699e6a0..9d5f687b83a7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java @@ -36,6 +36,8 @@ import static com.android.server.wm.ActivityStack.ActivityState.PAUSING; import static com.android.server.wm.ActivityStack.ActivityState.RESUMED; import static com.android.server.wm.ActivityStack.ActivityState.STOPPING; import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_DESTROYING; +import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_FREE_RESIZE; +import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE; import static com.google.common.truth.Truth.assertThat; @@ -687,6 +689,62 @@ public class ActivityStackTests extends ActivityTestsBase { } @Test + public void testHandleAppDied_RelaunchesAfterCrashDuringWindowingModeResize() { + final ActivityRecord activity = new ActivityBuilder(mService).setTask(mTask).build(); + + activity.mRelaunchReason = RELAUNCH_REASON_WINDOWING_MODE_RESIZE; + activity.launchCount = 1; + activity.haveState = false; + + mStack.handleAppDiedLocked(activity.app); + + assertEquals(1, mTask.mActivities.size()); + assertEquals(1, mStack.getAllTasks().size()); + } + + @Test + public void testHandleAppDied_NotRelaunchAfterThreeCrashesDuringWindowingModeResize() { + final ActivityRecord activity = new ActivityBuilder(mService).setTask(mTask).build(); + + activity.mRelaunchReason = RELAUNCH_REASON_WINDOWING_MODE_RESIZE; + activity.launchCount = 3; + activity.haveState = false; + + mStack.handleAppDiedLocked(activity.app); + + assertThat(mTask.mActivities).isEmpty(); + assertThat(mStack.getAllTasks()).isEmpty(); + } + + @Test + public void testHandleAppDied_RelaunchesAfterCrashDuringFreeResize() { + final ActivityRecord activity = new ActivityBuilder(mService).setTask(mTask).build(); + + activity.mRelaunchReason = RELAUNCH_REASON_FREE_RESIZE; + activity.launchCount = 1; + activity.haveState = false; + + mStack.handleAppDiedLocked(activity.app); + + assertEquals(1, mTask.mActivities.size()); + assertEquals(1, mStack.getAllTasks().size()); + } + + @Test + public void testHandleAppDied_NotRelaunchAfterThreeCrashesDuringFreeResize() { + final ActivityRecord activity = new ActivityBuilder(mService).setTask(mTask).build(); + + activity.mRelaunchReason = RELAUNCH_REASON_FREE_RESIZE; + activity.launchCount = 3; + activity.haveState = false; + + mStack.handleAppDiedLocked(activity.app); + + assertThat(mTask.mActivities).isEmpty(); + assertThat(mStack.getAllTasks()).isEmpty(); + } + + @Test public void testFinishCurrentActivity() { // Create 2 activities on a new display. final ActivityDisplay display = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP); 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 fa4228908cea..51bebbbcd6b0 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 android.view.Display; 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 @@ import org.junit.Test; @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/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index bbf3d45d7c99..613c4ffceffc 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -371,14 +371,15 @@ public class VoiceInteractionManagerService extends SystemService { } private boolean shouldEnableService(Context context) { - // VoiceInteractionService should not be enabled on any low RAM devices - // or devices that have not declared the recognition feature, unless the - // device's configuration has explicitly set the config flag for a fixed + // VoiceInteractionService should not be enabled on devices that have not declared the + // recognition feature (including low-ram devices where notLowRam="true" takes effect), + // unless the device's configuration has explicitly set the config flag for a fixed // voice interaction service. - return (!ActivityManager.isLowRamDeviceStatic() - && context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_VOICE_RECOGNIZERS)) || - getForceVoiceInteractionServicePackage(context.getResources()) != null; + if (getForceVoiceInteractionServicePackage(context.getResources()) != null) { + return true; + } + return context.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_VOICE_RECOGNIZERS); } private String getForceVoiceInteractionServicePackage(Resources res) { diff --git a/telephony/java/android/telephony/AvailableNetworkInfo.java b/telephony/java/android/telephony/AvailableNetworkInfo.java index fe07370394ad..4da79b34a55e 100644 --- a/telephony/java/android/telephony/AvailableNetworkInfo.java +++ b/telephony/java/android/telephony/AvailableNetworkInfo.java @@ -110,6 +110,7 @@ public final class AvailableNetworkInfo implements Parcelable { 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 babeb7b1b61c..3311218bdb3c 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -9963,7 +9963,7 @@ public class TelephonyManager { 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 96f7a1bc22a0..3408291825d1 100644 --- a/telephony/java/com/android/internal/telephony/DctConstants.java +++ b/telephony/java/com/android/internal/telephony/DctConstants.java @@ -92,6 +92,7 @@ public class DctConstants { 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 2a648bd8b252..8523554aae63 100644 --- a/telephony/java/com/android/internal/telephony/TelephonyIntents.java +++ b/telephony/java/com/android/internal/telephony/TelephonyIntents.java @@ -501,4 +501,18 @@ public class TelephonyIntents { */ 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/test-base/api/current.txt b/test-base/api/current.txt index 7ebd6aa8a4a2..91fcca5cac25 100644 --- a/test-base/api/current.txt +++ b/test-base/api/current.txt @@ -48,6 +48,9 @@ package android.test { method public abstract void startTiming(boolean); } + public abstract deprecated class RepetitiveTest implements java.lang.annotation.Annotation { + } + public abstract deprecated class UiThreadTest implements java.lang.annotation.Annotation { } diff --git a/test-base/src/android/test/RepetitiveTest.java b/test-base/src/android/test/RepetitiveTest.java index 6a7130e68e61..13e89d2d92a6 100644 --- a/test-base/src/android/test/RepetitiveTest.java +++ b/test-base/src/android/test/RepetitiveTest.java @@ -26,8 +26,10 @@ import java.lang.annotation.Target; * When the annotation is present, the test method is executed the number of times specified by * numIterations and defaults to 1. * - * {@hide} Not needed for public API. + * @deprecated New tests should be written using the + * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>. */ +@Deprecated @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RepetitiveTest { @@ -37,4 +39,4 @@ public @interface RepetitiveTest { * @return The total number of iterations, the default is 1. */ int numIterations() default 1; -}
\ No newline at end of file +} diff --git a/test-legacy/Android.bp b/test-legacy/Android.bp index 833c714f07b0..a69f422b3bdb 100644 --- a/test-legacy/Android.bp +++ b/test-legacy/Android.bp @@ -25,7 +25,7 @@ java_library_static { static_libs: [ "android.test.base-minus-junit", "android.test.runner-minus-junit", - "android.test.mock.impl", + "android.test.mock_static", ], no_framework_libs: true, diff --git a/test-mock/Android.bp b/test-mock/Android.bp index e1d6e01d6d06..43b765d8b783 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -30,3 +30,19 @@ java_sdk_library { srcs_lib_whitelist_pkgs: ["android"], compile_dex: true, } + +// Build the android.test.mock_static library +// ========================================== +// This is only intended for inclusion in the legacy-android-test. +// Must not be used elewhere. +java_library_static { + name: "android.test.mock_static", + + java_version: "1.8", + srcs: ["src/**/*.java"], + + no_framework_libs: true, + libs: [ + "framework", + ], +} diff --git a/tests/net/java/android/net/LinkPropertiesTest.java b/tests/net/java/android/net/LinkPropertiesTest.java index 932fee0c1956..299fbefc78e4 100644 --- a/tests/net/java/android/net/LinkPropertiesTest.java +++ b/tests/net/java/android/net/LinkPropertiesTest.java @@ -849,6 +849,18 @@ public class LinkPropertiesTest { 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 @@ public class LinkPropertiesTest { 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 bf3964416e11..2a92a7dabd98 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -4683,7 +4683,7 @@ public class ConnectivityServiceTest { 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 @@ public class ConnectivityServiceTest { 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 4c52d818269d..9578ded1a089 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.app.PendingIntent; 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 @@ public class LingerMonitorTest { LingerMonitor mMonitor; @Mock ConnectivityService mConnService; + @Mock INetd mNetd; + @Mock INetworkManagementService mNMS; @Mock Context mCtx; @Mock NetworkMisc mMisc; @Mock NetworkNotificationManager mNotifier; @@ -352,7 +356,7 @@ public class LingerMonitorTest { 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 bf42412d68d8..07b1d057c882 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.verifyNoMoreInteractions; 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 @@ public class Nat464XlatTest { @Mock ConnectivityService mConnectivity; @Mock NetworkMisc mMisc; + @Mock INetd mNetd; @Mock INetworkManagementService mNms; @Mock InterfaceConfiguration mConfig; @Mock NetworkAgentInfo mNai; @@ -65,7 +65,7 @@ public class Nat464XlatTest { Handler mHandler; Nat464Xlat makeNat464Xlat() { - return new Nat464Xlat(mNms, mNai); + return new Nat464Xlat(mNai, mNetd, mNms); } @Before @@ -129,7 +129,7 @@ public class Nat464XlatTest { 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 @@ public class Nat464XlatTest { // 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 @@ public class Nat464XlatTest { assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); assertIdle(nat); - verifyNoMoreInteractions(mNms, mConnectivity); + verifyNoMoreInteractions(mNetd, mNms, mConnectivity); } @Test @@ -168,7 +168,7 @@ public class Nat464XlatTest { 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 @@ public class Nat464XlatTest { 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 @@ public class Nat464XlatTest { // ConnectivityService stops clat: no-op. nat.stop(); - verifyNoMoreInteractions(mNms, mConnectivity); + verifyNoMoreInteractions(mNetd, mNms, mConnectivity); } @Test @@ -205,13 +205,13 @@ public class Nat464XlatTest { 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 @@ public class Nat464XlatTest { assertIdle(nat); - verifyNoMoreInteractions(mNms, mConnectivity); + verifyNoMoreInteractions(mNetd, mNms, mConnectivity); } @Test @@ -236,16 +236,16 @@ public class Nat464XlatTest { 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) { diff --git a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java index 859a54d29321..e63c3b02d1c3 100644 --- a/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java +++ b/tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java @@ -16,6 +16,9 @@ package com.android.server.net.ipmemorystore; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + import android.content.Context; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -26,6 +29,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.File; + /** Unit tests for {@link IpMemoryStoreServiceTest}. */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -36,6 +41,7 @@ public class IpMemoryStoreServiceTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(new File("/tmp/test.db")).when(mMockContext).getDatabasePath(anyString()); } @Test diff --git a/tests/net/java/com/android/server/net/ipmemorystore/RelevanceUtilsTests.java b/tests/net/java/com/android/server/net/ipmemorystore/RelevanceUtilsTests.java new file mode 100644 index 000000000000..8d367e2fc387 --- /dev/null +++ b/tests/net/java/com/android/server/net/ipmemorystore/RelevanceUtilsTests.java @@ -0,0 +1,149 @@ +/* + * 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. + */ + +package com.android.server.net.ipmemorystore; + +import static com.android.server.net.ipmemorystore.RelevanceUtils.CAPPED_RELEVANCE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link RelevanceUtils}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RelevanceUtilsTests { + @Test + public void testComputeRelevanceForTargetDate() { + final long dayInMillis = 24L * 60 * 60 * 1000; + final long base = 1_000_000L; // any given point in time + // Relevance when the network expires in 1000 years must be capped + assertEquals(CAPPED_RELEVANCE, RelevanceUtils.computeRelevanceForTargetDate( + base + 1000L * dayInMillis, base)); + // Relevance when expiry is before the date must be 0 + assertEquals(0, RelevanceUtils.computeRelevanceForTargetDate(base - 1, base)); + // Make sure the relevance for a given target date is higher if the expiry is further + // in the future + assertTrue(RelevanceUtils.computeRelevanceForTargetDate(base + 100 * dayInMillis, base) + < RelevanceUtils.computeRelevanceForTargetDate(base + 150 * dayInMillis, base)); + + // Make sure the relevance falls slower as the expiry is closing in. This is to ensure + // the decay is indeed logarithmic. + final int relevanceAtExpiry = RelevanceUtils.computeRelevanceForTargetDate(base, base); + final int relevance50DaysBeforeExpiry = + RelevanceUtils.computeRelevanceForTargetDate(base + 50 * dayInMillis, base); + final int relevance100DaysBeforeExpiry = + RelevanceUtils.computeRelevanceForTargetDate(base + 100 * dayInMillis, base); + final int relevance150DaysBeforeExpiry = + RelevanceUtils.computeRelevanceForTargetDate(base + 150 * dayInMillis, base); + assertEquals(0, relevanceAtExpiry); + assertTrue(relevance50DaysBeforeExpiry - relevanceAtExpiry + < relevance100DaysBeforeExpiry - relevance50DaysBeforeExpiry); + assertTrue(relevance100DaysBeforeExpiry - relevance50DaysBeforeExpiry + < relevance150DaysBeforeExpiry - relevance100DaysBeforeExpiry); + } + + @Test + public void testIncreaseRelevance() { + long expiry = System.currentTimeMillis(); + + final long firstBump = RelevanceUtils.bumpExpiryDate(expiry); + // Though a few milliseconds might have elapsed, the first bump should push the duration + // to days in the future, so unless this test takes literal days between these two lines, + // this should always pass. + assertTrue(firstBump > expiry); + + expiry = 0; + long lastDifference = Long.MAX_VALUE; + // The relevance should be capped in at most this many steps. Otherwise, fail. + final int steps = 1000; + for (int i = 0; i < steps; ++i) { + final long newExpiry = RelevanceUtils.bumpExpiryDuration(expiry); + if (newExpiry == expiry) { + // The relevance should be capped. Make sure it is, then exit without failure. + assertEquals(newExpiry, RelevanceUtils.CAPPED_RELEVANCE_LIFETIME_MS); + return; + } + // Make sure the new expiry is further in the future than last time. + assertTrue(newExpiry > expiry); + // Also check that it was not bumped as much as the last bump, because the + // decay must be exponential. + assertTrue(newExpiry - expiry < lastDifference); + lastDifference = newExpiry - expiry; + expiry = newExpiry; + } + fail("Relevance failed to go to the maximum value after " + steps + " bumps"); + } + + @Test + public void testContinuity() { + final long expiry = System.currentTimeMillis(); + + // Relevance at expiry and after expiry should be the cap. + final int relevanceBeforeMaxLifetime = RelevanceUtils.computeRelevanceForTargetDate(expiry, + expiry - (RelevanceUtils.CAPPED_RELEVANCE_LIFETIME_MS + 1_000_000)); + assertEquals(relevanceBeforeMaxLifetime, CAPPED_RELEVANCE); + final int relevanceForMaxLifetime = RelevanceUtils.computeRelevanceForTargetDate(expiry, + expiry - RelevanceUtils.CAPPED_RELEVANCE_LIFETIME_MS); + assertEquals(relevanceForMaxLifetime, CAPPED_RELEVANCE); + + // If the max relevance is reached at the cap lifetime, one millisecond less than this + // should be very close. Strictly speaking this is a bit brittle, but it should be + // good enough for the purposes of the memory store. + final int relevanceForOneMillisecLessThanCap = RelevanceUtils.computeRelevanceForTargetDate( + expiry, expiry - RelevanceUtils.CAPPED_RELEVANCE_LIFETIME_MS + 1); + assertTrue(relevanceForOneMillisecLessThanCap <= CAPPED_RELEVANCE); + assertTrue(relevanceForOneMillisecLessThanCap >= CAPPED_RELEVANCE - 10); + + // Likewise the relevance one millisecond before expiry should be very close to 0. It's + // fine if it rounds down to 0. + final int relevanceOneMillisecBeforeExpiry = RelevanceUtils.computeRelevanceForTargetDate( + expiry, expiry - 1); + assertTrue(relevanceOneMillisecBeforeExpiry <= 10); + assertTrue(relevanceOneMillisecBeforeExpiry >= 0); + + final int relevanceAtExpiry = RelevanceUtils.computeRelevanceForTargetDate(expiry, expiry); + assertEquals(relevanceAtExpiry, 0); + final int relevanceAfterExpiry = RelevanceUtils.computeRelevanceForTargetDate(expiry, + expiry + 1_000_000); + assertEquals(relevanceAfterExpiry, 0); + } + + // testIncreaseRelevance makes sure bumping the expiry continuously always yields a + // monotonically increasing date as a side effect, but this tests that the relevance (as + // opposed to the expiry date) increases monotonically with increasing periods. + @Test + public void testMonotonicity() { + // Hopefully the relevance is granular enough to give a different value for every one + // of this number of steps. + final int steps = 40; + final long expiry = System.currentTimeMillis(); + + int lastRelevance = -1; + for (int i = 0; i < steps; ++i) { + final long date = expiry - i * (RelevanceUtils.CAPPED_RELEVANCE_LIFETIME_MS / steps); + final int relevance = RelevanceUtils.computeRelevanceForTargetDate(expiry, date); + assertTrue(relevance > lastRelevance); + lastRelevance = relevance; + } + } +} |