diff options
122 files changed, 3220 insertions, 1536 deletions
diff --git a/Android.mk b/Android.mk index 749242d78bd2..8daab8ff800e 100644 --- a/Android.mk +++ b/Android.mk @@ -140,8 +140,8 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothInputHost.aidl \ core/java/android/bluetooth/IBluetoothHidDeviceCallback.aidl \ core/java/android/bluetooth/IBluetoothGatt.aidl \ - core/java/android/bluetooth/IBluetoothGattCallbackExt.aidl \ - core/java/android/bluetooth/IBluetoothGattServerCallbackExt.aidl \ + core/java/android/bluetooth/IBluetoothGattCallback.aidl \ + core/java/android/bluetooth/IBluetoothGattServerCallback.aidl \ core/java/android/bluetooth/le/IAdvertiserCallback.aidl \ core/java/android/bluetooth/le/IAdvertisingSetCallback.aidl \ core/java/android/bluetooth/le/IPeriodicAdvertisingCallback.aidl \ @@ -567,8 +567,9 @@ LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt okhttp bouncycastle ext LOCAL_STATIC_JAVA_LIBRARIES := \ framework-protos \ - android.hardware.thermal@1.0-java-constants \ android.hardware.health@1.0-java-constants \ + android.hardware.thermal@1.0-java-constants \ + android.hardware.tv.input@1.0-java-constants \ android.hardware.usb@1.0-java-constants \ android.hardware.vibrator@1.0-java-constants \ diff --git a/api/current.txt b/api/current.txt index c0f8ed43bc95..8d2790568f9b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -21818,23 +21818,23 @@ package android.media { } public final class MediaCas { - ctor public MediaCas(int) throws android.media.UnsupportedCasException; + ctor public MediaCas(int) throws android.media.MediaCasException.UnsupportedCasException; method public void closeSession(byte[]); method public static android.media.MediaCas.PluginDescriptor[] enumeratePlugins(); method public static boolean isSystemIdSupported(int); - method public byte[] openSession(int); - method public byte[] openSession(int, int); - method public void processEcm(byte[], byte[], int, int); - method public void processEcm(byte[], byte[]); - method public void processEmm(byte[], int, int); - method public void processEmm(byte[]); - method public void provision(java.lang.String); - method public void refreshEntitlements(int, byte[]); + method public byte[] openSession(int) throws android.media.MediaCasException; + method public byte[] openSession(int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[], int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[]) throws android.media.MediaCasException; + method public void processEmm(byte[], int, int) throws android.media.MediaCasException; + method public void processEmm(byte[]) throws android.media.MediaCasException; + method public void provision(java.lang.String) throws android.media.MediaCasException; + method public void refreshEntitlements(int, byte[]) throws android.media.MediaCasException; method public void release(); - method public void sendEvent(int, int, byte[]); + method public void sendEvent(int, int, byte[]) throws android.media.MediaCasException; method public void setEventListener(android.media.MediaCas.EventListener, android.os.Handler); - method public void setPrivateData(byte[]); - method public void setSessionPrivateData(byte[], byte[]); + method public void setPrivateData(byte[]) throws android.media.MediaCasException; + method public void setSessionPrivateData(byte[], byte[]) throws android.media.MediaCasException; } public static abstract interface MediaCas.EventListener { @@ -21847,7 +21847,22 @@ package android.media { } public class MediaCasException extends java.lang.Exception { - ctor public MediaCasException(java.lang.String); + } + + public static final class MediaCasException.DeniedByServerException extends android.media.MediaCasException { + } + + public static final class MediaCasException.NotProvisionedException extends android.media.MediaCasException { + } + + public static final class MediaCasException.ResourceBusyException extends android.media.MediaCasException { + } + + public static final class MediaCasException.UnsupportedCasException extends android.media.MediaCasException { + } + + public class MediaCasStateException extends java.lang.IllegalStateException { + method public java.lang.String getDiagnosticInfo(); } public final class MediaCodec { @@ -22275,7 +22290,7 @@ package android.media { } public final class MediaDescrambler { - ctor public MediaDescrambler(int) throws android.media.UnsupportedCasException; + ctor public MediaDescrambler(int) throws android.media.MediaCasException.UnsupportedCasException; method public final int descramble(java.nio.ByteBuffer, int, java.nio.ByteBuffer, int, android.media.MediaCodec.CryptoInfo); method public final void release(); method public final boolean requiresSecureDecoderComponent(java.lang.String); @@ -23586,10 +23601,6 @@ package android.media { field public static final int TONE_SUP_RINGTONE = 23; // 0x17 } - public final class UnsupportedCasException extends android.media.MediaCasException { - ctor public UnsupportedCasException(java.lang.String); - } - public final class UnsupportedSchemeException extends android.media.MediaDrmException { ctor public UnsupportedSchemeException(java.lang.String); } @@ -24326,9 +24337,9 @@ package android.media.session { method public void setRepeatMode(int); method public void setSessionActivity(android.app.PendingIntent); method public void setShuffleModeEnabled(boolean); - field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 + field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4 - field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 + field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 } public static abstract class MediaSession.Callback { @@ -34858,7 +34869,7 @@ package android.provider { field public static final java.lang.String WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN = "wifi_device_owner_configs_lockdown"; field public static final java.lang.String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; field public static final java.lang.String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = "wifi_mobile_data_transition_wakelock_timeout_ms"; - field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; + field public static final deprecated java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = "wifi_networks_available_repeat_delay"; field public static final java.lang.String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; field public static final java.lang.String WIFI_ON = "wifi_on"; diff --git a/api/system-current.txt b/api/system-current.txt index 3c0ab7275b2c..779a1d38b864 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -93,6 +93,7 @@ package android { field public static final java.lang.String CLEAR_APP_CACHE = "android.permission.CLEAR_APP_CACHE"; field public static final java.lang.String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA"; field public static final java.lang.String CONNECTIVITY_INTERNAL = "android.permission.CONNECTIVITY_INTERNAL"; + field public static final java.lang.String CONNECTIVITY_USE_RESTRICTED_NETWORKS = "android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"; field public static final java.lang.String CONTROL_INCALL_EXPERIENCE = "android.permission.CONTROL_INCALL_EXPERIENCE"; field public static final java.lang.String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES"; field public static final java.lang.String CONTROL_VPN = "android.permission.CONTROL_VPN"; @@ -23604,23 +23605,23 @@ package android.media { } public final class MediaCas { - ctor public MediaCas(int) throws android.media.UnsupportedCasException; + ctor public MediaCas(int) throws android.media.MediaCasException.UnsupportedCasException; method public void closeSession(byte[]); method public static android.media.MediaCas.PluginDescriptor[] enumeratePlugins(); method public static boolean isSystemIdSupported(int); - method public byte[] openSession(int); - method public byte[] openSession(int, int); - method public void processEcm(byte[], byte[], int, int); - method public void processEcm(byte[], byte[]); - method public void processEmm(byte[], int, int); - method public void processEmm(byte[]); - method public void provision(java.lang.String); - method public void refreshEntitlements(int, byte[]); + method public byte[] openSession(int) throws android.media.MediaCasException; + method public byte[] openSession(int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[], int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[]) throws android.media.MediaCasException; + method public void processEmm(byte[], int, int) throws android.media.MediaCasException; + method public void processEmm(byte[]) throws android.media.MediaCasException; + method public void provision(java.lang.String) throws android.media.MediaCasException; + method public void refreshEntitlements(int, byte[]) throws android.media.MediaCasException; method public void release(); - method public void sendEvent(int, int, byte[]); + method public void sendEvent(int, int, byte[]) throws android.media.MediaCasException; method public void setEventListener(android.media.MediaCas.EventListener, android.os.Handler); - method public void setPrivateData(byte[]); - method public void setSessionPrivateData(byte[], byte[]); + method public void setPrivateData(byte[]) throws android.media.MediaCasException; + method public void setSessionPrivateData(byte[], byte[]) throws android.media.MediaCasException; } public static abstract interface MediaCas.EventListener { @@ -23633,7 +23634,22 @@ package android.media { } public class MediaCasException extends java.lang.Exception { - ctor public MediaCasException(java.lang.String); + } + + public static final class MediaCasException.DeniedByServerException extends android.media.MediaCasException { + } + + public static final class MediaCasException.NotProvisionedException extends android.media.MediaCasException { + } + + public static final class MediaCasException.ResourceBusyException extends android.media.MediaCasException { + } + + public static final class MediaCasException.UnsupportedCasException extends android.media.MediaCasException { + } + + public class MediaCasStateException extends java.lang.IllegalStateException { + method public java.lang.String getDiagnosticInfo(); } public final class MediaCodec { @@ -24061,7 +24077,7 @@ package android.media { } public final class MediaDescrambler { - ctor public MediaDescrambler(int) throws android.media.UnsupportedCasException; + ctor public MediaDescrambler(int) throws android.media.MediaCasException.UnsupportedCasException; method public final int descramble(java.nio.ByteBuffer, int, java.nio.ByteBuffer, int, android.media.MediaCodec.CryptoInfo); method public final void release(); method public final boolean requiresSecureDecoderComponent(java.lang.String); @@ -25383,10 +25399,6 @@ package android.media { field public static final int TONE_SUP_RINGTONE = 23; // 0x17 } - public final class UnsupportedCasException extends android.media.MediaCasException { - ctor public UnsupportedCasException(java.lang.String); - } - public final class UnsupportedSchemeException extends android.media.MediaDrmException { ctor public UnsupportedSchemeException(java.lang.String); } @@ -26195,9 +26207,9 @@ package android.media.session { method public void setRepeatMode(int); method public void setSessionActivity(android.app.PendingIntent); method public void setShuffleModeEnabled(boolean); - field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 + field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4 - field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 + field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 } public static abstract class MediaSession.Callback { @@ -37871,7 +37883,7 @@ package android.provider { field public static final java.lang.String WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN = "wifi_device_owner_configs_lockdown"; field public static final java.lang.String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; field public static final java.lang.String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = "wifi_mobile_data_transition_wakelock_timeout_ms"; - field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; + field public static final deprecated java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = "wifi_networks_available_repeat_delay"; field public static final java.lang.String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; field public static final java.lang.String WIFI_ON = "wifi_on"; diff --git a/api/test-current.txt b/api/test-current.txt index 473a6e66d0a1..4d9c6d3c4c8f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -21929,23 +21929,23 @@ package android.media { } public final class MediaCas { - ctor public MediaCas(int) throws android.media.UnsupportedCasException; + ctor public MediaCas(int) throws android.media.MediaCasException.UnsupportedCasException; method public void closeSession(byte[]); method public static android.media.MediaCas.PluginDescriptor[] enumeratePlugins(); method public static boolean isSystemIdSupported(int); - method public byte[] openSession(int); - method public byte[] openSession(int, int); - method public void processEcm(byte[], byte[], int, int); - method public void processEcm(byte[], byte[]); - method public void processEmm(byte[], int, int); - method public void processEmm(byte[]); - method public void provision(java.lang.String); - method public void refreshEntitlements(int, byte[]); + method public byte[] openSession(int) throws android.media.MediaCasException; + method public byte[] openSession(int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[], int, int) throws android.media.MediaCasException; + method public void processEcm(byte[], byte[]) throws android.media.MediaCasException; + method public void processEmm(byte[], int, int) throws android.media.MediaCasException; + method public void processEmm(byte[]) throws android.media.MediaCasException; + method public void provision(java.lang.String) throws android.media.MediaCasException; + method public void refreshEntitlements(int, byte[]) throws android.media.MediaCasException; method public void release(); - method public void sendEvent(int, int, byte[]); + method public void sendEvent(int, int, byte[]) throws android.media.MediaCasException; method public void setEventListener(android.media.MediaCas.EventListener, android.os.Handler); - method public void setPrivateData(byte[]); - method public void setSessionPrivateData(byte[], byte[]); + method public void setPrivateData(byte[]) throws android.media.MediaCasException; + method public void setSessionPrivateData(byte[], byte[]) throws android.media.MediaCasException; } public static abstract interface MediaCas.EventListener { @@ -21958,7 +21958,22 @@ package android.media { } public class MediaCasException extends java.lang.Exception { - ctor public MediaCasException(java.lang.String); + } + + public static final class MediaCasException.DeniedByServerException extends android.media.MediaCasException { + } + + public static final class MediaCasException.NotProvisionedException extends android.media.MediaCasException { + } + + public static final class MediaCasException.ResourceBusyException extends android.media.MediaCasException { + } + + public static final class MediaCasException.UnsupportedCasException extends android.media.MediaCasException { + } + + public class MediaCasStateException extends java.lang.IllegalStateException { + method public java.lang.String getDiagnosticInfo(); } public final class MediaCodec { @@ -22386,7 +22401,7 @@ package android.media { } public final class MediaDescrambler { - ctor public MediaDescrambler(int) throws android.media.UnsupportedCasException; + ctor public MediaDescrambler(int) throws android.media.MediaCasException.UnsupportedCasException; method public final int descramble(java.nio.ByteBuffer, int, java.nio.ByteBuffer, int, android.media.MediaCodec.CryptoInfo); method public final void release(); method public final boolean requiresSecureDecoderComponent(java.lang.String); @@ -23697,10 +23712,6 @@ package android.media { field public static final int TONE_SUP_RINGTONE = 23; // 0x17 } - public final class UnsupportedCasException extends android.media.MediaCasException { - ctor public UnsupportedCasException(java.lang.String); - } - public final class UnsupportedSchemeException extends android.media.MediaDrmException { ctor public UnsupportedSchemeException(java.lang.String); } @@ -24437,9 +24448,9 @@ package android.media.session { method public void setRepeatMode(int); method public void setSessionActivity(android.app.PendingIntent); method public void setShuffleModeEnabled(boolean); - field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 + field public static final deprecated int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1 field public static final int FLAG_HANDLES_QUEUE_COMMANDS = 4; // 0x4 - field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 + field public static final deprecated int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2 } public static abstract class MediaSession.Callback { @@ -34997,7 +35008,7 @@ package android.provider { field public static final java.lang.String WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN = "wifi_device_owner_configs_lockdown"; field public static final java.lang.String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; field public static final java.lang.String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = "wifi_mobile_data_transition_wakelock_timeout_ms"; - field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; + field public static final deprecated java.lang.String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; field public static final java.lang.String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = "wifi_networks_available_repeat_delay"; field public static final java.lang.String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; field public static final java.lang.String WIFI_ON = "wifi_on"; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 124749a43d61..f719749cc9f9 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -93,6 +93,7 @@ import android.nfc.NfcManager; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.Build; +import android.os.Debug; import android.os.DropBoxManager; import android.os.HardwarePropertiesManager; import android.os.IBatteryPropertiesRegistrar; diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index 9e2eb84b13f9..aa61ce282c02 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -135,8 +135,8 @@ public final class BluetoothGatt implements BluetoothProfile { /** * Bluetooth GATT callbacks. Overrides the default BluetoothGattCallback implementation. */ - private final IBluetoothGattCallbackExt mBluetoothGattCallback = - new IBluetoothGattCallbackExt.Stub() { + private final IBluetoothGattCallback mBluetoothGattCallback = + new IBluetoothGattCallback.Stub() { /** * Application interface registered - app is ready to go * @hide diff --git a/core/java/android/bluetooth/BluetoothGattServer.java b/core/java/android/bluetooth/BluetoothGattServer.java index c991e2f71bb8..b35a59335939 100644 --- a/core/java/android/bluetooth/BluetoothGattServer.java +++ b/core/java/android/bluetooth/BluetoothGattServer.java @@ -59,8 +59,8 @@ public final class BluetoothGattServer implements BluetoothProfile { /** * Bluetooth GATT interface callbacks */ - private final IBluetoothGattServerCallbackExt mBluetoothGattServerCallback = - new IBluetoothGattServerCallbackExt.Stub() { + private final IBluetoothGattServerCallback mBluetoothGattServerCallback = + new IBluetoothGattServerCallback.Stub() { /** * Application interface registered - app is ready to go * @hide diff --git a/core/java/android/bluetooth/IBluetoothGatt.aidl b/core/java/android/bluetooth/IBluetoothGatt.aidl index 652a1c6098f2..0825ee88a456 100644 --- a/core/java/android/bluetooth/IBluetoothGatt.aidl +++ b/core/java/android/bluetooth/IBluetoothGatt.aidl @@ -29,8 +29,8 @@ import android.bluetooth.le.ResultStorageDescriptor; import android.os.ParcelUuid; import android.os.WorkSource; -import android.bluetooth.IBluetoothGattCallbackExt; -import android.bluetooth.IBluetoothGattServerCallbackExt; +import android.bluetooth.IBluetoothGattCallback; +import android.bluetooth.IBluetoothGattServerCallback; import android.bluetooth.le.IAdvertiserCallback; import android.bluetooth.le.IAdvertisingSetCallback; import android.bluetooth.le.IPeriodicAdvertisingCallback; @@ -66,7 +66,7 @@ interface IBluetoothGatt { void registerSync(in ScanResult scanResult, in int skip, in int timeout, in IPeriodicAdvertisingCallback callback); void unregisterSync(in IPeriodicAdvertisingCallback callback); - void registerClient(in ParcelUuid appId, in IBluetoothGattCallbackExt callback); + void registerClient(in ParcelUuid appId, in IBluetoothGattCallback callback); void unregisterClient(in int clientIf); void clientConnect(in int clientIf, in String address, in boolean isDirect, in int transport, in int phy); @@ -88,7 +88,7 @@ interface IBluetoothGatt { void configureMTU(in int clientIf, in String address, in int mtu); void connectionParameterUpdate(in int clientIf, in String address, in int connectionPriority); - void registerServer(in ParcelUuid appId, in IBluetoothGattServerCallbackExt callback); + void registerServer(in ParcelUuid appId, in IBluetoothGattServerCallback callback); void unregisterServer(in int serverIf); void serverConnect(in int serverIf, in String address, in boolean isDirect, in int transport); void serverDisconnect(in int serverIf, in String address); diff --git a/core/java/android/bluetooth/IBluetoothGattCallbackExt.aidl b/core/java/android/bluetooth/IBluetoothGattCallback.aidl index ed69e54671ca..4f85cdda87f7 100644 --- a/core/java/android/bluetooth/IBluetoothGattCallbackExt.aidl +++ b/core/java/android/bluetooth/IBluetoothGattCallback.aidl @@ -22,7 +22,7 @@ import android.bluetooth.BluetoothGattService; * Callback definitions for interacting with BLE / GATT * @hide */ -oneway interface IBluetoothGattCallbackExt { +oneway interface IBluetoothGattCallback { void onClientRegistered(in int status, in int clientIf); void onClientConnectionState(in int status, in int clientIf, in boolean connected, in String address); diff --git a/core/java/android/bluetooth/IBluetoothGattServerCallbackExt.aidl b/core/java/android/bluetooth/IBluetoothGattServerCallback.aidl index 267e8824439d..74ee11fbd321 100644 --- a/core/java/android/bluetooth/IBluetoothGattServerCallbackExt.aidl +++ b/core/java/android/bluetooth/IBluetoothGattServerCallback.aidl @@ -21,7 +21,7 @@ import android.bluetooth.BluetoothGattService; * Callback definitions for interacting with BLE / GATT * @hide */ -oneway interface IBluetoothGattServerCallbackExt { +oneway interface IBluetoothGattServerCallback { void onServerRegistered(in int status, in int serverIf); void onServerConnectionState(in int status, in int serverIf, in boolean connected, in String address); diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index bb844a327168..919f4baf3b86 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -27,6 +27,7 @@ import com.android.internal.util.CollectionUtils; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A request for the user to select a companion device to associate with. @@ -69,6 +70,20 @@ public final class AssociationRequest implements Parcelable { } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AssociationRequest that = (AssociationRequest) o; + return mSingleDevice == that.mSingleDevice && + Objects.equals(mDeviceFilters, that.mDeviceFilters); + } + + @Override + public int hashCode() { + return Objects.hash(mSingleDevice, mDeviceFilters); + } + + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (mSingleDevice ? 1 : 0)); dest.writeParcelableList(mDeviceFilters, flags); diff --git a/core/java/android/companion/BluetoothDeviceFilter.java b/core/java/android/companion/BluetoothDeviceFilter.java index 1d8df7f26f6e..84e15364c191 100644 --- a/core/java/android/companion/BluetoothDeviceFilter.java +++ b/core/java/android/companion/BluetoothDeviceFilter.java @@ -35,6 +35,7 @@ import com.android.internal.util.CollectionUtils; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -123,6 +124,22 @@ public final class BluetoothDeviceFilter implements DeviceFilter<BluetoothDevice } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BluetoothDeviceFilter that = (BluetoothDeviceFilter) o; + return Objects.equals(mNamePattern, that.mNamePattern) && + Objects.equals(mAddress, that.mAddress) && + Objects.equals(mServiceUuids, that.mServiceUuids) && + Objects.equals(mServiceUuidMasks, that.mServiceUuidMasks); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern, mAddress, mServiceUuids, mServiceUuidMasks); + } + + @Override public int describeContents() { return 0; } diff --git a/core/java/android/companion/BluetoothLEDeviceFilter.java b/core/java/android/companion/BluetoothLEDeviceFilter.java index e057fbcc901a..0444775871b9 100644 --- a/core/java/android/companion/BluetoothLEDeviceFilter.java +++ b/core/java/android/companion/BluetoothLEDeviceFilter.java @@ -36,6 +36,8 @@ import com.android.internal.util.BitUtils; import com.android.internal.util.ObjectUtils; import com.android.internal.util.Preconditions; +import java.util.Arrays; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -160,9 +162,39 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter<ScanResult> { } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BluetoothLEDeviceFilter that = (BluetoothLEDeviceFilter) o; + return mRenameBytesFrom == that.mRenameBytesFrom && + mRenameBytesTo == that.mRenameBytesTo && + mRenameBytesReverseOrder == that.mRenameBytesReverseOrder && + Objects.equals(mNamePattern, that.mNamePattern) && + Objects.equals(mScanFilter, that.mScanFilter) && + Arrays.equals(mRawDataFilter, that.mRawDataFilter) && + Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) && + Objects.equals(mRenamePrefix, that.mRenamePrefix) && + Objects.equals(mRenameSuffix, that.mRenameSuffix); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask, + mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesTo, + mRenameBytesReverseOrder); + } + + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(patternToString(getNamePattern())); dest.writeParcelable(mScanFilter, flags); + dest.writeByteArray(mRawDataFilter); + dest.writeByteArray(mRawDataFilterMask); + dest.writeString(mRenamePrefix); + dest.writeString(mRenameSuffix); + dest.writeInt(mRenameBytesFrom); + dest.writeInt(mRenameBytesTo); + dest.writeBoolean(mRenameBytesReverseOrder); } @Override @@ -174,13 +206,23 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter<ScanResult> { = new Creator<BluetoothLEDeviceFilter>() { @Override public BluetoothLEDeviceFilter createFromParcel(Parcel in) { - return new BluetoothLEDeviceFilter.Builder() + Builder builder = new Builder() .setNamePattern(patternFromString(in.readString())) - .setScanFilter(in.readParcelable(null)) - .setRawDataFilter(in.readBlob(), in.readBlob()) - .setRename(in.readString(), in.readString(), - in.readInt(), in.readInt(), in.readBoolean()) - .build(); + .setScanFilter(in.readParcelable(null)); + byte[] rawDataFilter = in.createByteArray(); + byte[] rawDataFilterMask = in.createByteArray(); + if (rawDataFilter != null) { + builder.setRawDataFilter(rawDataFilter, rawDataFilterMask); + } + String renamePrefix = in.readString(); + String suffix = in.readString(); + int bytesFrom = in.readInt(); + int bytesTo = in.readInt(); + boolean bytesReverseOrder = in.readBoolean(); + if (renamePrefix != null) { + builder.setRename(renamePrefix, suffix, bytesFrom, bytesTo, bytesReverseOrder); + } + return builder.build(); } @Override @@ -240,12 +282,14 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter<ScanResult> { */ @NonNull public Builder setRawDataFilter(@NonNull byte[] rawDataFilter, - @NonNull byte[] rawDataFilterMask) { + @Nullable byte[] rawDataFilterMask) { checkNotUsed(); - checkArgument(rawDataFilter.length == rawDataFilterMask.length, + Preconditions.checkNotNull(rawDataFilter); + checkArgument(rawDataFilterMask == null || + rawDataFilter.length == rawDataFilterMask.length, "Mask and filter should be the same length"); - mRawDataFilter = Preconditions.checkNotNull(rawDataFilter); - mRawDataFilterMask = Preconditions.checkNotNull(rawDataFilterMask); + mRawDataFilter = rawDataFilter; + mRawDataFilterMask = rawDataFilterMask; return this; } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 78e3de47df06..7b38863c9cc6 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -17,6 +17,8 @@ package android.companion; +import static com.android.internal.util.Preconditions.checkNotNull; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PendingIntent; @@ -24,7 +26,6 @@ import android.content.Context; import android.content.IntentSender; import android.content.pm.PackageManager; import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.util.Log; @@ -43,7 +44,7 @@ import java.util.List; */ public final class CompanionDeviceManager { - private static final boolean DEBUG = false; //TODO + private static final boolean DEBUG = false; private static final String LOG_TAG = "CompanionDeviceManager"; /** @@ -129,10 +130,9 @@ public final class CompanionDeviceManager { if (!checkFeaturePresent()) { return; } - - final Handler finalHandler = handler != null - ? handler - : new Handler(Looper.getMainLooper()); + checkNotNull(request, "Request cannot be null"); + checkNotNull(callback, "Callback cannot be null"); + final Handler finalHandler = Handler.mainIfNull(handler); try { mService.associate( request, diff --git a/core/java/android/companion/WifiDeviceFilter.java b/core/java/android/companion/WifiDeviceFilter.java index 1ab9ce11cb0f..b6e704c39998 100644 --- a/core/java/android/companion/WifiDeviceFilter.java +++ b/core/java/android/companion/WifiDeviceFilter.java @@ -29,6 +29,7 @@ import android.net.wifi.ScanResult; import android.os.Parcel; import android.provider.OneTimeUseBuilder; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -75,6 +76,19 @@ public final class WifiDeviceFilter implements DeviceFilter<ScanResult> { } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WifiDeviceFilter that = (WifiDeviceFilter) o; + return Objects.equals(mNamePattern, that.mNamePattern); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern); + } + + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(patternToString(getNamePattern())); } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 1fafe6534077..d264e094ca3f 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -4400,8 +4400,12 @@ public class PackageParser { defaultMaxAspectRatio = owner.applicationInfo.maxAspectRatio; } - aInfo.maxAspectRatio = Math.max(1.0f, sa.getFloat( - R.styleable.AndroidManifestActivity_maxAspectRatio, defaultMaxAspectRatio)); + aInfo.maxAspectRatio = sa.getFloat( + R.styleable.AndroidManifestActivity_maxAspectRatio, defaultMaxAspectRatio); + if (aInfo.maxAspectRatio < 1.0f && aInfo.maxAspectRatio != 0) { + // Ignore any value lesser than 1.0. + aInfo.maxAspectRatio = 0; + } } /** diff --git a/core/java/android/metrics/LogMaker.java b/core/java/android/metrics/LogMaker.java index 60b27b47960f..04482210c088 100644 --- a/core/java/android/metrics/LogMaker.java +++ b/core/java/android/metrics/LogMaker.java @@ -16,6 +16,7 @@ package android.metrics; import android.annotation.SystemApi; +import android.content.ComponentName; import android.util.Log; import android.util.SparseArray; @@ -118,6 +119,16 @@ public class LogMaker { return this; } + /** + * @param component to replace the existing setting. + * @hide + */ + public LogMaker setComponentName(ComponentName component) { + entries.put(MetricsEvent.RESERVED_FOR_LOGBUILDER_PACKAGENAME, component.getPackageName()); + entries.put(MetricsEvent.FIELD_CLASS_NAME, component.getClassName()); + return this; + } + /** Remove the package name property. */ public LogMaker clearPackageName() { entries.remove(MetricsEvent.RESERVED_FOR_LOGBUILDER_PACKAGENAME); diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java index 70ecf89dc155..29483cd98bd1 100644 --- a/core/java/android/net/NetworkScoreManager.java +++ b/core/java/android/net/NetworkScoreManager.java @@ -120,6 +120,14 @@ public class NetworkScoreManager { "android.net.wifi.use_open_wifi_package"; /** + * Meta-data specified on a {@link NetworkRecommendationProvider} that specifies the + * {@link android.app.NotificationChannel} ID used to post open network notifications. + * @hide + */ + public static final String NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA = + "android.net.wifi.notification_channel_id_network_available"; + + /** * Broadcast action: the active scorer has been changed. Scorer apps may listen to this to * perform initialization once selected as the active scorer, or clean up unneeded resources * if another scorer has been selected. This is an explicit broadcast only sent to the diff --git a/core/java/android/net/NetworkScorerAppData.java b/core/java/android/net/NetworkScorerAppData.java index 5bf1e103a2de..1734b347ab9c 100644 --- a/core/java/android/net/NetworkScorerAppData.java +++ b/core/java/android/net/NetworkScorerAppData.java @@ -23,13 +23,20 @@ public final class NetworkScorerAppData implements Parcelable { * wifi networks automatically" feature. */ private final ComponentName mEnableUseOpenWifiActivity; + /** + * The {@link android.app.NotificationChannel} ID used by {@link #mRecommendationService} to + * post open network notifications. + */ + private final String mNetworkAvailableNotificationChannelId; public NetworkScorerAppData(int packageUid, ComponentName recommendationServiceComp, - String recommendationServiceLabel, ComponentName enableUseOpenWifiActivity) { + String recommendationServiceLabel, ComponentName enableUseOpenWifiActivity, + String networkAvailableNotificationChannelId) { this.packageUid = packageUid; this.mRecommendationService = recommendationServiceComp; this.mRecommendationServiceLabel = recommendationServiceLabel; this.mEnableUseOpenWifiActivity = enableUseOpenWifiActivity; + this.mNetworkAvailableNotificationChannelId = networkAvailableNotificationChannelId; } protected NetworkScorerAppData(Parcel in) { @@ -37,6 +44,7 @@ public final class NetworkScorerAppData implements Parcelable { mRecommendationService = ComponentName.readFromParcel(in); mRecommendationServiceLabel = in.readString(); mEnableUseOpenWifiActivity = ComponentName.readFromParcel(in); + mNetworkAvailableNotificationChannelId = in.readString(); } @Override @@ -45,6 +53,7 @@ public final class NetworkScorerAppData implements Parcelable { ComponentName.writeToParcel(mRecommendationService, dest); dest.writeString(mRecommendationServiceLabel); ComponentName.writeToParcel(mEnableUseOpenWifiActivity, dest); + dest.writeString(mNetworkAvailableNotificationChannelId); } @Override @@ -83,6 +92,11 @@ public final class NetworkScorerAppData implements Parcelable { return mRecommendationServiceLabel; } + @Nullable + public String getNetworkAvailableNotificationChannelId() { + return mNetworkAvailableNotificationChannelId; + } + @Override public String toString() { return "NetworkScorerAppData{" + @@ -90,6 +104,8 @@ public final class NetworkScorerAppData implements Parcelable { ", mRecommendationService=" + mRecommendationService + ", mRecommendationServiceLabel=" + mRecommendationServiceLabel + ", mEnableUseOpenWifiActivity=" + mEnableUseOpenWifiActivity + + ", mNetworkAvailableNotificationChannelId=" + + mNetworkAvailableNotificationChannelId + '}'; } @@ -101,12 +117,14 @@ public final class NetworkScorerAppData implements Parcelable { return packageUid == that.packageUid && Objects.equals(mRecommendationService, that.mRecommendationService) && Objects.equals(mRecommendationServiceLabel, that.mRecommendationServiceLabel) && - Objects.equals(mEnableUseOpenWifiActivity, that.mEnableUseOpenWifiActivity); + Objects.equals(mEnableUseOpenWifiActivity, that.mEnableUseOpenWifiActivity) && + Objects.equals(mNetworkAvailableNotificationChannelId, + that.mNetworkAvailableNotificationChannelId); } @Override public int hashCode() { return Objects.hash(packageUid, mRecommendationService, mRecommendationServiceLabel, - mEnableUseOpenWifiActivity); + mEnableUseOpenWifiActivity, mNetworkAvailableNotificationChannelId); } } diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index 3c7c962eaa12..8678d95db17d 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -16,6 +16,8 @@ package android.os; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.Log; import android.util.Printer; @@ -69,6 +71,7 @@ public class Handler { */ private static final boolean FIND_POTENTIAL_LEAKS = false; private static final String TAG = "Handler"; + private static Handler MAIN_THREAD_HANDLER = null; /** * Callback interface you can use when instantiating a Handler to avoid @@ -231,6 +234,21 @@ public class Handler { mAsynchronous = async; } + /** @hide */ + @NonNull + public static Handler getMain() { + if (MAIN_THREAD_HANDLER == null) { + MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper()); + } + return MAIN_THREAD_HANDLER; + } + + /** @hide */ + @NonNull + public static Handler mainIfNull(@Nullable Handler handler) { + return handler == null ? getMain() : handler; + } + /** {@hide} */ public String getTraceName(Message message) { final StringBuilder sb = new StringBuilder(); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7005d44ee08a..371f14a9328b 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8232,9 +8232,14 @@ public final class Settings { * the open network(s) disappear, we remove the notification. When we * show the notification, we will not show it again for * {@link android.provider.Settings.Secure#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time. + * + * @deprecated This feature is no longer controlled by this setting in + * {@link android.os.Build.VERSION_CODES#O}. */ + @Deprecated public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; + /** * {@hide} */ diff --git a/core/java/android/util/ExceptionUtils.java b/core/java/android/util/ExceptionUtils.java index 87231e106ca3..44019c32560d 100644 --- a/core/java/android/util/ExceptionUtils.java +++ b/core/java/android/util/ExceptionUtils.java @@ -17,6 +17,7 @@ package android.util; import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.ParcelableException; import com.android.internal.util.Preconditions; @@ -55,10 +56,26 @@ public class ExceptionUtils { return getCompleteMessage(null, t); } + public static <E extends Throwable> void propagateIfInstanceOf( + @Nullable Throwable t, Class<E> c) throws E { + if (t != null && c.isInstance(t)) { + throw c.cast(t); + } + } + + /** + * @param <E> a checked exception that is ok to throw without wrapping + */ + public static <E extends Exception> RuntimeException propagate(@NonNull Throwable t, Class<E> c) + throws E { + propagateIfInstanceOf(t, c); + return propagate(t); + } + public static RuntimeException propagate(@NonNull Throwable t) { Preconditions.checkNotNull(t); - if (t instanceof Error) throw (Error)t; - if (t instanceof RuntimeException) throw (RuntimeException)t; + propagateIfInstanceOf(t, Error.class); + propagateIfInstanceOf(t, RuntimeException.class); throw new RuntimeException(t); } } diff --git a/core/java/android/view/textclassifier/SmartSelection.java b/core/java/android/view/textclassifier/SmartSelection.java index 9397a4163e97..c68c4492d4ba 100644 --- a/core/java/android/view/textclassifier/SmartSelection.java +++ b/core/java/android/view/textclassifier/SmartSelection.java @@ -26,6 +26,11 @@ final class SmartSelection { System.loadLibrary("textclassifier"); } + /** Hints the classifier that this may be a url. */ + static final int HINT_FLAG_URL = 0x01; + /** Hints the classifier that this may be an email. */ + static final int HINT_FLAG_EMAIL = 0x02; + private final long mCtx; /** @@ -59,7 +64,7 @@ final class SmartSelection { * scores for different collections. */ public ClassificationResult[] classifyText( - String context, int selectionBegin, int selectionEnd) { + String context, int selectionBegin, int selectionEnd, int hintFlags) { return nativeClassifyText(mCtx, context, selectionBegin, selectionEnd); } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index be12f5702129..66a62c316a03 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -35,6 +35,7 @@ import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.util.Linkify; import android.util.Log; +import android.util.Patterns; import android.view.View; import com.android.internal.util.Preconditions; @@ -88,7 +89,9 @@ final class TextClassifierImpl implements TextClassifier { if (start >= 0 && end <= string.length() && start <= end) { final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); final SmartSelection.ClassificationResult[] results = - getSmartSelection().classifyText(string, start, end); + getSmartSelection().classifyText( + string, start, end, + getHintFlags(string, start, end)); final int size = results.length; for (int i = 0; i < size; i++) { tsBuilder.setEntityType(results[i].mCollection, results[i].mScore); @@ -116,12 +119,14 @@ final class TextClassifierImpl implements TextClassifier { validateInput(text, startIndex, endIndex); try { if (text.length() > 0) { - final CharSequence classified = text.subSequence(startIndex, endIndex); + final String string = text.toString(); SmartSelection.ClassificationResult[] results = getSmartSelection() - .classifyText(text.toString(), startIndex, endIndex); + .classifyText(string, startIndex, endIndex, + getHintFlags(string, startIndex, endIndex)); if (results.length > 0) { final TextClassificationResult classificationResult = - createClassificationResult(results, classified); + createClassificationResult( + results, string.subSequence(startIndex, endIndex)); // TODO: Added this log for debug only. Remove before release. Log.d(LOG_TAG, String.format( "Classification type: %s", classificationResult)); @@ -208,6 +213,24 @@ final class TextClassifierImpl implements TextClassifier { return builder.build(); } + private static int getHintFlags(CharSequence text, int start, int end) { + int flag = 0; + final CharSequence subText = text.subSequence(start, end); + if (Patterns.AUTOLINK_EMAIL_ADDRESS.matcher(subText).matches()) { + flag |= SmartSelection.HINT_FLAG_EMAIL; + } + if (Patterns.AUTOLINK_WEB_URL.matcher(subText).matches() + && Linkify.sUrlMatchFilter.acceptMatch(text, start, end)) { + flag |= SmartSelection.HINT_FLAG_URL; + } + // TODO: Added this log for debug only. Remove before release. + Log.d(LOG_TAG, String.format("Email hint: %b", + (flag & SmartSelection.HINT_FLAG_EMAIL) != 0)); + Log.d(LOG_TAG, String.format("Url hint: %b", + (flag & SmartSelection.HINT_FLAG_URL) != 0)); + return flag; + } + private static String getHighestScoringType(SmartSelection.ClassificationResult[] types) { if (types.length < 1) { return ""; @@ -262,7 +285,9 @@ final class TextClassifierImpl implements TextClassifier { if (selectionStart >= 0 && selectionEnd <= text.length() && selectionStart <= selectionEnd) { final SmartSelection.ClassificationResult[] results = - smartSelection.classifyText(text, selectionStart, selectionEnd); + smartSelection.classifyText( + text, selectionStart, selectionEnd, + getHintFlags(text, selectionStart, selectionEnd)); if (results.length > 0) { final String type = getHighestScoringType(results); if (matches(type, linkMask)) { diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index b63b899d5c92..59fb02d353bb 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -2064,7 +2064,7 @@ public class PopupWindow { } if (update) { - update(mAnchor.get(), p); + update(mAnchor != null ? mAnchor.get() : null, p); } } @@ -2178,9 +2178,14 @@ public class PopupWindow { update = true; } - final View anchor = mAnchor.get(); - final int newAccessibilityIdOfAnchor = (anchor != null) - ? anchor.getAccessibilityViewId() : -1; + View anchor = null; + int newAccessibilityIdOfAnchor = -1; + + if (mAnchor != null && mAnchor.get() != null) { + anchor = mAnchor.get(); + newAccessibilityIdOfAnchor = anchor.getAccessibilityViewId(); + } + if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) { p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor; update = true; @@ -2366,7 +2371,8 @@ public class PopupWindow { } private class PopupDecorView extends FrameLayout { - private TransitionListenerAdapter mPendingExitListener; + /** Runnable used to clean up listeners after exit transition. */ + private Runnable mCleanupAfterExit; public PopupDecorView(Context context) { super(context); @@ -2477,7 +2483,7 @@ public class PopupWindow { * <p> * <strong>Note:</strong> The transition listener is guaranteed to have * its {@code onTransitionEnd} method called even if the transition - * never starts; however, it may be called with a {@code null} argument. + * never starts. */ public void startExitTransition(@NonNull Transition transition, @Nullable final View anchorRoot, @Nullable final Rect epicenter, @@ -2493,25 +2499,32 @@ public class PopupWindow { anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener); } - // The exit listener MUST be called for cleanup, even if the - // transition never starts or ends. Stash it for later. - mPendingExitListener = new TransitionListenerAdapter() { - @Override - public void onTransitionEnd(Transition t) { - if (anchorRoot != null) { - anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener); - } - - listener.onTransitionEnd(t); + // The cleanup runnable MUST be called even if the transition is + // canceled before it starts (and thus can't call onTransitionEnd). + mCleanupAfterExit = () -> { + listener.onTransitionEnd(transition); - // The listener was called. Our job here is done. - mPendingExitListener = null; - t.removeListener(this); + if (anchorRoot != null) { + anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener); } + + // The listener was called. Our job here is done. + mCleanupAfterExit = null; }; final Transition exitTransition = transition.clone(); - exitTransition.addListener(mPendingExitListener); + exitTransition.addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition t) { + t.removeListener(this); + + // This null check shouldn't be necessary, but it's easier + // to check here than it is to test every possible case. + if (mCleanupAfterExit != null) { + mCleanupAfterExit.run(); + } + } + }); exitTransition.setEpicenterCallback(new EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { @@ -2539,8 +2552,10 @@ public class PopupWindow { public void cancelTransitions() { TransitionManager.endTransitions(this); - if (mPendingExitListener != null) { - mPendingExitListener.onTransitionEnd(null); + // If the cleanup runnable is still around, that means the + // transition never started. We should run it now to clean up. + if (mCleanupAfterExit != null) { + mCleanupAfterExit.run(); } } diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index a03238374f39..003db061c140 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -289,7 +289,7 @@ final class SelectionActionModeHelper { */ private static final class TextClassificationHelper { - private static final int TRIM_DELTA = 50; // characters + private static final int TRIM_DELTA = 120; // characters private TextClassifier mTextClassifier; diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java index 6d814bf14bc0..d2a907222eda 100644 --- a/core/java/com/android/internal/widget/SwipeDismissLayout.java +++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; +import android.content.ReceiverCallNotAllowedException; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; @@ -86,24 +87,7 @@ public class SwipeDismissLayout extends FrameLayout { private OnDismissedListener mDismissedListener; private OnSwipeProgressChangedListener mProgressListener; - private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { - private Runnable mRunnable = new Runnable() { - @Override - public void run() { - if (mDismissed) { - dismiss(); - } else { - cancel(); - } - resetMembers(); - } - }; - - @Override - public void onReceive(Context context, Intent intent) { - post(mRunnable); - } - }; + private BroadcastReceiver mScreenOffReceiver; private IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF); @@ -146,12 +130,36 @@ public class SwipeDismissLayout extends FrameLayout { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter); + try { + mScreenOffReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + post(() -> { + if (mDismissed) { + dismiss(); + } else { + cancel(); + } + resetMembers(); + }); + } + }; + getContext().registerReceiver(mScreenOffReceiver, mScreenOffFilter); + } catch (ReceiverCallNotAllowedException e) { + /* Exception is thrown if the context is a ReceiverRestrictedContext object. As + * ReceiverRestrictedContext is not public, the context type cannot be checked before + * calling registerReceiver. The most likely scenario in which the exception would be + * thrown would be when a BroadcastReceiver creates a dialog to show the user. */ + mScreenOffReceiver = null; // clear receiver since it was not used. + } } @Override protected void onDetachedFromWindow() { - getContext().unregisterReceiver(mScreenOffReceiver); + if (mScreenOffReceiver != null) { + getContext().unregisterReceiver(mScreenOffReceiver); + mScreenOffReceiver = null; + } super.onDetachedFromWindow(); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8721f3414e4b..db8a9f487b61 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -313,6 +313,10 @@ <protected-broadcast android:name="android.net.wifi.STATE_CHANGE" /> <protected-broadcast android:name="android.net.wifi.LINK_CONFIGURATION_CHANGED" /> <protected-broadcast android:name="android.net.wifi.CONFIGURED_NETWORKS_CHANGE" /> + <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_DEAUTH_IMMINENT" /> + <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_ICON" /> + <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_OSU_PROVIDERS_LIST" /> + <protected-broadcast android:name="android.net.wifi.action.PASSPOINT_SUBSCRIPTION_REMEDIATION" /> <protected-broadcast android:name="android.net.wifi.supplicant.CONNECTION_CHANGE" /> <protected-broadcast android:name="android.net.wifi.supplicant.STATE_CHANGE" /> <protected-broadcast android:name="android.net.wifi.p2p.STATE_CHANGED" /> @@ -1346,7 +1350,7 @@ <permission android:name="android.permission.CONNECTIVITY_INTERNAL" android:protectionLevel="signature|privileged" /> - <!-- Allows an internal user to use restricted Networks. + <!-- @SystemApi Allows an internal user to use restricted Networks. @hide --> <permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" android:protectionLevel="signature|privileged" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 68e766e46279..7704519415b2 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2580,12 +2580,14 @@ <integer name="config_defaultPictureInPictureGravity">0x55</integer> <!-- The minimum aspect ratio (width/height) that is supported for picture-in-picture. Any - ratio smaller than this is considered too tall and thin to be usable. --> - <item name="config_pictureInPictureMinAspectRatio" format="float" type="dimen">0.5</item> + ratio smaller than this is considered too tall and thin to be usable. Currently, this + is the inverse of the max landscape aspect ratio (1:2.39), but this is an extremely + skinny aspect ratio that is not expected to be widely used. --> + <item name="config_pictureInPictureMinAspectRatio" format="float" type="dimen">0.41841004184</item> <!-- The minimum aspect ratio (width/height) that is supported for picture-in-picture. Any - ratio larger than this is considered to wide and short to be usable. --> - <item name="config_pictureInPictureMaxAspectRatio" format="float" type="dimen">2.35</item> + ratio larger than this is considered to wide and short to be usable. Currently 2.39:1. --> + <item name="config_pictureInPictureMaxAspectRatio" format="float" type="dimen">2.39</item> <!-- The snap mode to use for picture-in-picture. These values correspond to constants defined in PipSnapAlgorithm and should not be changed independently. diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp index e8972aab9f8f..d154730058ee 100644 --- a/libs/hwui/BakedOpRenderer.cpp +++ b/libs/hwui/BakedOpRenderer.cpp @@ -363,6 +363,7 @@ void BakedOpRenderer::renderFunctor(const FunctorOp& op, const BakedOpState& sta state.computedState.transform.copyTo(&info.transform[0]); mRenderState.invokeFunctor(op.functor, DrawGlInfo::kModeDraw, &info); + if (!mRenderTarget.frameBufferId) mHasDrawn = true; } void BakedOpRenderer::dirtyRenderTarget(const Rect& uiDirty) { diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp index 7be71eec4f76..8126d57a3a79 100644 --- a/libs/hwui/JankTracker.cpp +++ b/libs/hwui/JankTracker.cpp @@ -264,10 +264,15 @@ void JankTracker::addFrame(const FrameInfo& frame) { // the actual time spent blocked. nsecs_t forgiveAmount = std::min(expectedDequeueDuration, frame[FrameInfoIndex::DequeueBufferDuration]); + LOG_ALWAYS_FATAL_IF(forgiveAmount >= totalDuration, + "Impossible dequeue duration! dequeue duration reported %" PRId64 + ", total duration %" PRId64, forgiveAmount, totalDuration); totalDuration -= forgiveAmount; } } + LOG_ALWAYS_FATAL_IF(totalDuration <= 0, "Impossible totalDuration %" PRId64, totalDuration); uint32_t framebucket = frameCountIndexForFrameTime(totalDuration); + LOG_ALWAYS_FATAL_IF(framebucket < 0, "framebucket < 0 (%u)", framebucket); // Keep the fast path as fast as possible. if (CC_LIKELY(totalDuration < mFrameInterval)) { mData->frameCounts[framebucket]++; diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index e32fd63e1125..1450ec98dabf 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -98,6 +98,7 @@ void TaskQueue::queue(RenderTask* task) { } void TaskQueue::queueAtFront(RenderTask* task) { + LOG_ALWAYS_FATAL_IF(task->mNext || mHead == task, "Task is already in the queue!"); if (mTail) { task->mNext = mHead; mHead = task; diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 2e22132614d2..611fdd1d997b 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -18,6 +18,7 @@ package android.media; import android.annotation.NonNull; import android.annotation.Nullable; +import android.media.MediaCasException.*; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -28,6 +29,7 @@ import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.ServiceSpecificException; import android.util.Log; import android.util.Singleton; @@ -84,8 +86,6 @@ import android.util.Singleton; * sessionId of the descrambler can be retrieved by {@link MediaExtractor#getDrmInitData} * and used to initialize a MediaDescrambler object for MediaCodec. * <p> - * TODO: determine exception handling schemes. - * <p> * <h3>Listeners</h3> * <p>The app may register a listener to receive events from the CA system using * method {@link #setEventListener}. The exact format of the event is scheme-specific @@ -382,28 +382,22 @@ public final class MediaCas { mEventHandler = new EventHandler(looper); } - /* - * TODO: handle ServiceSpecificException from the IMediaCas - * All Drm-specific failures will be thrown by mICas as - * ServiceSpecificException exception with Drm error code. - * These need to be re-thrown as crypto exceptions. - */ - /** * Send the private data for the CA system. * * @param data byte array of the private data. * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - /* - * TODO: need to re-throw DRM-specific exceptions - */ - public void setPrivateData(@NonNull byte[] data) { + public void setPrivateData(@NonNull byte[] data) throws MediaCasException { validateInternalStates(); try { mICas.setPrivateData(data); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -416,14 +410,17 @@ public final class MediaCas { * * @return session id of the newly opened session. * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if a session for the program already exists. + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public byte[] openSession(int programNumber) { + public byte[] openSession(int programNumber) throws MediaCasException { validateInternalStates(); try { return mICas.openSession(programNumber); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -438,14 +435,18 @@ public final class MediaCas { * * @return session id of the newly opened session. * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if a session for the stream already exists. + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public byte[] openSession(int programNumber, int elementaryPID) { + public byte[] openSession(int programNumber, int elementaryPID) + throws MediaCasException { validateInternalStates(); try { return mICas.openSessionForStream(programNumber, elementaryPID); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -457,14 +458,16 @@ public final class MediaCas { * * @param sessionId the session to be closed. * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if the session is not valid. + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasStateException for CAS-specific state exceptions. */ public void closeSession(@NonNull byte[] sessionId) { validateInternalStates(); try { mICas.closeSession(sessionId); + } catch (ServiceSpecificException e) { + MediaCasStateException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -476,17 +479,18 @@ public final class MediaCas { * @param sessionId the session for which the private data is intended. * @param data byte array of the private data. * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if the session is not valid. - */ - /* - * TODO: need to re-throw DRM-specific exceptions + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public void setSessionPrivateData(@NonNull byte[] sessionId, @NonNull byte[] data) { + public void setSessionPrivateData(@NonNull byte[] sessionId, @NonNull byte[] data) + throws MediaCasException { validateInternalStates(); try { mICas.setSessionPrivateData(sessionId, data); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -500,19 +504,19 @@ public final class MediaCas { * @param offset position within data where the ECM data begins. * @param length length of the data (starting from offset). * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if the session is not valid. - */ - /* - * TODO: need to re-throw DRM-specific exceptions + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public void processEcm( - @NonNull byte[] sessionId, @NonNull byte[] data, int offset, int length) { + public void processEcm(@NonNull byte[] sessionId, @NonNull byte[] data, + int offset, int length) throws MediaCasException { validateInternalStates(); try { mCasData.set(data, offset, length); mICas.processEcm(sessionId, mCasData); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -526,13 +530,12 @@ public final class MediaCas { * @param sessionId the session for which the ECM is intended. * @param data byte array of the ECM data. * - * @throws IllegalStateException if the MediaCas instance is not valid, - * or IllegalArgumentException if the session is not valid. - */ - /* - * TODO: need to re-throw DRM-specific exceptions + * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public void processEcm(@NonNull byte[] sessionId, @NonNull byte[] data) { + public void processEcm(@NonNull byte[] sessionId, @NonNull byte[] data) + throws MediaCasException { processEcm(sessionId, data, 0, data.length); } @@ -544,16 +547,18 @@ public final class MediaCas { * @param length length of the data (starting from offset). * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - /* - * TODO: need to re-throw DRM-specific exceptions - */ - public void processEmm(@NonNull byte[] data, int offset, int length) { + public void processEmm(@NonNull byte[] data, int offset, int length) + throws MediaCasException { validateInternalStates(); try { mCasData.set(data, offset, length); mICas.processEmm(mCasData); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -567,11 +572,10 @@ public final class MediaCas { * @param data byte array of the EMM data. * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - /* - * TODO: need to re-throw DRM-specific exceptions - */ - public void processEmm(@NonNull byte[] data) { + public void processEmm(@NonNull byte[] data) throws MediaCasException { processEmm(data, 0, data.length); } @@ -584,12 +588,17 @@ public final class MediaCas { * @param data a byte array containing scheme-specific data for the event. * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public void sendEvent(int event, int arg, @Nullable byte[] data) { + public void sendEvent(int event, int arg, @Nullable byte[] data) + throws MediaCasException { validateInternalStates(); try { mICas.sendEvent(event, arg, data); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -603,12 +612,16 @@ public final class MediaCas { * specific. * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - public void provision(@NonNull String provisionString) { + public void provision(@NonNull String provisionString) throws MediaCasException { validateInternalStates(); try { mICas.provision(provisionString); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -621,15 +634,17 @@ public final class MediaCas { * @param refreshData private data associated with the refreshment. * * @throws IllegalStateException if the MediaCas instance is not valid. + * @throws MediaCasException for CAS-specific errors. + * @throws MediaCasStateException for CAS-specific state exceptions. */ - /* - * TODO: define enums for refreshType - */ - public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData) { + public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData) + throws MediaCasException { validateInternalStates(); try { mICas.refreshEntitlements(refreshType, refreshData); + } catch (ServiceSpecificException e) { + MediaCasException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } diff --git a/media/java/android/media/MediaCasException.java b/media/java/android/media/MediaCasException.java index 1d5d3cdb2134..485f6eebf88e 100644 --- a/media/java/android/media/MediaCasException.java +++ b/media/java/android/media/MediaCasException.java @@ -16,11 +16,104 @@ package android.media; +import android.os.ServiceSpecificException; + /** * Base class for MediaCas exceptions */ public class MediaCasException extends Exception { + + /** @hide */ + public static final int DRM_ERROR_BASE = -2000; + /** @hide */ + public static final int ERROR_DRM_UNKNOWN = DRM_ERROR_BASE; + /** @hide */ + public static final int ERROR_DRM_NO_LICENSE = DRM_ERROR_BASE - 1; + /** @hide */ + public static final int ERROR_DRM_LICENSE_EXPIRED = DRM_ERROR_BASE - 2; + /** @hide */ + public static final int ERROR_DRM_SESSION_NOT_OPENED = DRM_ERROR_BASE - 3; + /** @hide */ + public static final int ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED = DRM_ERROR_BASE - 4; + /** @hide */ + public static final int ERROR_DRM_DECRYPT = DRM_ERROR_BASE - 5; + /** @hide */ + public static final int ERROR_DRM_CANNOT_HANDLE = DRM_ERROR_BASE - 6; + /** @hide */ + public static final int ERROR_DRM_TAMPER_DETECTED = DRM_ERROR_BASE - 7; + /** @hide */ + public static final int ERROR_DRM_NOT_PROVISIONED = DRM_ERROR_BASE - 8; + /** @hide */ + public static final int ERROR_DRM_DEVICE_REVOKED = DRM_ERROR_BASE - 9; + /** @hide */ + public static final int ERROR_DRM_RESOURCE_BUSY = DRM_ERROR_BASE - 10; + /** @hide */ + public static final int ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION = DRM_ERROR_BASE - 11; + /** @hide */ + public static final int ERROR_DRM_LAST_USED_ERRORCODE = DRM_ERROR_BASE - 11; + /** @hide */ + public static final int ERROR_DRM_VENDOR_MAX = DRM_ERROR_BASE - 500; + /** @hide */ + public static final int ERROR_DRM_VENDOR_MIN = DRM_ERROR_BASE - 999; + + /** @hide */ public MediaCasException(String detailMessage) { super(detailMessage); } + + static void throwExceptions(ServiceSpecificException e) throws MediaCasException { + if (e.errorCode == ERROR_DRM_NOT_PROVISIONED) { + throw new NotProvisionedException(e.getMessage()); + } else if (e.errorCode == ERROR_DRM_RESOURCE_BUSY) { + throw new ResourceBusyException(e.getMessage()); + } else if (e.errorCode == ERROR_DRM_DEVICE_REVOKED) { + throw new DeniedByServerException(e.getMessage()); + } else { + MediaCasStateException.throwExceptions(e); + } + } + + /** + * Exception thrown when an attempt is made to construct a MediaCas object + * using a CA_system_id that is not supported by the device + */ + public static final class UnsupportedCasException extends MediaCasException { + /** @hide */ + public UnsupportedCasException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Exception thrown when an operation on a MediaCas object is attempted + * before it's provisioned successfully. + */ + public static final class NotProvisionedException extends MediaCasException { + /** @hide */ + public NotProvisionedException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Exception thrown when the provisioning server or key server denies a + * license for a device. + */ + public static final class DeniedByServerException extends MediaCasException { + /** @hide */ + public DeniedByServerException(String detailMessage) { + super(detailMessage); + } + } + + /** + * Exception thrown when an operation on a MediaCas object is attempted + * and hardware resources are not available, due to being in use. + */ + public static final class ResourceBusyException extends MediaCasException { + /** @hide */ + public ResourceBusyException(String detailMessage) { + super(detailMessage); + } + } } diff --git a/media/java/android/media/MediaCasStateException.java b/media/java/android/media/MediaCasStateException.java new file mode 100644 index 000000000000..cf05c2975272 --- /dev/null +++ b/media/java/android/media/MediaCasStateException.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 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.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ServiceSpecificException; + +import static android.media.MediaCasException.*; + +/** + * Base class for MediaCas runtime exceptions + */ +public class MediaCasStateException extends IllegalStateException { + private final int mErrorCode; + private final String mDiagnosticInfo; + + /** @hide */ + public MediaCasStateException(int err, @Nullable String msg, @Nullable String diagnosticInfo) { + super(msg); + mErrorCode = err; + mDiagnosticInfo = diagnosticInfo; + } + + static void throwExceptions(ServiceSpecificException e) { + String diagnosticInfo = ""; + switch (e.errorCode) { + case ERROR_DRM_UNKNOWN: + diagnosticInfo = "General CAS error"; + break; + case ERROR_DRM_NO_LICENSE: + diagnosticInfo = "No license"; + break; + case ERROR_DRM_LICENSE_EXPIRED: + diagnosticInfo = "License expired"; + break; + case ERROR_DRM_SESSION_NOT_OPENED: + diagnosticInfo = "Session not opened"; + break; + case ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED: + diagnosticInfo = "Not initialized"; + break; + case ERROR_DRM_DECRYPT: + diagnosticInfo = "Decrypt error"; + break; + case ERROR_DRM_CANNOT_HANDLE: + diagnosticInfo = "Unsupported scheme or data format"; + break; + case ERROR_DRM_TAMPER_DETECTED: + diagnosticInfo = "Tamper detected"; + break; + default: + diagnosticInfo = "Unknown CAS state exception"; + break; + } + throw new MediaCasStateException(e.errorCode, e.getMessage(), + String.format("%s (err=%d)", diagnosticInfo, e.errorCode)); + } + + /** + * Retrieve the associated error code + * + * @hide + */ + public int getErrorCode() { + return mErrorCode; + } + + /** + * Retrieve a developer-readable diagnostic information string + * associated with the exception. Do not show this to end-users, + * since this string will not be localized or generally comprehensible + * to end-users. + */ + @NonNull + public String getDiagnosticInfo() { + return mDiagnosticInfo; + } +} diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index 13a22b49ff87..e628d18d8490 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -1878,9 +1878,7 @@ final public class MediaCodec { * @param flags Specify {@link #CONFIGURE_FLAG_ENCODE} to configure the * component as an encoder. * @param descrambler Specify a descrambler object to facilitate secure - * descrambling of the media data. descrambler must not be - * null if this method is used. For non-secure codecs, use - * {@link #configure} and with null crypto parameter. + * descrambling of the media data, or null for non-secure codecs. * @throws IllegalArgumentException if the surface has been released (or is invalid), * or the format is unacceptable (e.g. missing a mandatory key), * or the flags are not set properly @@ -1891,8 +1889,9 @@ final public class MediaCodec { */ public void configure( @Nullable MediaFormat format, @Nullable Surface surface, - @ConfigureFlag int flags, @NonNull MediaDescrambler descrambler) { - configure(format, surface, null, descrambler.getBinder(), flags); + @ConfigureFlag int flags, @Nullable MediaDescrambler descrambler) { + configure(format, surface, null, + descrambler != null ? descrambler.getBinder() : null, flags); } private void configure( diff --git a/media/java/android/media/MediaDescrambler.java b/media/java/android/media/MediaDescrambler.java index f5eede859eb1..2dd109721ca8 100644 --- a/media/java/android/media/MediaDescrambler.java +++ b/media/java/android/media/MediaDescrambler.java @@ -17,10 +17,12 @@ package android.media; import android.annotation.NonNull; +import android.media.MediaCasException.UnsupportedCasException; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.util.Log; import java.nio.ByteBuffer; @@ -54,13 +56,13 @@ public final class MediaDescrambler { /** * Class for parceling descrambling parameters over IDescrambler binder. */ + // This class currently is not used by Java binder. descramble() goes through + // jni to use shared memory. However, the parcelable is still required for AIDL. static class DescrambleInfo implements Parcelable { private DescrambleInfo() { - // TODO: implement } private DescrambleInfo(Parcel in) { - // TODO: disable } @Override @@ -70,7 +72,6 @@ public final class MediaDescrambler { @Override public void writeToParcel(Parcel dest, int flags) { - // TODO: implement } public static final Parcelable.Creator<DescrambleInfo> CREATOR @@ -112,13 +113,6 @@ public final class MediaDescrambler { return mIDescrambler.asBinder(); } - /* - * TODO: handle ServiceSpecificException from the mIDescrambler - * All Drm-specific failures will be thrown by mIDescrambler as - * ServiceSpecificException exception with Drm error code. - * These need to be re-thrown as crypto exceptions. - */ - /** * Query if the scrambling scheme requires the use of a secure decoder * to decode data of the given mime type. @@ -150,14 +144,16 @@ public final class MediaDescrambler { * @param sessionId the MediaCas sessionId to associate with this * MediaDescrambler instance. * - * @throws IllegalStateException if the descrambler instance is not valid, - * or IllegalArgumentException if the sessionId is not valid. + * @throws IllegalStateException if the descrambler instance is not valid. + * @throws MediaCasStateException for CAS-specific state exceptions. */ public final void setMediaCasSession(@NonNull byte[] sessionId) { validateInternalStates(); try { mIDescrambler.setMediaCasSession(sessionId); + } catch (ServiceSpecificException e) { + MediaCasStateException.throwExceptions(e); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -179,9 +175,7 @@ public final class MediaDescrambler { * values indicating errors. * * @throws IllegalStateException if the descrambler instance is not valid. - */ - /* - * TODO: throw DRM-specific exception if decrambling is failing. + * @throws MediaCasStateException for CAS-specific state exceptions. */ public final int descramble( @NonNull ByteBuffer srcBuf, int srcPos, ByteBuffer dstBuf, int dstPos, @@ -208,12 +202,17 @@ public final class MediaDescrambler { "Invalid CryptoInfo: key array is invalid!"); } - return native_descramble( - cryptoInfo.key[0], - cryptoInfo.numSubSamples, - cryptoInfo.numBytesOfClearData, - cryptoInfo.numBytesOfEncryptedData, - srcBuf, srcPos, dstBuf, dstPos); + try { + return native_descramble( + cryptoInfo.key[0], + cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, + cryptoInfo.numBytesOfEncryptedData, + srcBuf, srcPos, dstBuf, dstPos); + } catch (ServiceSpecificException e) { + MediaCasStateException.throwExceptions(e); + } + return -1; } public final void release() { diff --git a/media/java/android/media/UnsupportedCasException.java b/media/java/android/media/UnsupportedCasException.java deleted file mode 100644 index 31676373783b..000000000000 --- a/media/java/android/media/UnsupportedCasException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2017 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.media; - -/** - * Exception thrown when an attempt is made to construct a MediaCas object - * using a CA_system_id that is not supported by the device - */ -public final class UnsupportedCasException extends MediaCasException { - public UnsupportedCasException(String detailMessage) { - super(detailMessage); - } -} diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index bee3f52c9b90..f10f4427d6f0 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -77,13 +77,19 @@ public final class MediaSession { /** * Set this flag on the session to indicate that it can handle media button * events. + * @deprecated This flag is no longer used. All media sessions are expected to handle media + * button events now. */ + @Deprecated public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; /** * Set this flag on the session to indicate that it handles transport * control commands through its {@link Callback}. + * @deprecated This flag is no longer used. All media sessions are expected to handle transport + * controls now. */ + @Deprecated public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; /** diff --git a/media/java/android/media/tv/TvInputHardwareInfo.java b/media/java/android/media/tv/TvInputHardwareInfo.java index 957c5820838e..762f0c07e121 100644 --- a/media/java/android/media/tv/TvInputHardwareInfo.java +++ b/media/java/android/media/tv/TvInputHardwareInfo.java @@ -20,6 +20,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.SystemApi; +import android.hardware.tv.input.V1_0.Constants; import android.media.AudioManager; import android.os.Parcel; import android.os.Parcelable; @@ -37,16 +38,16 @@ public final class TvInputHardwareInfo implements Parcelable { static final String TAG = "TvInputHardwareInfo"; // Match hardware/libhardware/include/hardware/tv_input.h - public static final int TV_INPUT_TYPE_OTHER_HARDWARE = 1; - public static final int TV_INPUT_TYPE_TUNER = 2; - public static final int TV_INPUT_TYPE_COMPOSITE = 3; - public static final int TV_INPUT_TYPE_SVIDEO = 4; - public static final int TV_INPUT_TYPE_SCART = 5; - public static final int TV_INPUT_TYPE_COMPONENT = 6; - public static final int TV_INPUT_TYPE_VGA = 7; - public static final int TV_INPUT_TYPE_DVI = 8; - public static final int TV_INPUT_TYPE_HDMI = 9; - public static final int TV_INPUT_TYPE_DISPLAY_PORT = 10; + public static final int TV_INPUT_TYPE_OTHER_HARDWARE = Constants.TV_INPUT_TYPE_OTHER; + public static final int TV_INPUT_TYPE_TUNER = Constants.TV_INPUT_TYPE_TUNER; + public static final int TV_INPUT_TYPE_COMPOSITE = Constants.TV_INPUT_TYPE_COMPOSITE; + public static final int TV_INPUT_TYPE_SVIDEO = Constants.TV_INPUT_TYPE_SVIDEO; + public static final int TV_INPUT_TYPE_SCART = Constants.TV_INPUT_TYPE_SCART; + public static final int TV_INPUT_TYPE_COMPONENT = Constants.TV_INPUT_TYPE_COMPONENT; + public static final int TV_INPUT_TYPE_VGA = Constants.TV_INPUT_TYPE_VGA; + public static final int TV_INPUT_TYPE_DVI = Constants.TV_INPUT_TYPE_DVI; + public static final int TV_INPUT_TYPE_HDMI = Constants.TV_INPUT_TYPE_HDMI; + public static final int TV_INPUT_TYPE_DISPLAY_PORT = Constants.TV_INPUT_TYPE_DISPLAY_PORT; /** @hide */ @Retention(SOURCE) @@ -58,17 +59,20 @@ public final class TvInputHardwareInfo implements Parcelable { /** * The hardware is unsure about the connection status or does not support cable detection. */ - public static final int CABLE_CONNECTION_STATUS_UNKNOWN = 0; + public static final int CABLE_CONNECTION_STATUS_UNKNOWN = + Constants.CABLE_CONNECTION_STATUS_UNKNOWN; /** * Cable is connected to the hardware. */ - public static final int CABLE_CONNECTION_STATUS_CONNECTED = 1; + public static final int CABLE_CONNECTION_STATUS_CONNECTED = + Constants.CABLE_CONNECTION_STATUS_CONNECTED; /** * Cable is disconnected to the hardware. */ - public static final int CABLE_CONNECTION_STATUS_DISCONNECTED = 2; + public static final int CABLE_CONNECTION_STATUS_DISCONNECTED = + Constants.CABLE_CONNECTION_STATUS_DISCONNECTED; public static final Parcelable.Creator<TvInputHardwareInfo> CREATOR = new Parcelable.Creator<TvInputHardwareInfo>() { diff --git a/media/jni/android_media_MediaDescrambler.cpp b/media/jni/android_media_MediaDescrambler.cpp index 75856648f498..f031dbb273c6 100644 --- a/media/jni/android_media_MediaDescrambler.cpp +++ b/media/jni/android_media_MediaDescrambler.cpp @@ -129,7 +129,7 @@ void JDescrambler::ensureBufferCapacity(size_t neededSize) { mMem = mDealer->allocate(neededSize); } -ssize_t JDescrambler::descramble( +Status JDescrambler::descramble( jbyte key, size_t numSubSamples, ssize_t totalLength, @@ -137,7 +137,8 @@ ssize_t JDescrambler::descramble( const void *srcPtr, jint srcOffset, void *dstPtr, - jint dstOffset) { + jint dstOffset, + ssize_t *result) { // TODO: IDescrambler::descramble() is re-entrant, however because we // only have 1 shared mem buffer, we can only do 1 descramble at a time. // Concurrency might be improved by allowing on-demand allocation of up @@ -159,16 +160,16 @@ ssize_t JDescrambler::descramble( info.dstPtr = NULL; info.dstOffset = 0; - int32_t result; - binder::Status status = mDescrambler->descramble(info, &result); + int32_t descrambleResult; + Status status = mDescrambler->descramble(info, &descrambleResult); - if (!status.isOk() || result > totalLength) { - return -1; - } - if (result > 0) { - memcpy((void*)((uint8_t*)dstPtr + dstOffset), mMem->pointer(), result); + if (status.isOk()) { + *result = (descrambleResult <= totalLength) ? descrambleResult : -1; + if (*result > 0) { + memcpy((void*)((uint8_t*)dstPtr + dstOffset), mMem->pointer(), *result); + } } - return result; + return status; } } // namespace android @@ -251,11 +252,45 @@ static ssize_t getSubSampleInfo(JNIEnv *env, jint numSubSamples, numBytesOfClearData = NULL; } + if (totalSize < 0) { + delete[] subSamples; + return -1; + } + *outSubSamples = subSamples; return totalSize; } +static jthrowable createServiceSpecificException( + JNIEnv *env, int serviceSpecificError, const char *msg) { + if (env->ExceptionCheck()) { + ALOGW("Discarding pending exception"); + env->ExceptionDescribe(); + env->ExceptionClear(); + } + + ScopedLocalRef<jclass> clazz( + env, env->FindClass("android/os/ServiceSpecificException")); + CHECK(clazz.get() != NULL); + + const jmethodID ctor = env->GetMethodID(clazz.get(), "<init>", "(ILjava/lang/String;)V"); + CHECK(ctor != NULL); + + ScopedLocalRef<jstring> msgObj( + env, env->NewStringUTF(msg != NULL ? + msg : String8::format("Error %#x", serviceSpecificError))); + + return (jthrowable)env->NewObject( + clazz.get(), ctor, serviceSpecificError, msgObj.get()); +} + +static void throwServiceSpecificException( + JNIEnv *env, int serviceSpecificError, const char *msg) { + jthrowable exception = createServiceSpecificException(env, serviceSpecificError, msg); + env->Throw(exception); +} + static jint android_media_MediaDescrambler_native_descramble( JNIEnv *env, jobject thiz, jbyte key, jint numSubSamples, jintArray numBytesOfClearDataObj, jintArray numBytesOfEncryptedDataObj, @@ -290,11 +325,11 @@ static jint android_media_MediaDescrambler_native_descramble( env, dstBuf, dstOffset, totalLength, &dstPtr, &dstArray); } } - + Status status; if (err == OK) { - result = descrambler->descramble( + status = descrambler->descramble( key, numSubSamples, totalLength, subSamples, - srcPtr, srcOffset, dstPtr, dstOffset); + srcPtr, srcOffset, dstPtr, dstOffset, &result); } delete[] subSamples; @@ -304,6 +339,51 @@ static jint android_media_MediaDescrambler_native_descramble( if (dstArray != NULL) { env->ReleaseByteArrayElements(dstArray, (jbyte *)dstPtr, 0); } + + if (!status.isOk()) { + switch (status.exceptionCode()) { + case Status::EX_SECURITY: + jniThrowException(env, "java/lang/SecurityException", + status.exceptionMessage()); + break; + case Status::EX_BAD_PARCELABLE: + jniThrowException(env, "java/lang/BadParcelableException", + status.exceptionMessage()); + break; + case Status::EX_ILLEGAL_ARGUMENT: + jniThrowException(env, "java/lang/IllegalArgumentException", + status.exceptionMessage()); + break; + case Status::EX_NULL_POINTER: + jniThrowException(env, "java/lang/NullPointerException", + status.exceptionMessage()); + break; + case Status::EX_ILLEGAL_STATE: + jniThrowException(env, "java/lang/IllegalStateException", + status.exceptionMessage()); + break; + case Status::EX_NETWORK_MAIN_THREAD: + jniThrowException(env, "java/lang/NetworkOnMainThreadException", + status.exceptionMessage()); + break; + case Status::EX_UNSUPPORTED_OPERATION: + jniThrowException(env, "java/lang/UnsupportedOperationException", + status.exceptionMessage()); + break; + case Status::EX_SERVICE_SPECIFIC: + throwServiceSpecificException(env, status.serviceSpecificErrorCode(), + status.exceptionMessage()); + break; + default: + { + String8 msg; + msg.appendFormat("Unknown exception code: %d, msg: %s", + status.exceptionCode(), status.exceptionMessage().string()); + jniThrowException(env, "java/lang/RuntimeException", msg.string()); + break; + } + } + } return result; } diff --git a/media/jni/android_media_MediaDescrambler.h b/media/jni/android_media_MediaDescrambler.h index e944a90dba7a..aeef05e7968d 100644 --- a/media/jni/android_media_MediaDescrambler.h +++ b/media/jni/android_media_MediaDescrambler.h @@ -19,6 +19,7 @@ #include "jni.h" +#include <binder/Status.h> #include <media/cas/DescramblerAPI.h> #include <media/stagefright/foundation/ABase.h> #include <utils/Mutex.h> @@ -31,11 +32,12 @@ namespace media { class IDescrambler; }; using namespace media; +using binder::Status; struct JDescrambler : public RefBase { JDescrambler(JNIEnv *env, jobject descramberBinderObj); - ssize_t descramble( + Status descramble( jbyte key, size_t numSubSamples, ssize_t totalLength, @@ -43,7 +45,8 @@ struct JDescrambler : public RefBase { const void *srcPtr, jint srcOffset, void *dstPtr, - jint dstOffset); + jint dstOffset, + ssize_t *result); protected: virtual ~JDescrambler(); diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 8df194c11800..e450283312c3 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -27,7 +27,9 @@ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" /> <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> - <application android:label="@string/app_name" > + <application + android:label="@string/app_name" + android:directBootAware="true"> <receiver android:name="com.android.carrierdefaultapp.CarrierDefaultBroadcastReceiver"> <intent-filter> <action android:name="com.android.internal.telephony.CARRIER_SIGNAL_REDIRECTED" /> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java index e49463f04ec6..1b6aca1ba65e 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java @@ -58,6 +58,7 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.internal.util.Preconditions; @@ -180,15 +181,20 @@ public class DeviceDiscoveryService extends Service { } private void startDiscovery(AssociationRequest request) { - mRequest = request; + if (!request.equals(mRequest)) { + mRequest = request; - mFilters = request.getDeviceFilters(); - mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); - mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); - mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLEDeviceFilter.class); - mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLEDeviceFilter::getScanFilter); + mFilters = request.getDeviceFilters(); + mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); + mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); + mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLEDeviceFilter.class); + mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLEDeviceFilter::getScanFilter); - reset(); + reset(); + } + if (!ArrayUtils.isEmpty(mDevicesFound)) { + onReadyToShowUI(); + } if (shouldScan(mBluetoothFilters)) { final IntentFilter intentFilter = new IntentFilter(); @@ -228,10 +234,18 @@ public class DeviceDiscoveryService extends Service { private void stopScan() { if (DEBUG) Log.i(LOG_TAG, "stopScan() called"); - mBluetoothAdapter.cancelDiscovery(); - mBLEScanner.stopScan(mBLEScanCallback); - unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver); - unregisterReceiver(mWifiDeviceFoundBroadcastReceiver); + + if (shouldScan(mBluetoothFilters)) { + mBluetoothAdapter.cancelDiscovery(); + unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver); + } + if (shouldScan(mBLEFilters)) { + mBLEScanner.stopScan(mBLEScanCallback); + } + if (shouldScan(mWifiFilters)) { + unregisterReceiver(mWifiDeviceFoundBroadcastReceiver); + } + stopSelf(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java index 0280f26e30b1..1f86f8b8d594 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java @@ -526,9 +526,13 @@ public class WifiTracker { // the given ScanResult. This is used for showing that a given AP // (ScanResult) is available via a Passpoint provider (provider friendly // name). - WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result); - if (config != null) { - accessPoint.update(config); + try { + WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result); + if (config != null) { + accessPoint.update(config); + } + } catch (UnsupportedOperationException e) { + // Passpoint not supported on the device. } } diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk index 1ebfbad6d206..635c96f23778 100644 --- a/packages/SystemUI/Android.mk +++ b/packages/SystemUI/Android.mk @@ -38,7 +38,6 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \ android-support-v17-leanback LOCAL_STATIC_JAVA_LIBRARIES := \ - framework-protos \ SystemUI-tags \ SystemUI-proto diff --git a/packages/SystemUI/plugin/Android.mk b/packages/SystemUI/plugin/Android.mk index 05ee6b23c4b7..e22dddb6d565 100644 --- a/packages/SystemUI/plugin/Android.mk +++ b/packages/SystemUI/plugin/Android.mk @@ -36,4 +36,6 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_JAVA_LIBRARIES := SystemUIPluginLib +LOCAL_PROGUARD_ENABLED := disabled + include $(BUILD_PACKAGE) diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 5b20716fa41f..bf17e387e321 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1878,6 +1878,15 @@ <!-- PiP BTW notification description. [CHAR LIMIT=NONE] --> <string name="pip_notification_message">If you don’t want <xliff:g id="name" example="Google Maps">%s</xliff:g> to use this feature, tap to open settings and turn it off.</string> + <!-- PiP section of the tuner. [CHAR LIMIT=NONE] --> + <string name="picture_in_picture" translatable="false">Picture-in-Picture</string> + + <!-- PiP minimize title. [CHAR LIMIT=NONE]--> + <string name="pip_minimize_title" translatable="false">Minimize</string> + + <!-- PiP minimize description. [CHAR LIMIT=NONE] --> + <string name="pip_minimize_description" translatable="false">Drag or fling the PIP to the edges of the screen to minimize it.</string> + <!-- Tuner string --> <string name="change_theme_reboot" translatable="false">Changing the theme requires a restart.</string> <!-- Tuner string --> diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml index bc3edd58f252..908fb20a45c2 100644 --- a/packages/SystemUI/res/xml/tuner_prefs.xml +++ b/packages/SystemUI/res/xml/tuner_prefs.xml @@ -122,6 +122,18 @@ </PreferenceScreen> <PreferenceScreen + android:key="picture_in_picture" + android:title="@string/picture_in_picture"> + + <com.android.systemui.tuner.TunerSwitch + android:key="pip_minimize" + android:title="@string/pip_minimize_title" + android:summary="@string/pip_minimize_description" + sysui:defValue="false" /> + + </PreferenceScreen> + + <PreferenceScreen android:key="doze" android:title="@string/tuner_doze"> diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index d058e7837423..79190cbb129f 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -24,6 +24,7 @@ import android.util.ArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.NightDisplayController; +import com.android.internal.logging.MetricsLogger; import com.android.internal.util.Preconditions; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.assist.AssistManager; @@ -257,6 +258,8 @@ public class Dependency extends SystemUI { mProviders.put(VolumeDialogController.class, () -> new VolumeDialogControllerImpl(mContext)); + mProviders.put(MetricsLogger.class, () -> new MetricsLogger()); + // Put all dependencies above here so the factory can override them if it wants. SystemUIFactory.getInstance().injectDependencies(mProviders, mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index f70d5b41ac3e..a0f491f3ba34 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -37,8 +37,10 @@ import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.policy.PipSnapAlgorithm; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.FlingAnimationUtils; +import com.android.systemui.tuner.TunerService; import java.io.PrintWriter; @@ -46,9 +48,11 @@ import java.io.PrintWriter; * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding * the PIP. */ -public class PipTouchHandler { +public class PipTouchHandler implements TunerService.Tunable { private static final String TAG = "PipTouchHandler"; + private static final String TUNER_KEY_MINIMIZE = "pip_minimize"; + // These values are used for metrics and should never change private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0; private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1; @@ -97,6 +101,9 @@ public class PipTouchHandler { } }; + // Allow the PIP to be dragged to the edge of the screen to be minimized. + private boolean mEnableMinimize = false; + // Behaviour states private boolean mIsMenuVisible; private boolean mIsMinimized; @@ -104,6 +111,8 @@ public class PipTouchHandler { private int mImeHeight; private float mSavedSnapFraction = -1f; private boolean mSendingHoverAccessibilityEvents; + private boolean mMovementWithinMinimize; + private boolean mMovementWithinDismiss; // Touch state private final PipTouchState mTouchState; @@ -167,6 +176,9 @@ public class PipTouchHandler { mExpandedShortestEdgeSize = context.getResources().getDimensionPixelSize( R.dimen.pip_expanded_shortest_edge_size); + // Register any tuner settings changes + Dependency.get(TunerService.class).addTunable(this, TUNER_KEY_MINIMIZE); + // Register the listener for input consumer touch events inputConsumerController.setTouchListener(this::handleTouchEvent); inputConsumerController.setRegistrationListener(this::onRegistrationChanged); @@ -187,6 +199,20 @@ public class PipTouchHandler { } } + @Override + public void onTuningChanged(String key, String newValue) { + if (newValue == null) { + // Reset back to default + mEnableMinimize = false; + return; + } + switch (key) { + case TUNER_KEY_MINIMIZE: + mEnableMinimize = Integer.parseInt(newValue) != 0; + break; + } + } + public void onConfigurationChanged() { mMotionHelper.onConfigurationChanged(); mMotionHelper.synchronizePinnedStackBounds(); @@ -366,6 +392,9 @@ public class PipTouchHandler { * Sets the minimized state. */ void setMinimizedStateInternal(boolean isMinimized) { + if (!mEnableMinimize) { + return; + } setMinimizedState(isMinimized, false /* fromController */); } @@ -373,6 +402,9 @@ public class PipTouchHandler { * Sets the minimized state. */ void setMinimizedState(boolean isMinimized, boolean fromController) { + if (!mEnableMinimize) { + return; + } if (mIsMinimized != isMinimized) { MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED, isMinimized); @@ -435,6 +467,8 @@ public class PipTouchHandler { * Gesture controlling normal movement of the PIP. */ private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() { + // Whether the PiP was on the left side of the screen at the start of the gesture + private boolean mStartedOnLeft; @Override public void onDown(PipTouchState touchState) { @@ -442,6 +476,10 @@ public class PipTouchHandler { return; } + mStartedOnLeft = mMotionHelper.getBounds().left < mMovementBounds.centerX(); + mMovementWithinMinimize = true; + mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; + // If the menu is still visible, and we aren't minimized, then just poke the menu // so that it will timeout after the user stops touching it if (mMenuController.isMenuVisible() && !mIsMinimized) { @@ -475,7 +513,7 @@ public class PipTouchHandler { final PointF lastDelta = touchState.getLastTouchDelta(); float left = mTmpBounds.left + lastDelta.x; float top = mTmpBounds.top + lastDelta.y; - if (!touchState.allowDraggingOffscreen()) { + if (!touchState.allowDraggingOffscreen() || !mEnableMinimize) { left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left)); } if (ENABLE_DISMISS_DRAG_TO_EDGE) { @@ -493,6 +531,18 @@ public class PipTouchHandler { if (ENABLE_DISMISS_DRAG_TO_EDGE) { updateDismissFraction(); } + + final PointF curPos = touchState.getLastTouchPosition(); + if (mMovementWithinMinimize) { + // Track if movement remains near starting edge to identify swipes to minimize + mMovementWithinMinimize = mStartedOnLeft + ? curPos.x <= mMovementBounds.left + mTmpBounds.width() + : curPos.x >= mMovementBounds.right; + } + if (mMovementWithinDismiss) { + // Track if movement remains near the bottom edge to identify swipe to dismiss + mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; + } return true; } return false; @@ -526,8 +576,15 @@ public class PipTouchHandler { } if (touchState.isDragging()) { - final boolean onLeft = mMotionHelper.getBounds().left < mMovementBounds.centerX(); - boolean isFlingToBot = isFlingTowardsEdge(touchState, 4 /* bottom */); + final PointF vel = touchState.getVelocity(); + final float velocity = PointF.length(vel.x, vel.y); + final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond(); + final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y); + final boolean isFlingToBot = isFling + && !isHorizontal && mMovementWithinDismiss && vel.y > 0; + final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize + && (mStartedOnLeft ? vel.x < 0 : vel.x > 0); + if (ENABLE_DISMISS_DRAG_TO_EDGE && (mMotionHelper.shouldDismissPip() || isFlingToBot)) { mMotionHelper.animateDragToEdgeDismiss(mMotionHelper.getBounds(), @@ -536,8 +593,8 @@ public class PipTouchHandler { MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED, METRIC_VALUE_DISMISSED_BY_DRAG); return true; - } else if (!mIsMinimized && (mMotionHelper.shouldMinimizePip() - || isFlingTowardsEdge(touchState, onLeft ? 2 : 3))) { + } else if (mEnableMinimize && + !mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) { // Pip should be minimized setMinimizedStateInternal(true); if (mMenuController.isMenuVisible()) { @@ -563,9 +620,7 @@ public class PipTouchHandler { mMenuController.showMenu(mMotionHelper.getBounds(), mMovementBounds); } - final PointF vel = mTouchState.getVelocity(); - final float velocity = PointF.length(vel.x, vel.y); - if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { + if (isFling) { mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds, mUpdateScrimListener); } else { @@ -585,42 +640,6 @@ public class PipTouchHandler { }; /** - * @return whether the gesture ending in {@param vel} is fast enough to be a fling and towards - * the provided {@param edge} where: - * - * 1 = top - * 2 = left - * 3 = right - * 4 = bottom - */ - private boolean isFlingTowardsEdge(PipTouchState touchState, int edge) { - final PointF vel = touchState.getVelocity(); - final PointF downPos = touchState.getDownTouchPosition(); - final Rect bounds = mMotionHelper.getBounds(); - final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y); - final boolean isFling = - PointF.length(vel.x, vel.y) > mFlingAnimationUtils.getMinVelocityPxPerSecond(); - if (!isFling) { - return false; - } - switch (edge) { - case 1: // top - return !isHorizontal && vel.y < 0 - && downPos.y <= mMovementBounds.top + bounds.height(); - case 2: // left - return isHorizontal && vel.x < 0 - && downPos.x <= mMovementBounds.left + bounds.width(); - case 3: // right - return isHorizontal && vel.x > 0 - && downPos.x >= mMovementBounds.right; - case 4: // bottom - return !isHorizontal && vel.y > 0 - && downPos.y >= mMovementBounds.bottom; - } - return false; - } - - /** * Updates the current movement bounds based on whether the menu is currently visible. */ private void updateMovementBounds(boolean isExpanded) { @@ -643,6 +662,7 @@ public class PipTouchHandler { pw.println(innerPrefix + "mImeHeight=" + mImeHeight); pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); pw.println(innerPrefix + "mEnableDragToDismiss=" + ENABLE_DISMISS_DRAG_TO_TARGET); + pw.println(innerPrefix + "mEnableMinimize=" + mEnableMinimize); mSnapAlgorithm.dump(pw, innerPrefix); mTouchState.dump(pw, innerPrefix); mMotionHelper.dump(pw, innerPrefix); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java index 1709718b1301..9efe224ff52f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java @@ -14,6 +14,8 @@ package com.android.systemui.qs; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS; + import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; @@ -197,7 +199,7 @@ public class QSDetail extends LinearLayout { mDetailContent.removeAllViews(); mDetailContent.addView(detailView); mDetailViews.put(viewCacheIndex, detailView); - MetricsLogger.visible(mContext, adapter.getMetricsCategory()); + Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory()); announceForAccessibility(mContext.getString( R.string.accessibility_quick_settings_detail, adapter.getTitle())); @@ -206,7 +208,7 @@ public class QSDetail extends LinearLayout { setVisibility(View.VISIBLE); } else { if (mDetailAdapter != null) { - MetricsLogger.hidden(mContext, mDetailAdapter.getMetricsCategory()); + Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory()); } mClosingDetail = true; mDetailAdapter = null; @@ -238,8 +240,12 @@ public class QSDetail extends LinearLayout { protected void setupDetailFooter(DetailAdapter adapter) { final Intent settingsIntent = adapter.getSettingsIntent(); mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); - mDetailSettingsButton.setOnClickListener(v -> Dependency.get(ActivityStarter.class) - .postStartActivityDismissingKeyguard(settingsIntent, 0)); + mDetailSettingsButton.setOnClickListener(v -> { + Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS, + mDetailAdapter.getMetricsCategory()); + Dependency.get(ActivityStarter.class) + .postStartActivityDismissingKeyguard(settingsIntent, 0); + }); } protected void setupDetailHeader(final DetailAdapter adapter) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java index 2202b5865def..a84138d85a8b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java @@ -16,6 +16,8 @@ package com.android.systemui.qs; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_DATE; + import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; @@ -358,6 +360,8 @@ public class QSFooter extends LinearLayout implements startSettingsActivity(); } } else if (v == mDateTimeGroup) { + Dependency.get(MetricsLogger.class).action(ACTION_QS_DATE, + mNextAlarm != null); if (mNextAlarm != null) { PendingIntent showIntent = mNextAlarm.getShowIntent(); mActivityStarter.startPendingIntentDismissingKeyguard(showIntent); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 29d547cfd5d2..8596b578f030 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -32,6 +32,8 @@ public interface QSHost { TileServices getTileServices(); void removeTile(String tileSpec); + int indexOf(String tileSpec); + interface Callback { void onTilesChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 8298cbb7d7fe..2e6116dba7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -59,6 +59,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback { protected final View mBrightnessView; private final H mHandler = new H(); private final View mPageIndicator; + private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private int mPanelPaddingBottom; private int mBrightnessPaddingTop; @@ -259,7 +260,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback { if (!mExpanded && mTileLayout instanceof PagedTileLayout) { ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); } - MetricsLogger.visibility(mContext, MetricsEvent.QS_PANEL, mExpanded); + mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); if (!mExpanded) { closeDetail(); } else { @@ -475,7 +476,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback { int newVis = visible ? VISIBLE : INVISIBLE; setVisibility(newVis); if (mGridContentVisible != visible) { - MetricsLogger.visibility(mContext, MetricsEvent.QS_PANEL, newVis); + mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis); } mGridContentVisible = visible; } @@ -483,7 +484,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback { private void logTiles() { for (int i = 0; i < mRecords.size(); i++) { TileRecord tileRecord = mRecords.get(i); - MetricsLogger.visible(mContext, tileRecord.tile.getMetricsCategory()); + mMetricsLogger.visible(tileRecord.tile.getMetricsCategory()); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 0ca115e43c07..933054138a8a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -157,6 +157,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory> { return mServices; } + public int indexOf(String spec) { + return mTileSpecs.indexOf(spec); + } + @Override public void onTuningChanged(String key, String newValue) { if (!TILES_SETTING.equals(key)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 6f35017b893b..b5c1bd9ff9db 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; +import android.metrics.LogMaker; import android.net.Uri; import android.os.Binder; import android.os.IBinder; @@ -155,6 +156,11 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener return mComponent; } + @Override + protected LogMaker populate(LogMaker logMaker) { + return super.populate(logMaker).setComponentName(mComponent); + } + public Tile getQsTile() { return mTile; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index 948954c2bd99..1aa51b1c38bf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -14,12 +14,19 @@ package com.android.systemui.qs.tileimpl; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.metrics.LogMaker; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -29,7 +36,6 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.Utils; import com.android.systemui.Dependency; @@ -58,6 +64,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile { protected final H mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); private final ArraySet<Object> mListeners = new ArraySet<>(); + private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final ArrayList<Callback> mCallbacks = new ArrayList<>(); protected TState mState = newTileState(); @@ -76,7 +83,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile { /** * Declare the category of this tile. * - * Categories are defined in {@link com.android.internal.logging.MetricsProto.MetricsEvent} + * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent} * by editing frameworks/base/proto/src/metrics_constants.proto. */ abstract public int getMetricsCategory(); @@ -152,17 +159,28 @@ public abstract class QSTileImpl<TState extends State> implements QSTile { } public void click() { + mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.CLICK); } public void secondaryClick() { + mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.SECONDARY_CLICK); } public void longClick() { + mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.LONG_CLICK); } + protected LogMaker populate(LogMaker logMaker) { + if (mState instanceof BooleanState) { + logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0); + } + return logMaker.setSubtype(getMetricsCategory()) + .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec)); + } + public void showDetail(boolean show) { mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); } @@ -224,7 +242,6 @@ public abstract class QSTileImpl<TState extends State> implements QSTile { } protected void handleLongClick() { - MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec()); Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( getLongClickIntent(), 0); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index ed6e6ef439b5..4e4de15567d4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -83,7 +83,6 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { protected void handleClick() { // Secondary clicks are header clicks, just toggle. final boolean isEnabled = (Boolean)mState.value; - MetricsLogger.action(mContext, getMetricsCategory(), !isEnabled); mController.setBluetoothEnabled(!isEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index a1d3d261c4d8..22b6a634cb9a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -108,13 +108,11 @@ public class CastTile extends QSTileImpl<BooleanState> { protected void handleClick() { if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) { mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - MetricsLogger.action(mContext, getMetricsCategory()); showDetail(true); mHost.openPanels(); }); return; } - MetricsLogger.action(mContext, getMetricsCategory()); showDetail(true); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index 4351b2ca6dc0..04be7de644e3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -98,7 +98,6 @@ public class CellularTile extends QSTileImpl<SignalState> { @Override protected void handleSecondaryClick() { - MetricsLogger.action(mContext, getMetricsCategory()); if (mDataController.isMobileDataSupported()) { showDetail(true); } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java index e33b6808d7dd..5b374b1bc286 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -84,7 +84,6 @@ public class ColorInversionTile extends QSTileImpl<BooleanState> { @Override protected void handleClick() { - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); mSetting.setValue(mState.value ? 0 : 1); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java index 7a25140c2fa0..b7964512a2fa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java @@ -87,7 +87,6 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements private void toggleDataSaver() { mState.value = !mDataSaverController.isDataSaverEnabled(); - MetricsLogger.action(mContext, getMetricsCategory(), mState.value); mDataSaverController.setDataSaverEnabled(mState.value); refreshState(mState.value); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index f35de68a837d..3c2e8973cd6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -146,7 +146,6 @@ public class DndTile extends QSTileImpl<BooleanState> { Toast.LENGTH_LONG).show(); return; } - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); showDetail(true); int zen = Prefs.getInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, Global.ZEN_MODE_ALARMS); mController.setZen(zen, null, TAG); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 7b0fd73446aa..6d2aa9065cff 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -87,7 +87,6 @@ public class FlashlightTile extends QSTileImpl<BooleanState> implements if (ActivityManager.isUserAMonkey()) { return; } - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); boolean newState = !mState.value; refreshState(newState); mFlashlightController.setFlashlight(newState); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index 66629371b942..5c3f65c471bf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -106,7 +106,6 @@ public class HotspotTile extends QSTileImpl<AirplaneBooleanState> { if (!isEnabled && mAirplaneMode.getValue() != 0) { return; } - MetricsLogger.action(mContext, getMetricsCategory(), !isEnabled); mController.setHotspotEnabled(!isEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java index c9533635d2a5..00cfbfa1eac7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java @@ -92,7 +92,6 @@ public class IntentTile extends QSTileImpl<State> { @Override protected void handleClick() { - MetricsLogger.action(mContext, getMetricsCategory(), mIntentPackage); sendIntent("click", mOnClick, mOnClickUri); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java index b5c02cb81b04..b11b15a73093 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java @@ -81,13 +81,11 @@ public class LocationTile extends QSTileImpl<BooleanState> { Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { final boolean wasEnabled = mState.value; mHost.openPanels(); - MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); mController.setLocationEnabled(!wasEnabled); }); return; } final boolean wasEnabled = mState.value; - MetricsLogger.action(mContext, getMetricsCategory(), !wasEnabled); mController.setLocationEnabled(!wasEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java index 32993391e29f..d147b6968856 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java @@ -85,7 +85,6 @@ public class NfcTile extends QSTileImpl<BooleanState> { @Override protected void handleClick() { if (mAdapter == null) return; - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); if (!mAdapter.isEnabled()) { mAdapter.enable(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java index 8b47216f17e4..8aa1e43dc106 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java @@ -54,7 +54,6 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> @Override protected void handleClick() { final boolean activated = !mState.value; - MetricsLogger.action(mContext, getMetricsCategory(), activated); mController.setActivated(activated); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java index 130304f8619e..fb937bd09bc2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java @@ -79,7 +79,6 @@ public class RotationLockTile extends QSTileImpl<BooleanState> { @Override protected void handleClick() { if (mController == null) return; - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); final boolean newState = !mState.value; mController.setRotationLocked(!newState); refreshState(newState); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java index fde2e0449c4e..79b4c4a87596 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -114,7 +114,6 @@ public class WifiTile extends QSTileImpl<SignalState> { protected void handleClick() { // Secondary clicks are header clicks, just toggle. mState.copyTo(mStateBeforeClick); - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); mController.setWifiEnabled(!mState.value); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index 5086091f7572..6c89241aebfe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -68,7 +68,6 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements @Override public void handleClick() { - MetricsLogger.action(mContext, getMetricsCategory(), !mState.value); mProfileController.setWorkModeEnabled(!mState.value); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index aa0fcbd28650..be162667751b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -82,6 +82,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area); mSignalClusterView = reinflateSignalCluster(mStatusBar); Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mSignalClusterView); + // Default to showing until we know otherwise. + showSystemIconArea(false); } @Override @@ -119,6 +121,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue .removeView(mNotificationIconAreaInner); } notificationIconArea.addView(mNotificationIconAreaInner); + // Default to showing until we know otherwise. + showNotificationIconArea(false); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java index b5f56c3b8182..4d99a46e2321 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenGestureLogger.java @@ -22,6 +22,7 @@ import android.util.ArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; @@ -33,7 +34,7 @@ public class LockscreenGestureLogger { private ArrayMap<Integer, Integer> mLegacyMap; private LogMaker mLogMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN) .setType(MetricsEvent.TYPE_ACTION); - private MetricsLogger mMetricsLogger = new MetricsLogger(); + private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); public LockscreenGestureLogger() { mLegacyMap = new ArrayMap<>(EventLogConstants.METRICS_GESTURE_TYPE_MAP.length); @@ -58,9 +59,4 @@ public class LockscreenGestureLogger { } return value; } - - @VisibleForTesting - void setMetricsLogger(MetricsLogger metricsLogger) { - mMetricsLogger = metricsLogger; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index b82b113f3f8d..5370cebdf7c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -484,7 +484,7 @@ public class StatusBar extends SystemUI implements DemoMode, private ScreenPinningRequest mScreenPinningRequest; - MetricsLogger mMetricsLogger = new MetricsLogger(); + private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); // ensure quick settings is disabled until the current user makes it through the setup wizard private boolean mUserSetup = false; @@ -748,12 +748,6 @@ public class StatusBar extends SystemUI implements DemoMode, private NavigationBarFragment mNavigationBar; private View mNavigationBarView; - @VisibleForTesting - void setMetricsLogger(MetricsLogger metricsLogger) { - mMetricsLogger = metricsLogger; - mLockscreenGestureLogger.setMetricsLogger(metricsLogger); - } - @Override public void start() { mNetworkController = Dependency.get(NetworkController.class); @@ -993,16 +987,19 @@ public class StatusBar extends SystemUI implements DemoMode, Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mNotificationIconAreaController); FragmentHostManager.get(mStatusBarWindow) .addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> { - CollapsedStatusBarFragment statusBarFragment = (CollapsedStatusBarFragment) fragment; + CollapsedStatusBarFragment statusBarFragment = + (CollapsedStatusBarFragment) fragment; statusBarFragment.initNotificationIconArea(mNotificationIconAreaController); mStatusBarView = (PhoneStatusBarView) fragment.getView(); mStatusBarView.setBar(this); mStatusBarView.setPanel(mNotificationPanel); mStatusBarView.setScrimController(mScrimController); setAreThereNotifications(); + checkBarModes(); }).getFragmentManager() .beginTransaction() - .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(), CollapsedStatusBarFragment.TAG) + .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(), + CollapsedStatusBarFragment.TAG) .commit(); Dependency.get(StatusBarIconController.class).addIconGroup( new IconManager((ViewGroup) mKeyguardStatusBar.findViewById(R.id.statusIcons))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java new file mode 100644 index 000000000000..c67cccc06169 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 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.systemui.qs; + +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; +import android.testing.ViewUtils; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.qs.DetailAdapter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class QSDetailTest extends SysuiTestCase { + + private MetricsLogger mMetricsLogger; + private QSDetail mQsDetail; + private QSPanel mQsPanel; + private QuickStatusBarHeader mQuickHeader; + private ActivityStarter mActivityStarter; + private DetailAdapter mMockDetailAdapter; + private TestableLooper mTestableLooper; + + @Before + public void setup() throws Exception { + mTestableLooper = TestableLooper.get(this); + mTestableLooper.runWithLooper(() -> { + mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); + mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class); + mQsDetail = (QSDetail) LayoutInflater.from(mContext).inflate(R.layout.qs_detail, null); + mQsPanel = mock(QSPanel.class); + mQuickHeader = mock(QuickStatusBarHeader.class); + mQsDetail.setQsPanel(mQsPanel, mQuickHeader); + + mMockDetailAdapter = mock(DetailAdapter.class); + when(mMockDetailAdapter.createDetailView(any(), any(), any())) + .thenReturn(mock(View.class)); + }); + } + + @Test + public void testShowDetail_Metrics() { + ViewUtils.attachView(mQsDetail); + mTestableLooper.processAllMessages(); + + mQsDetail.handleShowingDetail(mMockDetailAdapter, 0, 0, false); + verify(mMetricsLogger).visible(eq(mMockDetailAdapter.getMetricsCategory())); + mQsDetail.handleShowingDetail(null, 0, 0, false); + verify(mMetricsLogger).hidden(eq(mMockDetailAdapter.getMetricsCategory())); + + ViewUtils.detachView(mQsDetail); + mTestableLooper.processAllMessages(); + } + + @Test + public void testMoreSettingsButton() { + ViewUtils.attachView(mQsDetail); + mTestableLooper.processAllMessages(); + + mQsDetail.handleShowingDetail(mMockDetailAdapter, 0, 0, false); + mQsDetail.findViewById(android.R.id.button2).performClick(); + + int metricsCategory = mMockDetailAdapter.getMetricsCategory(); + verify(mMetricsLogger).action(eq(ACTION_QS_MORE_SETTINGS), eq(metricsCategory)); + + verify(mActivityStarter).postStartActivityDismissingKeyguard(any(), anyInt()); + + ViewUtils.detachView(mQsDetail); + mTestableLooper.processAllMessages(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index deb31dae6840..d77ed3d9cd6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.mock; import android.os.Looper; +import com.android.internal.logging.MetricsLogger; import com.android.keyguard.CarrierText; import com.android.systemui.Dependency; import com.android.systemui.R; @@ -44,12 +45,15 @@ import android.widget.FrameLayout; @RunWithLooper(setAsMainLooper = true) public class QSFragmentTest extends SysuiBaseFragmentTest { + private MetricsLogger mMockMetricsLogger; + public QSFragmentTest() { super(QSFragment.class); } @Before public void addLeakCheckDependencies() { + mMockMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, new LayoutInflaterBuilder(mContext) .replace("com.android.systemui.statusbar.policy.SplitClockView", diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java new file mode 100644 index 000000000000..49796843af6e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017 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.systemui.qs; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.qs.customize.QSCustomizer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class QSPanelTest extends SysuiTestCase { + + private MetricsLogger mMetricsLogger; + private QSPanel mQsPanel; + private QSTileHost mHost; + private QSCustomizer mCustomizer; + + @Before + public void setup() throws Exception { + TestableLooper.get(this).runWithLooper(() -> { + mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); + mQsPanel = new QSPanel(mContext, null); + mHost = mock(QSTileHost.class); + when(mHost.getTiles()).thenReturn(Collections.emptyList()); + mCustomizer = mock(QSCustomizer.class); + mQsPanel.setHost(mHost, mCustomizer); + }); + } + + @Test + public void testSetExpanded_Metrics() { + mQsPanel.setExpanded(true); + verify(mMetricsLogger).visibility(eq(MetricsEvent.QS_PANEL), eq(true)); + mQsPanel.setExpanded(false); + verify(mMetricsLogger).visibility(eq(MetricsEvent.QS_PANEL), eq(false)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java new file mode 100644 index 000000000000..9ed9d28cd96c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2017 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.systemui.qs.tileimpl; + +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.metrics.LogMaker; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.Dependency; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QSTileHost; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class QSTileImplTest extends SysuiTestCase { + + public static final int POSITION = 14; + private TestableLooper mTestableLooper; + private TileImpl mTile; + private QSTileHost mHost; + private MetricsLogger mMetricsLogger; + + @Before + public void setup() throws Exception { + String spec = "spec"; + mTestableLooper = TestableLooper.get(this); + mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); + mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); + mHost = mock(QSTileHost.class); + when(mHost.indexOf(spec)).thenReturn(POSITION); + mTestableLooper.runWithLooper(() -> { + mTile = new TileImpl(mHost); + mTile.setTileSpec(spec); + }); + } + + @Test + public void testClick_Metrics() { + mTile.click(); + verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_CLICK))); + } + + @Test + public void testSecondaryClick_Metrics() { + mTile.secondaryClick(); + verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_SECONDARY_CLICK))); + } + + @Test + public void testLongClick_Metrics() { + mTile.longClick(); + verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_LONG_PRESS))); + } + + @Test + public void testPopulate() { + LogMaker maker = mock(LogMaker.class); + when(maker.setSubtype(anyInt())).thenReturn(maker); + mTile.getState().value = true; + mTile.populate(maker); + verify(maker).addTaggedData(eq(FIELD_QS_VALUE), eq(1)); + verify(maker).addTaggedData(eq(FIELD_QS_POSITION), eq(POSITION)); + } + + private class TileLogMatcher implements ArgumentMatcher<LogMaker> { + + private final int mCategory; + public String mInvalid; + + public TileLogMatcher(int category) { + mCategory = category; + } + + @Override + public boolean matches(LogMaker arg) { + if (arg.getCategory() != mCategory) { + mInvalid = "Expected category " + mCategory + " but was " + arg.getCategory(); + return false; + } + if (arg.getType() != TYPE_ACTION) { + mInvalid = "Expected type " + TYPE_ACTION + " but was " + arg.getType(); + return false; + } + if (arg.getSubtype() != mTile.getMetricsCategory()) { + mInvalid = "Expected subtype " + mTile.getMetricsCategory() + " but was " + + arg.getSubtype(); + return false; + } + return true; + } + + @Override + public String toString() { + return mInvalid; + } + } + + private static class TileImpl extends QSTileImpl<QSTile.BooleanState> { + protected TileImpl(QSHost host) { + super(host); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + protected void handleClick() { + + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + + } + + @Override + public int getMetricsCategory() { + return 42; + } + + @Override + public Intent getLongClickIntent() { + return null; + } + + @Override + protected void setListening(boolean listening) { + + } + + @Override + public CharSequence getTileLabel() { + return null; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java index a9acda305663..6ddbffccaa96 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -104,6 +105,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { fragment.disable(0, 0, false); - Mockito.verify(mNotificationAreaInner).setVisibility(eq(View.VISIBLE)); + Mockito.verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index f48af7557593..bf6b3946d101 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -62,9 +62,9 @@ public class StatusBarTest extends SysuiTestCase { mKeyguardIndicationController = mock(KeyguardIndicationController.class); mStackScroller = mock(NotificationStackScrollLayout.class); mMetricsLogger = new FakeMetricsLogger(); + mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger); mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache, mKeyguardIndicationController, mStackScroller); - mStatusBar.setMetricsLogger(mMetricsLogger); doAnswer(invocation -> { OnDismissAction onDismissAction = (OnDismissAction) invocation.getArguments()[0]; diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index 783aae7121ad..da441f5eaff3 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -3656,7 +3656,7 @@ message MetricsEvent { ACTION_SETTINGS_MASTER_SWITCH_BLUETOOTH_TOGGLE = 870; // The name of the activity being launched in an app transition event. - APP_TRANSITION_ACTIVITY_NAME = 871; + FIELD_CLASS_NAME = 871; // ACTION: Settings > App detail > Uninstall ACTION_SETTINGS_UNINSTALL_APP = 872; @@ -3874,6 +3874,24 @@ message MetricsEvent { // OPEN: Settings -> System -> Reset options RESET_DASHBOARD = 924; + // ACTION: QS -> Tile clicked + ACTION_QS_CLICK = 925; + + // ACTION: QS -> Secondary click + ACTION_QS_SECONDARY_CLICK = 926; + + // FIELD: Position info in QS clicks + FIELD_QS_POSITION = 927; + + // FIELD: The value of a QS tile when clicked (if applicable) + FIELD_QS_VALUE = 928; + + // ACTION: QS -> Detail panel -> more settings + ACTION_QS_MORE_SETTINGS = 929; + + // ACTION: QS -> Click date + ACTION_QS_DATE = 930; + // ---- End O Constants, all O constants go above this line ---- // Add new aosp constants above this line. diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 4d783503cb5f..3d1c2511db27 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -19,15 +19,10 @@ package com.android.server.autofill; import static android.service.autofill.AutofillService.EXTRA_ACTIVITY_TOKEN; import static android.service.voice.VoiceInteractionSession.KEY_RECEIVER_EXTRAS; import static android.service.voice.VoiceInteractionSession.KEY_STRUCTURE; -import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED; -import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED; import static android.view.autofill.AutofillManager.FLAG_START_SESSION; -import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED; -import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST; import static com.android.server.autofill.Helper.DEBUG; import static com.android.server.autofill.Helper.VERBOSE; -import static com.android.server.autofill.Helper.findValue; import android.annotation.NonNull; import android.annotation.Nullable; @@ -35,32 +30,23 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.assist.AssistStructure; -import android.app.assist.AssistStructure.ViewNode; -import android.app.assist.AssistStructure.WindowNode; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.graphics.Rect; -import android.metrics.LogMaker; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; -import android.os.Parcelable; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.UserManager; import android.provider.Settings; import android.service.autofill.AutofillService; import android.service.autofill.AutofillServiceInfo; -import android.service.autofill.Dataset; -import android.service.autofill.FillResponse; import android.service.autofill.IAutoFillService; -import android.service.autofill.SaveInfo; import android.text.TextUtils; import android.util.ArrayMap; import android.util.LocalLog; @@ -68,21 +54,16 @@ import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Slog; import android.view.autofill.AutofillId; -import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; -import android.view.autofill.IAutofillWindowPresenter; + import com.android.internal.annotations.GuardedBy; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; import com.android.internal.os.HandlerCaller; import com.android.internal.os.IResultReceiver; import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Map; -import java.util.Map.Entry; /** * Bridge between the {@code system_server}'s {@link AutofillManagerService} and the @@ -93,13 +74,12 @@ final class AutofillManagerServiceImpl { private static final String TAG = "AutofillManagerServiceImpl"; - private static final int MSG_SERVICE_SAVE = 1; + static final int MSG_SERVICE_SAVE = 1; private final int mUserId; private final Context mContext; private final Object mLock; private final AutoFillUI mUi; - private final MetricsLogger mMetricsLogger = new MetricsLogger(); private RemoteCallbackList<IAutoFillManagerClient> mClients; private AutofillServiceInfo mInfo; @@ -358,8 +338,9 @@ final class AutofillManagerServiceImpl { private Session createSessionByTokenLocked(@NonNull IBinder activityToken, @Nullable IBinder windowToken, @NonNull IBinder appCallbackToken, boolean hasCallback, int flags, @NonNull String packageName) { - final Session newSession = new Session(mContext, activityToken, - windowToken, appCallbackToken, hasCallback, flags, packageName); + final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock, + activityToken, windowToken, appCallbackToken, hasCallback, flags, + mInfo.getServiceInfo().getComponentName(), packageName); mSessions.put(activityToken, newSession); /* @@ -400,6 +381,10 @@ final class AutofillManagerServiceImpl { session.updateLocked(autofillId, virtualBounds, value, flags); } + void removeSessionLocked(IBinder activityToken) { + mSessions.remove(activityToken); + } + private void handleSessionSave(IBinder activityToken) { synchronized (mLock) { final Session session = mSessions.get(activityToken); @@ -423,6 +408,25 @@ final class AutofillManagerServiceImpl { mSessions.clear(); } + void disableSelf() { + final long identity = Binder.clearCallingIdentity(); + try { + final String autoFillService = Settings.Secure.getStringForUser( + mContext.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, mUserId); + if (mInfo.getServiceInfo().getComponentName().equals( + ComponentName.unflattenFromString(autoFillService))) { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.AUTOFILL_SERVICE, null, mUserId); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + CharSequence getServiceLabel() { + return mInfo.getServiceInfo().loadLabel(mContext.getPackageManager()); + } + void dumpLocked(String prefix, PrintWriter pw) { final String prefix2 = prefix + " "; @@ -495,804 +499,4 @@ final class AutofillManagerServiceImpl { + ", component=" + (mInfo != null ? mInfo.getServiceInfo().getComponentName() : null) + "]"; } - - /** - * State for a given view with a AutofillId. - * - * <p>This class holds state about a view and calls its listener when the fill UI is ready to - * be displayed for the view. - */ - static final class ViewState { - interface Listener { - /** - * Called when the fill UI is ready to be shown for this view. - */ - void onFillReady(FillResponse fillResponse, AutofillId focusedId, - @Nullable AutofillValue value); - } - - final AutofillId mId; - private final Listener mListener; - // TODO(b/33197203): would not need a reference to response and session if it was an inner - // class of Session... - private final Session mSession; - // TODO(b/33197203): encapsulate access so it's not called by UI - FillResponse mResponse; - Intent mAuthIntent; - - private AutofillValue mAutofillValue; - - // Bounds if a virtual view, null otherwise - private Rect mVirtualBounds; - - private boolean mValueUpdated; - - ViewState(Session session, AutofillId id, Listener listener) { - mSession = session; - mId = id; - mListener = listener; - } - - /** - * Response should only be set once. - */ - void setResponse(FillResponse response) { - mResponse = response; - maybeCallOnFillReady(); - } - - /** - * Used when a {@link FillResponse} requires authentication to be unlocked. - */ - void setResponse(FillResponse response, Intent authIntent) { - mAuthIntent = authIntent; - setResponse(response); - } - - CharSequence getServiceName() { - return mSession.getServiceName(); - } - - // TODO(b/33197203): need to refactor / rename / document this method to make it clear that - // it can change the value and update the UI; similarly, should replace code that - // directly sets mAutoFilLValue to use encapsulation. - void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) { - if (autofillValue != null) { - mAutofillValue = autofillValue; - } - if (virtualBounds != null) { - mVirtualBounds = virtualBounds; - } - - maybeCallOnFillReady(); - } - - /** - * Calls {@link - * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the - * fill UI is ready to be displayed (i.e. when response and bounds are set). - */ - void maybeCallOnFillReady() { - if (mResponse != null && (mResponse.getAuthentication() != null - || mResponse.getDatasets() != null)) { - mListener.onFillReady(mResponse, mId, mAutofillValue); - } - } - - @Override - public String toString() { - return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds - + ", updated = " + mValueUpdated + "]"; - } - - void dump(String prefix, PrintWriter pw) { - pw.print(prefix); pw.print("id:" ); pw.println(mId); - pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue); - pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated); - pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds); - pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent); - } - } - - /** - * A session for a given activity. - * - * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track - * of the current {@link ViewState} to display the appropriate UI. - * - * <p>Although the autofill requests and callbacks are stateless from the service's point of - * view, we need to keep state in the framework side for cases such as authentication. For - * example, when service return a {@link FillResponse} that contains all the fields needed - * to fill the activity but it requires authentication first, that response need to be held - * until the user authenticates or it times out. - */ - // TODO(b/33197203): make sure sessions are removed (and tested by CTS): - // - On all authentication scenarios. - // - When user does not interact back after a while. - // - When service is unbound. - final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, - AutoFillUI.AutoFillUiCallback { - private final IBinder mActivityToken; - private final IBinder mWindowToken; - - /** Package name of the app that is auto-filled */ - @NonNull private final String mPackageName; - - @GuardedBy("mLock") - private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>(); - - @GuardedBy("mLock") - @Nullable - private ViewState mCurrentViewState; - - private final IAutoFillManagerClient mClient; - - @GuardedBy("mLock") - RemoteFillService mRemoteFillService; - - // TODO(b/33197203): Get a response per view instead of per activity. - @GuardedBy("mLock") - private FillResponse mCurrentResponse; - - /** - * Used to remember which {@link Dataset} filled the session. - */ - // TODO(b/33197203): might need more than one once we support partitions - @GuardedBy("mLock") - private Dataset mAutoFilledDataset; - - /** - * Assist structure sent by the app; it will be updated (sanitized, change values for save) - * before sent to {@link AutofillService}. - */ - @GuardedBy("mLock") - private AssistStructure mStructure; - - /** - * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}. - */ - private boolean mHasCallback; - - /** - * Flags used to start the session. - */ - private int mFlags; - private Session(@NonNull Context context, @NonNull IBinder activityToken, - @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback, - int flags, @NonNull String packageName) { - mRemoteFillService = new RemoteFillService(context, - mInfo.getServiceInfo().getComponentName(), mUserId, this); - mActivityToken = activityToken; - mWindowToken = windowToken; - mHasCallback = hasCallback; - mFlags = flags; - mPackageName = packageName; - - mClient = IAutoFillManagerClient.Stub.asInterface(client); - try { - client.linkToDeath(() -> { - if (DEBUG) { - Slog.d(TAG, "app binder died"); - } - - removeSelf(); - }, 0); - } catch (RemoteException e) { - Slog.w(TAG, "linkToDeath() on mClient failed: " + e); - } - - mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName); - } - - // FillServiceCallbacks - @Override - public void onFillRequestSuccess(@Nullable FillResponse response, - @NonNull String servicePackageName) { - if (response == null) { - // Nothing to be done, but need to notify client. - notifyUnavailableToClient(); - removeSelf(); - return; - } - - if ((response.getDatasets() == null || response.getDatasets().isEmpty()) - && response.getAuthentication() == null) { - // Response is "empty" from an UI point of view, need to notify client. - notifyUnavailableToClient(); - } - synchronized (mLock) { - processResponseLocked(response); - } - - LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST)) - .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS) - .setPackageName(mPackageName) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, - response.getDatasets() == null ? 0 : response.getDatasets().size()) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE, - servicePackageName); - mMetricsLogger.write(log); - } - - // FillServiceCallbacks - @Override - public void onFillRequestFailure(@Nullable CharSequence message, - @NonNull String servicePackageName) { - LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST)) - .setType(MetricsProto.MetricsEvent.TYPE_FAILURE) - .setPackageName(mPackageName) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE, - servicePackageName); - mMetricsLogger.write(log); - - getUiForShowing().showError(message); - removeSelf(); - } - - // FillServiceCallbacks - @Override - public void onSaveRequestSuccess(@NonNull String servicePackageName) { - LogMaker log = (new LogMaker( - MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) - .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS) - .setPackageName(mPackageName) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE, - servicePackageName); - mMetricsLogger.write(log); - - // Nothing left to do... - removeSelf(); - } - - // FillServiceCallbacks - @Override - public void onSaveRequestFailure(@Nullable CharSequence message, - @NonNull String servicePackageName) { - LogMaker log = (new LogMaker( - MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) - .setType(MetricsProto.MetricsEvent.TYPE_FAILURE) - .setPackageName(mPackageName) - .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE, - servicePackageName); - mMetricsLogger.write(log); - - getUiForShowing().showError(message); - removeSelf(); - } - - // FillServiceCallbacks - @Override - public void authenticate(IntentSender intent) { - final Intent fillInIntent; - synchronized (mLock) { - fillInIntent = createAuthFillInIntent(mStructure); - } - mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent)); - } - - // FillServiceCallbacks - @Override - public void onDisableSelf() { - final long identity = Binder.clearCallingIdentity(); - try { - final String autoFillService = Settings.Secure.getStringForUser( - mContext.getContentResolver(), - Settings.Secure.AUTOFILL_SERVICE, mUserId); - if (mInfo.getServiceInfo().getComponentName().equals( - ComponentName.unflattenFromString(autoFillService))) { - Settings.Secure.putStringForUser(mContext.getContentResolver(), - Settings.Secure.AUTOFILL_SERVICE, null, mUserId); - } - } finally { - Binder.restoreCallingIdentity(identity); - } - synchronized (mLock) { - removeSelfLocked(); - } - } - - // FillServiceCallbacks - @Override - public void onServiceDied(RemoteFillService service) { - // TODO(b/33197203): implement - } - - // AutoFillUiCallback - @Override - public void fill(Dataset dataset) { - mHandlerCaller.getHandler().post(() -> autoFill(dataset)); - } - - // AutoFillUiCallback - @Override - public void save() { - mHandlerCaller.getHandler().obtainMessage(MSG_SERVICE_SAVE, mActivityToken) - .sendToTarget(); - } - - // AutoFillUiCallback - @Override - public void cancelSave() { - mHandlerCaller.getHandler().post(() -> removeSelf()); - } - - // AutoFillUiCallback - @Override - public void requestShowFillUi(AutofillId id, int width, int height, - IAutofillWindowPresenter presenter) { - try { - mClient.requestShowFillUi(mWindowToken, id, width, height, - mCurrentViewState.mVirtualBounds, presenter); - } catch (RemoteException e) { - Slog.e(TAG, "Error requesting to show fill UI", e); - } - } - - // AutoFillUiCallback - @Override - public void requestHideFillUi(AutofillId id) { - try { - mClient.requestHideFillUi(mWindowToken, id); - } catch (RemoteException e) { - Slog.e(TAG, "Error requesting to hide fill UI", e); - } - } - - public void setAuthenticationResultLocked(Bundle data) { - if (mCurrentResponse == null || data == null) { - removeSelf(); - } else { - Parcelable result = data.getParcelable( - AutofillManager.EXTRA_AUTHENTICATION_RESULT); - if (result instanceof FillResponse) { - mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_AUTHENTICATED, - mPackageName); - - mCurrentResponse = (FillResponse) result; - processResponseLocked(mCurrentResponse); - } else if (result instanceof Dataset) { - Dataset dataset = (Dataset) result; - final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset); - if (index >= 0) { - mCurrentResponse.getDatasets().set(index, dataset); - autoFill(dataset); - } - } - } - } - - public void setHasCallback(boolean hasIt) { - mHasCallback = hasIt; - } - - /** - * Shows the save UI, when session can be saved. - * - * @return {@code true} if session is done, or {@code false} if it's pending user action. - */ - public boolean showSaveLocked() { - if (mStructure == null) { - Slog.wtf(TAG, "showSaveLocked(): no mStructure"); - return true; - } - if (mCurrentResponse == null) { - // Happens when the activity / session was finished before the service replied, or - // when the service cannot autofill it (and returned a null response). - if (DEBUG) { - Slog.d(TAG, "showSaveLocked(): no mCurrentResponse"); - } - return true; - } - final SaveInfo saveInfo = mCurrentResponse.getSaveInfo(); - if (DEBUG) { - Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo); - } - - /* - * The Save dialog is only shown if all conditions below are met: - * - * - saveInfo is not null - * - autofillValue of all required ids is not null - * - autofillValue of at least one id (required or optional) has changed. - */ - - if (saveInfo == null) { - return true; - } - - final AutofillId[] requiredIds = saveInfo.getRequiredIds(); - if (requiredIds == null || requiredIds.length == 0) { - Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo"); - return true; - } - - boolean allRequiredAreNotEmpty = true; - boolean atLeastOneChanged = false; - for (int i = 0; i < requiredIds.length; i++) { - final AutofillId id = requiredIds[i]; - final ViewState state = mViewStates.get(id); - if (state == null || state.mAutofillValue == null - || state.mAutofillValue.isEmpty()) { - final ViewNode node = findViewNodeByIdLocked(id); - if (node == null) { - Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id); - allRequiredAreNotEmpty = false; - break; - } - final AutofillValue initialValue = node.getAutofillValue(); - if (initialValue == null || initialValue.isEmpty()) { - if (DEBUG) { - Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id ); - } - allRequiredAreNotEmpty = false; - break; - } - } - if (state.mValueUpdated) { - final AutofillValue filledValue = findValue(mAutoFilledDataset, id); - if (!state.mAutofillValue.equals(filledValue)) { - if (DEBUG) { - Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": " - + filledValue + " => " + state.mAutofillValue); - } - atLeastOneChanged = true; - } - } else { - if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) { - if (DEBUG) { - Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": " - + state.mAutofillValue); - } - allRequiredAreNotEmpty = false; - break; - - } - } - } - - if (allRequiredAreNotEmpty) { - if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) { - for (int i = 0; i < saveInfo.getOptionalIds().length; i++) { - final AutofillId id = saveInfo.getOptionalIds()[i]; - final ViewState state = mViewStates.get(id); - if (state != null && state.mAutofillValue != null && state.mValueUpdated) { - final AutofillValue filledValue = findValue(mAutoFilledDataset, id); - if (!state.mAutofillValue.equals(filledValue)) { - if (DEBUG) { - Slog.d(TAG, "finishSessionLocked(): found a change on optional " - + id + ": " + filledValue + " => " - + state.mAutofillValue); - } - atLeastOneChanged = true; - break; - } - } - } - } - if (atLeastOneChanged) { - getUiForShowing().showSaveUi( - mInfo.getServiceInfo().loadLabel(mContext.getPackageManager()), - saveInfo, mPackageName); - return false; - } - } - // Nothing changed... - if (DEBUG) { - Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities." - + "allRequiredAreNotNull=" + allRequiredAreNotEmpty - + ", atLeastOneChanged=" + atLeastOneChanged); - } - return true; - } - - /** - * Calls service when user requested save. - */ - private void callSaveLocked() { - if (DEBUG) { - Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates); - } - - final Bundle extras = this.mCurrentResponse.getExtras(); - - for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) { - final AutofillValue value = entry.getValue().mAutofillValue; - if (value == null) { - if (VERBOSE) { - Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey()); - } - continue; - } - final AutofillId id = entry.getKey(); - final ViewNode node = findViewNodeByIdLocked(id); - if (node == null) { - Slog.w(TAG, "callSaveLocked(): did not find node with id " + id); - continue; - } - if (VERBOSE) { - Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value); - } - - node.updateAutofillValue(value); - } - - // Sanitize structure before it's sent to service. - mStructure.sanitizeForParceling(false); - - if (VERBOSE) { - Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()"); - mStructure.dump(); - } - - mRemoteFillService.onSaveRequest(mStructure, extras); - } - - void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) { - if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) { - // TODO(b/33197203): ignoring because we don't support partitions yet - Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled"); - return; - } - - ViewState viewState = mViewStates.get(id); - if (viewState == null) { - viewState = new ViewState(this, id, this); - mViewStates.put(id, viewState); - } - - if ((flags & FLAG_START_SESSION) != 0) { - // View is triggering autofill. - mCurrentViewState = viewState; - viewState.update(value, virtualBounds); - return; - } - - if ((flags & FLAG_VALUE_CHANGED) != 0) { - if (value != null && !value.equals(viewState.mAutofillValue)) { - viewState.mValueUpdated = true; - - // Must check if this update was caused by autofilling the view, in which - // case we just update the value, but not the UI. - if (mAutoFilledDataset != null) { - final AutofillValue filledValue = findValue(mAutoFilledDataset, id); - if (value.equals(filledValue)) { - viewState.mAutofillValue = value; - return; - } - } - - // Change value - viewState.mAutofillValue = value; - - // Update the chooser UI - if (value.isText()) { - getUiForShowing().filterFillUi(value.getTextValue().toString()); - } else { - getUiForShowing().filterFillUi(null); - } - } - - return; - } - - if ((flags & FLAG_VIEW_ENTERED) != 0) { - // Remove the UI if the ViewState has changed. - if (mCurrentViewState != viewState) { - mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null); - mCurrentViewState = viewState; - } - - // If the ViewState is ready to be displayed, onReady() will be called. - viewState.update(value, virtualBounds); - - // TODO(b/33197203): Remove when there is a response per activity. - if (mCurrentResponse != null) { - viewState.setResponse(mCurrentResponse); - } - - return; - } - - if ((flags & FLAG_VIEW_EXITED) != 0) { - if (mCurrentViewState == viewState) { - mUi.hideFillUi(viewState.mId); - mCurrentViewState = null; - } - return; - } - - Slog.w(TAG, "updateLocked(): unknown flags " + flags); - } - - @Override - public void onFillReady(FillResponse response, AutofillId filledId, - @Nullable AutofillValue value) { - String filterText = null; - if (value != null && value.isText()) { - filterText = value.getTextValue().toString(); - } - - getUiForShowing().showFillUi(filledId, response, filterText, mPackageName); - } - - private void notifyUnavailableToClient() { - if (mCurrentViewState == null) { - // TODO(b/33197203): temporary sanity check; should never happen - Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null"); - return; - } - if (!mHasCallback) return; - try { - mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId); - } catch (RemoteException e) { - Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken - + " id=" + mCurrentViewState.mId, e); - } - } - - private void processResponseLocked(FillResponse response) { - if (DEBUG) { - Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication() - + "):" + response); - } - - if (mCurrentViewState == null) { - // TODO(b/33197203): temporary sanity check; should never happen - Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null"); - return; - } - - mCurrentResponse = response; - - if (mCurrentResponse.getAuthentication() != null) { - // Handle authentication. - final Intent fillInIntent = createAuthFillInIntent(mStructure); - mCurrentViewState.setResponse(mCurrentResponse, fillInIntent); - return; - } - - if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null - && response.getDatasets().size() == 1) { - Slog.d(TAG, "autofilling manual request directly"); - autoFill(response.getDatasets().get(0)); - return; - } - - mCurrentViewState.setResponse(mCurrentResponse); - } - - void autoFill(Dataset dataset) { - synchronized (mLock) { - mAutoFilledDataset = dataset; - - // Autofill it directly... - if (dataset.getAuthentication() == null) { - autoFillApp(dataset); - return; - } - - // ...or handle authentication. - Intent fillInIntent = createAuthFillInIntent(mStructure); - startAuthentication(dataset.getAuthentication(), fillInIntent); - } - } - - CharSequence getServiceName() { - return AutofillManagerServiceImpl.this.getServiceName(); - } - - private Intent createAuthFillInIntent(AssistStructure structure) { - Intent fillInIntent = new Intent(); - fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure); - return fillInIntent; - } - - private void startAuthentication(IntentSender intent, Intent fillInIntent) { - try { - mClient.authenticate(intent, fillInIntent); - } catch (RemoteException e) { - Slog.e(TAG, "Error launching auth intent", e); - } - } - - void dumpLocked(String prefix, PrintWriter pw) { - pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken); - pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags); - pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse); - pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset); - pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState); - pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size()); - final String prefix2 = prefix + " "; - for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) { - pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey()); - entry.getValue().dump(prefix2, pw); - } - if (VERBOSE) { - pw.print(prefix); pw.print("mStructure: " ); - // TODO(b/33197203): add method do dump AssistStructure on pw - if (mStructure != null) { - pw.println("look at logcat" ); - mStructure.dump(); // dumps to logcat - } else { - pw.println("null"); - } - } - pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback); - mRemoteFillService.dump(prefix, pw); - } - - void autoFillApp(Dataset dataset) { - synchronized (mLock) { - try { - if (DEBUG) { - Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); - } - mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues()); - } catch (RemoteException e) { - Slog.w(TAG, "Error autofilling activity: " + e); - } - } - } - - private AutoFillUI getUiForShowing() { - synchronized (mLock) { - mUi.setCallback(this); - return mUi; - } - } - - private ViewNode findViewNodeByIdLocked(AutofillId id) { - final int size = mStructure.getWindowNodeCount(); - for (int i = 0; i < size; i++) { - final WindowNode window = mStructure.getWindowNodeAt(i); - final ViewNode root = window.getRootViewNode(); - if (id.equals(root.getAutofillId())) { - return root; - } - final ViewNode child = findViewNodeByIdLocked(root, id); - if (child != null) { - return child; - } - } - return null; - } - - private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) { - final int childrenSize = parent.getChildCount(); - if (childrenSize > 0) { - for (int i = 0; i < childrenSize; i++) { - final ViewNode child = parent.getChildAt(i); - if (id.equals(child.getAutofillId())) { - return child; - } - final ViewNode grandChild = findViewNodeByIdLocked(child, id); - if (grandChild != null && id.equals(grandChild.getAutofillId())) { - return grandChild; - } - } - } - return null; - } - - private void destroyLocked() { - mRemoteFillService.destroy(); - mUi.setCallback(null); - mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_FINISHED, - mPackageName); - } - - void removeSelf() { - synchronized (mLock) { - removeSelfLocked(); - } - } - - private void removeSelfLocked() { - if (VERBOSE) { - Slog.v(TAG, "removeSelfLocked()"); - } - destroyLocked(); - mSessions.remove(mActivityToken); - } - } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java new file mode 100644 index 000000000000..1093e9e27d93 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2017 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.autofill; + +import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST; +import static android.view.autofill.AutofillManager.FLAG_START_SESSION; +import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED; +import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED; +import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED; + +import static com.android.server.autofill.Helper.DEBUG; +import static com.android.server.autofill.Helper.VERBOSE; +import static com.android.server.autofill.Helper.findValue; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.assist.AssistStructure; +import android.app.assist.AssistStructure.ViewNode; +import android.app.assist.AssistStructure.WindowNode; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.graphics.Rect; +import android.metrics.LogMaker; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.os.RemoteException; +import android.provider.Settings; +import android.service.autofill.AutofillService; +import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; +import android.service.autofill.SaveInfo; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; +import android.view.autofill.AutofillValue; +import android.view.autofill.IAutoFillManagerClient; +import android.view.autofill.IAutofillWindowPresenter; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.os.HandlerCaller; +import com.android.server.autofill.ui.AutoFillUI; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.Map.Entry; + +/** + * A session for a given activity. + * + * <p>This class manages the multiple {@link ViewState}s for each view it has, and keeps track + * of the current {@link ViewState} to display the appropriate UI. + * + * <p>Although the autofill requests and callbacks are stateless from the service's point of + * view, we need to keep state in the framework side for cases such as authentication. For + * example, when service return a {@link FillResponse} that contains all the fields needed + * to fill the activity but it requires authentication first, that response need to be held + * until the user authenticates or it times out. + */ +// TODO(b/33197203): make sure sessions are removed (and tested by CTS): +// - On all authentication scenarios. +// - When user does not interact back after a while. +// - When service is unbound. +final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, + AutoFillUI.AutoFillUiCallback { + private static final String TAG = "AutofillSession"; + + private final AutofillManagerServiceImpl mService; + private final IBinder mActivityToken; + private final IBinder mWindowToken; + private final HandlerCaller mHandlerCaller; + private final Object mLock; + private final AutoFillUI mUi; + + private final MetricsLogger mMetricsLogger = new MetricsLogger(); + + /** Package name of the app that is auto-filled */ + @NonNull private final String mPackageName; + + @GuardedBy("mLock") + private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>(); + + @GuardedBy("mLock") + @Nullable + private ViewState mCurrentViewState; + + private final IAutoFillManagerClient mClient; + + @GuardedBy("mLock") + RemoteFillService mRemoteFillService; + + // TODO(b/33197203): Get a response per view instead of per activity. + @GuardedBy("mLock") + private FillResponse mCurrentResponse; + + /** + * Used to remember which {@link Dataset} filled the session. + */ + // TODO(b/33197203): might need more than one once we support partitions + @GuardedBy("mLock") + private Dataset mAutoFilledDataset; + + /** + * Assist structure sent by the app; it will be updated (sanitized, change values for save) + * before sent to {@link AutofillService}. + */ + @GuardedBy("mLock") AssistStructure mStructure; + + /** + * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}. + */ + private boolean mHasCallback; + + /** + * Flags used to start the session. + */ + int mFlags; + + Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui, + @NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId, + @NonNull Object lock, @NonNull IBinder activityToken, + @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback, + int flags, @NonNull ComponentName componentName, @NonNull String packageName) { + mService = service; + mLock = lock; + mUi = ui; + mHandlerCaller = handlerCaller; + mRemoteFillService = new RemoteFillService(context, componentName, userId, this); + mActivityToken = activityToken; + mWindowToken = windowToken; + mHasCallback = hasCallback; + mPackageName = packageName; + mFlags = flags; + + mClient = IAutoFillManagerClient.Stub.asInterface(client); + try { + client.linkToDeath(() -> { + if (DEBUG) { + Slog.d(TAG, "app binder died"); + } + + removeSelf(); + }, 0); + } catch (RemoteException e) { + Slog.w(TAG, "linkToDeath() on mClient failed: " + e); + } + + mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName); + } + + // FillServiceCallbacks + @Override + public void onFillRequestSuccess(@Nullable FillResponse response, + @NonNull String servicePackageName) { + if (response == null) { + // Nothing to be done, but need to notify client. + notifyUnavailableToClient(); + removeSelf(); + return; + } + + if ((response.getDatasets() == null || response.getDatasets().isEmpty()) + && response.getAuthentication() == null) { + // Response is "empty" from an UI point of view, need to notify client. + notifyUnavailableToClient(); + } + synchronized (mLock) { + processResponseLocked(response); + } + + LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) + .setType(MetricsEvent.TYPE_SUCCESS) + .setPackageName(mPackageName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, + response.getDatasets() == null ? 0 : response.getDatasets().size()) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, + servicePackageName); + mMetricsLogger.write(log); + } + + // FillServiceCallbacks + @Override + public void onFillRequestFailure(@Nullable CharSequence message, + @NonNull String servicePackageName) { + LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) + .setType(MetricsEvent.TYPE_FAILURE) + .setPackageName(mPackageName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); + mMetricsLogger.write(log); + + getUiForShowing().showError(message); + removeSelf(); + } + + // FillServiceCallbacks + @Override + public void onSaveRequestSuccess(@NonNull String servicePackageName) { + LogMaker log = (new LogMaker( + MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) + .setType(MetricsEvent.TYPE_SUCCESS) + .setPackageName(mPackageName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); + mMetricsLogger.write(log); + + // Nothing left to do... + removeSelf(); + } + + // FillServiceCallbacks + @Override + public void onSaveRequestFailure(@Nullable CharSequence message, + @NonNull String servicePackageName) { + LogMaker log = (new LogMaker( + MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) + .setType(MetricsEvent.TYPE_FAILURE) + .setPackageName(mPackageName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); + mMetricsLogger.write(log); + + getUiForShowing().showError(message); + removeSelf(); + } + + // FillServiceCallbacks + @Override + public void authenticate(IntentSender intent) { + final Intent fillInIntent; + synchronized (mLock) { + fillInIntent = createAuthFillInIntent(mStructure); + } + mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent)); + } + + // FillServiceCallbacks + @Override + public void onDisableSelf() { + mService.disableSelf(); + synchronized (mLock) { + removeSelfLocked(); + } + } + + // FillServiceCallbacks + @Override + public void onServiceDied(RemoteFillService service) { + // TODO(b/33197203): implement + } + + // AutoFillUiCallback + @Override + public void fill(Dataset dataset) { + mHandlerCaller.getHandler().post(() -> autoFill(dataset)); + } + + // AutoFillUiCallback + @Override + public void save() { + mHandlerCaller.getHandler() + .obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, mActivityToken) + .sendToTarget(); + } + + // AutoFillUiCallback + @Override + public void cancelSave() { + mHandlerCaller.getHandler().post(() -> removeSelf()); + } + + // AutoFillUiCallback + @Override + public void requestShowFillUi(AutofillId id, int width, int height, + IAutofillWindowPresenter presenter) { + try { + mClient.requestShowFillUi(mWindowToken, id, width, height, + mCurrentViewState.mVirtualBounds, presenter); + } catch (RemoteException e) { + Slog.e(TAG, "Error requesting to show fill UI", e); + } + } + + // AutoFillUiCallback + @Override + public void requestHideFillUi(AutofillId id) { + try { + mClient.requestHideFillUi(mWindowToken, id); + } catch (RemoteException e) { + Slog.e(TAG, "Error requesting to hide fill UI", e); + } + } + + public void setAuthenticationResultLocked(Bundle data) { + if (mCurrentResponse == null || data == null) { + removeSelf(); + } else { + Parcelable result = data.getParcelable( + AutofillManager.EXTRA_AUTHENTICATION_RESULT); + if (result instanceof FillResponse) { + mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName); + + mCurrentResponse = (FillResponse) result; + processResponseLocked(mCurrentResponse); + } else if (result instanceof Dataset) { + Dataset dataset = (Dataset) result; + final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset); + if (index >= 0) { + mCurrentResponse.getDatasets().set(index, dataset); + autoFill(dataset); + } + } + } + } + + public void setHasCallback(boolean hasIt) { + mHasCallback = hasIt; + } + + /** + * Shows the save UI, when session can be saved. + * + * @return {@code true} if session is done, or {@code false} if it's pending user action. + */ + public boolean showSaveLocked() { + if (mStructure == null) { + Slog.wtf(TAG, "showSaveLocked(): no mStructure"); + return true; + } + if (mCurrentResponse == null) { + // Happens when the activity / session was finished before the service replied, or + // when the service cannot autofill it (and returned a null response). + if (DEBUG) { + Slog.d(TAG, "showSaveLocked(): no mCurrentResponse"); + } + return true; + } + final SaveInfo saveInfo = mCurrentResponse.getSaveInfo(); + if (DEBUG) { + Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo); + } + + /* + * The Save dialog is only shown if all conditions below are met: + * + * - saveInfo is not null + * - autofillValue of all required ids is not null + * - autofillValue of at least one id (required or optional) has changed. + */ + + if (saveInfo == null) { + return true; + } + + final AutofillId[] requiredIds = saveInfo.getRequiredIds(); + if (requiredIds == null || requiredIds.length == 0) { + Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo"); + return true; + } + + boolean allRequiredAreNotEmpty = true; + boolean atLeastOneChanged = false; + for (int i = 0; i < requiredIds.length; i++) { + final AutofillId id = requiredIds[i]; + final ViewState state = mViewStates.get(id); + if (state == null || state.mAutofillValue == null + || state.mAutofillValue.isEmpty()) { + final ViewNode node = findViewNodeByIdLocked(id); + if (node == null) { + Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id); + allRequiredAreNotEmpty = false; + break; + } + final AutofillValue initialValue = node.getAutofillValue(); + if (initialValue == null || initialValue.isEmpty()) { + if (DEBUG) { + Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id ); + } + allRequiredAreNotEmpty = false; + break; + } + } + if (state.mValueUpdated) { + final AutofillValue filledValue = findValue(mAutoFilledDataset, id); + if (!state.mAutofillValue.equals(filledValue)) { + if (DEBUG) { + Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": " + + filledValue + " => " + state.mAutofillValue); + } + atLeastOneChanged = true; + } + } else { + if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) { + if (DEBUG) { + Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": " + + state.mAutofillValue); + } + allRequiredAreNotEmpty = false; + break; + + } + } + } + + if (allRequiredAreNotEmpty) { + if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) { + for (int i = 0; i < saveInfo.getOptionalIds().length; i++) { + final AutofillId id = saveInfo.getOptionalIds()[i]; + final ViewState state = mViewStates.get(id); + if (state != null && state.mAutofillValue != null && state.mValueUpdated) { + final AutofillValue filledValue = findValue(mAutoFilledDataset, id); + if (!state.mAutofillValue.equals(filledValue)) { + if (DEBUG) { + Slog.d(TAG, "finishSessionLocked(): found a change on optional " + + id + ": " + filledValue + " => " + + state.mAutofillValue); + } + atLeastOneChanged = true; + break; + } + } + } + } + if (atLeastOneChanged) { + getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName); + return false; + } + } + // Nothing changed... + if (DEBUG) { + Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities." + + "allRequiredAreNotNull=" + allRequiredAreNotEmpty + + ", atLeastOneChanged=" + atLeastOneChanged); + } + return true; + } + + /** + * Calls service when user requested save. + */ + void callSaveLocked() { + if (DEBUG) { + Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates); + } + + final Bundle extras = this.mCurrentResponse.getExtras(); + + for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) { + final AutofillValue value = entry.getValue().mAutofillValue; + if (value == null) { + if (VERBOSE) { + Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey()); + } + continue; + } + final AutofillId id = entry.getKey(); + final ViewNode node = findViewNodeByIdLocked(id); + if (node == null) { + Slog.w(TAG, "callSaveLocked(): did not find node with id " + id); + continue; + } + if (VERBOSE) { + Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value); + } + + node.updateAutofillValue(value); + } + + // Sanitize structure before it's sent to service. + mStructure.sanitizeForParceling(false); + + if (VERBOSE) { + Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()"); + mStructure.dump(); + } + + mRemoteFillService.onSaveRequest(mStructure, extras); + } + + void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) { + if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) { + // TODO(b/33197203): ignoring because we don't support partitions yet + Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled"); + return; + } + + ViewState viewState = mViewStates.get(id); + if (viewState == null) { + viewState = new ViewState(this, id, this); + mViewStates.put(id, viewState); + } + + if ((flags & FLAG_START_SESSION) != 0) { + // View is triggering autofill. + mCurrentViewState = viewState; + viewState.update(value, virtualBounds); + return; + } + + if ((flags & FLAG_VALUE_CHANGED) != 0) { + if (value != null && !value.equals(viewState.mAutofillValue)) { + viewState.mValueUpdated = true; + + // Must check if this update was caused by autofilling the view, in which + // case we just update the value, but not the UI. + if (mAutoFilledDataset != null) { + final AutofillValue filledValue = findValue(mAutoFilledDataset, id); + if (value.equals(filledValue)) { + viewState.mAutofillValue = value; + return; + } + } + + // Change value + viewState.mAutofillValue = value; + + // Update the chooser UI + if (value.isText()) { + getUiForShowing().filterFillUi(value.getTextValue().toString()); + } else { + getUiForShowing().filterFillUi(null); + } + } + + return; + } + + if ((flags & FLAG_VIEW_ENTERED) != 0) { + // Remove the UI if the ViewState has changed. + if (mCurrentViewState != viewState) { + mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null); + mCurrentViewState = viewState; + } + + // If the ViewState is ready to be displayed, onReady() will be called. + viewState.update(value, virtualBounds); + + // TODO(b/33197203): Remove when there is a response per activity. + if (mCurrentResponse != null) { + viewState.setResponse(mCurrentResponse); + } + + return; + } + + if ((flags & FLAG_VIEW_EXITED) != 0) { + if (mCurrentViewState == viewState) { + mUi.hideFillUi(viewState.mId); + mCurrentViewState = null; + } + return; + } + + Slog.w(TAG, "updateLocked(): unknown flags " + flags); + } + + @Override + public void onFillReady(FillResponse response, AutofillId filledId, + @Nullable AutofillValue value) { + String filterText = null; + if (value != null && value.isText()) { + filterText = value.getTextValue().toString(); + } + + getUiForShowing().showFillUi(filledId, response, filterText, mPackageName); + } + + private void notifyUnavailableToClient() { + if (mCurrentViewState == null) { + // TODO(b/33197203): temporary sanity check; should never happen + Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null"); + return; + } + if (!mHasCallback) return; + try { + mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken + + " id=" + mCurrentViewState.mId, e); + } + } + + private void processResponseLocked(FillResponse response) { + if (DEBUG) { + Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication() + + "):" + response); + } + + if (mCurrentViewState == null) { + // TODO(b/33197203): temporary sanity check; should never happen + Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null"); + return; + } + + mCurrentResponse = response; + + if (mCurrentResponse.getAuthentication() != null) { + // Handle authentication. + final Intent fillInIntent = createAuthFillInIntent(mStructure); + mCurrentViewState.setResponse(mCurrentResponse, fillInIntent); + return; + } + + if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null + && response.getDatasets().size() == 1) { + Slog.d(TAG, "autofilling manual request directly"); + autoFill(response.getDatasets().get(0)); + return; + } + + mCurrentViewState.setResponse(mCurrentResponse); + } + + void autoFill(Dataset dataset) { + synchronized (mLock) { + mAutoFilledDataset = dataset; + + // Autofill it directly... + if (dataset.getAuthentication() == null) { + autoFillApp(dataset); + return; + } + + // ...or handle authentication. + Intent fillInIntent = createAuthFillInIntent(mStructure); + startAuthentication(dataset.getAuthentication(), fillInIntent); + } + } + + CharSequence getServiceName() { + return mService.getServiceName(); + } + + private Intent createAuthFillInIntent(AssistStructure structure) { + Intent fillInIntent = new Intent(); + fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure); + return fillInIntent; + } + + private void startAuthentication(IntentSender intent, Intent fillInIntent) { + try { + mClient.authenticate(intent, fillInIntent); + } catch (RemoteException e) { + Slog.e(TAG, "Error launching auth intent", e); + } + } + + void dumpLocked(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken); + pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags); + pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse); + pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset); + pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState); + pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size()); + final String prefix2 = prefix + " "; + for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) { + pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey()); + entry.getValue().dump(prefix2, pw); + } + if (VERBOSE) { + pw.print(prefix); pw.print("mStructure: " ); + // TODO(b/33197203): add method do dump AssistStructure on pw + if (mStructure != null) { + pw.println("look at logcat" ); + mStructure.dump(); // dumps to logcat + } else { + pw.println("null"); + } + } + pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback); + mRemoteFillService.dump(prefix, pw); + } + + void autoFillApp(Dataset dataset) { + synchronized (mLock) { + try { + if (DEBUG) { + Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); + } + mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues()); + } catch (RemoteException e) { + Slog.w(TAG, "Error autofilling activity: " + e); + } + } + } + + private AutoFillUI getUiForShowing() { + synchronized (mLock) { + mUi.setCallback(this); + return mUi; + } + } + + private ViewNode findViewNodeByIdLocked(AutofillId id) { + final int size = mStructure.getWindowNodeCount(); + for (int i = 0; i < size; i++) { + final WindowNode window = mStructure.getWindowNodeAt(i); + final ViewNode root = window.getRootViewNode(); + if (id.equals(root.getAutofillId())) { + return root; + } + final ViewNode child = findViewNodeByIdLocked(root, id); + if (child != null) { + return child; + } + } + return null; + } + + private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) { + final int childrenSize = parent.getChildCount(); + if (childrenSize > 0) { + for (int i = 0; i < childrenSize; i++) { + final ViewNode child = parent.getChildAt(i); + if (id.equals(child.getAutofillId())) { + return child; + } + final ViewNode grandChild = findViewNodeByIdLocked(child, id); + if (grandChild != null && id.equals(grandChild.getAutofillId())) { + return grandChild; + } + } + } + return null; + } + + void destroyLocked() { + mRemoteFillService.destroy(); + mUi.setCallback(null); + mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName); + } + + void removeSelf() { + synchronized (mLock) { + removeSelfLocked(); + } + } + + void removeSelfLocked() { + if (VERBOSE) { + Slog.v(TAG, "removeSelfLocked()"); + } + destroyLocked(); + mService.removeSessionLocked(mActivityToken); + } +}
\ No newline at end of file diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java new file mode 100644 index 000000000000..d31dcfd061c0 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017 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.autofill; + +import android.annotation.Nullable; +import android.content.Intent; +import android.graphics.Rect; +import android.service.autofill.FillResponse; +import android.view.autofill.AutofillId; +import android.view.autofill.AutofillValue; + +import java.io.PrintWriter; + +/** + * State for a given view with a AutofillId. + * + * <p>This class holds state about a view and calls its listener when the fill UI is ready to + * be displayed for the view. + */ +final class ViewState { + interface Listener { + /** + * Called when the fill UI is ready to be shown for this view. + */ + void onFillReady(FillResponse fillResponse, AutofillId focusedId, + @Nullable AutofillValue value); + } + + final AutofillId mId; + private final Listener mListener; + // TODO(b/33197203): would not need a reference to response and session if it was an inner + // class of Session... + private final Session mSession; + private FillResponse mResponse; + private Intent mAuthIntent; + + // TODO(b/33197203): encapsulate access so it's not called by UI + AutofillValue mAutofillValue; + + // TODO(b/33197203): encapsulate access so it's not called by UI + // Bounds if a virtual view, null otherwise + Rect mVirtualBounds; + + boolean mValueUpdated; + + ViewState(Session session, AutofillId id, Listener listener) { + mSession = session; + mId = id; + mListener = listener; + } + + /** + * Response should only be set once. + */ + void setResponse(FillResponse response) { + mResponse = response; + maybeCallOnFillReady(); + } + + /** + * Used when a {@link FillResponse} requires authentication to be unlocked. + */ + void setResponse(FillResponse response, Intent authIntent) { + mAuthIntent = authIntent; + setResponse(response); + } + + CharSequence getServiceName() { + return mSession.getServiceName(); + } + + // TODO(b/33197203): need to refactor / rename / document this method to make it clear that + // it can change the value and update the UI; similarly, should replace code that + // directly sets mAutoFilLValue to use encapsulation. + void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) { + if (autofillValue != null) { + mAutofillValue = autofillValue; + } + if (virtualBounds != null) { + mVirtualBounds = virtualBounds; + } + + maybeCallOnFillReady(); + } + + /** + * Calls {@link + * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the + * fill UI is ready to be displayed (i.e. when response and bounds are set). + */ + void maybeCallOnFillReady() { + if (mResponse != null && (mResponse.getAuthentication() != null + || mResponse.getDatasets() != null)) { + mListener.onFillReady(mResponse, mId, mAutofillValue); + } + } + + @Override + public String toString() { + return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds + + ", updated = " + mValueUpdated + "]"; + } + + void dump(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("id:" ); pw.println(mId); + pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue); + pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated); + pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds); + pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/NetworkScorerAppManager.java b/services/core/java/com/android/server/NetworkScorerAppManager.java index 5b627d934209..1a3bb357eb7b 100644 --- a/services/core/java/com/android/server/NetworkScorerAppManager.java +++ b/services/core/java/com/android/server/NetworkScorerAppManager.java @@ -88,9 +88,12 @@ public class NetworkScorerAppManager { final String serviceLabel = getRecommendationServiceLabel(serviceInfo, pm); final ComponentName useOpenWifiNetworksActivity = findUseOpenWifiNetworksActivity(serviceInfo); + final String networkAvailableNotificationChannelId = + getNetworkAvailableNotificationChannelId(serviceInfo); appDataList.add( new NetworkScorerAppData(serviceInfo.applicationInfo.uid, - serviceComponentName, serviceLabel, useOpenWifiNetworksActivity)); + serviceComponentName, serviceLabel, useOpenWifiNetworksActivity, + networkAvailableNotificationChannelId)); } else { if (VERBOSE) Log.v(TAG, serviceInfo.packageName + " is NOT a valid scorer/recommender."); @@ -145,6 +148,20 @@ public class NetworkScorerAppManager { return null; } + @Nullable + private static String getNetworkAvailableNotificationChannelId(ServiceInfo serviceInfo) { + if (serviceInfo.metaData == null) { + if (DEBUG) { + Log.d(TAG, "No metadata found on " + serviceInfo.getComponentName()); + } + return null; + } + + return serviceInfo.metaData.getString( + NetworkScoreManager.NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA); + } + + /** * Get the application to use for scoring networks. * diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 8e3e3eaa1a60..77a02b4d2e9d 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -202,16 +202,17 @@ public class AccountManagerService new HashMap<Account, Integer>(); final Object cacheLock = new Object(); /** protected by the {@link #cacheLock} */ - final HashMap<String, Account[]> accountCache = - new LinkedHashMap<>(); + final HashMap<String, Account[]> accountCache = new LinkedHashMap<>(); /** protected by the {@link #cacheLock} */ private final Map<Account, Map<String, String>> userDataCache = new HashMap<>(); /** protected by the {@link #cacheLock} */ private final Map<Account, Map<String, String>> authTokenCache = new HashMap<>(); /** protected by the {@link #cacheLock} */ private final TokenCache accountTokenCaches = new TokenCache(); + /** protected by the {@link #cacheLock} */ + private final Map<Account, Map<String, Integer>> visibilityCache = new HashMap<>(); - /** protected by the {@link #mReceiversForType} + /** protected by the {@link #mReceiversForType}, * type -> (packageName -> number of active receivers) * type == null is used to get notifications about all account types */ @@ -524,25 +525,29 @@ public class AccountManagerService String.format("uid %s cannot get secrets for account %s", callingUid, account); throw new SecurityException(msg); } - return getPackagesAndVisibilityForAccount(account, accounts); + synchronized (accounts.cacheLock) { + return getPackagesAndVisibilityForAccountLocked(account, accounts); + } } /** - * Returns all package names and visibility values, which were set for given account. + * Returns Map with all package names and visibility values for given account. + * The method and returned map must be guarded by accounts.cacheLock * * @param account Account to get visibility values. * @param accounts UserAccount that currently hosts the account and application * - * @return Map from package names to visibility. + * @return Map with cache for package names to visibility. */ - private Map<String, Integer> getPackagesAndVisibilityForAccount(Account account, + private @NonNull Map<String, Integer> getPackagesAndVisibilityForAccountLocked(Account account, UserAccounts accounts) { - final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); - try { - return accounts.accountsDb.findAllVisibilityValuesForAccount(account); - } finally { - StrictMode.setThreadPolicy(oldPolicy); + Map<String, Integer> accountVisibility = accounts.visibilityCache.get(account); + if (accountVisibility == null) { + Log.d(TAG, "Visibility was not initialized"); + accountVisibility = new HashMap<>(); + accounts.visibilityCache.put(account, accountVisibility); } + return accountVisibility; } @Override @@ -572,14 +577,13 @@ public class AccountManagerService * @return Visibility value, AccountManager.VISIBILITY_UNDEFINED if no value was stored. * */ - private int getAccountVisibility(Account account, String packageName, UserAccounts accounts) { - final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); - try { - Integer visibility = - accounts.accountsDb.findAccountVisibility(account, packageName); + private int getAccountVisibilityFromCache(Account account, String packageName, + UserAccounts accounts) { + synchronized (accounts.cacheLock) { + Map<String, Integer> accountVisibility = + getPackagesAndVisibilityForAccountLocked(account, accounts); + Integer visibility = accountVisibility.get(packageName); return visibility != null ? visibility : AccountManager.VISIBILITY_UNDEFINED; - } finally { - StrictMode.setThreadPolicy(oldPolicy); } } @@ -595,9 +599,7 @@ public class AccountManagerService */ private Integer resolveAccountVisibility(Account account, @NonNull String packageName, UserAccounts accounts) { - Preconditions.checkNotNull(packageName, "packageName cannot be null"); - int uid = -1; try { long identityToken = clearCallingIdentity(); @@ -630,7 +632,7 @@ public class AccountManagerService } // Return stored value if it was set. - int visibility = getAccountVisibility(account, packageName, accounts); + int visibility = getAccountVisibilityFromCache(account, packageName, accounts); if (AccountManager.VISIBILITY_UNDEFINED != visibility) { return visibility; @@ -652,13 +654,13 @@ public class AccountManagerService || canReadContacts || isPrivileged) { // Use legacy for preO apps with GET_ACCOUNTS permission or pre/postO with signature // match. - visibility = getAccountVisibility(account, + visibility = getAccountVisibilityFromCache(account, AccountManager.PACKAGE_NAME_KEY_LEGACY_VISIBLE, accounts); if (AccountManager.VISIBILITY_UNDEFINED == visibility) { visibility = AccountManager.VISIBILITY_USER_MANAGED_VISIBLE; } } else { - visibility = getAccountVisibility(account, + visibility = getAccountVisibilityFromCache(account, AccountManager.PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE, accounts); if (AccountManager.VISIBILITY_UNDEFINED == visibility) { visibility = AccountManager.VISIBILITY_USER_MANAGED_NOT_VISIBLE; @@ -751,21 +753,10 @@ public class AccountManagerService packagesToVisibility = new HashMap<>(); } - final long accountId = accounts.accountsDb.findDeAccountId(account); - if (accountId < 0) { + if (!updateAccountVisibilityLocked(account, packageName, newVisibility, accounts)) { return false; } - final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); - try { - if (!accounts.accountsDb.setAccountVisibility(accountId, packageName, - newVisibility)) { - return false; - } - } finally { - StrictMode.setThreadPolicy(oldPolicy); - } - if (notify) { for (Entry<String, Integer> packageToVisibility : packagesToVisibility.entrySet()) { if (packageToVisibility.getValue() != AccountManager.VISIBILITY_NOT_VISIBLE) { @@ -778,6 +769,29 @@ public class AccountManagerService } } + // Update account visibility in cache and database. + private boolean updateAccountVisibilityLocked(Account account, String packageName, + int newVisibility, UserAccounts accounts) { + final long accountId = accounts.accountsDb.findDeAccountId(account); + if (accountId < 0) { + return false; + } + + final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); + try { + if (!accounts.accountsDb.setAccountVisibility(accountId, packageName, + newVisibility)) { + return false; + } + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + Map<String, Integer> accountVisibility = + getPackagesAndVisibilityForAccountLocked(account, accounts); + accountVisibility.put(packageName, newVisibility); + return true; + } + @Override public void registerAccountListener(String[] accountTypes, String opPackageName) { int callingUid = Binder.getCallingUid(); @@ -1042,9 +1056,12 @@ public class AccountManagerService accounts.userDataCache.remove(account); accounts.authTokenCache.remove(account); accounts.accountTokenCaches.remove(account); + accounts.visibilityCache.remove(account); - for (Entry<String, Integer> packageToVisibility : packagesToVisibility.entrySet()) { - if (packageToVisibility.getValue() != AccountManager.VISIBILITY_NOT_VISIBLE) { + for (Entry<String, Integer> packageToVisibility : + packagesToVisibility.entrySet()) { + if (packageToVisibility.getValue() + != AccountManager.VISIBILITY_NOT_VISIBLE) { notifyPackage(packageToVisibility.getKey(), accounts); } } @@ -1067,6 +1084,7 @@ public class AccountManagerService } accounts.accountCache.put(accountType, accountsForType); } + accounts.visibilityCache.putAll(accountsDb.findAllVisibilityValues()); } finally { if (accountDeleted) { sendAccountsChangedBroadcast(accounts.userId); @@ -1181,19 +1199,31 @@ public class AccountManagerService } private void removeVisibilityValuesForPackage(String packageName) { + if (isSpecialPackageKey(packageName)) { + return; + } synchronized (mUsers) { - for (int i = 0; i < mUsers.size(); i++) { - UserAccounts accounts = mUsers.valueAt(i); - try { - int uid = mPackageManager.getPackageUidAsUser(packageName, accounts.userId); - } catch (NameNotFoundException e) { - // package does not exist - remove visibility values - accounts.accountsDb.deleteAccountVisibilityForPackage(packageName); + int numberOfUsers = mUsers.size(); + for (int i = 0; i < numberOfUsers; i++) { + UserAccounts accounts = mUsers.valueAt(i); + try { + mPackageManager.getPackageUidAsUser(packageName, accounts.userId); + } catch (NameNotFoundException e) { + // package does not exist - remove visibility values + accounts.accountsDb.deleteAccountVisibilityForPackage(packageName); + synchronized(accounts.cacheLock) { + for (Account account : accounts.visibilityCache.keySet()) { + Map<String, Integer> accountVisibility = + getPackagesAndVisibilityForAccountLocked(account, accounts); + accountVisibility.remove(packageName); + } + } } } } } + private void onCleanupUser(int userId) { Log.i(TAG, "onCleanupUser " + userId); UserAccounts accounts; @@ -1849,6 +1879,7 @@ public class AccountManagerService */ Map<String, String> tmpData = accounts.userDataCache.get(accountToRename); Map<String, String> tmpTokens = accounts.authTokenCache.get(accountToRename); + Map<String, Integer> tmpVisibility = accounts.visibilityCache.get(accountToRename); removeAccountFromCacheLocked(accounts, accountToRename); /* * Update the cached data associated with the renamed @@ -1856,6 +1887,7 @@ public class AccountManagerService */ accounts.userDataCache.put(renamedAccount, tmpData); accounts.authTokenCache.put(renamedAccount, tmpTokens); + accounts.visibilityCache.put(renamedAccount, tmpVisibility); accounts.previousNameCache.put( renamedAccount, new AtomicReference<>(accountToRename.name)); @@ -5369,6 +5401,7 @@ public class AccountManagerService accounts.userDataCache.remove(account); accounts.authTokenCache.remove(account); accounts.previousNameCache.remove(account); + accounts.visibilityCache.remove(account); } /** diff --git a/services/core/java/com/android/server/accounts/AccountsDb.java b/services/core/java/com/android/server/accounts/AccountsDb.java index 22543cb9e022..8ca7ea1768b9 100644 --- a/services/core/java/com/android/server/accounts/AccountsDb.java +++ b/services/core/java/com/android/server/accounts/AccountsDb.java @@ -909,7 +909,7 @@ class AccountsDb implements AutoCloseable { } Integer findAccountVisibility(Account account, String packageName) { - SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); final Cursor cursor = db.query(TABLE_VISIBILITY, new String[] {VISIBILITY_VALUE}, SELECTION_ACCOUNTS_ID_BY_ACCOUNT + " AND " + VISIBILITY_PACKAGE + "=? ", new String[] {account.name, account.type, packageName}, null, null, null); @@ -924,7 +924,7 @@ class AccountsDb implements AutoCloseable { } Integer findAccountVisibility(long accountId, String packageName) { - SQLiteDatabase db = mDeDatabase.getWritableDatabase(); + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); final Cursor cursor = db.query(TABLE_VISIBILITY, new String[] {VISIBILITY_VALUE}, VISIBILITY_ACCOUNTS_ID + "=? AND " + VISIBILITY_PACKAGE + "=? ", new String[] {String.valueOf(accountId), packageName}, null, null, null); @@ -972,6 +972,41 @@ class AccountsDb implements AutoCloseable { return result; } + /** + * Returns a map account -> (package -> visibility) + */ + Map <Account, Map<String, Integer>> findAllVisibilityValues() { + SQLiteDatabase db = mDeDatabase.getReadableDatabase(); + Map<Account, Map<String, Integer>> result = new HashMap<>(); + Cursor cursor = db.rawQuery( + "SELECT " + TABLE_VISIBILITY + "." + VISIBILITY_PACKAGE + + ", " + TABLE_VISIBILITY + "." + VISIBILITY_VALUE + + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME + + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + + " FROM " + TABLE_VISIBILITY + + " JOIN " + TABLE_ACCOUNTS + + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID + + " = " + TABLE_VISIBILITY + "." + VISIBILITY_ACCOUNTS_ID, null); + try { + while (cursor.moveToNext()) { + String packageName = cursor.getString(0); + Integer visibility = cursor.getInt(1); + String accountName = cursor.getString(2); + String accountType = cursor.getString(3); + Account account = new Account(accountName, accountType); + Map <String, Integer> accountVisibility = result.get(account); + if (accountVisibility == null) { + accountVisibility = new HashMap<>(); + result.put(account, accountVisibility); + } + accountVisibility.put(packageName, visibility); + } + } finally { + cursor.close(); + } + return result; + } + boolean deleteAccountVisibilityForPackage(String packageName) { SQLiteDatabase db = mDeDatabase.getWritableDatabase(); return db.delete(TABLE_VISIBILITY, VISIBILITY_PACKAGE + "=? ", diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 272fbf86aedd..7a83436493b8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2743,6 +2743,7 @@ public class ActivityManagerService extends IActivityManager.Stub @VisibleForTesting public ActivityManagerService(Injector injector) { mInjector = injector; + mContext = mInjector.getContext(); GL_ES_VERSION = 0; mActivityStarter = null; mAppErrors = null; @@ -23875,6 +23876,10 @@ public class ActivityManagerService extends IActivityManager.Stub public static class Injector { private NetworkManagementInternal mNmi; + public Context getContext() { + return null; + } + public AppOpsService getAppOpsService(File file, Handler handler) { return new AppOpsService(file, handler); } diff --git a/services/core/java/com/android/server/am/ActivityMetricsLogger.java b/services/core/java/com/android/server/am/ActivityMetricsLogger.java index 04a09fe4305e..5edfb0651fe7 100644 --- a/services/core/java/com/android/server/am/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/am/ActivityMetricsLogger.java @@ -10,8 +10,8 @@ import static android.app.ActivityManager.StackId.INVALID_STACK_ID; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; import static android.app.ActivityManagerInternal.APP_TRANSITION_TIMEOUT; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION_ACTIVITY_NAME; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION_CALLING_PACKAGE_NAME; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_CLASS_NAME; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION_DELAY_MS; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION_DEVICE_UPTIME_SECONDS; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.APP_TRANSITION_IS_EPHEMERAL; @@ -313,7 +313,7 @@ class ActivityMetricsLogger { final LogMaker builder = new LogMaker(APP_TRANSITION); builder.setPackageName(info.launchedActivity.packageName); builder.setType(type); - builder.addTaggedData(APP_TRANSITION_ACTIVITY_NAME, info.launchedActivity.info.name); + builder.addTaggedData(FIELD_CLASS_NAME, info.launchedActivity.info.name); if (info.launchedActivity.launchedFromPackage != null) { builder.addTaggedData(APP_TRANSITION_CALLING_PACKAGE_NAME, info.launchedActivity.launchedFromPackage); diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 9a4f804cb1b3..28a4e1ab298f 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -759,6 +759,12 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai } } + final void removeActivitiesFromLRUListLocked(TaskRecord task) { + for (ActivityRecord r : task.mActivities) { + mLRUActivities.remove(r); + } + } + final boolean updateLRUListLocked(ActivityRecord r) { final boolean hadit = mLRUActivities.remove(r); mLRUActivities.add(r); @@ -4947,6 +4953,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai } } mTaskHistory.remove(task); + removeActivitiesFromLRUListLocked(task); updateTaskMovement(task, true); if (mode == REMOVE_TASK_MODE_DESTROYING && task.mActivities.isEmpty()) { @@ -5114,6 +5121,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai // Apps may depend on onResume()/onPause() being called in pairs. if (setResume) { mResumedActivity = r; + updateLRUListLocked(r); } // If the activity was previously pausing, then ensure we transfer that as well if (setPause) { diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 27b8e91981c0..8559dcaf06b2 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -3555,8 +3555,16 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D stackHeader.append(" mFullscreen=" + stack.mFullscreen); stackHeader.append("\n"); stackHeader.append(" mBounds=" + stack.mBounds); - printed |= stack.dumpActivitiesLocked(fd, pw, dumpAll, dumpClient, dumpPackage, - needSep, stackHeader.toString()); + + final boolean printedStackHeader = stack.dumpActivitiesLocked(fd, pw, dumpAll, + dumpClient, dumpPackage, needSep, stackHeader.toString()); + printed |= printedStackHeader; + if (!printedStackHeader) { + // Ensure we always dump the stack header even if there are no activities + pw.println(); + pw.println(stackHeader); + } + printed |= dumpHistoryList(fd, pw, stack.mLRUActivities, " ", "Run", false, !dumpAll, false, dumpPackage, true, " Running activities (most recent first):", null); diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 7b1af38da9ed..c10f77cefbe4 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -870,7 +870,7 @@ class AppErrors { nativeProcs = NATIVE_STACKS_OF_INTEREST; } - int[] pids = Process.getPidsForCommands(nativeProcs); + int[] pids = nativeProcs == null ? null : Process.getPidsForCommands(nativeProcs); ArrayList<Integer> nativePids = null; if (pids != null) { diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java index fd65c101919d..bff5f51b9af4 100644 --- a/services/core/java/com/android/server/am/TaskRecord.java +++ b/services/core/java/com/android/server/am/TaskRecord.java @@ -617,15 +617,15 @@ final class TaskRecord extends ConfigurationContainer implements TaskWindowConta boolean kept = true; try { final ActivityRecord r = topRunningActivityLocked(); - final boolean wasFocused = supervisor.isFocusedStack(sourceStack) + final boolean wasFocused = r != null && supervisor.isFocusedStack(sourceStack) && (topRunningActivityLocked() == r); - final boolean wasResumed = sourceStack.mResumedActivity == r; - final boolean wasPaused = sourceStack.mPausingActivity == r; + final boolean wasResumed = r != null && sourceStack.mResumedActivity == r; + final boolean wasPaused = r != null && sourceStack.mPausingActivity == r; // In some cases the focused stack isn't the front stack. E.g. pinned stack. // Whenever we are moving the top activity from the front stack we want to make sure to // move the stack to the front. - final boolean wasFront = supervisor.isFrontStackOnDisplay(sourceStack) + final boolean wasFront = r != null && supervisor.isFrontStackOnDisplay(sourceStack) && (sourceStack.topRunningActivityLocked() == r); // Adjust the position for the new parent stack as needed. @@ -667,8 +667,10 @@ final class TaskRecord extends ConfigurationContainer implements TaskWindowConta // new stack by moving the stack to the front. final boolean moveStackToFront = moveStackMode == REPARENT_MOVE_STACK_TO_FRONT || (moveStackMode == REPARENT_KEEP_STACK_AT_FRONT && (wasFocused || wasFront)); - toStack.moveToFrontAndResumeStateIfNeeded(r, moveStackToFront, wasResumed, wasPaused, - reason); + if (r != null) { + toStack.moveToFrontAndResumeStateIfNeeded(r, moveStackToFront, wasResumed, + wasPaused, reason); + } if (!animate) { toStack.mNoAnimActivities.add(topActivity); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 333d27bd5360..c139fa85bf3b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6555,7 +6555,7 @@ public class AudioService extends IAudioService.Stub public void registerPlaybackCallback(IPlaybackConfigDispatcher pcdb) { final boolean isPrivileged = - (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission( + (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING)); mPlaybackMonitor.registerPlaybackCallback(pcdb, isPrivileged); } @@ -6566,7 +6566,7 @@ public class AudioService extends IAudioService.Stub public List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() { final boolean isPrivileged = - (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission( + (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING)); return mPlaybackMonitor.getActivePlaybackConfigurations(isPrivileged); } diff --git a/services/core/java/com/android/server/media/AudioPlaybackMonitor.java b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java new file mode 100644 index 000000000000..c6dc11c53393 --- /dev/null +++ b/services/core/java/com/android/server/media/AudioPlaybackMonitor.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2017 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 android.content.Context; +import android.media.AudioManager.AudioPlaybackCallback; +import android.media.AudioPlaybackConfiguration; +import android.media.IAudioService; +import android.media.IPlaybackConfigDispatcher; +import android.os.Binder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.IntArray; +import android.util.Log; +import android.util.SparseArray; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Monitors changes in audio playback and notify the newly started audio playback through the + * {@link OnAudioPlaybackStartedListener}. + */ +class AudioPlaybackMonitor extends IPlaybackConfigDispatcher.Stub { + private static boolean DEBUG = MediaSessionService.DEBUG; + private static String TAG = "AudioPlaybackMonitor"; + + /** + * Called when audio playback is started for a given UID. + */ + interface OnAudioPlaybackStartedListener { + void onAudioPlaybackStarted(int uid); + } + + private final Object mLock = new Object(); + private final Context mContext; + private final OnAudioPlaybackStartedListener mListener; + + private Set<Integer> mActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>(); + private Set<Integer> mActiveAudioPlaybackClientUids = new HashSet<>(); + + // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video) + // The UID whose audio playback becomes active at the last comes first. + // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID. + private final IntArray mSortedAudioPlaybackClientUids = new IntArray(); + + AudioPlaybackMonitor(Context context, IAudioService audioService, + OnAudioPlaybackStartedListener listener) { + mContext = context; + mListener = listener; + try { + audioService.registerPlaybackCallback(this); + } catch (RemoteException e) { + Log.wtf(TAG, "Failed to register playback callback", e); + } + } + + /** + * Called when the {@link AudioPlaybackConfiguration} is updated. + * <p>If an app starts audio playback, the app's local media session will be the media button + * session. If the app has multiple media sessions, the playback active local session will be + * picked. + * + * @param configs List of the current audio playback configuration + */ + @Override + public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) { + final long token = Binder.clearCallingIdentity(); + try { + Set<Integer> newActiveAudioPlaybackPlayerInterfaceIds = new HashSet<>(); + List<Integer> newActiveAudioPlaybackClientUids = new ArrayList<>(); + synchronized (mLock) { + mActiveAudioPlaybackClientUids.clear(); + for (AudioPlaybackConfiguration config : configs) { + // Ignore inactive (i.e. not playing) or PLAYER_TYPE_JAM_SOUNDPOOL + // (i.e. playback from the SoundPool class which is only for sound effects) + // playback. + // Note that we shouldn't ignore PLAYER_TYPE_UNKNOWN because it might be OEM + // specific audio/video players. + if (!config.isActive() + || config.getPlayerType() + == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) { + continue; + } + mActiveAudioPlaybackClientUids.add(config.getClientUid()); + + newActiveAudioPlaybackPlayerInterfaceIds.add(config.getPlayerInterfaceId()); + if (!mActiveAudioPlaybackPlayerInterfaceIds.contains( + config.getPlayerInterfaceId())) { + if (DEBUG) { + Log.d(TAG, "Found a new active media playback. " + + AudioPlaybackConfiguration.toLogFriendlyString(config)); + } + // New active audio playback. + newActiveAudioPlaybackClientUids.add(config.getClientUid()); + int index = mSortedAudioPlaybackClientUids.indexOf(config.getClientUid()); + if (index == 0) { + // It's the lastly played music app already. Skip updating. + continue; + } else if (index > 0) { + mSortedAudioPlaybackClientUids.remove(index); + } + mSortedAudioPlaybackClientUids.add(0, config.getClientUid()); + } + } + mActiveAudioPlaybackPlayerInterfaceIds.clear(); + mActiveAudioPlaybackPlayerInterfaceIds = newActiveAudioPlaybackPlayerInterfaceIds; + } + for (int uid : newActiveAudioPlaybackClientUids) { + mListener.onAudioPlaybackStarted(uid); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an + * audio/video) The UID whose audio playback becomes active at the last comes first. + */ + public IntArray getSortedAudioPlaybackClientUids() { + IntArray sortedAudioPlaybackClientUids = new IntArray(); + synchronized (mLock) { + sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids); + } + return sortedAudioPlaybackClientUids; + } + + /** + * Returns if the audio playback is active for the uid. + */ + public boolean isPlaybackActive(int uid) { + synchronized (mLock) { + return mActiveAudioPlaybackClientUids.contains(uid); + } + } + + /** + * Cleans up the sorted list of audio playback client UIDs with given {@param + * mediaButtonSessionUid}. + * <p>UIDs whose audio playback started after the media button session's audio playback + * cannot be the lastly played media app. So they won't needed anymore. + * + * @param mediaButtonSessionUid UID of the media button session. + */ + public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) { + synchronized (mLock) { + int userId = UserHandle.getUserId(mediaButtonSessionUid); + for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) { + if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) { + break; + } + if (userId == UserHandle.getUserId(mSortedAudioPlaybackClientUids.get(i))) { + // Clean up unnecessary UIDs. + // It doesn't need to be managed profile aware because it's just to prevent + // the list from increasing indefinitely. The media button session updating + // shouldn't be affected by cleaning up. + mSortedAudioPlaybackClientUids.remove(i); + } + } + } + } + + /** + * Dumps {@link AudioPlaybackMonitor}. + */ + public void dump(PrintWriter pw, String prefix) { + synchronized (mLock) { + pw.println(prefix + "Audio playback (lastly played comes first)"); + String indent = prefix + " "; + for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) { + int uid = mSortedAudioPlaybackClientUids.get(i); + pw.print(indent + "uid=" + uid + " packages="); + String[] packages = mContext.getPackageManager().getPackagesForUid(uid); + if (packages != null && packages.length > 0) { + for (int j = 0; j < packages.length; j++) { + pw.print(packages[j] + " "); + } + } + pw.println(); + } + } + } +} diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 20663a09699d..7f75c83f8d51 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -66,12 +66,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** - * The length of time a session will still be considered active after - * pausing in ms. - */ - private static final int ACTIVE_BUFFER = 30000; - - /** * The amount of time we'll send an assumed volume after the last volume * command before reverting to the last reported volume. */ @@ -109,7 +103,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private int mRatingType; private int mRepeatMode; private boolean mShuffleModeEnabled; - private long mLastActiveTime; // End TransportPerformer fields // Volume handling fields @@ -130,7 +123,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private String mCallingPackage; public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName, - ISessionCallback cb, String tag, MediaSessionService service, Handler handler) { + ISessionCallback cb, String tag, MediaSessionService service, Looper handlerLooper) { mOwnerPid = ownerPid; mOwnerUid = ownerUid; mUserId = userId; @@ -140,7 +133,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { mSession = new SessionStub(); mSessionCb = new SessionCb(cb); mService = service; - mHandler = new MessageHandler(handler.getLooper()); + mHandler = new MessageHandler(handlerLooper); mAudioManager = (AudioManager) service.getContext().getSystemService(Context.AUDIO_SERVICE); mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class); mAudioAttrs = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); @@ -211,6 +204,15 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** + * Get the UID this session was created for. + * + * @return The UID for this session. + */ + public int getUid() { + return mOwnerUid; + } + + /** * Get the user id this session was created for. * * @return The user id for this session. @@ -244,7 +246,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { public void adjustVolume(int direction, int flags, String packageName, int uid, boolean useSuggested) { int previousFlagPlaySound = flags & AudioManager.FLAG_PLAY_SOUND; - if (isPlaybackActive(false) || hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) { + if (isPlaybackActive() || hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) { flags &= ~AudioManager.FLAG_PLAY_SOUND; } if (mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL) { @@ -320,25 +322,13 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** - * Check if the session is currently performing playback. This will also - * return true if the session was recently paused. + * Check if the session is currently performing playback. * - * @param includeRecentlyActive True if playback that was recently paused - * should count, false if it shouldn't. * @return True if the session is performing playback, false otherwise. */ - public boolean isPlaybackActive(boolean includeRecentlyActive) { - int state = mPlaybackState == null ? 0 : mPlaybackState.getState(); - if (MediaSession.isActiveState(state)) { - return true; - } - if (includeRecentlyActive && state == mPlaybackState.STATE_PAUSED) { - long inactiveTime = SystemClock.uptimeMillis() - mLastActiveTime; - if (inactiveTime < ACTIVE_BUFFER) { - return true; - } - } - return false; + public boolean isPlaybackActive() { + int state = mPlaybackState == null ? PlaybackState.STATE_NONE : mPlaybackState.getState(); + return MediaSession.isActiveState(state); } /** @@ -456,7 +446,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { @Override public String toString() { - return mPackageName + "/" + mTag + " (uid=" + mUserId + ")"; + return mPackageName + "/" + mTag + " (userId=" + mUserId + ")"; } private void postAdjustLocalVolume(final int stream, final int direction, final int flags, @@ -817,6 +807,12 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { @Override public void setMediaButtonReceiver(PendingIntent pi) { mMediaButtonReceiver = pi; + final long token = Binder.clearCallingIdentity(); + try { + mService.onMediaButtonReceiverChanged(MediaSessionRecord.this); + } finally { + Binder.restoreCallingIdentity(token); + } } @Override @@ -842,15 +838,19 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { @Override public void setPlaybackState(PlaybackState state) { - int oldState = mPlaybackState == null ? 0 : mPlaybackState.getState(); - int newState = state == null ? 0 : state.getState(); - if (MediaSession.isActiveState(oldState) && newState == PlaybackState.STATE_PAUSED) { - mLastActiveTime = SystemClock.elapsedRealtime(); - } + int oldState = mPlaybackState == null + ? PlaybackState.STATE_NONE : mPlaybackState.getState(); + int newState = state == null + ? PlaybackState.STATE_NONE : state.getState(); synchronized (mLock) { mPlaybackState = state; } - mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState); + final long token = Binder.clearCallingIdentity(); + try { + mService.onSessionPlaystateChanged(MediaSessionRecord.this, oldState, newState); + } finally { + Binder.restoreCallingIdentity(token); + } mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE); } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index ea9128fd6e2f..4bf9d8f5ff53 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -57,11 +57,13 @@ import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.speech.RecognizerIntent; import android.text.TextUtils; +import android.util.IntArray; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -85,7 +87,7 @@ import java.util.List; */ public class MediaSessionService extends SystemService implements Monitor { private static final String TAG = "MediaSessionService"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // Leave log for key event always. private static final boolean DEBUG_KEY_EVENT = true; @@ -114,6 +116,7 @@ public class MediaSessionService extends SystemService implements Monitor { // It's always not null after the MediaSessionService is started. private FullUserRecord mCurrentFullUserRecord; private MediaSessionRecord mGlobalPrioritySession; + private AudioPlaybackMonitor mAudioPlaybackMonitor; // Used to notify system UI when remote volume was changed. TODO find a // better way to handle this. @@ -134,6 +137,19 @@ public class MediaSessionService extends SystemService implements Monitor { mKeyguardManager = (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE); mAudioService = getAudioService(); + mAudioPlaybackMonitor = new AudioPlaybackMonitor(getContext(), mAudioService, + new AudioPlaybackMonitor.OnAudioPlaybackStartedListener() { + @Override + public void onAudioPlaybackStarted(int uid) { + synchronized (mLock) { + FullUserRecord user = + getFullUserRecordLocked(UserHandle.getUserId(uid)); + if (user != null) { + user.mPriorityStack.updateMediaButtonSessionIfNeeded(); + } + } + } + }); mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class); mContentResolver = getContext().getContentResolver(); mSettingsObserver = new SettingsObserver(); @@ -161,9 +177,10 @@ public class MediaSessionService extends SystemService implements Monitor { user.mPriorityStack.onSessionStateChange(record); if ((record.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) { mGlobalPrioritySession = record; + user.pushAddressedPlayerChangedLocked(); } + mHandler.postSessionsChanged(record.getUserId()); } - mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0); } /** @@ -180,18 +197,14 @@ public class MediaSessionService extends SystemService implements Monitor { } } - public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) { - boolean updateSessions = false; + 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; } - updateSessions = user.mPriorityStack.onPlaystateChange(record, oldState, newState); - } - if (updateSessions) { - mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0); + user.mPriorityStack.onPlaystateChanged(record, oldState, newState); } } @@ -332,6 +345,9 @@ public class MediaSessionService extends SystemService implements Monitor { } if (mGlobalPrioritySession == session) { mGlobalPrioritySession = null; + if (session.isActive() && user != null) { + user.pushAddressedPlayerChangedLocked(); + } } try { @@ -340,8 +356,7 @@ public class MediaSessionService extends SystemService implements Monitor { // ignore exceptions while destroying a session. } session.onDestroy(); - - mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, session.getUserId(), 0); + mHandler.postSessionsChanged(session.getUserId()); } private void enforcePackageName(String packageName, int uid) { @@ -461,7 +476,7 @@ public class MediaSessionService extends SystemService implements Monitor { } final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId, - callerPackageName, cb, tag, this, mHandler); + callerPackageName, cb, tag, this, mHandler.getLooper()); try { cb.asBinder().linkToDeath(session, 0); } catch (RemoteException e) { @@ -469,8 +484,7 @@ public class MediaSessionService extends SystemService implements Monitor { } user.addSessionLocked(session); - - mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, userId, 0); + mHandler.postSessionsChanged(userId); if (DEBUG) { Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag); @@ -496,10 +510,6 @@ public class MediaSessionService extends SystemService implements Monitor { } List<MediaSessionRecord> records = user.mPriorityStack.getActiveSessions(userId); int size = records.size(); - if (size > 0 && records.get(0).isPlaybackActive(false)) { - user.rememberMediaButtonReceiverLocked(records.get(0)); - } - user.pushAddressedPlayerChangedLocked(); ArrayList<MediaSession.Token> tokens = new ArrayList<MediaSession.Token>(); for (int i = 0; i < size; i++) { tokens.add(new MediaSession.Token(records.get(i).getControllerBinder())); @@ -536,6 +546,22 @@ public class MediaSessionService extends SystemService implements Monitor { } } + /** + * Called when the media button receiver for the {@param 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 = getContext().getPackageManager().getPackagesForUid(uid); if (packages != null && packages.length > 0) { @@ -568,10 +594,10 @@ public class MediaSessionService extends SystemService implements Monitor { * place makes more sense and increases the readability.</p> * <p>The contents of this object is guarded by {@link #mLock}. */ - final class FullUserRecord { + final class FullUserRecord implements MediaSessionStack.OnMediaButtonSessionChangedListener { private static final String COMPONENT_NAME_USER_ID_DELIM = ","; private final int mFullUserId; - private final MediaSessionStack mPriorityStack = new MediaSessionStack(); + private final MediaSessionStack mPriorityStack; private PendingIntent mLastMediaButtonReceiver; private ComponentName mRestoredMediaButtonReceiver; private int mRestoredMediaButtonReceiverUserId; @@ -588,6 +614,7 @@ public class MediaSessionService extends SystemService implements Monitor { public FullUserRecord(int fullUserId) { mFullUserId = fullUserId; + mPriorityStack = new MediaSessionStack(mAudioPlaybackMonitor, this); // Restore the remembered media button receiver before the boot. String mediaButtonReceiver = Settings.Secure.getStringForUser(mContentResolver, Settings.System.MEDIA_BUTTON_RECEIVER, mFullUserId); @@ -603,15 +630,14 @@ public class MediaSessionService extends SystemService implements Monitor { } public void destroySessionsForUserLocked(int userId) { - List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, 0, userId); + List<MediaSessionRecord> sessions = mPriorityStack.getPriorityList(false, userId); for (MediaSessionRecord session : sessions) { MediaSessionService.this.destroySessionLocked(session); } } public void addSessionLocked(MediaSessionRecord session) { - mPriorityStack.addSession(session, - mFullUserId == mFullUserIds.get(session.getUserId())); + mPriorityStack.addSession(session); } public void removeSessionLocked(MediaSessionRecord session) { @@ -642,21 +668,41 @@ public class MediaSessionService extends SystemService implements Monitor { mPriorityStack.dump(pw, indent); } - // Remember the media button receiver and keep it in the persistent storage. - private void rememberMediaButtonReceiverLocked(MediaSessionRecord record) { - PendingIntent receiver = record.getMediaButtonReceiver(); - if (receiver == null) { - return; + @Override + public void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession, + MediaSessionRecord newMediaButtonSession) { + if (DEBUG_KEY_EVENT) { + Log.d(TAG, "Media button session will be 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; - ComponentName component = receiver.getIntent().getComponent(); - if (component != null && record.getPackageName().equals(component.getPackageName())) { - String componentName = component.flattenToString(); - Settings.Secure.putStringForUser(mContentResolver, - Settings.System.MEDIA_BUTTON_RECEIVER, - componentName + COMPONENT_NAME_USER_ID_DELIM + record.getUserId(), - mFullUserId); + mRestoredMediaButtonReceiver = null; + String componentName = ""; + if (receiver != null) { + ComponentName component = receiver.getIntent().getComponent(); + if (component != null + && record.getPackageName().equals(component.getPackageName())) { + componentName = component.flattenToString(); + } } + Settings.Secure.putStringForUser(mContentResolver, + Settings.System.MEDIA_BUTTON_RECEIVER, + componentName + COMPONENT_NAME_USER_ID_DELIM + record.getUserId(), + mFullUserId); } private void pushAddressedPlayerChangedLocked() { @@ -682,14 +728,8 @@ public class MediaSessionService extends SystemService implements Monitor { } private MediaSessionRecord getMediaButtonSessionLocked() { - if (isGlobalPriorityActiveLocked()) { - return mGlobalPrioritySession; - } - // If we don't have a media button receiver to fall back on - // include non-playing sessions for dispatching. - boolean useNotPlayingSessions = (mLastMediaButtonReceiver == null - && mRestoredMediaButtonReceiver == null); - return mPriorityStack.getDefaultMediaButtonSession(useNotPlayingSessions); + return isGlobalPriorityActiveLocked() + ? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession(); } } @@ -1262,6 +1302,7 @@ public class MediaSessionService extends SystemService implements Monitor { for (int i = 0; i < count; i++) { mUserRecords.valueAt(i).dumpLocked(pw, ""); } + mAudioPlaybackMonitor.dump(pw, ""); } } @@ -1390,7 +1431,7 @@ public class MediaSessionService extends SystemService implements Monitor { PendingIntent receiver = mCurrentFullUserRecord.mLastMediaButtonReceiver; if (DEBUG_KEY_EVENT) { Log.d(TAG, "Sending " + keyEvent - + " to the last known pendingIntent " + receiver); + + " to the last known PendingIntent " + receiver); } receiver.send(getContext(), needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1, @@ -1413,7 +1454,8 @@ public class MediaSessionService extends SystemService implements Monitor { } mediaButtonIntent.setComponent(receiver); getContext().sendBroadcastAsUser(mediaButtonIntent, - UserHandle.of(mCurrentFullUserRecord.mRestoredMediaButtonReceiverUserId)); + UserHandle.of(mCurrentFullUserRecord + .mRestoredMediaButtonReceiverUserId)); if (mCurrentFullUserRecord.mCallback != null) { mCurrentFullUserRecord.mCallback .onMediaKeyEventDispatchedToMediaButtonReceiver( @@ -1426,23 +1468,6 @@ public class MediaSessionService extends SystemService implements Monitor { } catch (RemoteException e) { Log.w(TAG, "Failed to send callback", e); } - } else { - if (DEBUG) { - Log.d(TAG, "Sending media key ordered broadcast"); - } - if (needWakeLock) { - mMediaEventWakeLock.acquire(); - } - // Fallback to legacy behavior - Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); - keyIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); - if (needWakeLock) { - keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED); - } - // Send broadcast only to the full user. - getContext().sendOrderedBroadcastAsUser(keyIntent, UserHandle.CURRENT, - null, mKeyEventDone, mHandler, Activity.RESULT_OK, null, null); } } @@ -1637,12 +1662,13 @@ public class MediaSessionService extends SystemService implements Monitor { 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(msg.arg1); + pushSessionsChanged((int) msg.obj); break; case MSG_VOLUME_INITIAL_DOWN: synchronized (mLock) { @@ -1657,8 +1683,15 @@ public class MediaSessionService extends SystemService implements Monitor { } } - public void post(int what, int arg1, int arg2) { - obtainMessage(what, arg1, arg2).sendToTarget(); + 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(); } } } diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java index 8b8073454f8c..b0d8adcc122e 100644 --- a/services/core/java/com/android/server/media/MediaSessionStack.java +++ b/services/core/java/com/android/server/media/MediaSessionStack.java @@ -16,12 +16,13 @@ package com.android.server.media; -import android.app.ActivityManager; import android.media.session.MediaController.PlaybackInfo; -import android.media.session.PlaybackState; import android.media.session.MediaSession; -import android.os.RemoteException; +import android.media.session.PlaybackState; +import android.os.Debug; import android.os.UserHandle; +import android.util.IntArray; +import android.util.Log; import java.io.PrintWriter; import java.util.ArrayList; @@ -33,6 +34,20 @@ import java.util.List; * <p>This class isn't thread-safe. The caller should take care of the synchronization. */ class MediaSessionStack { + private static final boolean DEBUG = MediaSessionService.DEBUG; + private static final String TAG = "MediaSessionStack"; + + /** + * Listens the change in the media button session. + */ + interface OnMediaButtonSessionChangedListener { + /** + * Called when the media button session is changed. + */ + void onMediaButtonSessionChanged(MediaSessionRecord oldMediaButtonSession, + MediaSessionRecord newMediaButtonSession); + } + /** * These are states that usually indicate the user took an action and should * bump priority regardless of the old state. @@ -51,57 +66,45 @@ class MediaSessionStack { PlaybackState.STATE_CONNECTING, PlaybackState.STATE_PLAYING }; - private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); + /** + * Sorted list of the media sessions. + * The session of which PlaybackState is changed to ALWAYS_PRIORITY_STATES or + * TRANSITION_PRIORITY_STATES comes first. + * @see #shouldUpdatePriority + */ + private final List<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); + + private final AudioPlaybackMonitor mAudioPlaybackMonitor; + private final OnMediaButtonSessionChangedListener mOnMediaButtonSessionChangedListener; + + /** + * The media button session which receives media key events. + * It could be null if the previous media buttion session is released. + */ + private MediaSessionRecord mMediaButtonSession; - // The last record that either entered one of the playing states or was - // added. - private MediaSessionRecord mLastInterestingRecord; - private MediaSessionRecord mCachedButtonReceiver; private MediaSessionRecord mCachedDefault; private MediaSessionRecord mCachedVolumeDefault; private ArrayList<MediaSessionRecord> mCachedActiveList; - private ArrayList<MediaSessionRecord> mCachedTransportControlList; - /** - * Checks if a media session is created from the most recent app. - * - * @param record A media session record to be examined. - * @return {@code true} if the media session's package name equals to the most recent app, false - * otherwise. - */ - private static boolean isFromMostRecentApp(MediaSessionRecord record) { - try { - List<ActivityManager.RecentTaskInfo> tasks = - ActivityManager.getService().getRecentTasks(1, - ActivityManager.RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS | - ActivityManager.RECENT_IGNORE_UNAVAILABLE | - ActivityManager.RECENT_INCLUDE_PROFILES | - ActivityManager.RECENT_WITH_EXCLUDED, record.getUserId()).getList(); - if (tasks != null && !tasks.isEmpty()) { - ActivityManager.RecentTaskInfo recentTask = tasks.get(0); - if (recentTask.userId == record.getUserId() && recentTask.baseIntent != null) { - return recentTask.baseIntent.getComponent().getPackageName() - .equals(record.getPackageName()); - } - } - } catch (RemoteException e) { - return false; - } - return false; + MediaSessionStack(AudioPlaybackMonitor monitor, OnMediaButtonSessionChangedListener listener) { + mAudioPlaybackMonitor = monitor; + mOnMediaButtonSessionChangedListener = listener; } /** * Add a record to the priority tracker. * * @param record The record to add. - * @param fromForegroundUser {@code true} if the session is created by the foreground user. */ - public void addSession(MediaSessionRecord record, boolean fromForegroundUser) { + public void addSession(MediaSessionRecord record) { mSessions.add(record); clearCache(); - if (fromForegroundUser && isFromMostRecentApp(record)) { - mLastInterestingRecord = record; - } + + // Update the media button session. + // The added session could be the session from the package with the audio playback. + // This can happen if an app starts audio playback before creating media session. + updateMediaButtonSessionIfNeeded(); } /** @@ -111,6 +114,11 @@ class MediaSessionStack { */ public void removeSession(MediaSessionRecord record) { mSessions.remove(record); + if (mMediaButtonSession == record) { + // When the media button session is gone, try to find the alternative media session + // in the media button session app. + onMediaSessionChangeInMediaButtonSessionApp(); + } clearCache(); } @@ -122,32 +130,33 @@ class MediaSessionStack { } /** - * Notify the priority tracker that a session's state changed. + * Notify the priority tracker that a session's playback state changed. * * @param record The record that changed. * @param oldState Its old playback state. * @param newState Its new playback state. - * @return true if the priority order was updated, false otherwise. */ - public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) { + public void onPlaystateChanged(MediaSessionRecord record, int oldState, int newState) { if (shouldUpdatePriority(oldState, newState)) { mSessions.remove(record); mSessions.add(0, record); clearCache(); - // This becomes the last interesting record since it entered a - // playing state - mLastInterestingRecord = record; - return true; } else if (!MediaSession.isActiveState(newState)) { // Just clear the volume cache when a state goes inactive mCachedVolumeDefault = null; } - return false; + + // In most cases, playback state isn't needed for finding media buttion session, + // but we only use it as a hint if an app has multiple local media sessions. + // In that case, we pick the media session whose PlaybackState matches + // the audio playback configuration. + if (mMediaButtonSession != null && mMediaButtonSession.getUid() == record.getUid()) { + onMediaSessionChangeInMediaButtonSessionApp(); + } } /** - * Handle any stack changes that need to occur in response to a session - * state change. TODO add the old and new session state as params + * Handle the change in activeness for a session. * * @param record The record that changed. */ @@ -158,6 +167,81 @@ class MediaSessionStack { } /** + * Update the media button session if needed. + * <p>The media button session is the session that will receive the media button events. + * <p>We send the media button events to the lastly played app. If the app has the media + * session, the session will receive the media button events. + */ + public void updateMediaButtonSessionIfNeeded() { + if (DEBUG) { + Log.d(TAG, "updateMediaButtonSessionIfNeeded, callers=" + Debug.getCallers(2)); + } + IntArray audioPlaybackUids = mAudioPlaybackMonitor.getSortedAudioPlaybackClientUids(); + for (int i = 0; i < audioPlaybackUids.size(); i++) { + MediaSessionRecord mediaButtonSession = + findMediaButtonSession(audioPlaybackUids.get(i)); + if (mediaButtonSession != null) { + // Found the media button session. + mAudioPlaybackMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid()); + if (mMediaButtonSession != mediaButtonSession) { + mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged( + mMediaButtonSession, mediaButtonSession); + mMediaButtonSession = mediaButtonSession; + } + return; + } + } + } + + /** + * Handle the change in a media session in the media button session app. + * <p>If the app has multiple media sessions, change in a media sesion in the app may change + * the media button session. + * @see #findMediaButtonSession + */ + private void onMediaSessionChangeInMediaButtonSessionApp() { + MediaSessionRecord newMediaButtonSession = + findMediaButtonSession(mMediaButtonSession.getUid()); + if (newMediaButtonSession != mMediaButtonSession) { + mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(mMediaButtonSession, + newMediaButtonSession); + mMediaButtonSession = newMediaButtonSession; + } + } + + /** + * Find the media button session with the given {@param uid}. + * If the app has multiple media sessions, the media session matches the audio playback state + * becomes the media button session. + * + * @return The media button session. Returns {@code null} if the app doesn't have a media + * session. + */ + private MediaSessionRecord findMediaButtonSession(int uid) { + MediaSessionRecord mediaButtonSession = null; + for (MediaSessionRecord session : mSessions) { + // Since the media buttons come with the headset/speaker, users will expect changes in + // the headset/speaker when they press media buttons. So only consider local playback + // for the media button session. + if (uid == session.getUid() + && session.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_LOCAL) { + if (session.isPlaybackActive() == + mAudioPlaybackMonitor.isPlaybackActive(session.getUid())) { + // If there's a media session whose PlaybackState matches + // the audio playback state, return it immediately. + return session; + } + if (mediaButtonSession == null) { + // Among the media sessions whose PlaybackState doesn't match + // the audio playback state, pick the top priority. + mediaButtonSession = session; + } + } + } + return mediaButtonSession; + } + + /** * Get the current priority sorted list of active sessions. The most * important session is at index 0 and the least important at size - 1. * @@ -166,57 +250,29 @@ class MediaSessionStack { */ public ArrayList<MediaSessionRecord> getActiveSessions(int userId) { if (mCachedActiveList == null) { - mCachedActiveList = getPriorityList(true, 0, userId); + mCachedActiveList = getPriorityList(true, userId); } return mCachedActiveList; } /** - * Get the highest priority session that can handle media buttons. + * Get the media button session which receives the media button events. * - * @param includeNotPlaying Return a non-playing session if nothing else is - * available - * @return The default media button session or null. + * @return The media button session or null. */ - public MediaSessionRecord getDefaultMediaButtonSession(boolean includeNotPlaying) { - if (mCachedButtonReceiver != null) { - return mCachedButtonReceiver; - } - ArrayList<MediaSessionRecord> records = getPriorityList(true, - MediaSession.FLAG_HANDLES_MEDIA_BUTTONS, UserHandle.USER_ALL); - if (records.size() > 0) { - MediaSessionRecord record = records.get(0); - if (record.isPlaybackActive(false)) { - // Since we're going to send a button event to this record make - // it the last interesting one. - mLastInterestingRecord = record; - mCachedButtonReceiver = record; - } else if (mLastInterestingRecord != null) { - if (records.contains(mLastInterestingRecord)) { - mCachedButtonReceiver = mLastInterestingRecord; - } else { - // That record is no longer used. Clear its reference. - mLastInterestingRecord = null; - } - } - if (includeNotPlaying && mCachedButtonReceiver == null) { - // If we really want a record and we didn't find one yet use the - // highest priority session even if it's not playing. - mCachedButtonReceiver = record; - } - } - return mCachedButtonReceiver; + public MediaSessionRecord getMediaButtonSession() { + return mMediaButtonSession; } public MediaSessionRecord getDefaultVolumeSession() { if (mCachedVolumeDefault != null) { return mCachedVolumeDefault; } - ArrayList<MediaSessionRecord> records = getPriorityList(true, 0, UserHandle.USER_ALL); + ArrayList<MediaSessionRecord> records = getPriorityList(true, UserHandle.USER_ALL); int size = records.size(); for (int i = 0; i < size; i++) { MediaSessionRecord record = records.get(i); - if (record.isPlaybackActive(false)) { + if (record.isPlaybackActive()) { mCachedVolumeDefault = record; return record; } @@ -225,7 +281,7 @@ class MediaSessionStack { } public MediaSessionRecord getDefaultRemoteSession(int userId) { - ArrayList<MediaSessionRecord> records = getPriorityList(true, 0, userId); + ArrayList<MediaSessionRecord> records = getPriorityList(true, userId); int size = records.size(); for (int i = 0; i < size; i++) { @@ -238,9 +294,10 @@ class MediaSessionStack { } public void dump(PrintWriter pw, String prefix) { - ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false, 0, + ArrayList<MediaSessionRecord> sortedSessions = getPriorityList(false, UserHandle.USER_ALL); int count = sortedSessions.size(); + pw.println(prefix + "Media button session is " + mMediaButtonSession); pw.println(prefix + "Sessions Stack - have " + count + " sessions:"); String indent = prefix + " "; for (int i = 0; i < count; i++) { @@ -252,22 +309,23 @@ class MediaSessionStack { /** * Get a priority sorted list of sessions. Can filter to only return active - * sessions or sessions with specific flags. + * sessions or sessions. + * <p>Here's the priority order. + * <li>System priority session (session with FLAG_EXCLUSIVE_GLOBAL_PRIORITY)</li> + * <li>Active sessions whose PlaybackState is active</li> + * <li>Active sessions whose PlaybackState is inactive</li> + * <li>Inactive sessions</li> * * @param activeOnly True to only return active sessions, false to return * all sessions. - * @param withFlags Only return sessions with all the specified flags set. 0 - * returns all sessions. * @param userId The user to get sessions for. {@link UserHandle#USER_ALL} * will return sessions for all users. * @return The priority sorted list of sessions. */ - public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int withFlags, - int userId) { + public ArrayList<MediaSessionRecord> getPriorityList(boolean activeOnly, int userId) { ArrayList<MediaSessionRecord> result = new ArrayList<MediaSessionRecord>(); - int lastLocalIndex = 0; + int lastPlaybackActiveIndex = 0; int lastActiveIndex = 0; - int lastPublishedIndex = 0; int size = mSessions.size(); for (int i = 0; i < size; i++) { @@ -277,10 +335,7 @@ class MediaSessionStack { // Filter out sessions for the wrong user continue; } - if ((session.getFlags() & withFlags) != withFlags) { - // Filter out sessions with the wrong flags - continue; - } + if (!session.isActive()) { if (!activeOnly) { // If we're getting unpublished as well always put them at @@ -294,28 +349,13 @@ class MediaSessionStack { // System priority sessions are special and always go at the // front. We expect there to only be one of these at a time. result.add(0, session); - lastLocalIndex++; + lastPlaybackActiveIndex++; + lastActiveIndex++; + } else if (session.isPlaybackActive()) { + result.add(lastPlaybackActiveIndex++, session); lastActiveIndex++; - lastPublishedIndex++; - } else if (session.isPlaybackActive(true)) { - // TODO this with real local route check - if (true) { - // Active local sessions get top priority - result.add(lastLocalIndex, session); - lastLocalIndex++; - lastActiveIndex++; - lastPublishedIndex++; - } else { - // Then active remote sessions - result.add(lastActiveIndex, session); - lastActiveIndex++; - lastPublishedIndex++; - } } else { - // inactive sessions go at the end in order of whoever last did - // something. - result.add(lastPublishedIndex, session); - lastPublishedIndex++; + result.add(lastActiveIndex++, session); } } @@ -345,8 +385,6 @@ class MediaSessionStack { private void clearCache() { mCachedDefault = null; mCachedVolumeDefault = null; - mCachedButtonReceiver = null; mCachedActiveList = null; - mCachedTransportControlList = null; } } diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java index 41c5331e77b0..82dd9ace6a5e 100644 --- a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java +++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java @@ -197,7 +197,7 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { .setQuota(CacheQuotaHint.QUOTA_NOT_SET) .build()); } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Unable to find package for quota calculation", e); + // This may happen if an app has a recorded usage, but has been uninstalled. continue; } } diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java index ebbb8b315d00..42f12eb23d39 100644 --- a/services/core/java/com/android/server/tv/TvInputHal.java +++ b/services/core/java/com/android/server/tv/TvInputHal.java @@ -16,6 +16,7 @@ package com.android.server.tv; +import android.hardware.tv.input.V1_0.Constants; import android.media.tv.TvInputHardwareInfo; import android.media.tv.TvStreamConfig; import android.os.Handler; @@ -41,9 +42,10 @@ final class TvInputHal implements Handler.Callback { public final static int ERROR_STALE_CONFIG = -2; public final static int ERROR_UNKNOWN = -3; - public static final int EVENT_DEVICE_AVAILABLE = 1; - public static final int EVENT_DEVICE_UNAVAILABLE = 2; - public static final int EVENT_STREAM_CONFIGURATION_CHANGED = 3; + public static final int EVENT_DEVICE_AVAILABLE = Constants.EVENT_DEVICE_AVAILABLE; + public static final int EVENT_DEVICE_UNAVAILABLE = Constants.EVENT_DEVICE_UNAVAILABLE; + public static final int EVENT_STREAM_CONFIGURATION_CHANGED = + Constants.EVENT_STREAM_CONFIGURATIONS_CHANGED; public static final int EVENT_FIRST_FRAME_CAPTURED = 4; public interface Callback { diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index 72ae90d71a9e..1decf4e19e9f 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.StackId; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; @@ -1204,12 +1205,23 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree * in anyway. */ @Override - int getOrientation() { + int getOrientation(int candidate) { + if (!fillsParent()) { + // Can't specify orientation if app doesn't fill parent. + return SCREEN_ORIENTATION_UNSET; + } + + if (candidate == SCREEN_ORIENTATION_BEHIND) { + // Allow app to specify orientation regardless of its visibility state if the current + // candidate want us to use orientation behind. I.e. the visible app on-top of this one + // wants us to use the orientation of the app behind it. + return mOrientation; + } + // The {@link AppWindowToken} should only specify an orientation when it is not closing or // going to the bottom. Allowing closing {@link AppWindowToken} to participate can lead to // an Activity in another task being started in the wrong orientation during the transition. - if (fillsParent() - && !(sendingToBottom || mService.mClosingApps.contains(this)) + if (!(sendingToBottom || mService.mClosingApps.contains(this)) && (isVisible() || mService.mOpeningApps.contains(this))) { return mOrientation; } diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java index 012480e0410d..3ce61f027b4c 100644 --- a/services/core/java/com/android/server/wm/PinnedStackController.java +++ b/services/core/java/com/android/server/wm/PinnedStackController.java @@ -195,7 +195,8 @@ class PinnedStackController { * @return whether the given {@param aspectRatio} is valid. */ public boolean isValidPictureInPictureAspectRatio(float aspectRatio) { - return mMinAspectRatio <= aspectRatio && aspectRatio <= mMaxAspectRatio; + return Float.compare(mMinAspectRatio, aspectRatio) <= 0 && + Float.compare(aspectRatio, mMaxAspectRatio) <= 0; } /** diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 68d0f2496d5a..b0e3e3221415 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -152,8 +152,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> { } WindowState computeFocusedWindow() { - final int count = mChildren.size(); - for (int i = 0; i < count; i++) { + for (int i = mChildren.size() - 1; i >= 0; i--) { final DisplayContent dc = mChildren.get(i); final WindowState win = dc.findFocusedWindow(); if (win != null) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 2a02359cc529..84ba139ee5a9 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -516,14 +516,22 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon mOrientation = orientation; } + int getOrientation() { + return getOrientation(mOrientation); + } + /** * Returns the specified orientation for this window container or one of its children is there * is one set, or {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSET} if no * specification is set. * NOTE: {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} is a * specification... + * + * @param candidate The current orientation candidate that will be returned if we don't find a + * better match. + * @return The orientation as specified by this branch or the window hierarchy. */ - int getOrientation() { + int getOrientation(int candidate) { if (!fillsParent()) { // Ignore containers that don't completely fill their parents. return SCREEN_ORIENTATION_UNSET; @@ -537,12 +545,14 @@ class WindowContainer<E extends WindowContainer> implements Comparable<WindowCon && mOrientation != SCREEN_ORIENTATION_UNSPECIFIED) { return mOrientation; } - int candidate = mOrientation; for (int i = mChildren.size() - 1; i >= 0; --i) { final WindowContainer wc = mChildren.get(i); - final int orientation = wc.getOrientation(); + // TODO: Maybe mOrientation should default to SCREEN_ORIENTATION_UNSET vs. + // SCREEN_ORIENTATION_UNSPECIFIED? + final int orientation = wc.getOrientation(candidate == SCREEN_ORIENTATION_BEHIND + ? SCREEN_ORIENTATION_BEHIND : SCREEN_ORIENTATION_UNSET); if (orientation == SCREEN_ORIENTATION_BEHIND) { // container wants us to use the orientation of the container behind it. See if we // can find one. Else return SCREEN_ORIENTATION_BEHIND so the caller can choose to diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp index b4333508a84a..78c0fa7d334a 100644 --- a/services/core/jni/com_android_server_tv_TvInputHal.cpp +++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp @@ -577,7 +577,6 @@ JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { } Return<void> JTvInputHal::TvInputCallback::notify(const TvInputEvent& event) { - // TODO(b/32200867): Ensure the event type values are in sync with the framework code. mHal->mLooper->sendMessage(new NotifyHandler(mHal, event), static_cast<int>(event.type)); return Void(); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index f0732dd56f7a..da49eb37ebec 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -317,7 +317,7 @@ public final class SystemServer { // In case the runtime switched since last boot (such as when // the old runtime was removed in an OTA), set the system - // property so that it is in sync. We can't do this in + // property so that it is in sync. We can | xq oqi't do this in // libnativehelper's JniInvocation::Init code where we already // had to fallback to a different runtime because it is // running as root and we need to be the system user to set diff --git a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java index 9356dacc29f8..7790698d02e9 100644 --- a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java +++ b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java @@ -20,6 +20,7 @@ package com.android.server.print; import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; +import android.annotation.CheckResult; import android.annotation.Nullable; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; @@ -36,8 +37,11 @@ import android.content.pm.PackageManager; import android.net.NetworkPolicyManager; import android.os.Binder; import android.os.Environment; +import android.os.Handler; import android.os.IBinder; import android.os.IDeviceIdleController; +import android.os.IInterface; +import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -68,11 +72,13 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Function; //TODO move to own package! -//TODO un/linkToDeath & onBinderDied - unbind //TODO onStop schedule unbind in 5 seconds -//TODO Prune association on app uninstall +//TODO make sure APIs are only callable from currently focused app +//TODO schedule stopScan on activity destroy(except if configuration change) +//TODO on associate called again after configuration change -> replace old callback with new +//TODO avoid leaking calling activity in IFindDeviceCallback (see PrintManager#print for example) /** @hide */ -public class CompanionDeviceManagerService extends SystemService { +public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient { private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative( CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, @@ -90,6 +96,8 @@ public class CompanionDeviceManagerService extends SystemService { private final CompanionDeviceManagerImpl mImpl; private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>(); private IDeviceIdleController mIdleController; + private IFindDeviceCallback mFindDeviceCallback; + private ServiceConnection mServiceConnection; public CompanionDeviceManagerService(Context context) { super(context); @@ -125,7 +133,51 @@ public class CompanionDeviceManagerService extends SystemService { publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl); } + @Override + public void binderDied() { + Handler.getMain().post(this::handleBinderDied); + } + + private void handleBinderDied() { + mServiceConnection = unbind(mServiceConnection); + mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0); + } + + /** + * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); } + */ + @Nullable + @CheckResult + private static <T extends IInterface> T unlinkToDeath(T iinterface, + IBinder.DeathRecipient deathRecipient, int flags) { + if (iinterface != null) { + iinterface.asBinder().unlinkToDeath(deathRecipient, flags); + } + return null; + } + + @Nullable + @CheckResult + private ServiceConnection unbind(@Nullable ServiceConnection conn) { + if (conn != null) { + getContext().unbindService(conn); + } + return null; + } + class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (Throwable e) { + Slog.e(LOG_TAG, "Error during IPC", e); + throw ExceptionUtils.propagate(e, RemoteException.class); + } + } + @Override public void associate( AssociationRequest request, @@ -135,14 +187,14 @@ public class CompanionDeviceManagerService extends SystemService { Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback + ", callingPackage = " + callingPackage + ")"); } - checkNotNull(request); - checkNotNull(callback); + checkNotNull(request, "Request cannot be null"); + checkNotNull(callback, "Callback cannot be null"); final long callingIdentity = Binder.clearCallingIdentity(); try { //TODO bindServiceAsUser getContext().bindService( new Intent().setComponent(SERVICE_TO_BIND_TO), - getServiceConnection(request, callback, callingPackage), + createServiceConnection(request, callback, callingPackage), Context.BIND_AUTO_CREATE); } finally { Binder.restoreCallingIdentity(callingIdentity); @@ -168,11 +220,11 @@ public class CompanionDeviceManagerService extends SystemService { return UserHandle.getUserId(Binder.getCallingUid()); } - private ServiceConnection getServiceConnection( + private ServiceConnection createServiceConnection( final AssociationRequest request, final IFindDeviceCallback findDeviceCallback, final String callingPackage) { - return new ServiceConnection() { + mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { if (DEBUG) { @@ -180,6 +232,14 @@ public class CompanionDeviceManagerService extends SystemService { "onServiceConnected(name = " + name + ", service = " + service + ")"); } + mFindDeviceCallback = findDeviceCallback; + try { + mFindDeviceCallback.asBinder().linkToDeath( + CompanionDeviceManagerService.this, 0); + } catch (RemoteException e) { + handleBinderDied(); + return; + } try { ICompanionDeviceDiscoveryService.Stub .asInterface(service) @@ -198,6 +258,7 @@ public class CompanionDeviceManagerService extends SystemService { if (DEBUG) Slog.i(LOG_TAG, "onServiceDisconnected(name = " + name + ")"); } }; + return mServiceConnection; } private ICompanionDeviceDiscoveryServiceCallback.Stub getServiceCallback() { diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index 2a8f4a34f158..bb4507d8386a 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -52,6 +52,7 @@ LOCAL_JNI_SHARED_LIBRARIES := \ LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk LOCAL_JACK_FLAGS := --multi-dex native +LOCAL_DX_FLAGS := --multi-dex LOCAL_STATIC_JAVA_LIBRARIES += ub-uiautomator diff --git a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java index a9c69f6bd3d2..e0ac393290ac 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java @@ -128,9 +128,11 @@ public class NetworkScoreServiceTest { private static final ScoredNetwork SCORED_NETWORK_2 = new ScoredNetwork(new NetworkKey(new WifiKey(quote(SSID_2), "00:00:00:00:00:00")), null /* rssiCurve*/); + private static final String NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID = + "networkAvailableNotificationChannelId"; private static final NetworkScorerAppData NEW_SCORER = new NetworkScorerAppData( 1, RECOMMENDATION_SERVICE_COMP, RECOMMENDATION_SERVICE_LABEL, - USE_WIFI_ENABLE_ACTIVITY_COMP); + USE_WIFI_ENABLE_ACTIVITY_COMP, NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID); @Mock private NetworkScorerAppManager mNetworkScorerAppManager; @Mock private Context mContext; @@ -965,7 +967,7 @@ public class NetworkScoreServiceTest { .thenReturn(PackageManager.PERMISSION_GRANTED); NetworkScorerAppData expectedAppData = new NetworkScorerAppData(Binder.getCallingUid(), RECOMMENDATION_SERVICE_COMP, RECOMMENDATION_SERVICE_LABEL, - USE_WIFI_ENABLE_ACTIVITY_COMP); + USE_WIFI_ENABLE_ACTIVITY_COMP, NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID); bindToScorer(expectedAppData); assertEquals(expectedAppData, mNetworkScoreService.getActiveScorer()); } @@ -1007,7 +1009,7 @@ public class NetworkScoreServiceTest { final int callingUid = callerIsScorer ? Binder.getCallingUid() : Binder.getCallingUid() + 1; NetworkScorerAppData appData = new NetworkScorerAppData(callingUid, RECOMMENDATION_SERVICE_COMP, RECOMMENDATION_SERVICE_LABEL, - USE_WIFI_ENABLE_ACTIVITY_COMP); + USE_WIFI_ENABLE_ACTIVITY_COMP, NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID); bindToScorer(appData); } diff --git a/services/tests/servicestests/src/com/android/server/NetworkScorerAppManagerTest.java b/services/tests/servicestests/src/com/android/server/NetworkScorerAppManagerTest.java index e8663a2f2a22..9197ccf972c4 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkScorerAppManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/NetworkScorerAppManagerTest.java @@ -62,6 +62,8 @@ import java.util.List; public class NetworkScorerAppManagerTest { private static String MOCK_SERVICE_LABEL = "Mock Service"; private static String MOCK_OVERRIDEN_SERVICE_LABEL = "Mock Service Label Override"; + private static String MOCK_NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID = + "Mock Network Available Notification Channel Id"; @Mock private Context mMockContext; @Mock private PackageManager mMockPm; @@ -168,13 +170,30 @@ public class NetworkScorerAppManagerTest { mockScoreNetworksGranted(recoComponent.getPackageName()); mockRecommendationServiceAvailable(recoComponent, 924 /* packageUid */, enableUseOpenWifiComponent.getPackageName()); - mockEnableUseOpenWifiActivity(enableUseOpenWifiComponent); final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer(); assertNotNull(activeScorer); assertEquals(recoComponent, activeScorer.getRecommendationServiceComponent()); assertEquals(924, activeScorer.packageUid); assertEquals(enableUseOpenWifiComponent, activeScorer.getEnableUseOpenWifiActivity()); + assertNull(activeScorer.getNetworkAvailableNotificationChannelId()); + } + + @Test + public void testGetActiveScorer_providerAvailable_networkAvailableNotificationChannelIdSet() { + final ComponentName recoComponent = new ComponentName("package1", "class1"); + setNetworkRecoPackageSetting(recoComponent.getPackageName()); + mockScoreNetworksGranted(recoComponent.getPackageName()); + mockRecommendationServiceAvailable(recoComponent, 924 /* packageUid */, + null /* enableUseOpenWifiActivityPackage */, false /* serviceLabelOverride */, + true /* setNotificationChannelId */); + + final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer(); + assertNotNull(activeScorer); + assertEquals(recoComponent, activeScorer.getRecommendationServiceComponent()); + assertEquals(924, activeScorer.packageUid); + assertEquals(MOCK_NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID, + activeScorer.getNetworkAvailableNotificationChannelId()); } @Test @@ -368,6 +387,13 @@ public class NetworkScorerAppManagerTest { private void mockRecommendationServiceAvailable(final ComponentName compName, int packageUid, String enableUseOpenWifiActivityPackage, boolean serviceLabelOverride) { + mockRecommendationServiceAvailable(compName, packageUid, enableUseOpenWifiActivityPackage, + serviceLabelOverride, false); + } + + private void mockRecommendationServiceAvailable(final ComponentName compName, int packageUid, + String enableUseOpenWifiActivityPackage, boolean serviceLabelOverride, + boolean setNotificationChannel) { final ResolveInfo serviceInfo = new ResolveInfo(); serviceInfo.serviceInfo = new ServiceInfo(); serviceInfo.serviceInfo.name = compName.getClassName(); @@ -390,6 +416,14 @@ public class NetworkScorerAppManagerTest { } else { serviceInfo.serviceInfo.nonLocalizedLabel = MOCK_SERVICE_LABEL; } + if (setNotificationChannel) { + if (serviceInfo.serviceInfo.metaData == null) { + serviceInfo.serviceInfo.metaData = new Bundle(); + } + serviceInfo.serviceInfo.metaData.putString( + NetworkScoreManager.NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA, + MOCK_NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID); + } final int flags = PackageManager.GET_META_DATA; when(mMockPm.resolveService( diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java index 374aee10da45..00f6273d14bf 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java @@ -94,7 +94,7 @@ import java.util.concurrent.atomic.AtomicLong; * <p>Run with:<pre> * mmma -j40 frameworks/base/services/tests/servicestests * adb install -r ${OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk - * adb shell am instrument -w -e class package com.android.server.accounts \ + * adb shell am instrument -w -e package com.android.server.accounts \ * com.android.frameworks.servicestests\ * /android.support.test.runner.AndroidJUnitRunner * </pre> diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountsDbTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountsDbTest.java index 2cb8af4e97e7..aa374073e4a5 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountsDbTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountsDbTest.java @@ -385,6 +385,42 @@ public class AccountsDbTest { } @Test + public void testFindAllVisibilityValues() { + long accId = 10; + long accId2 = 11; + String packageName1 = "com.example.one"; + String packageName2 = "com.example.two"; + Account account = new Account("name", "example.com"); + Account account2 = new Account("name2", "example2.com"); + assertNull(mAccountsDb.findAccountVisibility(account, packageName1)); + + mAccountsDb.insertDeAccount(account, accId); + assertNull(mAccountsDb.findAccountVisibility(account, packageName1)); + assertNull(mAccountsDb.findAccountVisibility(accId, packageName1)); + mAccountsDb.insertDeAccount(account2, accId2); + + mAccountsDb.setAccountVisibility(accId, packageName1, 1); + mAccountsDb.setAccountVisibility(accId, packageName2, 2); + mAccountsDb.setAccountVisibility(accId2, packageName1, 1); + + Map<Account, Map<String, Integer>> vis = mAccountsDb.findAllVisibilityValues(); + assertEquals(vis.size(), 2); + Map<String, Integer> accnt1Visibility = vis.get(account); + assertEquals(accnt1Visibility.size(), 2); + assertEquals(accnt1Visibility.get(packageName1), Integer.valueOf(1)); + assertEquals(accnt1Visibility.get(packageName2), Integer.valueOf(2)); + Map<String, Integer> accnt2Visibility = vis.get(account2); + assertEquals(accnt2Visibility.size(), 1); + assertEquals(accnt2Visibility.get(packageName1), Integer.valueOf(1)); + + mAccountsDb.setAccountVisibility(accId2, packageName2, 3); + vis = mAccountsDb.findAllVisibilityValues(); + accnt2Visibility = vis.get(account2); + assertEquals(accnt2Visibility.size(), 2); + assertEquals(accnt2Visibility.get(packageName2), Integer.valueOf(3)); + } + + @Test public void testVisibilityCleanupTrigger() { long accId = 10; String packageName1 = "com.example.one"; diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java index b12da34f4375..1e038dfaf1af 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -51,7 +51,9 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.IUidObserver; +import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -116,7 +118,9 @@ public class ActivityManagerServiceTest { UidRecord.CHANGE_ACTIVE }; + @Mock private Context mContext; @Mock private AppOpsService mAppOpsService; + @Mock private PackageManager mPackageManager; private TestInjector mInjector; private ActivityManagerService mAms; @@ -133,6 +137,8 @@ public class ActivityManagerServiceTest { mInjector = new TestInjector(); mAms = new ActivityManagerService(mInjector); mAms.mWaitForNetworkTimeoutMs = 100; + + when(mContext.getPackageManager()).thenReturn(mPackageManager); } @After @@ -601,10 +607,12 @@ public class ActivityManagerServiceTest { uidRecord.pendingChange = changeItem; uidRecord.curProcStateSeq = TEST_PROC_STATE_SEQ2; verifyLastProcStateSeqUpdated(uidRecord, -1, TEST_PROC_STATE_SEQ2); + } + @Test + public void testEnqueueUidChangeLocked_nullUidRecord() { // Use "null" uidRecord to make sure there is no crash. - // TODO: currently it crashes, uncomment after fixing it. - // mAms.enqueueUidChangeLocked(null, TEST_UID, UidRecord.CHANGE_ACTIVE); + mAms.enqueueUidChangeLocked(null, TEST_UID, UidRecord.CHANGE_ACTIVE); } private void verifyLastProcStateSeqUpdated(UidRecord uidRecord, int uid, long curProcstateSeq) { @@ -770,6 +778,11 @@ public class ActivityManagerServiceTest { private boolean mRestricted = true; @Override + public Context getContext() { + return mContext; + } + + @Override public AppOpsService getAppOpsService(File file, Handler handler) { return mAppOpsService; } diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java index b5826f0c1106..2003b91bcfad 100644 --- a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenTests.java @@ -26,8 +26,10 @@ import android.support.test.runner.AndroidJUnit4; import android.view.Surface; import android.view.WindowManager; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; @@ -160,4 +162,22 @@ public class AppWindowTokenTests extends WindowTestsBase { sWm.mRoot.mOrientationChangeComplete = true; sWm.mRoot.performSurfacePlacement(false /* recoveringMemory */); } + + @Test + public void testGetOrientation() throws Exception { + final TestAppWindowToken token = new TestAppWindowToken(sDisplayContent); + token.setOrientation(SCREEN_ORIENTATION_LANDSCAPE); + + token.setFillsParent(false); + // Can not specify orientation if app doesn't fill parent. + assertEquals(SCREEN_ORIENTATION_UNSET, token.getOrientation()); + + token.setFillsParent(true); + token.hidden = true; + token.sendingToBottom = true; + // Can not specify orientation if app isn't visible even though it fills parent. + assertEquals(SCREEN_ORIENTATION_UNSET, token.getOrientation()); + // Can specify orientation if the current orientation candidate is orientation behind. + assertEquals(SCREEN_ORIENTATION_LANDSCAPE, token.getOrientation(SCREEN_ORIENTATION_BEHIND)); + } } diff --git a/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java index 3868242616b2..dd94a2123d58 100644 --- a/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/DisplayContentTests.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; +import static com.android.server.wm.WindowContainer.POSITION_TOP; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -233,6 +234,26 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(currentOverrideConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */); } + @Test + public void testFocusedWindowMultipleDisplays() throws Exception { + // Create a focusable window and check that focus is calcualted correctly + final WindowState window1 = + createWindow(null, TYPE_BASE_APPLICATION, sDisplayContent, "window1"); + assertEquals(window1, sWm.mRoot.computeFocusedWindow()); + + // Check that a new display doesn't affect focus + final DisplayContent dc = createNewDisplay(); + assertEquals(window1, sWm.mRoot.computeFocusedWindow()); + + // Add a window to the second display, and it should be focused + final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, dc, "window2"); + assertEquals(window2, sWm.mRoot.computeFocusedWindow()); + + // Move the first window to the to including parents, and make sure focus is updated + window1.getParent().positionChildAt(POSITION_TOP, window1, true); + assertEquals(window1, sWm.mRoot.computeFocusedWindow()); + } + private void assertForAllWindowsOrder(List<WindowState> expectedWindows) { final LinkedList<WindowState> actualWindows = new LinkedList(); diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java index 3ce3df1557dd..9dbd8a617eec 100644 --- a/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/TaskStackTests.java @@ -76,9 +76,9 @@ public class TaskStackTests extends WindowTestsBase { task2.addChild(appWindowToken2, 0); appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT); - assertEquals(stack.getOrientation(), SCREEN_ORIENTATION_PORTRAIT); + assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation()); sWm.mClosingApps.add(appWindowToken2); - assertEquals(stack.getOrientation(), SCREEN_ORIENTATION_LANDSCAPE); + assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation()); } @Test @@ -94,9 +94,9 @@ public class TaskStackTests extends WindowTestsBase { task2.addChild(appWindowToken2, 0); appWindowToken2.setOrientation(SCREEN_ORIENTATION_PORTRAIT); - assertEquals(stack.getOrientation(), SCREEN_ORIENTATION_PORTRAIT); + assertEquals(SCREEN_ORIENTATION_PORTRAIT, stack.getOrientation()); task2.setSendingToBottom(true); - assertEquals(stack.getOrientation(), SCREEN_ORIENTATION_LANDSCAPE); + assertEquals(SCREEN_ORIENTATION_LANDSCAPE, stack.getOrientation()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java index 80b2e7d9bc3c..a7d594cbd256 100644 --- a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java @@ -422,7 +422,7 @@ public class WindowContainerTests extends WindowTestsBase { final TestWindowContainer child1 = root.addChildWindow(builder); child1.setFillsParent(true); - assertTrue(root.getOrientation() == expectedOrientation); + assertEquals(expectedOrientation, root.getOrientation()); } @Test @@ -805,8 +805,13 @@ public class WindowContainerTests extends WindowTestsBase { } @Override + int getOrientation(int candidate) { + return mOrientation != null ? mOrientation : super.getOrientation(candidate); + } + + @Override int getOrientation() { - return mOrientation != null ? mOrientation : super.getOrientation(); + return getOrientation(super.mOrientation); } @Override diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java index 48799d23c06e..a9d930f5c893 100644 --- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static android.view.View.VISIBLE; + import android.app.ActivityManager.TaskDescription; import android.content.res.Configuration; import android.graphics.Rect; @@ -233,7 +235,7 @@ class WindowTestsBase { attrs.setTitle(name); final WindowState w = new WindowState(sWm, sMockSession, sIWindow, token, parent, OP_NONE, - 0, attrs, 0, 0, ownerCanAddInternalSystemWindow); + 0, attrs, VISIBLE, 0, ownerCanAddInternalSystemWindow); // TODO: Probably better to make this call in the WindowState ctor to avoid errors with // adding it to the token... token.addWindow(w); diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java index 1b28db72b9b3..e55e073cf063 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -20,6 +20,7 @@ import android.app.usage.TimeSparseArray; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.os.Build; +import android.os.SystemProperties; import android.util.AtomicFile; import android.util.Slog; import android.util.TimeUtils; @@ -56,6 +57,9 @@ class UsageStatsDatabase { private static final boolean DEBUG = UsageStatsService.DEBUG; private static final String BAK_SUFFIX = ".bak"; private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX; + private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention"; + private static final int SELECTION_LOG_RETENTION_LEN = + SystemProperties.getInt(RETENTION_LEN_KEY, 14); private final Object mLock = new Object(); private final File[] mIntervalDirs; @@ -504,7 +508,7 @@ class UsageStatsDatabase { mCal.getTimeInMillis()); mCal.setTimeInMillis(currentTimeMillis); - mCal.addDays(-14); + mCal.addDays(-SELECTION_LOG_RETENTION_LEN); for (int i = 0; i < mIntervalDirs.length; ++i) { pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis()); } diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index c79090203c0b..92233b1fed39 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -125,8 +125,9 @@ public final class Call { public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts"; /** - * Extra key used to indicate the time (in millis) when the last outgoing emergency call was - * made. This is used to identify potential emergency callbacks. + * Extra key used to indicate the time (in milliseconds since midnight, January 1, 1970 UTC) + * when the last outgoing emergency call was made. This is used to identify potential emergency + * callbacks. */ public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; |