Merge "Pass in correct user to AccountManagerBackupHelper"
diff --git a/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS b/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
index 0ec8253..270cb18 100644
--- a/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
+++ b/core/java/android/app/admin/EnterprisePlatformSecurity_OWNERS
@@ -1,5 +1,4 @@
rubinxu@google.com
-acjohnston@google.com
pgrafov@google.com
ayushsha@google.com
alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/app/admin/EnterprisePlatform_OWNERS b/core/java/android/app/admin/EnterprisePlatform_OWNERS
index fb00fe5..6ce25cc 100644
--- a/core/java/android/app/admin/EnterprisePlatform_OWNERS
+++ b/core/java/android/app/admin/EnterprisePlatform_OWNERS
@@ -1,2 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
file:WorkDeviceExperience_OWNERS
+file:Provisioning_OWNERS
+file:WorkProfile_OWNERS
file:EnterprisePlatformSecurity_OWNERS
\ No newline at end of file
diff --git a/core/java/android/app/admin/Provisioning_OWNERS b/core/java/android/app/admin/Provisioning_OWNERS
new file mode 100644
index 0000000..c59a9dc
--- /dev/null
+++ b/core/java/android/app/admin/Provisioning_OWNERS
@@ -0,0 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
+petuska@google.com
+nupursn@google.com
+shreyacsingh@google.com
+alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/app/admin/WorkDeviceExperience_OWNERS b/core/java/android/app/admin/WorkDeviceExperience_OWNERS
index 82afddd..2033343 100644
--- a/core/java/android/app/admin/WorkDeviceExperience_OWNERS
+++ b/core/java/android/app/admin/WorkDeviceExperience_OWNERS
@@ -1,5 +1,7 @@
+# Assign bugs to android-enterprise-triage@google.com
work-device-experience+reviews@google.com
-scottjonathan@google.com
-kholoudm@google.com
-eliselliott@google.com
+scottjonathan@google.com #{LAST_RESORT_SUGGESTION}
+eliselliott@google.com #{LAST_RESORT_SUGGESTION}
+kholoudm@google.com #{LAST_RESORT_SUGGESTION}
+acjohnston@google.com #{LAST_RESORT_SUGGESTION}
alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/core/java/android/app/admin/WorkProfile_OWNERS b/core/java/android/app/admin/WorkProfile_OWNERS
new file mode 100644
index 0000000..260b672
--- /dev/null
+++ b/core/java/android/app/admin/WorkProfile_OWNERS
@@ -0,0 +1,5 @@
+# Assign bugs to android-enterprise-triage@google.com
+liahav@google.com
+olit@google.com
+scottjonathan@google.com #{LAST_RESORT_SUGGESTION}
+alexkershaw@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java
index 58f7759..0311da4 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -23,6 +23,7 @@
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
/**
* Describes a keyboard layout.
@@ -30,6 +31,37 @@
* @hide
*/
public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> {
+
+ /** Undefined keyboard layout */
+ public static final String LAYOUT_TYPE_UNDEFINED = "undefined";
+
+ /** Qwerty-based keyboard layout */
+ public static final String LAYOUT_TYPE_QWERTY = "qwerty";
+
+ /** Qwertz-based keyboard layout */
+ public static final String LAYOUT_TYPE_QWERTZ = "qwertz";
+
+ /** Azerty-based keyboard layout */
+ public static final String LAYOUT_TYPE_AZERTY = "azerty";
+
+ /** Dvorak keyboard layout */
+ public static final String LAYOUT_TYPE_DVORAK = "dvorak";
+
+ /** Colemak keyboard layout */
+ public static final String LAYOUT_TYPE_COLEMAK = "colemak";
+
+ /** Workman keyboard layout */
+ public static final String LAYOUT_TYPE_WORKMAN = "workman";
+
+ /** Turkish-F keyboard layout */
+ public static final String LAYOUT_TYPE_TURKISH_F = "turkish_f";
+
+ /** Turkish-Q keyboard layout */
+ public static final String LAYOUT_TYPE_TURKISH_Q = "turkish_q";
+
+ /** Keyboard layout that has been enhanced with a large number of extra characters */
+ public static final String LAYOUT_TYPE_EXTENDED = "extended";
+
private final String mDescriptor;
private final String mLabel;
private final String mCollection;
@@ -42,31 +74,24 @@
/** Currently supported Layout types in the KCM files */
private enum LayoutType {
- UNDEFINED(0, "undefined"),
- QWERTY(1, "qwerty"),
- QWERTZ(2, "qwertz"),
- AZERTY(3, "azerty"),
- DVORAK(4, "dvorak"),
- COLEMAK(5, "colemak"),
- WORKMAN(6, "workman"),
- TURKISH_F(7, "turkish_f"),
- TURKISH_Q(8, "turkish_q"),
- EXTENDED(9, "extended");
+ UNDEFINED(0, LAYOUT_TYPE_UNDEFINED),
+ QWERTY(1, LAYOUT_TYPE_QWERTY),
+ QWERTZ(2, LAYOUT_TYPE_QWERTZ),
+ AZERTY(3, LAYOUT_TYPE_AZERTY),
+ DVORAK(4, LAYOUT_TYPE_DVORAK),
+ COLEMAK(5, LAYOUT_TYPE_COLEMAK),
+ WORKMAN(6, LAYOUT_TYPE_WORKMAN),
+ TURKISH_F(7, LAYOUT_TYPE_TURKISH_F),
+ TURKISH_Q(8, LAYOUT_TYPE_TURKISH_Q),
+ EXTENDED(9, LAYOUT_TYPE_EXTENDED);
private final int mValue;
private final String mName;
private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
static {
- VALUE_TO_ENUM_MAP.put(UNDEFINED.mValue, UNDEFINED);
- VALUE_TO_ENUM_MAP.put(QWERTY.mValue, QWERTY);
- VALUE_TO_ENUM_MAP.put(QWERTZ.mValue, QWERTZ);
- VALUE_TO_ENUM_MAP.put(AZERTY.mValue, AZERTY);
- VALUE_TO_ENUM_MAP.put(DVORAK.mValue, DVORAK);
- VALUE_TO_ENUM_MAP.put(COLEMAK.mValue, COLEMAK);
- VALUE_TO_ENUM_MAP.put(WORKMAN.mValue, WORKMAN);
- VALUE_TO_ENUM_MAP.put(TURKISH_F.mValue, TURKISH_F);
- VALUE_TO_ENUM_MAP.put(TURKISH_Q.mValue, TURKISH_Q);
- VALUE_TO_ENUM_MAP.put(EXTENDED.mValue, EXTENDED);
+ for (LayoutType type : LayoutType.values()) {
+ VALUE_TO_ENUM_MAP.put(type.mValue, type);
+ }
}
private static LayoutType of(int value) {
@@ -207,6 +232,9 @@
// keyboards to be listed before lower priority keyboards.
int result = Integer.compare(another.mPriority, mPriority);
if (result == 0) {
+ result = Integer.compare(mLayoutType.mValue, another.mLayoutType.mValue);
+ }
+ if (result == 0) {
result = mLabel.compareToIgnoreCase(another.mLabel);
}
if (result == 0) {
@@ -226,4 +254,21 @@
+ ", vendorId: " + mVendorId
+ ", productId: " + mProductId;
}
+
+ /**
+ * Check if the provided layout type is supported/valid.
+ *
+ * @param layoutName name of layout type
+ * @return {@code true} if the provided layout type is supported/valid.
+ */
+ public static boolean isLayoutTypeValid(@NonNull String layoutName) {
+ Objects.requireNonNull(layoutName, "Provided layout name should not be null");
+ for (LayoutType layoutType : LayoutType.values()) {
+ if (layoutName.equals(layoutType.getName())) {
+ return true;
+ }
+ }
+ // Layout doesn't match any supported layout types
+ return false;
+ }
}
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 34aa7ef..547d406 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -1142,6 +1142,16 @@
@BatteryConsumer.ProcessState int processState);
+
+ /**
+ * Returns the battery consumption (in microcoulombs) of UID's camera usage, derived from
+ * on-device power measurement data.
+ * Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable.
+ *
+ * {@hide}
+ */
+ public abstract long getCameraEnergyConsumptionUC();
+
/**
* Returns the battery consumption (in microcoulombs) used by this uid for each
* {@link android.hardware.power.stats.EnergyConsumer.ordinal} of (custom) energy consumer
@@ -2921,6 +2931,15 @@
public abstract long getWifiEnergyConsumptionUC();
/**
+ * Returns the battery consumption (in microcoulombs) of camera, derived from on
+ * device power measurement data.
+ * Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable.
+ *
+ * {@hide}
+ */
+ public abstract long getCameraEnergyConsumptionUC();
+
+ /**
* Returns the battery consumption (in microcoulombs) that each
* {@link android.hardware.power.stats.EnergyConsumer.ordinal} of (custom) energy consumer
* type {@link android.hardware.power.stats.EnergyConsumerType#OTHER}) consumed.
diff --git a/core/java/com/android/internal/power/EnergyConsumerStats.java b/core/java/com/android/internal/power/EnergyConsumerStats.java
index 325df57..8cf17cda 100644
--- a/core/java/com/android/internal/power/EnergyConsumerStats.java
+++ b/core/java/com/android/internal/power/EnergyConsumerStats.java
@@ -58,7 +58,8 @@
public static final int POWER_BUCKET_BLUETOOTH = 5;
public static final int POWER_BUCKET_GNSS = 6;
public static final int POWER_BUCKET_MOBILE_RADIO = 7;
- public static final int NUMBER_STANDARD_POWER_BUCKETS = 8; // Buckets above this are custom.
+ public static final int POWER_BUCKET_CAMERA = 8;
+ public static final int NUMBER_STANDARD_POWER_BUCKETS = 9; // Buckets above this are custom.
@IntDef(prefix = {"POWER_BUCKET_"}, value = {
POWER_BUCKET_UNKNOWN,
@@ -70,6 +71,7 @@
POWER_BUCKET_BLUETOOTH,
POWER_BUCKET_GNSS,
POWER_BUCKET_MOBILE_RADIO,
+ POWER_BUCKET_CAMERA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface StandardPowerBucket {
diff --git a/media/java/android/media/tv/TableRequest.java b/media/java/android/media/tv/TableRequest.java
index a1a6b51..a9ea6d3 100644
--- a/media/java/android/media/tv/TableRequest.java
+++ b/media/java/android/media/tv/TableRequest.java
@@ -33,11 +33,54 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TABLE_NAME_PAT, TABLE_NAME_PMT})
+ @IntDef({TABLE_NAME_PAT, TABLE_NAME_PMT, TABLE_NAME_CAT})
public @interface TableName {}
+ /** Program Association Table */
public static final int TABLE_NAME_PAT = 0;
+ /** Program Mapping Table */
public static final int TABLE_NAME_PMT = 1;
+ /**
+ * Conditional Access Table
+ * @hide
+ */
+ public static final int TABLE_NAME_CAT = 2;
+ /**
+ * Network Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_NIT = 3;
+ /**
+ * Bouquet Association Table
+ * @hide
+ */
+ public static final int TABLE_NAME_BAT = 4;
+ /**
+ * Service Description Table
+ * @hide
+ */
+ public static final int TABLE_NAME_SDT = 5;
+ /**
+ * Event Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_EIT = 6;
+ /**
+ * Time and Date Table
+ * @hide
+ */
+ public static final int TABLE_NAME_TDT = 7;
+ /**
+ * Time Offset Table
+ * @hide
+ */
+ public static final int TABLE_NAME_TOT = 8;
+ /**
+ * Selection Information Table
+ * @hide
+ */
+ public static final int TABLE_NAME_SIT = 9;
+
public static final @NonNull Parcelable.Creator<TableRequest> CREATOR =
new Parcelable.Creator<TableRequest>() {
diff --git a/media/java/android/media/tv/TableResponse.java b/media/java/android/media/tv/TableResponse.java
index afc9bee..1c314b0 100644
--- a/media/java/android/media/tv/TableResponse.java
+++ b/media/java/android/media/tv/TableResponse.java
@@ -21,6 +21,7 @@
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SharedMemory;
/**
* A response for Table from broadcast signal.
@@ -46,6 +47,8 @@
private final Uri mTableUri;
private final int mVersion;
private final int mSize;
+ private final byte[] mTableByteArray;
+ private final SharedMemory mTableSharedMemory;
static TableResponse createFromParcelBody(Parcel in) {
return new TableResponse(in);
@@ -54,9 +57,33 @@
public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
@Nullable Uri tableUri, int version, int size) {
super(RESPONSE_TYPE, requestId, sequence, responseResult);
- mTableUri = tableUri;
mVersion = version;
mSize = size;
+ mTableUri = tableUri;
+ mTableByteArray = null;
+ mTableSharedMemory = null;
+ }
+
+ /** @hide */
+ public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
+ @NonNull byte[] tableByteArray, int version, int size) {
+ super(RESPONSE_TYPE, requestId, sequence, responseResult);
+ mVersion = version;
+ mSize = size;
+ mTableUri = null;
+ mTableByteArray = tableByteArray;
+ mTableSharedMemory = null;
+ }
+
+ /** @hide */
+ public TableResponse(int requestId, int sequence, @ResponseResult int responseResult,
+ @NonNull SharedMemory tableSharedMemory, int version, int size) {
+ super(RESPONSE_TYPE, requestId, sequence, responseResult);
+ mVersion = version;
+ mSize = size;
+ mTableUri = null;
+ mTableByteArray = null;
+ mTableSharedMemory = tableSharedMemory;
}
TableResponse(Parcel source) {
@@ -65,6 +92,14 @@
mTableUri = uriString == null ? null : Uri.parse(uriString);
mVersion = source.readInt();
mSize = source.readInt();
+ int arrayLength = source.readInt();
+ if (arrayLength >= 0) {
+ mTableByteArray = new byte[arrayLength];
+ source.readByteArray(mTableByteArray);
+ } else {
+ mTableByteArray = null;
+ }
+ mTableSharedMemory = (SharedMemory) source.readTypedObject(SharedMemory.CREATOR);
}
/**
@@ -76,6 +111,30 @@
}
/**
+ * Gets the data of the table as a byte array.
+ *
+ * @return the table data as a byte array, or {@code null} if the data is not stored as a byte
+ * array.
+ * @hide
+ */
+ @Nullable
+ public byte[] getTableByteArray() {
+ return mTableByteArray;
+ }
+
+ /**
+ * Gets the data of the table as a {@link SharedMemory} object.
+ *
+ * @return the table data as a {@link SharedMemory} object, or {@code null} if the data is not
+ * stored in shared memory.
+ * @hide
+ */
+ @Nullable
+ public SharedMemory getTableSharedMemory() {
+ return mTableSharedMemory;
+ }
+
+ /**
* Gets the version number of requested table. If it is null, value will be -1.
* <p>The consistency of version numbers between request and response depends on
* {@link BroadcastInfoRequest.RequestOption}. If the request has RequestOption value
@@ -106,5 +165,12 @@
dest.writeString(uriString);
dest.writeInt(mVersion);
dest.writeInt(mSize);
+ if (mTableByteArray != null) {
+ dest.writeInt(mTableByteArray.length);
+ dest.writeByteArray(mTableByteArray);
+ } else {
+ dest.writeInt(-1);
+ }
+ dest.writeTypedObject(mTableSharedMemory, flags);
}
}
diff --git a/media/java/android/media/tv/TimelineRequest.java b/media/java/android/media/tv/TimelineRequest.java
index 03c62f0..d04c58a 100644
--- a/media/java/android/media/tv/TimelineRequest.java
+++ b/media/java/android/media/tv/TimelineRequest.java
@@ -42,6 +42,7 @@
};
private final int mIntervalMillis;
+ private final String mSelector;
static TimelineRequest createFromParcelBody(Parcel in) {
return new TimelineRequest(in);
@@ -50,11 +51,21 @@
public TimelineRequest(int requestId, @RequestOption int option, int intervalMillis) {
super(REQUEST_TYPE, requestId, option);
mIntervalMillis = intervalMillis;
+ mSelector = null;
+ }
+
+ /** @hide */
+ public TimelineRequest(int requestId, @RequestOption int option, int intervalMillis,
+ @NonNull String selector) {
+ super(REQUEST_TYPE, requestId, option);
+ mIntervalMillis = intervalMillis;
+ mSelector = selector;
}
TimelineRequest(Parcel source) {
super(REQUEST_TYPE, source);
mIntervalMillis = source.readInt();
+ mSelector = source.readString();
}
/**
@@ -64,6 +75,18 @@
return mIntervalMillis;
}
+ /**
+ * Gets the timeline selector.
+ * <p>The selector describes the type and location of timeline signalling. For example
+ * {@code urn:dvb:css:timeline:pts} is a selector in DVB standard.
+ *
+ * @return the selector if it's set; {@code null} otherwise.
+ * @hide
+ */
+ public String getSelector() {
+ return mSelector;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -73,5 +96,6 @@
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mIntervalMillis);
+ dest.writeString(mSelector);
}
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 494571f..ad9312f 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -43,6 +43,7 @@
void onCommandRequest(in String cmdType, in Bundle parameters, int seq);
void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters, int seq);
void onSetVideoBounds(in Rect rect, int seq);
+ void onRequestCurrentVideoBounds(int seq);
void onRequestCurrentChannelUri(int seq);
void onRequestCurrentChannelLcn(int seq);
void onRequestStreamVolume(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 599922c..c0723f7 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -48,6 +48,7 @@
in IBinder sessionToken, in Uri biIAppUri, in Bundle params, int userId);
void destroyBiInteractiveApp(in IBinder sessionToken, in String biIAppId, int userId);
void setTeletextAppEnabled(in IBinder sessionToken, boolean enable, int userId);
+ void sendCurrentVideoBounds(in IBinder sessionToken, in Rect bounds, int userId);
void sendCurrentChannelUri(in IBinder sessionToken, in Uri channelUri, int userId);
void sendCurrentChannelLcn(in IBinder sessionToken, int lcn, int userId);
void sendStreamVolume(in IBinder sessionToken, float volume, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 17a70d1..9ae9ca7 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -40,6 +40,7 @@
void createBiInteractiveApp(in Uri biIAppUri, in Bundle params);
void destroyBiInteractiveApp(in String biIAppId);
void setTeletextAppEnabled(boolean enable);
+ void sendCurrentVideoBounds(in Rect bounds);
void sendCurrentChannelUri(in Uri channelUri);
void sendCurrentChannelLcn(int lcn);
void sendStreamVolume(float volume);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 0565742..d84affd 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -42,6 +42,7 @@
void onCommandRequest(in String cmdType, in Bundle parameters);
void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters);
void onSetVideoBounds(in Rect rect);
+ void onRequestCurrentVideoBounds();
void onRequestCurrentChannelUri();
void onRequestCurrentChannelLcn();
void onRequestStreamVolume();
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index b8158cd..8a23e65 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -94,6 +94,7 @@
private static final int DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED = 37;
private static final int DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED = 38;
private static final int DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED = 39;
+ private static final int DO_SEND_CURRENT_VIDEO_BOUNDS = 40;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -157,6 +158,10 @@
mSessionImpl.setTeletextAppEnabled((Boolean) msg.obj);
break;
}
+ case DO_SEND_CURRENT_VIDEO_BOUNDS: {
+ mSessionImpl.sendCurrentVideoBounds((Rect) msg.obj);
+ break;
+ }
case DO_SEND_CURRENT_CHANNEL_URI: {
mSessionImpl.sendCurrentChannelUri((Uri) msg.obj);
break;
@@ -355,6 +360,12 @@
}
@Override
+ public void sendCurrentVideoBounds(@Nullable Rect bounds) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_CURRENT_VIDEO_BOUNDS, bounds));
+ }
+
+ @Override
public void sendCurrentChannelUri(@Nullable Uri channelUri) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageO(DO_SEND_CURRENT_CHANNEL_URI, channelUri));
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 7c91844..fd3c29b 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -445,6 +445,18 @@
}
@Override
+ public void onRequestCurrentVideoBounds(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCurrentVideoBounds();
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -1084,6 +1096,18 @@
}
}
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCurrentVideoBounds(mToken, bounds, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void sendCurrentChannelUri(@Nullable Uri channelUri) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1898,6 +1922,15 @@
});
}
+ void postRequestCurrentVideoBounds() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCurrentVideoBounds(mSession);
+ }
+ });
+ }
+
void postRequestCurrentChannelUri() {
mHandler.post(new Runnable() {
@Override
@@ -2119,6 +2152,15 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#RequestCurrentVideoBounds} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+ */
+ public void onRequestCurrentVideoBounds(Session session) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#RequestCurrentChannelUri} is
* called.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 949e017..be2c16c 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.annotation.CallSuper;
+import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -139,6 +140,38 @@
* Playback command type: select the given track.
*/
public static final String PLAYBACK_COMMAND_TYPE_SELECT_TRACK = "select_track";
+
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "PLAYBACK_COMMAND_STOP_MODE_", value = {
+ PLAYBACK_COMMAND_STOP_MODE_BLANK,
+ PLAYBACK_COMMAND_STOP_MODE_FREEZE
+ })
+ public @interface PlaybackCommandStopMode {}
+
+ /**
+ * Playback command stop mode: show a blank screen.
+ * @hide
+ */
+ public static final int PLAYBACK_COMMAND_STOP_MODE_BLANK = 1;
+
+ /**
+ * Playback command stop mode: freeze the video.
+ * @hide
+ */
+ public static final int PLAYBACK_COMMAND_STOP_MODE_FREEZE = 2;
+
+ /**
+ * Playback command parameter: stop mode.
+ * <p>Type: int
+ *
+ * @see #PLAYBACK_COMMAND_TYPE_STOP
+ * @hide
+ */
+ public static final String COMMAND_PARAMETER_KEY_STOP_MODE = "command_stop_mode";
+
/**
* Playback command parameter: channel URI.
* <p>Type: android.net.Uri
@@ -498,6 +531,13 @@
}
/**
+ * Receives current video bounds.
+ * @hide
+ */
+ public void onCurrentVideoBounds(@NonNull Rect bounds) {
+ }
+
+ /**
* Receives current channel URI.
*/
public void onCurrentChannelUri(@Nullable Uri channelUri) {
@@ -983,6 +1023,30 @@
}
/**
+ * Requests the bounds of the current video.
+ * @hide
+ */
+ @CallSuper
+ public void requestCurrentVideoBounds() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCurrentVideoBounds");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCurrentVideoBounds();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCurrentVideoBounds", e);
+ }
+ }
+ });
+ }
+
+ /**
* Requests the URI of the current channel.
*/
@CallSuper
@@ -1309,6 +1373,10 @@
onSetTeletextAppEnabled(enable);
}
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ onCurrentVideoBounds(bounds);
+ }
+
void sendCurrentChannelUri(@Nullable Uri channelUri) {
onCurrentChannelUri(channelUri);
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 9211a12..af03bb8 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -514,6 +514,19 @@
}
/**
+ * Sends current video bounds to related TV interactive app.
+ * @hide
+ */
+ public void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCurrentVideoBounds");
+ }
+ if (mSession != null) {
+ mSession.sendCurrentVideoBounds(bounds);
+ }
+ }
+
+ /**
* Sends current channel URI to related TV interactive app.
*
* @param channelUri The current channel URI; {@code null} if there is no currently tuned
@@ -944,6 +957,16 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestCurrentVideoBounds()}
+ * is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @hide
+ */
+ public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#requestCurrentChannelUri()} is
* called.
*
@@ -1265,6 +1288,28 @@
}
@Override
+ public void onRequestCurrentVideoBounds(Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCurrentVideoBounds - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestCurrentVideoBounds(mIAppServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri(Session session) {
if (DEBUG) {
Log.d(TAG, "onRequestCurrentChannelUri");
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 17b6f5d..ec09acd 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -170,7 +170,7 @@
"android.hardware.ir-V1-java",
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
- "android.hardware.power.stats-V1-java",
+ "android.hardware.power.stats-V2-java",
"android.hardware.power-V4-java",
"android.hidl.manager-V1.2-java",
"icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index ea7f0bb..e86632c 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -70,6 +70,7 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
@@ -158,6 +159,11 @@
private static final AdditionalDisplayInputProperties
DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
+ // To disable Keyboard backlight control via Framework, run:
+ // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
+ private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
+ "persist.input.keyboard.backlight_control.enabled", true);
+
private final NativeInputManagerService mNative;
private final Context mContext;
@@ -305,7 +311,7 @@
private final BatteryController mBatteryController;
// Manages Keyboard backlight
- private final KeyboardBacklightController mKeyboardBacklightController;
+ private final KeyboardBacklightControllerInterface mKeyboardBacklightController;
// Manages Keyboard modifier keys remapping
private final KeyRemapper mKeyRemapper;
@@ -422,8 +428,10 @@
mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
injector.getLooper());
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
- mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative,
- mDataStore, injector.getLooper());
+ mKeyboardBacklightController =
+ KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext,
+ mNative, mDataStore, injector.getLooper())
+ : new KeyboardBacklightControllerInterface() {};
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
@@ -3346,10 +3354,7 @@
public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
- if (DEBUG) {
- Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
- + " subtypeHandle=" + subtypeHandle);
- }
+ mKeyboardLayoutManager.onInputMethodSubtypeChanged(userId, subtypeHandle, subtype);
}
@Override
@@ -3478,4 +3483,13 @@
applyAdditionalDisplayInputPropertiesLocked(properties);
}
}
+
+ interface KeyboardBacklightControllerInterface {
+ default void incrementKeyboardBacklight(int deviceId) {}
+ default void decrementKeyboardBacklight(int deviceId) {}
+ default void registerKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
+ default void unregisterKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
+ default void systemRunning() {}
+ default void dump(PrintWriter pw) {}
+ }
}
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 77b0d4f..653a821 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -47,7 +47,8 @@
* A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
* backlight for supported keyboards.
*/
-final class KeyboardBacklightController implements InputManager.InputDeviceListener {
+final class KeyboardBacklightController implements
+ InputManagerService.KeyboardBacklightControllerInterface, InputManager.InputDeviceListener {
private static final String TAG = "KbdBacklightController";
@@ -96,7 +97,8 @@
mHandler = new Handler(looper, this::handleMessage);
}
- void systemRunning() {
+ @Override
+ public void systemRunning() {
InputManager inputManager = Objects.requireNonNull(
mContext.getSystemService(InputManager.class));
inputManager.registerInputDeviceListener(this, mHandler);
@@ -106,11 +108,13 @@
}
}
+ @Override
public void incrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
}
+ @Override
public void decrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
@@ -232,6 +236,7 @@
/** Register the keyboard backlight listener for a process. */
@BinderThread
+ @Override
public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
@@ -252,6 +257,7 @@
/** Unregister the keyboard backlight listener for a process. */
@BinderThread
+ @Override
public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
@@ -286,7 +292,8 @@
}
}
- void dump(PrintWriter pw) {
+ @Override
+ public void dump(PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
ipw.increaseIndent();
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index b8eb901..9e8b9f1 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -37,6 +37,8 @@
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout;
+import android.icu.lang.UScript;
+import android.icu.util.ULocale;
import android.os.Bundle;
import android.os.Handler;
import android.os.LocaleList;
@@ -45,6 +47,8 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Slog;
import android.view.InputDevice;
@@ -54,6 +58,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.XmlUtils;
@@ -63,10 +68,12 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
@@ -94,10 +101,18 @@
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
+
private final List<InputDevice> mKeyboardsWithMissingLayouts = new ArrayList<>();
private boolean mKeyboardLayoutNotificationShown = false;
private Toast mSwitchedKeyboardLayoutToast;
+ // This cache stores "best-matched" layouts so that we don't need to run the matching
+ // algorithm repeatedly.
+ @GuardedBy("mKeyboardLayoutCache")
+ private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
+ @Nullable
+ private ImeInfo mCurrentImeInfo;
+
KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
PersistentDataStore dataStore, Looper looper) {
mContext = context;
@@ -139,8 +154,10 @@
@Override
public void onInputDeviceRemoved(int deviceId) {
- mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
- maybeUpdateNotification();
+ if (!useNewSettingsUi()) {
+ mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
+ maybeUpdateNotification();
+ }
}
@Override
@@ -149,18 +166,21 @@
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
- synchronized (mDataStore) {
- String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
- if (layout == null) {
- layout = getDefaultKeyboardLayout(inputDevice);
- if (layout != null) {
- setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
- } else {
- mKeyboardsWithMissingLayouts.add(inputDevice);
+ if (!useNewSettingsUi()) {
+ synchronized (mDataStore) {
+ String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
+ if (layout == null) {
+ layout = getDefaultKeyboardLayout(inputDevice);
+ if (layout != null) {
+ setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
+ } else {
+ mKeyboardsWithMissingLayouts.add(inputDevice);
+ }
}
+ maybeUpdateNotification();
}
- maybeUpdateNotification();
}
+ // TODO(b/259530132): Show notification for new Settings UI
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
@@ -244,6 +264,12 @@
}
}
+ synchronized (mKeyboardLayoutCache) {
+ // Invalidate the cache: With packages being installed/removed, existing cache of
+ // auto-selected layout might not be the best layouts anymore.
+ mKeyboardLayoutCache.clear();
+ }
+
// Reload keyboard layouts.
reloadKeyboardLayouts();
}
@@ -256,6 +282,9 @@
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ return new KeyboardLayout[0];
+ }
final String[] enabledLayoutDescriptors =
getEnabledKeyboardLayoutsForInputDevice(identifier);
final ArrayList<KeyboardLayout> enabledLayouts =
@@ -296,6 +325,7 @@
KeyboardLayout[]::new);
}
+ @Nullable
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -434,21 +464,25 @@
return LocaleList.forLanguageTags(languageTags.replace('|', ','));
}
- /**
- * Builds a layout descriptor for the vendor/product. This returns the
- * descriptor for ids that aren't useful (such as the default 0, 0).
- */
- private String getLayoutDescriptor(InputDeviceIdentifier identifier) {
+ private static String getLayoutDescriptor(@NonNull InputDeviceIdentifier identifier) {
Objects.requireNonNull(identifier, "identifier must not be null");
Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
return identifier.getDescriptor();
}
+ // If vendor id and product id is available, use it as keys. This allows us to have the
+ // same setup for all keyboards with same product and vendor id. i.e. User can swap 2
+ // identical keyboards and still get the same setup.
return "vendor:" + identifier.getVendorId() + ",product:" + identifier.getProductId();
}
+ @Nullable
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported");
+ return null;
+ }
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String layout;
@@ -468,9 +502,13 @@
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "setCurrentKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
+
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
-
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
@@ -489,6 +527,10 @@
}
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
+ return new String[0];
+ }
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String[] layouts = mDataStore.getKeyboardLayouts(key);
@@ -502,6 +544,10 @@
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "addKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -525,6 +571,10 @@
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "removeKeyboardLayoutForInputDevice API not supported");
+ return;
+ }
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -551,31 +601,11 @@
}
}
- public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype) {
- // TODO(b/259530132): Implement the new keyboard layout API: Returning non-IME specific
- // layout for now.
- return getCurrentKeyboardLayoutForInputDevice(identifier);
- }
-
- public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) {
- // TODO(b/259530132): Implement the new keyboard layout API: setting non-IME specific
- // layout for now.
- setCurrentKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor);
- }
-
- public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
- @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
- @Nullable InputMethodSubtype imeSubtype) {
- // TODO(b/259530132): Implement the new keyboard layout API: Returning list of all
- // layouts for now.
- return getKeyboardLayouts();
- }
-
public void switchKeyboardLayout(int deviceId, int direction) {
+ if (useNewSettingsUi()) {
+ Slog.e(TAG, "switchKeyboardLayout API not supported");
+ return;
+ }
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
}
@@ -616,8 +646,21 @@
}
}
+ @Nullable
public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
- String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
+ String keyboardLayoutDescriptor;
+ if (useNewSettingsUi()) {
+ if (mCurrentImeInfo == null) {
+ // Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
+ // keyboard layouts once we receive the callback.
+ return null;
+ }
+
+ keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
+ mCurrentImeInfo);
+ } else {
+ keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
+ }
if (keyboardLayoutDescriptor == null) {
return null;
}
@@ -640,6 +683,287 @@
return result;
}
+ @Nullable
+ public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported");
+ return null;
+ }
+ InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype);
+ String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
+ new ImeInfo(userId, subtypeHandle, imeSubtype));
+ if (DEBUG) {
+ Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
+ + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout);
+ }
+ return layout;
+ }
+
+ public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype,
+ String keyboardLayoutDescriptor) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "setKeyboardLayoutForInputDevice() API not supported");
+ return;
+ }
+ Objects.requireNonNull(keyboardLayoutDescriptor,
+ "keyboardLayoutDescriptor must not be null");
+ String key = createLayoutKey(identifier, userId,
+ InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
+ synchronized (mDataStore) {
+ try {
+ // Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
+ if (mDataStore.setKeyboardLayout(getLayoutDescriptor(identifier), key,
+ keyboardLayoutDescriptor)) {
+ if (DEBUG) {
+ Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier
+ + " key: " + key
+ + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
+ }
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ } finally {
+ mDataStore.saveIfNeeded();
+ }
+ }
+ }
+
+ public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
+ @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
+ @Nullable InputMethodSubtype imeSubtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported");
+ return new KeyboardLayout[0];
+ }
+ return getKeyboardLayoutListForInputDeviceInternal(identifier, new ImeInfo(userId,
+ InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
+ }
+
+ private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
+ InputDeviceIdentifier identifier, ImeInfo imeInfo) {
+ String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+
+ // Fetch user selected layout and always include it in layout list.
+ String userSelectedLayout;
+ synchronized (mDataStore) {
+ userSelectedLayout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
+ }
+
+ final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
+ String imeLanguageTag;
+ if (imeInfo.mImeSubtype == null) {
+ imeLanguageTag = "";
+ } else {
+ ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
+ imeLanguageTag = imeLocale != null ? imeLocale.toLanguageTag()
+ : imeInfo.mImeSubtype.getCanonicalizedLanguageTag();
+ }
+
+ visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
+ boolean mDeviceSpecificLayoutAvailable;
+
+ @Override
+ public void visitKeyboardLayout(Resources resources,
+ int keyboardLayoutResId, KeyboardLayout layout) {
+ // Next find any potential layouts that aren't yet enabled for the device. For
+ // devices that have special layouts we assume there's a reason that the generic
+ // layouts don't work for them, so we don't want to return them since it's likely
+ // to result in a poor user experience.
+ if (layout.getVendorId() == identifier.getVendorId()
+ && layout.getProductId() == identifier.getProductId()) {
+ if (!mDeviceSpecificLayoutAvailable) {
+ mDeviceSpecificLayoutAvailable = true;
+ potentialLayouts.clear();
+ }
+ potentialLayouts.add(layout);
+ } else if (layout.getVendorId() == -1 && layout.getProductId() == -1
+ && !mDeviceSpecificLayoutAvailable && isLayoutCompatibleWithLanguageTag(
+ layout, imeLanguageTag)) {
+ potentialLayouts.add(layout);
+ } else if (layout.getDescriptor().equals(userSelectedLayout)) {
+ potentialLayouts.add(layout);
+ }
+ }
+ });
+ // Sort the Keyboard layouts. This is done first by priority then by label. So, system
+ // layouts will come above 3rd party layouts.
+ Collections.sort(potentialLayouts);
+ return potentialLayouts.toArray(new KeyboardLayout[0]);
+ }
+
+ public void onInputMethodSubtypeChanged(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype) {
+ if (!useNewSettingsUi()) {
+ Slog.e(TAG, "onInputMethodSubtypeChanged() API not supported");
+ return;
+ }
+ if (subtypeHandle == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "No InputMethod is running, ignoring change");
+ }
+ return;
+ }
+ if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
+ || mCurrentImeInfo.mUserId != userId) {
+ mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ if (DEBUG) {
+ Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
+ + " subtypeHandle=" + subtypeHandle);
+ }
+ }
+ }
+
+ @Nullable
+ private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
+ ImeInfo imeInfo) {
+ InputDevice inputDevice = getInputDevice(identifier);
+ if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
+ return null;
+ }
+ String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+ String layout;
+ synchronized (mDataStore) {
+ layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
+ }
+ if (layout == null) {
+ synchronized (mKeyboardLayoutCache) {
+ // Check Auto-selected layout cache to see if layout had been previously selected
+ if (mKeyboardLayoutCache.containsKey(key)) {
+ layout = mKeyboardLayoutCache.get(key);
+ } else {
+ // NOTE: This list is already filtered based on IME Script code
+ KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
+ identifier, imeInfo);
+ // Call auto-matching algorithm to find the best matching layout
+ layout = getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo,
+ layoutList);
+ mKeyboardLayoutCache.put(key, layout);
+ }
+ }
+ }
+ return layout;
+ }
+
+ @Nullable
+ private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
+ ImeInfo imeInfo, KeyboardLayout[] layoutList) {
+ if (imeInfo.mImeSubtypeHandle == null) {
+ return null;
+ }
+
+ Arrays.sort(layoutList);
+
+ // Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
+ for (KeyboardLayout layout : layoutList) {
+ if (layout.getVendorId() == inputDevice.getVendorId()
+ && layout.getProductId() == inputDevice.getProductId()) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "vendor and product Ids. " + inputDevice.getIdentifier()
+ + " : " + layout.getDescriptor());
+ }
+ return layout.getDescriptor();
+ }
+ }
+
+ // Check layout type, language tag information from InputDevice for matching
+ String inputLanguageTag = inputDevice.getKeyboardLanguageTag();
+ if (inputLanguageTag != null) {
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
+ inputLanguageTag, inputDevice.getKeyboardLayoutType());
+
+ if (layoutDesc != null) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "HW information (Language tag and Layout type). "
+ + inputDevice.getIdentifier() + " : " + layoutDesc);
+ }
+ return layoutDesc;
+ }
+ }
+
+ InputMethodSubtype subtype = imeInfo.mImeSubtype;
+ // Can't auto select layout based on IME if subtype or language tag is null
+ if (subtype == null) {
+ return null;
+ }
+
+ // Check layout type, language tag information from IME for matching
+ ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
+ String pkLanguageTag =
+ pkLocale != null ? pkLocale.toLanguageTag() : subtype.getCanonicalizedLanguageTag();
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
+ pkLanguageTag, subtype.getPhysicalKeyboardHintLayoutType());
+ if (DEBUG) {
+ Slog.d(TAG,
+ "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ + "IME locale matching. " + inputDevice.getIdentifier() + " : "
+ + layoutDesc);
+ }
+ return layoutDesc;
+ }
+
+ @Nullable
+ private static String getMatchingLayoutForProvidedLanguageTagAndLayoutType(
+ KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType) {
+ if (layoutType == null || !KeyboardLayout.isLayoutTypeValid(layoutType)) {
+ layoutType = KeyboardLayout.LAYOUT_TYPE_UNDEFINED;
+ }
+ List<KeyboardLayout> layoutsFilteredByLayoutType = new ArrayList<>();
+ for (KeyboardLayout layout : layoutList) {
+ if (layout.getLayoutType().equals(layoutType)) {
+ layoutsFilteredByLayoutType.add(layout);
+ }
+ }
+ String layoutDesc = getMatchingLayoutForProvidedLanguageTag(layoutsFilteredByLayoutType,
+ languageTag);
+ if (layoutDesc != null) {
+ return layoutDesc;
+ }
+
+ return getMatchingLayoutForProvidedLanguageTag(Arrays.asList(layoutList), languageTag);
+ }
+
+ @Nullable
+ private static String getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList,
+ @NonNull String languageTag) {
+ Locale locale = Locale.forLanguageTag(languageTag);
+ String layoutMatchingLanguage = null;
+ String layoutMatchingLanguageAndCountry = null;
+
+ for (KeyboardLayout layout : layoutList) {
+ final LocaleList locales = layout.getLocales();
+ for (int i = 0; i < locales.size(); i++) {
+ final Locale l = locales.get(i);
+ if (l == null) {
+ continue;
+ }
+ if (l.getLanguage().equals(locale.getLanguage())) {
+ if (layoutMatchingLanguage == null) {
+ layoutMatchingLanguage = layout.getDescriptor();
+ }
+ if (l.getCountry().equals(locale.getCountry())) {
+ if (layoutMatchingLanguageAndCountry == null) {
+ layoutMatchingLanguageAndCountry = layout.getDescriptor();
+ }
+ if (l.getVariant().equals(locale.getVariant())) {
+ return layout.getDescriptor();
+ }
+ }
+ }
+ }
+ }
+ return layoutMatchingLanguageAndCountry != null
+ ? layoutMatchingLanguageAndCountry : layoutMatchingLanguage;
+ }
+
private void reloadKeyboardLayouts() {
if (DEBUG) {
Slog.d(TAG, "Reloading keyboard layouts.");
@@ -734,11 +1058,65 @@
}
}
+ private boolean useNewSettingsUi() {
+ return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
+ }
+
+ @Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
}
+ @Nullable
+ private InputDevice getInputDevice(InputDeviceIdentifier identifier) {
+ InputManager inputManager = mContext.getSystemService(InputManager.class);
+ return inputManager != null ? inputManager.getInputDeviceByDescriptor(
+ identifier.getDescriptor()) : null;
+ }
+
+ private static String createLayoutKey(InputDeviceIdentifier identifier, int userId,
+ @NonNull InputMethodSubtypeHandle subtypeHandle) {
+ Objects.requireNonNull(subtypeHandle, "subtypeHandle must not be null");
+ return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + userId
+ + ",subtypeHandle:" + subtypeHandle.toStringHandle();
+ }
+
+ private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
+ @NonNull String languageTag) {
+ final int[] scriptsFromLanguageTag = UScript.getCode(Locale.forLanguageTag(languageTag));
+ if (scriptsFromLanguageTag.length == 0) {
+ // If no scripts inferred from languageTag then allowing the layout
+ return true;
+ }
+ LocaleList locales = layout.getLocales();
+ if (locales.isEmpty()) {
+ // KCM file doesn't have an associated language tag. This can be from
+ // a 3rd party app so need to include it as a potential layout.
+ return true;
+ }
+ for (int i = 0; i < locales.size(); i++) {
+ final Locale locale = locales.get(i);
+ if (locale == null) {
+ continue;
+ }
+ int[] scripts = UScript.getCode(locale);
+ if (scripts != null && haveCommonValue(scripts, scriptsFromLanguageTag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean haveCommonValue(int[] arr1, int[] arr2) {
+ for (int a1 : arr1) {
+ for (int a2 : arr2) {
+ if (a1 == a2) return true;
+ }
+ }
+ return false;
+ }
+
private static final class KeyboardLayoutDescriptor {
public String packageName;
public String receiverName;
@@ -767,6 +1145,19 @@
}
}
+ private static class ImeInfo {
+ @UserIdInt int mUserId;
+ @NonNull InputMethodSubtypeHandle mImeSubtypeHandle;
+ @Nullable InputMethodSubtype mImeSubtype;
+
+ ImeInfo(@UserIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle,
+ @Nullable InputMethodSubtype imeSubtype) {
+ mUserId = userId;
+ mImeSubtypeHandle = imeSubtypeHandle;
+ mImeSubtype = imeSubtype;
+ }
+ }
+
private interface KeyboardLayoutVisitor {
void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout);
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 375377a7..a2b18362 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.hardware.input.TouchCalibration;
+import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseIntArray;
@@ -42,6 +43,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
@@ -121,6 +123,7 @@
return false;
}
+ @Nullable
public String getCurrentKeyboardLayout(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
return state != null ? state.getCurrentKeyboardLayout() : null;
@@ -136,6 +139,22 @@
return false;
}
+ @Nullable
+ public String getKeyboardLayout(String inputDeviceDescriptor, String key) {
+ InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
+ return state != null ? state.getKeyboardLayout(key) : null;
+ }
+
+ public boolean setKeyboardLayout(String inputDeviceDescriptor, String key,
+ String keyboardLayoutDescriptor) {
+ InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
+ if (state.setKeyboardLayout(key, keyboardLayoutDescriptor)) {
+ setDirty();
+ return true;
+ }
+ return false;
+ }
+
public String[] getKeyboardLayouts(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
if (state == null) {
@@ -387,6 +406,8 @@
private final ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray();
+ private final Map<String, String> mKeyboardLayoutMap = new ArrayMap<>();
+
public TouchCalibration getTouchCalibration(int surfaceRotation) {
try {
return mTouchCalibration[surfaceRotation];
@@ -410,6 +431,15 @@
}
@Nullable
+ public String getKeyboardLayout(String key) {
+ return mKeyboardLayoutMap.get(key);
+ }
+
+ public boolean setKeyboardLayout(String key, String keyboardLayout) {
+ return !Objects.equals(mKeyboardLayoutMap.put(key, keyboardLayout), keyboardLayout);
+ }
+
+ @Nullable
public String getCurrentKeyboardLayout() {
return mCurrentKeyboardLayout;
}
@@ -507,6 +537,18 @@
changed = true;
}
}
+ List<String> removedEntries = new ArrayList<>();
+ for (String key : mKeyboardLayoutMap.keySet()) {
+ if (!availableKeyboardLayouts.contains(mKeyboardLayoutMap.get(key))) {
+ removedEntries.add(key);
+ }
+ }
+ if (!removedEntries.isEmpty()) {
+ for (String key : removedEntries) {
+ mKeyboardLayoutMap.remove(key);
+ }
+ changed = true;
+ }
return changed;
}
@@ -534,6 +576,18 @@
}
mCurrentKeyboardLayout = descriptor;
}
+ } else if (parser.getName().equals("keyed-keyboard-layout")) {
+ String key = parser.getAttributeValue(null, "key");
+ if (key == null) {
+ throw new XmlPullParserException(
+ "Missing key attribute on keyed-keyboard-layout.");
+ }
+ String layout = parser.getAttributeValue(null, "layout");
+ if (layout == null) {
+ throw new XmlPullParserException(
+ "Missing layout attribute on keyed-keyboard-layout.");
+ }
+ mKeyboardLayoutMap.put(key, layout);
} else if (parser.getName().equals("light-info")) {
int lightId = parser.getAttributeInt(null, "light-id");
int lightBrightness = parser.getAttributeInt(null, "light-brightness");
@@ -607,6 +661,13 @@
serializer.endTag(null, "keyboard-layout");
}
+ for (String key : mKeyboardLayoutMap.keySet()) {
+ serializer.startTag(null, "keyed-keyboard-layout");
+ serializer.attribute(null, "key", key);
+ serializer.attribute(null, "layout", mKeyboardLayoutMap.get(key));
+ serializer.endTag(null, "keyed-keyboard-layout");
+ }
+
for (int i = 0; i < mKeyboardBacklightBrightnessMap.size(); i++) {
serializer.startTag(null, "light-info");
serializer.attributeInt(null, "light-id", mKeyboardBacklightBrightnessMap.keyAt(i));
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index c90554d..8182fe9 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -70,11 +70,13 @@
private final BluetoothAdapter mBluetoothAdapter;
private final BluetoothRoutesUpdatedListener mListener;
private final AudioManager mAudioManager;
- private final Map<String, BluetoothEventReceiver> mEventReceiverMap = new HashMap<>();
- private final IntentFilter mIntentFilter = new IntentFilter();
- private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener();
+ private final AdapterStateChangedReceiver mAdapterStateChangedReceiver =
+ new AdapterStateChangedReceiver();
+ private final DeviceStateChangedReceiver mDeviceStateChangedReceiver =
+ new DeviceStateChangedReceiver();
+
private BluetoothA2dp mA2dpProfile;
private BluetoothHearingAid mHearingAidProfile;
private BluetoothLeAudio mLeAudioProfile;
@@ -105,32 +107,45 @@
buildBluetoothRoutes();
}
+ /**
+ * Registers listener to bluetooth status changes as the provided user.
+ *
+ * The registered receiver listens to {@link BluetoothA2dp#ACTION_ACTIVE_DEVICE_CHANGED} and
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED } events for {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEARING_AID}, and {@link BluetoothProfile#LE_AUDIO} bluetooth profiles.
+ *
+ * @param user {@code UserHandle} as which receiver is registered
+ */
void start(UserHandle user) {
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID);
mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO);
- // Bluetooth on/off broadcasts
- addEventReceiver(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedReceiver());
+ IntentFilter adapterStateChangedIntentFilter = new IntentFilter();
- DeviceStateChangedReceiver deviceStateChangedReceiver = new DeviceStateChangedReceiver();
- addEventReceiver(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, deviceStateChangedReceiver);
- addEventReceiver(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED, deviceStateChangedReceiver);
- addEventReceiver(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED,
- deviceStateChangedReceiver);
- addEventReceiver(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
- deviceStateChangedReceiver);
+ adapterStateChangedIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiverAsUser(mAdapterStateChangedReceiver, user,
+ adapterStateChangedIntentFilter, null, null);
- mContext.registerReceiverAsUser(mBroadcastReceiver, user,
- mIntentFilter, null, null);
+ IntentFilter deviceStateChangedIntentFilter = new IntentFilter();
+
+ deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+ deviceStateChangedIntentFilter.addAction(
+ BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
+
+ mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user,
+ deviceStateChangedIntentFilter, null, null);
}
void stop() {
- mContext.unregisterReceiver(mBroadcastReceiver);
+ mContext.unregisterReceiver(mAdapterStateChangedReceiver);
+ mContext.unregisterReceiver(mDeviceStateChangedReceiver);
}
/**
@@ -167,11 +182,6 @@
}
}
- private void addEventReceiver(String action, BluetoothEventReceiver eventReceiver) {
- mEventReceiverMap.put(action, eventReceiver);
- mIntentFilter.addAction(action);
- }
-
private void buildBluetoothRoutes() {
mBluetoothRoutes.clear();
Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
@@ -495,26 +505,9 @@
}
}
- private class BluetoothBroadcastReceiver extends BroadcastReceiver {
+ private class AdapterStateChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
-
- BluetoothEventReceiver receiver = mEventReceiverMap.get(action);
- if (receiver != null) {
- receiver.onReceive(context, intent, device);
- }
- }
- }
-
- private interface BluetoothEventReceiver {
- void onReceive(Context context, Intent intent, BluetoothDevice device);
- }
-
- private class AdapterStateChangedReceiver implements BluetoothEventReceiver {
- @Override
- public void onReceive(Context context, Intent intent, BluetoothDevice device) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (state == BluetoothAdapter.STATE_OFF
|| state == BluetoothAdapter.STATE_TURNING_OFF) {
@@ -529,9 +522,12 @@
}
}
- private class DeviceStateChangedReceiver implements BluetoothEventReceiver {
+ private class DeviceStateChangedReceiver extends BroadcastReceiver {
@Override
- public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ public void onReceive(Context context, Intent intent) {
+ BluetoothDevice device = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
+
switch (intent.getAction()) {
case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
clearActiveRoutesWithType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index 60dbbdd..3fcb08a 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -710,6 +710,11 @@
if (gnssChargeUC != EnergyConsumerSnapshot.UNAVAILABLE) {
mStats.updateGnssEnergyConsumerStatsLocked(gnssChargeUC, elapsedRealtime);
}
+
+ final long cameraChargeUC = energyConsumerDeltas.cameraChargeUC;
+ if (cameraChargeUC != EnergyConsumerSnapshot.UNAVAILABLE) {
+ mStats.updateCameraEnergyConsumerStatsLocked(cameraChargeUC, elapsedRealtime);
+ }
}
// Inform mStats about each applicable custom energy bucket.
if (energyConsumerDeltas != null
@@ -904,6 +909,9 @@
case EnergyConsumerType.WIFI:
buckets[EnergyConsumerStats.POWER_BUCKET_WIFI] = true;
break;
+ case EnergyConsumerType.CAMERA:
+ buckets[EnergyConsumerStats.POWER_BUCKET_CAMERA] = true;
+ break;
}
}
return buckets;
@@ -955,6 +963,9 @@
if ((flags & UPDATE_WIFI) != 0) {
addEnergyConsumerIdLocked(energyConsumerIds, EnergyConsumerType.WIFI);
}
+ if ((flags & UPDATE_CAMERA) != 0) {
+ addEnergyConsumerIdLocked(energyConsumerIds, EnergyConsumerType.CAMERA);
+ }
if (energyConsumerIds.size() == 0) {
return null;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index c559436..d622fd7 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -178,7 +178,7 @@
// TODO: remove "tcp" from network methods, since we measure total stats.
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
- public static final int VERSION = 210;
+ public static final int VERSION = 211;
// The maximum number of names wakelocks we will keep track of
// per uid; once the limit is reached, we batch the remaining wakelocks
@@ -649,10 +649,12 @@
int UPDATE_BT = 0x08;
int UPDATE_RPM = 0x10;
int UPDATE_DISPLAY = 0x20;
- int RESET = 0x40;
+ int UPDATE_CAMERA = 0x40;
+ int RESET = 0x80;
int UPDATE_ALL =
- UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT | UPDATE_RPM | UPDATE_DISPLAY;
+ UPDATE_CPU | UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT | UPDATE_RPM | UPDATE_DISPLAY
+ | UPDATE_CAMERA;
int UPDATE_ON_PROC_STATE_CHANGE = UPDATE_WIFI | UPDATE_RADIO | UPDATE_BT;
@@ -665,6 +667,7 @@
UPDATE_BT,
UPDATE_RPM,
UPDATE_DISPLAY,
+ UPDATE_CAMERA,
UPDATE_ALL,
})
@Retention(RetentionPolicy.SOURCE)
@@ -6244,6 +6247,8 @@
}
getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
.noteCameraTurnedOnLocked(elapsedRealtimeMs);
+
+ scheduleSyncExternalStatsLocked("camera-on", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -6259,6 +6264,8 @@
}
getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
.noteCameraTurnedOffLocked(elapsedRealtimeMs);
+
+ scheduleSyncExternalStatsLocked("camera-off", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -6273,6 +6280,8 @@
uid.noteResetCameraLocked(elapsedRealtimeMs);
}
}
+
+ scheduleSyncExternalStatsLocked("camera-reset", ExternalStatsSync.UPDATE_CAMERA);
}
@GuardedBy("this")
@@ -7414,6 +7423,12 @@
return getPowerBucketConsumptionUC(EnergyConsumerStats.POWER_BUCKET_WIFI);
}
+ @GuardedBy("this")
+ @Override
+ public long getCameraEnergyConsumptionUC() {
+ return getPowerBucketConsumptionUC(EnergyConsumerStats.POWER_BUCKET_CAMERA);
+ }
+
/**
* Returns the consumption (in microcoulombs) that the given standard power bucket consumed.
* Will return {@link #POWER_DATA_UNAVAILABLE} if data is unavailable
@@ -8427,6 +8442,12 @@
processState);
}
+ @GuardedBy("mBsi")
+ @Override
+ public long getCameraEnergyConsumptionUC() {
+ return getEnergyConsumptionUC(EnergyConsumerStats.POWER_BUCKET_CAMERA);
+ }
+
/**
* Gets the minimum of the uid's foreground activity time and its PROCESS_STATE_TOP time
* since last marked. Also sets the mark time for both these timers.
@@ -8477,6 +8498,20 @@
return gnssTimeUs;
}
+ /**
+ * Gets the uid's time spent using the camera since last marked. Also sets the mark time for
+ * the camera timer.
+ */
+ private long markCameraTimeUs(long elapsedRealtimeMs) {
+ final StopwatchTimer timer = mCameraTurnedOnTimer;
+ if (timer == null) {
+ return 0;
+ }
+ final long cameraTimeUs = timer.getTimeSinceMarkLocked(elapsedRealtimeMs * 1000);
+ timer.setMark(elapsedRealtimeMs);
+ return cameraTimeUs;
+ }
+
public StopwatchTimer createAudioTurnedOnTimerLocked() {
if (mAudioTurnedOnTimer == null) {
mAudioTurnedOnTimer = new StopwatchTimer(mBsi.mClock, Uid.this, AUDIO_TURNED_ON,
@@ -12902,6 +12937,53 @@
}
/**
+ * Accumulate camera charge consumption and distribute it to the correct state and the apps.
+ *
+ * @param chargeUC amount of charge (microcoulombs) used by the camera since this was last
+ * called.
+ */
+ @GuardedBy("this")
+ public void updateCameraEnergyConsumerStatsLocked(long chargeUC, long elapsedRealtimeMs) {
+ if (DEBUG_ENERGY) Slog.d(TAG, "Updating camera stats: " + chargeUC);
+ if (mGlobalEnergyConsumerStats == null) {
+ return;
+ }
+
+ if (!mOnBatteryInternal || chargeUC <= 0) {
+ // There's nothing further to update.
+ return;
+ }
+
+ if (mIgnoreNextExternalStats) {
+ // Although under ordinary resets we won't get here, and typically a new sync will
+ // happen right after the reset, strictly speaking we need to set all mark times to now.
+ final int uidStatsSize = mUidStats.size();
+ for (int i = 0; i < uidStatsSize; i++) {
+ final Uid uid = mUidStats.valueAt(i);
+ uid.markCameraTimeUs(elapsedRealtimeMs);
+ }
+ return;
+ }
+
+ mGlobalEnergyConsumerStats.updateStandardBucket(
+ EnergyConsumerStats.POWER_BUCKET_CAMERA, chargeUC);
+
+ // Collect the per uid time since mark so that we can normalize power.
+ final SparseDoubleArray cameraTimeUsArray = new SparseDoubleArray();
+
+ // Note: Iterating over all UIDs may be suboptimal.
+ final int uidStatsSize = mUidStats.size();
+ for (int i = 0; i < uidStatsSize; i++) {
+ final Uid uid = mUidStats.valueAt(i);
+ final long cameraTimeUs = uid.markCameraTimeUs(elapsedRealtimeMs);
+ if (cameraTimeUs == 0) continue;
+ cameraTimeUsArray.put(uid.getUid(), (double) cameraTimeUs);
+ }
+ distributeEnergyToUidsLocked(EnergyConsumerStats.POWER_BUCKET_CAMERA, chargeUC,
+ cameraTimeUsArray, 0, elapsedRealtimeMs);
+ }
+
+ /**
* Accumulate Custom power bucket charge, globally and for each app.
*
* @param totalChargeUC charge (microcoulombs) used for this bucket since this was last called.
diff --git a/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java b/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
index 16892034..89991bf 100644
--- a/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/CameraPowerCalculator.java
@@ -48,27 +48,44 @@
long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {
super.calculate(builder, batteryStats, rawRealtimeUs, rawUptimeUs, query);
- final long durationMs = batteryStats.getCameraOnTime(rawRealtimeUs,
- BatteryStats.STATS_SINCE_CHARGED) / 1000;
- final double powerMah = mPowerEstimator.calculatePower(durationMs);
+ long consumptionUc = batteryStats.getCameraEnergyConsumptionUC();
+ int powerModel = getPowerModel(consumptionUc, query);
+ long durationMs =
+ batteryStats.getCameraOnTime(
+ rawRealtimeUs, BatteryStats.STATS_SINCE_CHARGED) / 1000;
+ double powerMah;
+ if (powerModel == BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION) {
+ powerMah = uCtoMah(consumptionUc);
+ } else {
+ powerMah = mPowerEstimator.calculatePower(durationMs);
+ }
+
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
builder.getAggregateBatteryConsumerBuilder(
- BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
}
@Override
protected void calculateApp(UidBatteryConsumer.Builder app, BatteryStats.Uid u,
long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {
- final long durationMs =
+ long consumptionUc = app.getBatteryStatsUid().getCameraEnergyConsumptionUC();
+ int powerModel = getPowerModel(consumptionUc, query);
+ long durationMs =
mPowerEstimator.calculateDuration(u.getCameraTurnedOnTimer(), rawRealtimeUs,
BatteryStats.STATS_SINCE_CHARGED);
- final double powerMah = mPowerEstimator.calculatePower(durationMs);
+ double powerMah;
+ if (powerModel == BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION) {
+ powerMah = uCtoMah(consumptionUc);
+ } else {
+ powerMah = mPowerEstimator.calculatePower(durationMs);
+ }
+
app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA, durationMs)
- .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah);
+ .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA, powerMah, powerModel);
}
}
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java b/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
index 18595ca..939a08b 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerSnapshot.java
@@ -123,6 +123,9 @@
/** The chargeUC for {@link EnergyConsumerType#WIFI}. */
public long wifiChargeUC = UNAVAILABLE;
+ /** The chargeUC for {@link EnergyConsumerType#CAMERA}. */
+ public long cameraChargeUC = UNAVAILABLE;
+
/** Map of {@link EnergyConsumerType#OTHER} ordinals to their total chargeUC. */
public @Nullable long[] otherTotalChargeUC = null;
@@ -256,6 +259,10 @@
output.wifiChargeUC = deltaChargeUC;
break;
+ case EnergyConsumerType.CAMERA:
+ output.cameraChargeUC = deltaChargeUC;
+ break;
+
case EnergyConsumerType.OTHER:
if (output.otherTotalChargeUC == null) {
output.otherTotalChargeUC = new long[mNumOtherOrdinals];
@@ -458,6 +465,9 @@
case EnergyConsumerType.WIFI:
chargeUC[i] = delta.wifiChargeUC;
break;
+ case EnergyConsumerType.CAMERA:
+ chargeUC[i] = delta.cameraChargeUC;
+ break;
case EnergyConsumerType.OTHER:
if (delta.otherTotalChargeUC != null) {
chargeUC[i] = delta.otherTotalChargeUC[energyConsumer.ordinal];
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 50b7fd2..d4f2f2d 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1336,6 +1336,31 @@
}
@Override
+ public void sendCurrentVideoBounds(IBinder sessionToken, Rect bounds, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCurrentVideoBounds(bounds=%s)", bounds.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCurrentVideoBounds");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendCurrentVideoBounds(bounds);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCurrentVideoBounds", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void sendCurrentChannelUri(IBinder sessionToken, Uri channelUri, int userId) {
if (DEBUG) {
Slogf.d(TAG, "sendCurrentChannelUri(channelUri=%s)", channelUri.toString());
@@ -2475,6 +2500,23 @@
}
@Override
+ public void onRequestCurrentVideoBounds() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCurrentVideoBounds");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCurrentVideoBounds(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCurrentVideoBounds", e);
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentChannelUri() {
synchronized (mLock) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 8c58e15..6edfebf 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2852,8 +2852,8 @@
setWallpaperComponent(name, UserHandle.getCallingUserId(), FLAG_SYSTEM);
}
- private void setWallpaperComponent(ComponentName name, @SetWallpaperFlags int which,
- int userId) {
+ @VisibleForTesting
+ void setWallpaperComponent(ComponentName name, @SetWallpaperFlags int which, int userId) {
userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId,
false /* all */, true /* full */, "changing live wallpaper", null /* pkg */);
checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT);
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 52ed3bc..c08f6bf 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -71,7 +71,6 @@
import android.util.Xml;
import android.view.Display;
-import androidx.test.filters.FlakyTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -89,6 +88,7 @@
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -110,7 +110,6 @@
* atest FrameworksMockingServicesTests:WallpaperManagerServiceTests
*/
@Presubmit
-@FlakyTest(bugId = 129797242)
@RunWith(AndroidJUnit4.class)
public class WallpaperManagerServiceTests {
@@ -273,8 +272,10 @@
/**
* Tests setWallpaperComponent and clearWallpaper should work as expected.
+ * TODO ignored since the assumption never passes. to be investigated.
*/
@Test
+ @Ignore("b/264533465")
public void testSetThenClearComponent() {
// Skip if there is no pre-defined default wallpaper component.
assumeThat(sDefaultWallpaperComponent,
@@ -285,7 +286,7 @@
verifyLastWallpaperData(testUserId, sDefaultWallpaperComponent);
verifyCurrentSystemData(testUserId);
- mService.setWallpaperComponent(sImageWallpaperComponentName);
+ mService.setWallpaperComponent(sImageWallpaperComponentName, FLAG_SYSTEM, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
@@ -312,7 +313,7 @@
WallpaperManagerService.WallpaperConnection.DisplayConnector connector =
mService.mLastWallpaper.connection.getDisplayConnectorOrCreate(DEFAULT_DISPLAY);
- mService.setWallpaperComponent(sDefaultWallpaperComponent);
+ mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId);
verify(connector.mEngine).dispatchWallpaperCommand(
eq(COMMAND_REAPPLY), anyInt(), anyInt(), anyInt(), any());
@@ -454,7 +455,7 @@
public void testGetAdjustedWallpaperColorsOnDimming() throws RemoteException {
final int testUserId = USER_SYSTEM;
mService.switchUser(testUserId, null);
- mService.setWallpaperComponent(sDefaultWallpaperComponent);
+ mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId);
WallpaperData wallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, testUserId);
// Mock a wallpaper data with color hints that support dark text and dark theme
diff --git a/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm b/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm
new file mode 100644
index 0000000..ea6bc98
--- /dev/null
+++ b/services/tests/servicestests/res/raw/dummy_keyboard_layout.kcm
@@ -0,0 +1,311 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# English (US) keyboard layout.
+# Unlike the default (generic) keyboard layout, English (US) does not contain any
+# special ALT characters.
+#
+
+type OVERLAY
+
+### ROW 1
+
+key GRAVE {
+ label: '`'
+ base: '`'
+ shift: '~'
+}
+
+key 1 {
+ label: '1'
+ base: '1'
+ shift: '!'
+}
+
+key 2 {
+ label: '2'
+ base: '2'
+ shift: '@'
+}
+
+key 3 {
+ label: '3'
+ base: '3'
+ shift: '#'
+}
+
+key 4 {
+ label: '4'
+ base: '4'
+ shift: '$'
+}
+
+key 5 {
+ label: '5'
+ base: '5'
+ shift: '%'
+}
+
+key 6 {
+ label: '6'
+ base: '6'
+ shift: '^'
+}
+
+key 7 {
+ label: '7'
+ base: '7'
+ shift: '&'
+}
+
+key 8 {
+ label: '8'
+ base: '8'
+ shift: '*'
+}
+
+key 9 {
+ label: '9'
+ base: '9'
+ shift: '('
+}
+
+key 0 {
+ label: '0'
+ base: '0'
+ shift: ')'
+}
+
+key MINUS {
+ label: '-'
+ base: '-'
+ shift: '_'
+}
+
+key EQUALS {
+ label: '='
+ base: '='
+ shift: '+'
+}
+
+### ROW 2
+
+key Q {
+ label: 'Q'
+ base: 'q'
+ shift, capslock: 'Q'
+}
+
+key W {
+ label: 'W'
+ base: 'w'
+ shift, capslock: 'W'
+}
+
+key E {
+ label: 'E'
+ base: 'e'
+ shift, capslock: 'E'
+}
+
+key R {
+ label: 'R'
+ base: 'r'
+ shift, capslock: 'R'
+}
+
+key T {
+ label: 'T'
+ base: 't'
+ shift, capslock: 'T'
+}
+
+key Y {
+ label: 'Y'
+ base: 'y'
+ shift, capslock: 'Y'
+}
+
+key U {
+ label: 'U'
+ base: 'u'
+ shift, capslock: 'U'
+}
+
+key I {
+ label: 'I'
+ base: 'i'
+ shift, capslock: 'I'
+}
+
+key O {
+ label: 'O'
+ base: 'o'
+ shift, capslock: 'O'
+}
+
+key P {
+ label: 'P'
+ base: 'p'
+ shift, capslock: 'P'
+}
+
+key LEFT_BRACKET {
+ label: '['
+ base: '['
+ shift: '{'
+}
+
+key RIGHT_BRACKET {
+ label: ']'
+ base: ']'
+ shift: '}'
+}
+
+key BACKSLASH {
+ label: '\\'
+ base: '\\'
+ shift: '|'
+}
+
+### ROW 3
+
+key A {
+ label: 'A'
+ base: 'a'
+ shift, capslock: 'A'
+}
+
+key S {
+ label: 'S'
+ base: 's'
+ shift, capslock: 'S'
+}
+
+key D {
+ label: 'D'
+ base: 'd'
+ shift, capslock: 'D'
+}
+
+key F {
+ label: 'F'
+ base: 'f'
+ shift, capslock: 'F'
+}
+
+key G {
+ label: 'G'
+ base: 'g'
+ shift, capslock: 'G'
+}
+
+key H {
+ label: 'H'
+ base: 'h'
+ shift, capslock: 'H'
+}
+
+key J {
+ label: 'J'
+ base: 'j'
+ shift, capslock: 'J'
+}
+
+key K {
+ label: 'K'
+ base: 'k'
+ shift, capslock: 'K'
+}
+
+key L {
+ label: 'L'
+ base: 'l'
+ shift, capslock: 'L'
+}
+
+key SEMICOLON {
+ label: ';'
+ base: ';'
+ shift: ':'
+}
+
+key APOSTROPHE {
+ label: '\''
+ base: '\''
+ shift: '"'
+}
+
+### ROW 4
+
+key Z {
+ label: 'Z'
+ base: 'z'
+ shift, capslock: 'Z'
+}
+
+key X {
+ label: 'X'
+ base: 'x'
+ shift, capslock: 'X'
+}
+
+key C {
+ label: 'C'
+ base: 'c'
+ shift, capslock: 'C'
+}
+
+key V {
+ label: 'V'
+ base: 'v'
+ shift, capslock: 'V'
+}
+
+key B {
+ label: 'B'
+ base: 'b'
+ shift, capslock: 'B'
+}
+
+key N {
+ label: 'N'
+ base: 'n'
+ shift, capslock: 'N'
+}
+
+key M {
+ label: 'M'
+ base: 'm'
+ shift, capslock: 'M'
+}
+
+key COMMA {
+ label: ','
+ base: ','
+ shift: '<'
+}
+
+key PERIOD {
+ label: '.'
+ base: '.'
+ shift: '>'
+}
+
+key SLASH {
+ label: '/'
+ base: '/'
+ shift: '?'
+}
diff --git a/services/tests/servicestests/res/xml/keyboard_layouts.xml b/services/tests/servicestests/res/xml/keyboard_layouts.xml
new file mode 100644
index 0000000..b5a05fc
--- /dev/null
+++ b/services/tests/servicestests/res/xml/keyboard_layouts.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <keyboard-layout
+ android:name="keyboard_layout_english_uk"
+ android:label="English (UK)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-GB"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us"
+ android:label="English (US)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn,en-Latn-US"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_intl"
+ android:label="English (International)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="extended" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_dvorak"
+ android:label="English (Dvorak)"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="dvorak" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_german"
+ android:label="German"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="de-Latn"
+ android:keyboardLayoutType="qwertz" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_french"
+ android:label="French"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="fr-Latn-FR"
+ android:keyboardLayoutType="azerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_russian_qwerty"
+ android:label="Russian"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="ru-Cyrl"
+ android:keyboardLayoutType="qwerty" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_russian"
+ android:label="Russian"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ android:keyboardLocale="ru-Cyrl" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_vendorId:1,productId:1"
+ android:label="vendorId:1,productId:1"
+ android:keyboardLayout="@raw/dummy_keyboard_layout"
+ androidprv:vendorId="1"
+ androidprv:productId="1" />
+</keyboard-layouts>
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
new file mode 100644
index 0000000..34540c3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -0,0 +1,820 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.hardware.input.KeyboardLayout
+import android.icu.lang.UScript
+import android.icu.util.ULocale
+import android.os.Bundle
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.provider.Settings
+import android.view.InputDevice
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodSubtype
+import androidx.test.core.R
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.Locale
+
+private fun createKeyboard(
+ deviceId: Int,
+ vendorId: Int,
+ productId: Int,
+ languageTag: String,
+ layoutType: String
+): InputDevice =
+ InputDevice.Builder()
+ .setId(deviceId)
+ .setName("Device $deviceId")
+ .setDescriptor("descriptor $deviceId")
+ .setSources(InputDevice.SOURCE_KEYBOARD)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setExternal(true)
+ .setVendorId(vendorId)
+ .setProductId(productId)
+ .setKeyboardLanguageTag(languageTag)
+ .setKeyboardLayoutType(layoutType)
+ .build()
+
+/**
+ * Tests for {@link Default UI} and {@link New UI}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:KeyboardLayoutManagerTests
+ */
+@Presubmit
+class KeyboardLayoutManagerTests {
+ companion object {
+ const val DEVICE_ID = 1
+ const val VENDOR_SPECIFIC_DEVICE_ID = 2
+ const val ENGLISH_DVORAK_DEVICE_ID = 3
+ const val USER_ID = 4
+ const val IME_ID = "ime_id"
+ const val PACKAGE_NAME = "KeyboardLayoutManagerTests"
+ const val RECEIVER_NAME = "DummyReceiver"
+ private const val ENGLISH_US_LAYOUT_NAME = "keyboard_layout_english_us"
+ private const val ENGLISH_UK_LAYOUT_NAME = "keyboard_layout_english_uk"
+ private const val VENDOR_SPECIFIC_LAYOUT_NAME = "keyboard_layout_vendorId:1,productId:1"
+ }
+
+ private val ENGLISH_US_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_US_LAYOUT_NAME)
+ private val ENGLISH_UK_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_UK_LAYOUT_NAME)
+ private val VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR =
+ createLayoutDescriptor(VENDOR_SPECIFIC_LAYOUT_NAME)
+
+ @get:Rule
+ val rule = MockitoJUnit.rule()!!
+
+ @Mock
+ private lateinit var iInputManager: IInputManager
+
+ @Mock
+ private lateinit var native: NativeInputManagerService
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+ private lateinit var keyboardLayoutManager: KeyboardLayoutManager
+
+ private lateinit var imeInfo: InputMethodInfo
+ private var nextImeSubtypeId = 0
+ private lateinit var context: Context
+ private lateinit var dataStore: PersistentDataStore
+ private lateinit var testLooper: TestLooper
+
+ // Devices
+ private lateinit var keyboardDevice: InputDevice
+ private lateinit var vendorSpecificKeyboardDevice: InputDevice
+ private lateinit var englishDvorakKeyboardDevice: InputDevice
+
+ @Before
+ fun setup() {
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
+ override fun openRead(): InputStream? {
+ throw FileNotFoundException()
+ }
+
+ override fun startWrite(): FileOutputStream? {
+ throw IOException()
+ }
+
+ override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
+ })
+ testLooper = TestLooper()
+ keyboardLayoutManager = KeyboardLayoutManager(context, native, dataStore, testLooper.looper)
+ setupInputDevices()
+ setupBroadcastReceiver()
+ setupIme()
+ }
+
+ private fun setupInputDevices() {
+ val inputManager = InputManager.resetInstance(iInputManager)
+ Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+
+ keyboardDevice = createKeyboard(DEVICE_ID, 0, 0, "", "")
+ vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, "", "")
+ englishDvorakKeyboardDevice =
+ createKeyboard(ENGLISH_DVORAK_DEVICE_ID, 0, 0, "en", "dvorak")
+ Mockito.`when`(iInputManager.inputDeviceIds)
+ .thenReturn(intArrayOf(DEVICE_ID, VENDOR_SPECIFIC_DEVICE_ID, ENGLISH_DVORAK_DEVICE_ID))
+ Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
+ Mockito.`when`(iInputManager.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID))
+ .thenReturn(vendorSpecificKeyboardDevice)
+ Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID))
+ .thenReturn(englishDvorakKeyboardDevice)
+ }
+
+ private fun setupBroadcastReceiver() {
+ Mockito.`when`(context.packageManager).thenReturn(packageManager)
+
+ val info = createMockReceiver()
+ Mockito.`when`(packageManager.queryBroadcastReceivers(Mockito.any(), Mockito.anyInt()))
+ .thenReturn(listOf(info))
+ Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt()))
+ .thenReturn(info.activityInfo)
+
+ val resources = context.resources
+ Mockito.`when`(
+ packageManager.getResourcesForApplication(
+ Mockito.any(
+ ApplicationInfo::class.java
+ )
+ )
+ ).thenReturn(resources)
+ }
+
+ private fun setupIme() {
+ imeInfo = InputMethodInfo(PACKAGE_NAME, RECEIVER_NAME, "", "", 0)
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayouts() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
+ assertNotEquals(
+ "Default UI: Keyboard layout API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "Default UI: Keyboard layout API should provide English(US) layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayouts() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
+ assertNotEquals(
+ "New UI: Keyboard layout API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "New UI: Keyboard layout API should provide English(US) layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
+ assertNotEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should not return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue(
+ "Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " +
+ "layout",
+ hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+
+ val vendorSpecificKeyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(
+ vendorSpecificKeyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " +
+ "specific layout",
+ 1,
+ vendorSpecificKeyboardLayouts.size
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " +
+ "layout",
+ VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR,
+ vendorSpecificKeyboardLayouts[0].descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
+ assertEquals(
+ "New UI: getKeyboardLayoutsForInputDevice API should always return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ assertNull(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " +
+ "nothing was set",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ val keyboardLayout =
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " +
+ "layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertNull(
+ "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " +
+ "even after setCurrentKeyboardLayoutForInputDevice",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+
+ val keyboardLayouts =
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ )
+ assertEquals(
+ "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " +
+ "layout",
+ 1,
+ keyboardLayouts.size
+ )
+ assertEquals(
+ "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " +
+ "English(US) layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayouts[0]
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(US) layout (Auto select the first enabled layout)",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.removeKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts",
+ 0,
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ ).size
+ )
+ assertNull(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " +
+ "the enabled layout is removed",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+
+ assertEquals(
+ "New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " +
+ "an empty array",
+ 0,
+ keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
+ keyboardDevice.identifier
+ ).size
+ )
+ assertNull(
+ "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_switchKeyboardLayout() {
+ NewSettingsApiFlag(false).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(US) layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+
+ keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
+
+ // Throws null pointer because trying to show toast using TestLooper
+ assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() }
+ assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
+ "English(UK) layout",
+ ENGLISH_UK_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_switchKeyboardLayout() {
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayoutManager.addKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+
+ keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
+ testLooper.dispatchAll()
+
+ assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " +
+ "null",
+ keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayout() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
+ assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " +
+ "available layouts",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout!!.descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayout() {
+ NewSettingsApiFlag(true).use {
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
+ assertEquals("New UI: getKeyboardLayout API should return correct Layout from " +
+ "available layouts",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayout!!.descriptor
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() {
+ NewSettingsApiFlag(false).use {
+ val imeSubtype = createImeSubtype()
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ val keyboardLayout =
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ assertNull(
+ "Default UI: getKeyboardLayoutForInputDevice API should always return null",
+ keyboardLayout
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() {
+ NewSettingsApiFlag(true).use {
+ val imeSubtype = createImeSubtype()
+
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "New UI: getKeyboardLayoutForInputDevice API should return the set layout",
+ ENGLISH_UK_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+
+ // This should replace previously set layout
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertEquals(
+ "New UI: getKeyboardLayoutForInputDevice API should return the last set layout",
+ ENGLISH_US_LAYOUT_DESCRIPTOR,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDefaultUi_getKeyboardLayoutListForInputDevice() {
+ NewSettingsApiFlag(false).use {
+ val keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtype()
+ )
+ assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " +
+ "return empty array",
+ 0,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getKeyboardLayoutListForInputDevice() {
+ NewSettingsApiFlag(true).use {
+ // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts
+ var keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("hi-Latn")
+ )
+ assertNotEquals(
+ "New UI: getKeyboardLayoutListForInputDevice API should return the list of " +
+ "supported layouts with matching script code",
+ 0,
+ keyboardLayouts.size
+ )
+
+ val englishScripts = UScript.getCode(Locale.forLanguageTag("hi-Latn"))
+ for (kl in keyboardLayouts) {
+ var isCompatible = false
+ for (i in 0 until kl.locales.size()) {
+ val locale: Locale = kl.locales.get(i) ?: continue
+ val scripts = UScript.getCode(locale)
+ if (scripts != null && areScriptsCompatible(scripts, englishScripts)) {
+ isCompatible = true
+ break
+ }
+ }
+ assertTrue(
+ "New UI: getKeyboardLayoutListForInputDevice API should only return " +
+ "compatible layouts but found " + kl.descriptor,
+ isCompatible
+ )
+ }
+
+ // Check Layouts for "hi" which by default uses 'Deva' script.
+ keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("hi")
+ )
+ assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " +
+ "list if no supported layouts available",
+ 0,
+ keyboardLayouts.size
+ )
+
+ // If user manually selected some layout, always provide it in the layout list
+ val imeSubtype = createImeSubtypeForLanguageTag("hi")
+ keyboardLayoutManager.setKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ imeSubtype
+ )
+ assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " +
+ "selected layout even if the script is incompatible with IME",
+ 1,
+ keyboardLayouts.size
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() {
+ NewSettingsApiFlag(true).use {
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("en-US"),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("en-GB"),
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("de"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("fr-FR"),
+ createLayoutDescriptor("keyboard_layout_french")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTag("ru"),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ assertNull(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
+ "layout available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("it")
+ )
+ )
+ assertNull(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
+ "layout for script code is available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTag("en-Deva")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() {
+ NewSettingsApiFlag(true).use {
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+ // Try to match layout type even if country doesn't match
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+ // Choose layout based on layout type priority, if layout type is not provided by IME
+ // (Qwerty > Dvorak > Extended)
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-US", ""),
+ ENGLISH_US_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"),
+ ENGLISH_UK_LAYOUT_DESCRIPTOR
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ // Wrong layout type should match with language if provided layout type not available
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_german")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"),
+ createLayoutDescriptor("keyboard_layout_french")
+ )
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_russian_qwerty")
+ )
+ // If layout type is empty then prioritize KCM with empty layout type
+ assertCorrectLayout(
+ keyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " +
+ "no layout for script code is available",
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo,
+ createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() {
+ NewSettingsApiFlag(true).use {
+ // Should return English dvorak even if IME current layout is qwerty, since HW says the
+ // keyboard is a Dvorak keyboard
+ assertCorrectLayout(
+ englishDvorakKeyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("en", "qwerty"),
+ createLayoutDescriptor("keyboard_layout_english_us_dvorak")
+ )
+
+ // Fallback to IME information if the HW provided layout script is incompatible with the
+ // provided IME subtype
+ assertCorrectLayout(
+ englishDvorakKeyboardDevice,
+ createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ }
+ }
+
+ private fun assertCorrectLayout(
+ device: InputDevice,
+ imeSubtype: InputMethodSubtype,
+ expectedLayout: String
+ ) {
+ assertEquals(
+ "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout",
+ expectedLayout,
+ keyboardLayoutManager.getKeyboardLayoutForInputDevice(
+ device.identifier, USER_ID, imeInfo, imeSubtype
+ )
+ )
+ }
+
+ private fun createImeSubtype(): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++).build()
+
+ private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
+ .setLanguageTag(languageTag).build()
+
+ private fun createImeSubtypeForLanguageTagAndLayoutType(
+ languageTag: String,
+ layoutType: String
+ ): InputMethodSubtype =
+ InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
+ .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build()
+
+ private fun hasLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean {
+ for (kl in layoutList) {
+ if (kl.descriptor == layoutDesc) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun createLayoutDescriptor(keyboardName: String): String =
+ "$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName"
+
+ private fun areScriptsCompatible(scriptList1: IntArray, scriptList2: IntArray): Boolean {
+ for (s1 in scriptList1) {
+ for (s2 in scriptList2) {
+ if (s1 == s2) return true
+ }
+ }
+ return false
+ }
+
+ private fun createMockReceiver(): ResolveInfo {
+ val info = ResolveInfo()
+ info.activityInfo = ActivityInfo()
+ info.activityInfo.packageName = PACKAGE_NAME
+ info.activityInfo.name = RECEIVER_NAME
+ info.activityInfo.applicationInfo = ApplicationInfo()
+ info.activityInfo.metaData = Bundle()
+ info.activityInfo.metaData.putInt(
+ InputManager.META_DATA_KEYBOARD_LAYOUTS,
+ R.xml.keyboard_layouts
+ )
+ info.serviceInfo = ServiceInfo()
+ info.serviceInfo.packageName = PACKAGE_NAME
+ info.serviceInfo.name = RECEIVER_NAME
+ return info
+ }
+
+ private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable {
+ init {
+ Settings.Global.putString(
+ context.contentResolver,
+ "settings_new_keyboard_ui", enabled.toString()
+ )
+ }
+
+ override fun close() {
+ Settings.Global.putString(
+ context.contentResolver,
+ "settings_new_keyboard_ui",
+ ""
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 2ebe215..cff4cc7 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -18,6 +18,7 @@
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_ALL;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_BT;
+import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_CAMERA;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_DISPLAY;
import static com.android.server.power.stats.BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO;
@@ -104,6 +105,11 @@
tempAllIds.add(gnssId);
mPowerStatsInternal.incrementEnergyConsumption(gnssId, 787878);
+ final int cameraId =
+ mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.CAMERA, 0, "camera");
+ tempAllIds.add(cameraId);
+ mPowerStatsInternal.incrementEnergyConsumption(cameraId, 901234);
+
final int mobileRadioId = mPowerStatsInternal.addEnergyConsumer(
EnergyConsumerType.MOBILE_RADIO, 0, "mobile_radio");
tempAllIds.add(mobileRadioId);
@@ -171,6 +177,12 @@
Arrays.sort(receivedCpuIds);
assertArrayEquals(cpuClusterIds, receivedCpuIds);
+ final EnergyConsumerResult[] cameraResults =
+ mBatteryExternalStatsWorker.getEnergyConsumersLocked(UPDATE_CAMERA).getNow(null);
+ // Results should only have the camera energy consumer
+ assertEquals(1, cameraResults.length);
+ assertEquals(cameraId, cameraResults[0].id);
+
final EnergyConsumerResult[] allResults =
mBatteryExternalStatsWorker.getEnergyConsumersLocked(UPDATE_ALL).getNow(null);
// All energy consumer results should be available
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
index e4ab21b..5fce32f0 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
@@ -36,41 +36,113 @@
public class CameraPowerCalculatorTest {
private static final double PRECISION = 0.00001;
- private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP1_UID = Process.FIRST_APPLICATION_UID + 42;
+ private static final int APP2_UID = Process.FIRST_APPLICATION_UID + 43;
@Rule
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
- .setAveragePower(PowerProfile.POWER_CAMERA, 360.0);
+ .setAveragePower(PowerProfile.POWER_CAMERA, 360.0)
+ .initMeasuredEnergyStatsLocked();
@Test
public void testTimerBasedModel() {
BatteryStatsImpl stats = mStatsRule.getBatteryStats();
- stats.noteCameraOnLocked(APP_UID, 1000, 1000);
- stats.noteCameraOffLocked(APP_UID, 2000, 2000);
+ synchronized (stats) { // To keep the GuardedBy check happy
+ stats.noteCameraOnLocked(APP1_UID, 1000, 1000);
+ stats.noteCameraOffLocked(APP1_UID, 2000, 2000);
+ stats.noteCameraOnLocked(APP2_UID, 3000, 3000);
+ stats.noteCameraOffLocked(APP2_UID, 5000, 5000);
+ }
+
+ CameraPowerCalculator calculator =
+ new CameraPowerCalculator(mStatsRule.getPowerProfile());
+
+ mStatsRule.apply(BatteryUsageStatsRule.POWER_PROFILE_MODEL_ONLY, calculator);
+
+ UidBatteryConsumer app1Consumer = mStatsRule.getUidBatteryConsumer(APP1_UID);
+ assertThat(app1Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(1000);
+ assertThat(app1Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.1);
+ assertThat(app1Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ UidBatteryConsumer app2Consumer = mStatsRule.getUidBatteryConsumer(APP2_UID);
+ assertThat(app2Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(2000);
+ assertThat(app2Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.2);
+ assertThat(app2Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ final BatteryConsumer deviceBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
+ assertThat(deviceBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(3000);
+ assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(deviceBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+
+ final BatteryConsumer appsBatteryConsumer = mStatsRule.getAppsBatteryConsumer();
+ assertThat(appsBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(3000);
+ assertThat(appsBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(appsBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_POWER_PROFILE);
+ }
+
+ @Test
+ public void testEnergyConsumptionBasedModel() {
+ BatteryStatsImpl stats = mStatsRule.getBatteryStats();
+ synchronized (stats) { // To keep the GuardedBy check happy
+ stats.noteCameraOnLocked(APP1_UID, 1000, 1000);
+ stats.noteCameraOffLocked(APP1_UID, 2000, 2000);
+ stats.updateCameraEnergyConsumerStatsLocked(720_000, 2100); // 0.72C == 0.2mAh
+ stats.noteCameraOnLocked(APP2_UID, 3000, 3000);
+ stats.noteCameraOffLocked(APP2_UID, 5000, 5000);
+ stats.updateCameraEnergyConsumerStatsLocked(1_080_000, 5100); // 0.3mAh
+ }
CameraPowerCalculator calculator =
new CameraPowerCalculator(mStatsRule.getPowerProfile());
mStatsRule.apply(calculator);
- UidBatteryConsumer consumer = mStatsRule.getUidBatteryConsumer(APP_UID);
- assertThat(consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ UidBatteryConsumer app1Consumer = mStatsRule.getUidBatteryConsumer(APP1_UID);
+ assertThat(app1Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
.isEqualTo(1000);
- assertThat(consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ assertThat(app1Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.2);
+ assertThat(app1Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
+
+ UidBatteryConsumer app2Consumer = mStatsRule.getUidBatteryConsumer(APP2_UID);
+ assertThat(app2Consumer.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(2000);
+ assertThat(app2Consumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isWithin(PRECISION).of(0.3);
+ assertThat(app2Consumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
final BatteryConsumer deviceBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
assertThat(deviceBatteryConsumer
.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isEqualTo(1000);
+ .isEqualTo(3000);
assertThat(deviceBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ .isWithin(PRECISION).of(0.5);
+ assertThat(deviceBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
final BatteryConsumer appsBatteryConsumer = mStatsRule.getAppsBatteryConsumer();
assertThat(appsBatteryConsumer
.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isEqualTo(1000);
+ .isEqualTo(3000);
assertThat(appsBatteryConsumer.getConsumedPower(BatteryConsumer.POWER_COMPONENT_CAMERA))
- .isWithin(PRECISION).of(0.1);
+ .isWithin(PRECISION).of(0.5);
+ assertThat(appsBatteryConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_CAMERA))
+ .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java b/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
index 558f396..28f4799 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java
@@ -248,6 +248,32 @@
assertThat(details.toString()).isEqualTo("DISPLAY=2667 HPU=3200000 GPU=0 IPU &_=0");
}
+ @Test
+ public void testUpdateAndGetDelta_updatesCameraCharge() {
+ EnergyConsumer cameraConsumer =
+ createEnergyConsumer(7, 0, EnergyConsumerType.CAMERA, "CAMERA");
+ final EnergyConsumerSnapshot snapshot =
+ new EnergyConsumerSnapshot(createIdToConsumerMap(cameraConsumer));
+
+ // An initial result with only one energy consumer
+ EnergyConsumerResult[] result0 = new EnergyConsumerResult[]{
+ createEnergyConsumerResult(cameraConsumer.id, 60_000, null, null),
+ };
+ snapshot.updateAndGetDelta(result0, VOLTAGE_1);
+
+ // A subsequent result
+ EnergyConsumerResult[] result1 = new EnergyConsumerResult[]{
+ createEnergyConsumerResult(cameraConsumer.id, 90_000, null, null),
+ };
+ EnergyConsumerDeltaData delta = snapshot.updateAndGetDelta(result1, VOLTAGE_1);
+
+ // Verify that the delta between the two results is reported.
+ BatteryStats.EnergyConsumerDetails details = snapshot.getEnergyConsumerDetails(delta);
+ assertThat(details.consumers).hasLength(1);
+ long expectedDeltaUC = calculateChargeConsumedUC(60_000, VOLTAGE_1, 90_000, VOLTAGE_1);
+ assertThat(details.chargeUC[0]).isEqualTo(expectedDeltaUC);
+ }
+
private static EnergyConsumer createEnergyConsumer(int id, int ord, byte type, String name) {
final EnergyConsumer ec = new EnergyConsumer();
ec.id = id;