Merge "Various API changes including:"
diff --git a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
index 4c8790f..1e92826 100644
--- a/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
+++ b/apex/statsd/service/java/com/android/server/stats/StatsCompanionService.java
@@ -15,161 +15,51 @@
*/
package com.android.server.stats;
-import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
-import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
-import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
-import static android.os.Process.getUidForPid;
-import static android.os.storage.VolumeInfo.TYPE_PRIVATE;
-import static android.os.storage.VolumeInfo.TYPE_PUBLIC;
-import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
-import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs;
-import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs;
-import static com.android.server.stats.pull.ProcfsMemoryUtil.forEachPid;
-import static com.android.server.stats.pull.ProcfsMemoryUtil.readCmdlineFromProcfs;
-import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
import android.app.AlarmManager.OnAlarmListener;
-import android.app.AppOpsManager;
-import android.app.AppOpsManager.HistoricalOps;
-import android.app.AppOpsManager.HistoricalOpsRequest;
-import android.app.AppOpsManager.HistoricalPackageOps;
-import android.app.AppOpsManager.HistoricalUidOps;
-import android.app.INotificationManager;
-import android.app.ProcessMemoryState;
import android.app.StatsManager;
-import android.bluetooth.BluetoothActivityEnergyInfo;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.UidTraffic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PermissionInfo;
-import android.content.pm.UserInfo;
-import android.hardware.biometrics.BiometricsProtoEnums;
-import android.hardware.face.FaceManager;
-import android.hardware.fingerprint.FingerprintManager;
-import android.net.ConnectivityManager;
-import android.net.INetworkStatsService;
-import android.net.Network;
-import android.net.NetworkRequest;
-import android.net.NetworkStats;
-import android.net.wifi.WifiManager;
-import android.os.BatteryStats;
-import android.os.BatteryStatsInternal;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
-import android.os.CoolingDevice;
-import android.os.Environment;
-import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IStatsCompanionService;
import android.os.IStatsd;
-import android.os.IStoraged;
-import android.os.IThermalEventListener;
-import android.os.IThermalService;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.StatsFrameworkInitializer;
-import android.os.StatFs;
-import android.os.StatsLogEventWrapper;
-import android.os.SynchronousResultReceiver;
import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.os.Temperature;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.connectivity.WifiActivityEnergyInfo;
-import android.os.storage.DiskInfo;
-import android.os.storage.StorageManager;
-import android.os.storage.VolumeInfo;
-import android.provider.Settings;
-import android.stats.storage.StorageEnums;
-import android.telephony.ModemActivityInfo;
-import android.telephony.TelephonyManager;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Log;
import android.util.Slog;
-import android.util.StatsLog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.app.procstats.IProcessStats;
-import com.android.internal.app.procstats.ProcessStats;
-import com.android.internal.os.BatterySipper;
-import com.android.internal.os.BatteryStatsHelper;
-import com.android.internal.os.BinderCallsStats.ExportedCallStat;
-import com.android.internal.os.KernelCpuSpeedReader;
-import com.android.internal.os.KernelCpuThreadReader;
-import com.android.internal.os.KernelCpuThreadReaderDiff;
-import com.android.internal.os.KernelCpuThreadReaderSettingsObserver;
-import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidActiveTimeReader;
-import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidClusterTimeReader;
-import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidFreqTimeReader;
-import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidUserSysTimeReader;
-import com.android.internal.os.KernelWakelockReader;
-import com.android.internal.os.KernelWakelockStats;
import com.android.internal.os.LooperStats;
-import com.android.internal.os.PowerProfile;
-import com.android.internal.os.ProcessCpuTracker;
-import com.android.internal.os.StoragedUidIoStatsReader;
import com.android.internal.util.DumpUtils;
import com.android.server.BinderCallsStatsService;
import com.android.server.LocalServices;
-import com.android.server.SystemServiceManager;
-import com.android.server.am.MemoryStatUtil.MemoryStat;
-import com.android.server.notification.NotificationManagerService;
-import com.android.server.role.RoleManagerInternal;
-import com.android.server.stats.pull.IonMemoryUtil.IonAllocations;
-import com.android.server.stats.pull.ProcfsMemoryUtil.MemorySnapshot;
-import com.android.server.storage.DiskStatsFileLogger;
-import com.android.server.storage.DiskStatsLoggingService;
-
-import com.google.android.collect.Sets;
import libcore.io.IoUtils;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
/**
* Helper service for statsd (the native stats management service in cmds/statsd/).
@@ -178,10 +68,7 @@
* @hide
*/
public class StatsCompanionService extends IStatsCompanionService.Stub {
- /**
- * How long to wait on an individual subsystem to return its stats.
- */
- private static final long EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS = 2000;
+
private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1);
public static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity";
@@ -201,45 +88,6 @@
private static final int INSTALLER_FIELD_ID = 5;
public static final int DEATH_THRESHOLD = 10;
- /**
- * Which native processes to snapshot memory for.
- *
- * <p>Processes are matched by their cmdline in procfs. Example: cat /proc/pid/cmdline returns
- * /system/bin/statsd for the stats daemon.
- */
- private static final Set<String> MEMORY_INTERESTING_NATIVE_PROCESSES = Sets.newHashSet(
- "/system/bin/statsd", // Stats daemon.
- "/system/bin/surfaceflinger",
- "/system/bin/apexd", // APEX daemon.
- "/system/bin/audioserver",
- "/system/bin/cameraserver",
- "/system/bin/drmserver",
- "/system/bin/healthd",
- "/system/bin/incidentd",
- "/system/bin/installd",
- "/system/bin/lmkd", // Low memory killer daemon.
- "/system/bin/logd",
- "media.codec",
- "media.extractor",
- "media.metrics",
- "/system/bin/mediadrmserver",
- "/system/bin/mediaserver",
- "/system/bin/performanced",
- "/system/bin/tombstoned",
- "/system/bin/traced", // Perfetto.
- "/system/bin/traced_probes", // Perfetto.
- "webview_zygote",
- "zygote",
- "zygote64");
- /**
- * Lowest available uid for apps.
- *
- * <p>Used to quickly discard memory snapshots of the zygote forks from native process
- * measurements.
- */
- private static final int MIN_APP_UID = 10_000;
-
- private static final int CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES = 8;
static final class CompanionHandler extends Handler {
CompanionHandler(Looper looper) {
@@ -249,7 +97,6 @@
private final Context mContext;
private final AlarmManager mAlarmManager;
- private final INetworkStatsService mNetworkStatsService;
@GuardedBy("sStatsdLock")
private static IStatsd sStatsd;
private static final Object sStatsdLock = new Object();
@@ -263,52 +110,16 @@
private StatsManagerService mStatsManagerService;
- private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader();
- private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats();
- private WifiManager mWifiManager = null;
- private TelephonyManager mTelephony = null;
@GuardedBy("sStatsdLock")
private final HashSet<Long> mDeathTimeMillis = new HashSet<>();
@GuardedBy("sStatsdLock")
private final HashMap<Long, String> mDeletedFiles = new HashMap<>();
private final CompanionHandler mHandler;
- // Disables throttler on CPU time readers.
- private KernelCpuUidUserSysTimeReader mCpuUidUserSysTimeReader =
- new KernelCpuUidUserSysTimeReader(false);
- private KernelCpuSpeedReader[] mKernelCpuSpeedReaders;
- private KernelCpuUidFreqTimeReader mCpuUidFreqTimeReader =
- new KernelCpuUidFreqTimeReader(false);
- private KernelCpuUidActiveTimeReader mCpuUidActiveTimeReader =
- new KernelCpuUidActiveTimeReader(false);
- private KernelCpuUidClusterTimeReader mCpuUidClusterTimeReader =
- new KernelCpuUidClusterTimeReader(false);
- private StoragedUidIoStatsReader mStoragedUidIoStatsReader =
- new StoragedUidIoStatsReader();
- @Nullable
- private final KernelCpuThreadReaderDiff mKernelCpuThreadReader;
-
- private long mDebugElapsedClockPreviousValue = 0;
- private long mDebugElapsedClockPullCount = 0;
- private long mDebugFailingElapsedClockPreviousValue = 0;
- private long mDebugFailingElapsedClockPullCount = 0;
- private BatteryStatsHelper mBatteryStatsHelper = null;
- private static final int MAX_BATTERY_STATS_HELPER_FREQUENCY_MS = 1000;
- private long mBatteryStatsHelperTimestampMs = -MAX_BATTERY_STATS_HELPER_FREQUENCY_MS;
-
- private static IThermalService sThermalService;
- private File mBaseDir =
- new File(SystemServiceManager.ensureSystemDir(), "stats_companion");
- @GuardedBy("this")
- ProcessCpuTracker mProcessCpuTracker = null;
-
public StatsCompanionService(Context context) {
super();
mContext = context;
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
- mNetworkStatsService = INetworkStatsService.Stub.asInterface(
- ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
- mBaseDir.mkdirs();
mAppUpdateReceiver = new AppUpdateReceiver();
mUserUpdateReceiver = new BroadcastReceiver() {
@Override
@@ -331,47 +142,10 @@
};
mShutdownEventReceiver = new ShutdownEventReceiver();
if (DEBUG) Slog.d(TAG, "Registered receiver for ACTION_PACKAGE_REPLACED and ADDED.");
- PowerProfile powerProfile = new PowerProfile(context);
- final int numClusters = powerProfile.getNumCpuClusters();
- mKernelCpuSpeedReaders = new KernelCpuSpeedReader[numClusters];
- int firstCpuOfCluster = 0;
- for (int i = 0; i < numClusters; i++) {
- final int numSpeedSteps = powerProfile.getNumSpeedStepsInCpuCluster(i);
- mKernelCpuSpeedReaders[i] = new KernelCpuSpeedReader(firstCpuOfCluster,
- numSpeedSteps);
- firstCpuOfCluster += powerProfile.getNumCoresInCpuCluster(i);
- }
-
- // Enable push notifications of throttling from vendor thermal
- // management subsystem via thermalservice.
- IBinder b = ServiceManager.getService("thermalservice");
-
- if (b != null) {
- sThermalService = IThermalService.Stub.asInterface(b);
- try {
- sThermalService.registerThermalEventListener(
- new ThermalEventListener());
- Slog.i(TAG, "register thermal listener successfully");
- } catch (RemoteException e) {
- // Should never happen.
- Slog.e(TAG, "register thermal listener error");
- }
- } else {
- Slog.e(TAG, "cannot find thermalservice, no throttling push notifications");
- }
-
- // Default NetworkRequest should cover all transport types.
- final NetworkRequest request = new NetworkRequest.Builder().build();
- final ConnectivityManager connectivityManager =
- (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- connectivityManager.registerNetworkCallback(request, new ConnectivityStatsCallback());
-
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mHandler = new CompanionHandler(handlerThread.getLooper());
- mKernelCpuThreadReader =
- KernelCpuThreadReaderSettingsObserver.getSettingsModifiedReader(mContext);
}
private final static int[] toIntArray(List<Integer> list) {
@@ -855,8 +629,8 @@
mDeathTimeMillis.add(now);
if (mDeathTimeMillis.size() >= DEATH_THRESHOLD) {
mDeathTimeMillis.clear();
- File[] configs = FileUtils.listFilesOrEmpty(new File(CONFIG_DIR));
- if (configs.length > 0) {
+ File[] configs = new File(CONFIG_DIR).listFiles();
+ if (configs != null && configs.length > 0) {
String fileName = configs[0].getName();
if (configs[0].delete()) {
mDeletedFiles.put(now, fileName);
@@ -909,28 +683,4 @@
}
}
}
-
- // Thermal event received from vendor thermal management subsystem
- private static final class ThermalEventListener extends IThermalEventListener.Stub {
- @Override
- public void notifyThrottling(Temperature temp) {
- StatsLog.write(StatsLog.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED, temp.getType(),
- temp.getName(), (int) (temp.getValue() * 10), temp.getStatus());
- }
- }
-
- private static final class ConnectivityStatsCallback extends
- ConnectivityManager.NetworkCallback {
- @Override
- public void onAvailable(Network network) {
- StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId,
- StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__CONNECTED);
- }
-
- @Override
- public void onLost(Network network) {
- StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId,
- StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__DISCONNECTED);
- }
- }
}
diff --git a/api/current.txt b/api/current.txt
index 0a81742..1cfc99a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6831,6 +6831,7 @@
method public int getLockTaskFeatures(@NonNull android.content.ComponentName);
method @NonNull public String[] getLockTaskPackages(@NonNull android.content.ComponentName);
method @Nullable public CharSequence getLongSupportMessage(@NonNull android.content.ComponentName);
+ method public long getManagedProfileMaximumTimeOff(@NonNull android.content.ComponentName);
method public int getMaximumFailedPasswordsForWipe(@Nullable android.content.ComponentName);
method public long getMaximumTimeToLock(@Nullable android.content.ComponentName);
method @NonNull public java.util.List<java.lang.String> getMeteredDataDisabledPackages(@NonNull android.content.ComponentName);
@@ -6960,6 +6961,7 @@
method public void setLockdownAdminConfiguredNetworks(@NonNull android.content.ComponentName, boolean);
method public void setLogoutEnabled(@NonNull android.content.ComponentName, boolean);
method public void setLongSupportMessage(@NonNull android.content.ComponentName, @Nullable CharSequence);
+ method public void setManagedProfileMaximumTimeOff(@NonNull android.content.ComponentName, long);
method public void setMasterVolumeMuted(@NonNull android.content.ComponentName, boolean);
method public void setMaximumFailedPasswordsForWipe(@NonNull android.content.ComponentName, int);
method public void setMaximumTimeToLock(@NonNull android.content.ComponentName, long);
@@ -7146,6 +7148,7 @@
field public static final int PERMISSION_POLICY_PROMPT = 0; // 0x0
field public static final int PERSONAL_APPS_NOT_SUSPENDED = 0; // 0x0
field public static final int PERSONAL_APPS_SUSPENDED_EXPLICITLY = 1; // 0x1
+ field public static final int PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT = 2; // 0x2
field public static final String POLICY_DISABLE_CAMERA = "policy_disable_camera";
field public static final String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
@@ -16734,6 +16737,7 @@
field public static final String STRING_TYPE_GYROSCOPE_UNCALIBRATED = "android.sensor.gyroscope_uncalibrated";
field public static final String STRING_TYPE_HEART_BEAT = "android.sensor.heart_beat";
field public static final String STRING_TYPE_HEART_RATE = "android.sensor.heart_rate";
+ field public static final String STRING_TYPE_HINGE_ANGLE = "android.sensor.hinge_angle";
field public static final String STRING_TYPE_LIGHT = "android.sensor.light";
field public static final String STRING_TYPE_LINEAR_ACCELERATION = "android.sensor.linear_acceleration";
field public static final String STRING_TYPE_LOW_LATENCY_OFFBODY_DETECT = "android.sensor.low_latency_offbody_detect";
@@ -16763,6 +16767,7 @@
field public static final int TYPE_GYROSCOPE_UNCALIBRATED = 16; // 0x10
field public static final int TYPE_HEART_BEAT = 31; // 0x1f
field public static final int TYPE_HEART_RATE = 21; // 0x15
+ field public static final int TYPE_HINGE_ANGLE = 36; // 0x24
field public static final int TYPE_LIGHT = 5; // 0x5
field public static final int TYPE_LINEAR_ACCELERATION = 10; // 0xa
field public static final int TYPE_LOW_LATENCY_OFFBODY_DETECT = 34; // 0x22
@@ -25983,6 +25988,7 @@
field public static final String KEY_CAPTURE_RATE = "capture-rate";
field public static final String KEY_CHANNEL_COUNT = "channel-count";
field public static final String KEY_CHANNEL_MASK = "channel-mask";
+ field public static final String KEY_CODECS_STRING = "codecs-string";
field public static final String KEY_COLOR_FORMAT = "color-format";
field public static final String KEY_COLOR_RANGE = "color-range";
field public static final String KEY_COLOR_STANDARD = "color-standard";
@@ -30043,6 +30049,7 @@
method public int getLinkDownstreamBandwidthKbps();
method public int getLinkUpstreamBandwidthKbps();
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+ method public int getOwnerUid();
method public int getSignalStrength();
method @Nullable public android.net.TransportInfo getTransportInfo();
method public boolean hasCapability(int);
@@ -30052,6 +30059,7 @@
method @NonNull public android.net.NetworkCapabilities setLinkDownstreamBandwidthKbps(int);
method @NonNull public android.net.NetworkCapabilities setLinkUpstreamBandwidthKbps(int);
method @NonNull public android.net.NetworkCapabilities setNetworkSpecifier(@NonNull android.net.NetworkSpecifier);
+ method public void setOwnerUid(int);
method @NonNull public android.net.NetworkCapabilities setSignalStrength(int);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
@@ -43122,6 +43130,307 @@
}
+package android.service.controls {
+
+ public final class Control implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.app.PendingIntent getAppIntent();
+ method @NonNull public String getControlId();
+ method @NonNull public android.service.controls.templates.ControlTemplate getControlTemplate();
+ method @Nullable public android.content.res.ColorStateList getCustomColor();
+ method @Nullable public android.graphics.drawable.Icon getCustomIcon();
+ method public int getDeviceType();
+ method public int getStatus();
+ method @NonNull public CharSequence getStatusText();
+ method @Nullable public CharSequence getStructure();
+ method @NonNull public CharSequence getSubtitle();
+ method @NonNull public CharSequence getTitle();
+ method @Nullable public CharSequence getZone();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.controls.Control> CREATOR;
+ field public static final int STATUS_DISABLED = 4; // 0x4
+ field public static final int STATUS_ERROR = 3; // 0x3
+ field public static final int STATUS_NOT_FOUND = 2; // 0x2
+ field public static final int STATUS_OK = 1; // 0x1
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class Control.StatefulBuilder {
+ ctor public Control.StatefulBuilder(@NonNull String, @NonNull android.app.PendingIntent);
+ ctor public Control.StatefulBuilder(@NonNull android.service.controls.Control);
+ method @NonNull public android.service.controls.Control build();
+ method @NonNull public android.service.controls.Control.StatefulBuilder setAppIntent(@NonNull android.app.PendingIntent);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setControlId(@NonNull String);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setControlTemplate(@NonNull android.service.controls.templates.ControlTemplate);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setCustomColor(@Nullable android.content.res.ColorStateList);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setCustomIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setDeviceType(int);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setStatus(int);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setStatusText(@NonNull CharSequence);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setStructure(@Nullable CharSequence);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setSubtitle(@NonNull CharSequence);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setTitle(@NonNull CharSequence);
+ method @NonNull public android.service.controls.Control.StatefulBuilder setZone(@Nullable CharSequence);
+ }
+
+ public static final class Control.StatelessBuilder {
+ ctor public Control.StatelessBuilder(@NonNull String, @NonNull android.app.PendingIntent);
+ ctor public Control.StatelessBuilder(@NonNull android.service.controls.Control);
+ method @NonNull public android.service.controls.Control build();
+ method @NonNull public android.service.controls.Control.StatelessBuilder setAppIntent(@NonNull android.app.PendingIntent);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setControlId(@NonNull String);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setCustomColor(@Nullable android.content.res.ColorStateList);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setCustomIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setDeviceType(int);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setStructure(@Nullable CharSequence);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setSubtitle(@NonNull CharSequence);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setTitle(@NonNull CharSequence);
+ method @NonNull public android.service.controls.Control.StatelessBuilder setZone(@Nullable CharSequence);
+ }
+
+ public abstract class ControlsProviderService extends android.app.Service {
+ ctor public ControlsProviderService();
+ method public abstract void loadAvailableControls(@NonNull java.util.function.Consumer<java.util.List<android.service.controls.Control>>);
+ method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public abstract void performControlAction(@NonNull String, @NonNull android.service.controls.actions.ControlAction, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @NonNull public abstract java.util.concurrent.Flow.Publisher<android.service.controls.Control> publisherFor(@NonNull java.util.List<java.lang.String>);
+ field public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService";
+ field @NonNull public static final String TAG = "ControlsProviderService";
+ }
+
+ public class DeviceTypes {
+ method public static boolean validDeviceType(int);
+ field public static final int TYPE_AC_HEATER = 1; // 0x1
+ field public static final int TYPE_AC_UNIT = 2; // 0x2
+ field public static final int TYPE_AIR_FRESHENER = 3; // 0x3
+ field public static final int TYPE_AIR_PURIFIER = 4; // 0x4
+ field public static final int TYPE_AWNING = 33; // 0x21
+ field public static final int TYPE_BLINDS = 34; // 0x22
+ field public static final int TYPE_CAMERA = 50; // 0x32
+ field public static final int TYPE_CLOSET = 35; // 0x23
+ field public static final int TYPE_COFFEE_MAKER = 5; // 0x5
+ field public static final int TYPE_CURTAIN = 36; // 0x24
+ field public static final int TYPE_DEHUMIDIFIER = 6; // 0x6
+ field public static final int TYPE_DISHWASHER = 24; // 0x18
+ field public static final int TYPE_DISPLAY = 7; // 0x7
+ field public static final int TYPE_DOOR = 37; // 0x25
+ field public static final int TYPE_DOORBELL = 51; // 0x33
+ field public static final int TYPE_DRAWER = 38; // 0x26
+ field public static final int TYPE_DRYER = 25; // 0x19
+ field public static final int TYPE_FAN = 8; // 0x8
+ field public static final int TYPE_GARAGE = 39; // 0x27
+ field public static final int TYPE_GATE = 40; // 0x28
+ field public static final int TYPE_GENERIC_ARM_DISARM = -5; // 0xfffffffb
+ field public static final int TYPE_GENERIC_LOCK_UNLOCK = -4; // 0xfffffffc
+ field public static final int TYPE_GENERIC_ON_OFF = -1; // 0xffffffff
+ field public static final int TYPE_GENERIC_OPEN_CLOSE = -3; // 0xfffffffd
+ field public static final int TYPE_GENERIC_START_STOP = -2; // 0xfffffffe
+ field public static final int TYPE_GENERIC_TEMP_SETTING = -6; // 0xfffffffa
+ field public static final int TYPE_GENERIC_VIEWSTREAM = -7; // 0xfffffff9
+ field public static final int TYPE_HEATER = 47; // 0x2f
+ field public static final int TYPE_HOOD = 10; // 0xa
+ field public static final int TYPE_HUMIDIFIER = 11; // 0xb
+ field public static final int TYPE_KETTLE = 12; // 0xc
+ field public static final int TYPE_LIGHT = 13; // 0xd
+ field public static final int TYPE_LOCK = 45; // 0x2d
+ field public static final int TYPE_MICROWAVE = 14; // 0xe
+ field public static final int TYPE_MOP = 26; // 0x1a
+ field public static final int TYPE_MOWER = 27; // 0x1b
+ field public static final int TYPE_MULTICOOKER = 28; // 0x1c
+ field public static final int TYPE_OUTLET = 15; // 0xf
+ field public static final int TYPE_PERGOLA = 41; // 0x29
+ field public static final int TYPE_RADIATOR = 16; // 0x10
+ field public static final int TYPE_REFRIGERATOR = 48; // 0x30
+ field public static final int TYPE_REMOTE_CONTROL = 17; // 0x11
+ field public static final int TYPE_SECURITY_SYSTEM = 46; // 0x2e
+ field public static final int TYPE_SET_TOP = 18; // 0x12
+ field public static final int TYPE_SHOWER = 29; // 0x1d
+ field public static final int TYPE_SHUTTER = 42; // 0x2a
+ field public static final int TYPE_SPRINKLER = 30; // 0x1e
+ field public static final int TYPE_STANDMIXER = 19; // 0x13
+ field public static final int TYPE_STYLER = 20; // 0x14
+ field public static final int TYPE_SWITCH = 21; // 0x15
+ field public static final int TYPE_THERMOSTAT = 49; // 0x31
+ field public static final int TYPE_TV = 22; // 0x16
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_VACUUM = 32; // 0x20
+ field public static final int TYPE_VALVE = 44; // 0x2c
+ field public static final int TYPE_WASHER = 31; // 0x1f
+ field public static final int TYPE_WATER_HEATER = 23; // 0x17
+ field public static final int TYPE_WINDOW = 43; // 0x2b
+ }
+
+}
+
+package android.service.controls.actions {
+
+ public final class BooleanAction extends android.service.controls.actions.ControlAction {
+ ctor public BooleanAction(@NonNull String, boolean);
+ ctor public BooleanAction(@NonNull String, boolean, @Nullable String);
+ method public int getActionType();
+ method public boolean getNewState();
+ }
+
+ public final class CommandAction extends android.service.controls.actions.ControlAction {
+ ctor public CommandAction(@NonNull String, @Nullable String);
+ ctor public CommandAction(@NonNull String);
+ method public int getActionType();
+ }
+
+ public abstract class ControlAction {
+ method public abstract int getActionType();
+ method @Nullable public String getChallengeValue();
+ method @NonNull public String getTemplateId();
+ method public static final boolean isValidResponse(int);
+ field @NonNull public static final android.service.controls.actions.ControlAction ERROR_ACTION;
+ field public static final int RESPONSE_CHALLENGE_ACK = 3; // 0x3
+ field public static final int RESPONSE_CHALLENGE_PASSPHRASE = 5; // 0x5
+ field public static final int RESPONSE_CHALLENGE_PIN = 4; // 0x4
+ field public static final int RESPONSE_FAIL = 2; // 0x2
+ field public static final int RESPONSE_OK = 1; // 0x1
+ field public static final int RESPONSE_UNKNOWN = 0; // 0x0
+ field public static final int TYPE_BOOLEAN = 1; // 0x1
+ field public static final int TYPE_COMMAND = 5; // 0x5
+ field public static final int TYPE_ERROR = -1; // 0xffffffff
+ field public static final int TYPE_FLOAT = 2; // 0x2
+ field public static final int TYPE_MODE = 4; // 0x4
+ field public static final int TYPE_MULTI_FLOAT = 3; // 0x3
+ }
+
+ public final class FloatAction extends android.service.controls.actions.ControlAction {
+ ctor public FloatAction(@NonNull String, float);
+ ctor public FloatAction(@NonNull String, float, @Nullable String);
+ method public int getActionType();
+ method public float getNewValue();
+ }
+
+ public final class ModeAction extends android.service.controls.actions.ControlAction {
+ ctor public ModeAction(@NonNull String, int, @Nullable String);
+ ctor public ModeAction(@NonNull String, int);
+ method public int getActionType();
+ method public int getNewMode();
+ }
+
+ public final class MultiFloatAction extends android.service.controls.actions.ControlAction {
+ ctor public MultiFloatAction(@NonNull String, @NonNull float[], @Nullable String);
+ ctor public MultiFloatAction(@NonNull String, @NonNull float[]);
+ method public int getActionType();
+ method @NonNull public float[] getNewValues();
+ }
+
+}
+
+package android.service.controls.templates {
+
+ public final class ControlButton implements android.os.Parcelable {
+ ctor public ControlButton(boolean, @NonNull CharSequence);
+ method public int describeContents();
+ method @NonNull public CharSequence getActionDescription();
+ method public boolean isChecked();
+ method @NonNull public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.controls.templates.ControlButton> CREATOR;
+ }
+
+ public abstract class ControlTemplate {
+ method @NonNull public String getTemplateId();
+ method public abstract int getTemplateType();
+ field @NonNull public static final android.service.controls.templates.ControlTemplate ERROR_TEMPLATE;
+ field @NonNull public static final android.service.controls.templates.ControlTemplate NO_TEMPLATE;
+ field public static final int TYPE_DISCRETE_TOGGLE = 4; // 0x4
+ field public static final int TYPE_ERROR = -1; // 0xffffffff
+ field public static final int TYPE_NONE = 0; // 0x0
+ field public static final int TYPE_RANGE = 2; // 0x2
+ field public static final int TYPE_STATELESS = 8; // 0x8
+ field public static final int TYPE_TEMPERATURE = 7; // 0x7
+ field public static final int TYPE_THUMBNAIL = 3; // 0x3
+ field public static final int TYPE_TOGGLE = 1; // 0x1
+ field public static final int TYPE_TOGGLE_RANGE = 6; // 0x6
+ }
+
+ public final class CoordinatedRangeTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public CoordinatedRangeTemplate(@NonNull String, float, @NonNull android.service.controls.templates.RangeTemplate, @NonNull android.service.controls.templates.RangeTemplate);
+ ctor public CoordinatedRangeTemplate(@NonNull String, float, float, float, float, float, float, float, float, @Nullable CharSequence);
+ method public float getCurrentValueHigh();
+ method public float getCurrentValueLow();
+ method @NonNull public CharSequence getFormatString();
+ method public float getMaxValueHigh();
+ method public float getMaxValueLow();
+ method public float getMinGap();
+ method public float getMinValueHigh();
+ method public float getMinValueLow();
+ method @NonNull public android.service.controls.templates.RangeTemplate getRangeHigh();
+ method @NonNull public android.service.controls.templates.RangeTemplate getRangeLow();
+ method public float getStepValue();
+ method public int getTemplateType();
+ }
+
+ public final class DiscreteToggleTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public DiscreteToggleTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlButton, @NonNull android.service.controls.templates.ControlButton);
+ method @NonNull public android.service.controls.templates.ControlButton getNegativeButton();
+ method @NonNull public android.service.controls.templates.ControlButton getPositiveButton();
+ method public int getTemplateType();
+ }
+
+ public final class RangeTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public RangeTemplate(@NonNull String, float, float, float, float, @Nullable CharSequence);
+ method public float getCurrentValue();
+ method @NonNull public CharSequence getFormatString();
+ method public float getMaxValue();
+ method public float getMinValue();
+ method public float getStepValue();
+ method public int getTemplateType();
+ }
+
+ public final class StatelessTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public StatelessTemplate(@NonNull String);
+ method public int getTemplateType();
+ }
+
+ public final class TemperatureControlTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public TemperatureControlTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlTemplate, int, int, int);
+ method public int getCurrentActiveMode();
+ method public int getCurrentMode();
+ method public int getModes();
+ method @NonNull public android.service.controls.templates.ControlTemplate getTemplate();
+ method public int getTemplateType();
+ field public static final int FLAG_MODE_COOL = 8; // 0x8
+ field public static final int FLAG_MODE_ECO = 32; // 0x20
+ field public static final int FLAG_MODE_HEAT = 4; // 0x4
+ field public static final int FLAG_MODE_HEAT_COOL = 16; // 0x10
+ field public static final int FLAG_MODE_OFF = 2; // 0x2
+ field public static final int MODE_COOL = 3; // 0x3
+ field public static final int MODE_ECO = 5; // 0x5
+ field public static final int MODE_HEAT = 2; // 0x2
+ field public static final int MODE_HEAT_COOL = 4; // 0x4
+ field public static final int MODE_OFF = 1; // 0x1
+ field public static final int MODE_UNKNOWN = 0; // 0x0
+ }
+
+ public final class ThumbnailTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public ThumbnailTemplate(@NonNull String, @NonNull android.graphics.drawable.Icon, @NonNull CharSequence);
+ method @NonNull public CharSequence getContentDescription();
+ method public int getTemplateType();
+ method @NonNull public android.graphics.drawable.Icon getThumbnail();
+ }
+
+ public final class ToggleRangeTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public ToggleRangeTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlButton, @NonNull android.service.controls.templates.RangeTemplate);
+ ctor public ToggleRangeTemplate(@NonNull String, boolean, @NonNull CharSequence, @NonNull android.service.controls.templates.RangeTemplate);
+ method @NonNull public CharSequence getActionDescription();
+ method @NonNull public android.service.controls.templates.RangeTemplate getRange();
+ method public int getTemplateType();
+ method public boolean isChecked();
+ }
+
+ public final class ToggleTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public ToggleTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlButton);
+ method @NonNull public CharSequence getContentDescription();
+ method public int getTemplateType();
+ method public boolean isChecked();
+ }
+
+}
+
package android.service.dreams {
public class DreamService extends android.app.Service implements android.view.Window.Callback {
diff --git a/api/system-current.txt b/api/system-current.txt
index 2d70cff..1b25e25 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1966,8 +1966,8 @@
public class OverlayManager {
method @Nullable public android.content.om.OverlayInfo getOverlayInfo(@NonNull String, @NonNull android.os.UserHandle);
method @NonNull @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public java.util.List<android.content.om.OverlayInfo> getOverlayInfosForTarget(@NonNull String, @NonNull android.os.UserHandle);
- method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public void setEnabled(@NonNull String, boolean, @NonNull android.os.UserHandle);
- method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public void setEnabledExclusiveInCategory(@NonNull String, @NonNull android.os.UserHandle);
+ method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public void setEnabled(@NonNull String, boolean, @NonNull android.os.UserHandle) throws java.lang.IllegalStateException, java.lang.SecurityException;
+ method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public void setEnabledExclusiveInCategory(@NonNull String, @NonNull android.os.UserHandle) throws java.lang.IllegalStateException, java.lang.SecurityException;
}
}
@@ -3727,10 +3727,12 @@
method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public void setCurrentFunctions(long);
field @RequiresPermission(android.Manifest.permission.MANAGE_USB) public static final String ACTION_USB_PORT_CHANGED = "android.hardware.usb.action.USB_PORT_CHANGED";
field public static final String ACTION_USB_STATE = "android.hardware.usb.action.USB_STATE";
+ field public static final long FUNCTION_NCM = 1024L; // 0x400L
field public static final long FUNCTION_NONE = 0L; // 0x0L
field public static final long FUNCTION_RNDIS = 32L; // 0x20L
field public static final String USB_CONFIGURED = "configured";
field public static final String USB_CONNECTED = "connected";
+ field public static final String USB_FUNCTION_NCM = "ncm";
field public static final String USB_FUNCTION_RNDIS = "rndis";
}
@@ -6488,6 +6490,7 @@
field public static final String EXTRA_ERRORED_TETHER = "erroredArray";
field public static final int TETHERING_BLUETOOTH = 2; // 0x2
field public static final int TETHERING_INVALID = -1; // 0xffffffff
+ field public static final int TETHERING_NCM = 4; // 0x4
field public static final int TETHERING_USB = 1; // 0x1
field public static final int TETHERING_WIFI = 0; // 0x0
field public static final int TETHERING_WIFI_P2P = 3; // 0x3
@@ -8332,7 +8335,7 @@
method @NonNull public android.os.BatterySaverPolicyConfig.Builder setLocationMode(int);
}
- public final class BatteryStatsManager {
+ public class BatteryStatsManager {
method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.os.connectivity.CellularBatteryStats getCellularBatteryStats();
method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.os.connectivity.WifiBatteryStats getWifiBatteryStats();
method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void reportFullWifiLockAcquiredFromSource(@NonNull android.os.WorkSource);
@@ -12191,6 +12194,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void requestNumberVerification(@NonNull android.telephony.PhoneNumberRange, long, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.NumberVerificationCallback);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetAllCarrierActions();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetCarrierKeysForImsiEncryption();
+ method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @WorkerThread public void resetIms(int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean resetRadioConfig();
method @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL) public void resetSettings();
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setAllowedCarriers(int, java.util.List<android.service.carrier.CarrierIdentifier>);
diff --git a/api/test-current.txt b/api/test-current.txt
index 200d465..76af403 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1759,6 +1759,7 @@
field public static final String EXTRA_ERRORED_TETHER = "erroredArray";
field public static final int TETHERING_BLUETOOTH = 2; // 0x2
field public static final int TETHERING_INVALID = -1; // 0xffffffff
+ field public static final int TETHERING_NCM = 4; // 0x4
field public static final int TETHERING_USB = 1; // 0x1
field public static final int TETHERING_WIFI = 0; // 0x0
field public static final int TETHERING_WIFI_P2P = 3; // 0x3
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index d9b3a6c..7950626 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -341,6 +341,8 @@
NotificationReported notification_reported = 244;
NotificationPanelReported notification_panel_reported = 245;
NotificationChannelModified notification_panel_modified = 246;
+ IntegrityCheckResultReported integrity_check_result_reported = 247;
+ IntegrityRulesPushed integrity_rules_pushed = 248;
}
// Pulled events will start at field 10000.
@@ -8070,3 +8072,43 @@
// State of primary user's encryption storage at the moment boot completed. Always set.
optional UserEncryptionState user_encryption_state = 3;
}
+
+/*
+ * Logs integrity check information during each install.
+ *
+ * Logged from:
+ * frameworks/base/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+ */
+message IntegrityCheckResultReported {
+ optional string package_name = 1;
+ optional string app_certificate_hash = 2;
+ optional int64 version_code = 3;
+ optional string installer_package_name = 4;
+ enum Response {
+ UNKNOWN = 0;
+ ALLOWED = 1;
+ REJECTED = 2;
+ FORCE_ALLOWED = 3;
+ }
+ optional Response response = 5;
+ // An estimate on the cause of the response. This will only be populated for
+ // REJECTED and FORCE_ALLOWED
+ optional bool caused_by_app_cert_rule = 6;
+ optional bool caused_by_installer_rule = 7;
+}
+
+/**
+ * Logs the information about the rules and the provider whenever rules are
+ * pushed into AppIntegrityManager.
+ *
+ * Logged from:
+ * frameworks/base/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+ */
+message IntegrityRulesPushed {
+ optional bool success = 1;
+ // Package name of the app that pushed the rules.
+ optional string rule_provider = 2;
+ // Version string of arbitrary format provided by the rule provider to
+ // identify the rules.
+ optional string rule_version = 3;
+}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index fa9dd27..f4dc0bf 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2403,11 +2403,19 @@
public static final int PERSONAL_APPS_SUSPENDED_EXPLICITLY = 1 << 0;
/**
+ * Flag for {@link #getPersonalAppsSuspendedReasons} return value. Set when personal apps are
+ * suspended by framework because managed profile was off for longer than allowed by policy.
+ * @see #setManagedProfileMaximumTimeOff
+ */
+ public static final int PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT = 1 << 1;
+
+ /**
* @hide
*/
@IntDef(flag = true, prefix = { "PERSONAL_APPS_" }, value = {
PERSONAL_APPS_NOT_SUSPENDED,
- PERSONAL_APPS_SUSPENDED_EXPLICITLY
+ PERSONAL_APPS_SUSPENDED_EXPLICITLY,
+ PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT
})
@Retention(RetentionPolicy.SOURCE)
public @interface PersonalAppSuspensionReason {}
@@ -11763,6 +11771,8 @@
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with
* @param suspended Whether personal apps should be suspended.
+ * @throws IllegalStateException if the profile owner doesn't have an activity that handles
+ * {@link #ACTION_CHECK_POLICY_COMPLIANCE}
*/
public void setPersonalAppsSuspended(@NonNull ComponentName admin, boolean suspended) {
throwIfParentInstance("setPersonalAppsSuspended");
@@ -11774,4 +11784,52 @@
}
}
}
+
+ /**
+ * Called by a profile owner of an organization-owned managed profile to set maximum time
+ * the profile is allowed to be turned off. If the profile is turned off for longer, personal
+ * apps are suspended on the device.
+ *
+ * <p>When personal apps are suspended, an ongoing notification about that is shown to the user.
+ * When the user taps the notification, system invokes {@link #ACTION_CHECK_POLICY_COMPLIANCE}
+ * in the profile owner package. Profile owner implementation that uses personal apps suspension
+ * must handle this intent.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with
+ * @param timeoutMs Maximum time the profile is allowed to be off in milliseconds or 0 if
+ * not limited.
+ * @throws IllegalStateException if the profile owner doesn't have an activity that handles
+ * {@link #ACTION_CHECK_POLICY_COMPLIANCE}
+ * @see #setPersonalAppsSuspended
+ */
+ public void setManagedProfileMaximumTimeOff(@NonNull ComponentName admin, long timeoutMs) {
+ throwIfParentInstance("setManagedProfileMaximumTimeOff");
+ if (mService != null) {
+ try {
+ mService.setManagedProfileMaximumTimeOff(admin, timeoutMs);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by a profile owner of an organization-owned managed profile to get maximum time
+ * the profile is allowed to be turned off.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with
+ * @return Maximum time the profile is allowed to be off in milliseconds or 0 if not limited.
+ * @see #setPersonalAppsSuspended
+ */
+ public long getManagedProfileMaximumTimeOff(@NonNull ComponentName admin) {
+ throwIfParentInstance("getManagedProfileMaximumTimeOff");
+ if (mService != null) {
+ try {
+ return mService.getManagedProfileMaximumTimeOff(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return 0;
+ }
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 3d6bf9d..ab0598b 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -473,4 +473,7 @@
int getPersonalAppsSuspendedReasons(in ComponentName admin);
void setPersonalAppsSuspended(in ComponentName admin, boolean suspended);
+
+ long getManagedProfileMaximumTimeOff(in ComponentName admin);
+ void setManagedProfileMaximumTimeOff(in ComponentName admin, long timeoutMs);
}
diff --git a/core/java/android/content/om/OverlayManager.java b/core/java/android/content/om/OverlayManager.java
index a2f8886..33d1776 100644
--- a/core/java/android/content/om/OverlayManager.java
+++ b/core/java/android/content/om/OverlayManager.java
@@ -22,7 +22,11 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.Context;
+import android.os.Build;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
@@ -40,6 +44,10 @@
private final IOverlayManager mService;
private final Context mContext;
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
+ private static final long THROW_SECURITY_EXCEPTIONS = 147340954;
+
/**
* Creates a new instance.
*
@@ -69,6 +77,9 @@
* @param packageName the name of the overlay package to enable.
* @param user The user for which to change the overlay.
*
+ * @throws SecurityException when caller is not allowed to enable {@param packageName}
+ * @throws IllegalStateException when enabling fails otherwise
+ *
* @hide
*/
@SystemApi
@@ -77,11 +88,13 @@
"android.permission.INTERACT_ACROSS_USERS_FULL"
})
public void setEnabledExclusiveInCategory(@NonNull final String packageName,
- @NonNull UserHandle user) {
+ @NonNull UserHandle user) throws SecurityException, IllegalStateException {
try {
if (!mService.setEnabledExclusiveInCategory(packageName, user.getIdentifier())) {
throw new IllegalStateException("setEnabledExclusiveInCategory failed");
}
+ } catch (SecurityException e) {
+ rethrowSecurityException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -97,6 +110,9 @@
* @param enable {@code false} if the overlay should be turned off.
* @param user The user for which to change the overlay.
*
+ * @throws SecurityException when caller is not allowed to enable/disable {@param packageName}
+ * @throws IllegalStateException when enabling/disabling fails otherwise
+ *
* @hide
*/
@SystemApi
@@ -105,15 +121,16 @@
"android.permission.INTERACT_ACROSS_USERS_FULL"
})
public void setEnabled(@NonNull final String packageName, final boolean enable,
- @NonNull UserHandle user) {
+ @NonNull UserHandle user) throws SecurityException, IllegalStateException {
try {
if (!mService.setEnabled(packageName, enable, user.getIdentifier())) {
throw new IllegalStateException("setEnabled failed");
}
+ } catch (SecurityException e) {
+ rethrowSecurityException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- return;
}
/**
@@ -187,4 +204,29 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Starting on R, actor enforcement and app visibility changes introduce additional failure
+ * cases, but the SecurityException thrown with these checks is unexpected for existing
+ * consumers of the API.
+ *
+ * The only prior case it would be thrown is with a permission failure, but the calling
+ * application would be able to verify that themselves, and so they may choose to ignore
+ * catching SecurityException when calling these APIs.
+ *
+ * For R, this no longer holds true, and SecurityExceptions can be thrown for any number of
+ * reasons, none of which are exposed to the caller. So for consumers targeting below R,
+ * transform these SecurityExceptions into IllegalStateExceptions, which are a little more
+ * expected to be thrown by the setEnabled APIs.
+ *
+ * This will mask the prior permission exception if it applies, but it's assumed that apps
+ * wouldn't call the APIs without the permission on prior versions, and so it's safe to ignore.
+ */
+ private void rethrowSecurityException(SecurityException e) {
+ if (!Compatibility.isChangeEnabled(THROW_SECURITY_EXCEPTIONS)) {
+ throw new IllegalStateException(e);
+ } else {
+ throw e;
+ }
+ }
}
diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java
index a71a7b6..cc4c456 100644
--- a/core/java/android/hardware/Sensor.java
+++ b/core/java/android/hardware/Sensor.java
@@ -693,6 +693,22 @@
"android.sensor.accelerometer_uncalibrated";
/**
+ * A constant describing a hinge angle sensor.
+ *
+ * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
+ *
+ */
+ public static final int TYPE_HINGE_ANGLE = 36;
+
+ /**
+ * A constant string describing a hinge angle sensor.
+ *
+ * @see #TYPE_HINGE_ANGLE
+ *
+ */
+ public static final String STRING_TYPE_HINGE_ANGLE = "android.sensor.hinge_angle";
+
+ /**
* A constant describing all sensor types.
*/
@@ -811,6 +827,7 @@
16, // skip over additional sensor info type
1, // SENSOR_TYPE_LOW_LATENCY_OFFBODY_DETECT
6, // SENSOR_TYPE_ACCELEROMETER_UNCALIBRATED
+ 1, // SENSOR_TYPE_HINGE_ANGLE
};
/**
@@ -1226,6 +1243,8 @@
case TYPE_ACCELEROMETER_UNCALIBRATED:
mStringType = STRING_TYPE_ACCELEROMETER_UNCALIBRATED;
return true;
+ case TYPE_HINGE_ANGLE:
+ mStringType = STRING_TYPE_HINGE_ANGLE;
default:
return false;
}
diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java
index 64c45bf..5fbf0da 100644
--- a/core/java/android/hardware/SensorEvent.java
+++ b/core/java/android/hardware/SensorEvent.java
@@ -630,6 +630,16 @@
* x_bias, y_bias, z_bias are the estimated biases.
* </p>
*
+ * <h4>{@link android.hardware.Sensor#TYPE_HINGE_ANGLE Sensor.TYPE_HINGE_ANGLE}:</h4>
+ *
+ * A sensor of this type measures the angle, in degrees, between two integral parts of the
+ * device. Movement of a hinge measured by this sensor type is expected to alter the ways in
+ * which the user may interact with the device, for example by unfolding or revealing a display.
+ *
+ * <ul>
+ * <li> values[0]: Measured hinge angle between 0 and 360 degrees inclusive</li>
+ * </ul>
+ *
* @see GeomagneticField
*/
public final float[] values;
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index 827353b..086db10 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -262,6 +262,15 @@
public static final String USB_FUNCTION_ACCESSORY = "accessory";
/**
+ * Name of the NCM USB function.
+ * Used in extras for the {@link #ACTION_USB_STATE} broadcast
+ *
+ * {@hide}
+ */
+ @SystemApi
+ public static final String USB_FUNCTION_NCM = "ncm";
+
+ /**
* Name of extra for {@link #ACTION_USB_PORT_CHANGED}
* containing the {@link UsbPort} object for the port.
*
@@ -367,8 +376,15 @@
*/
public static final long FUNCTION_ADB = GadgetFunction.ADB;
+ /**
+ * Code for the ncm source usb function.
+ * {@hide}
+ */
+ @SystemApi
+ public static final long FUNCTION_NCM = 1 << 10;
+
private static final long SETTABLE_FUNCTIONS = FUNCTION_MTP | FUNCTION_PTP | FUNCTION_RNDIS
- | FUNCTION_MIDI;
+ | FUNCTION_MIDI | FUNCTION_NCM;
private static final Map<String, Long> FUNCTION_NAME_TO_CODE = new HashMap<>();
@@ -380,6 +396,7 @@
FUNCTION_NAME_TO_CODE.put(UsbManager.USB_FUNCTION_ACCESSORY, FUNCTION_ACCESSORY);
FUNCTION_NAME_TO_CODE.put(UsbManager.USB_FUNCTION_AUDIO_SOURCE, FUNCTION_AUDIO_SOURCE);
FUNCTION_NAME_TO_CODE.put(UsbManager.USB_FUNCTION_ADB, FUNCTION_ADB);
+ FUNCTION_NAME_TO_CODE.put(UsbManager.USB_FUNCTION_NCM, FUNCTION_NCM);
}
private final Context mContext;
@@ -954,6 +971,9 @@
if ((functions & FUNCTION_AUDIO_SOURCE) != 0) {
joiner.add(UsbManager.USB_FUNCTION_AUDIO_SOURCE);
}
+ if ((functions & FUNCTION_NCM) != 0) {
+ joiner.add(UsbManager.USB_FUNCTION_NCM);
+ }
if ((functions & FUNCTION_ADB) != 0) {
joiner.add(UsbManager.USB_FUNCTION_ADB);
}
diff --git a/core/java/android/net/ConnectivityDiagnosticsManager.java b/core/java/android/net/ConnectivityDiagnosticsManager.java
index 4564813..b13e4b7 100644
--- a/core/java/android/net/ConnectivityDiagnosticsManager.java
+++ b/core/java/android/net/ConnectivityDiagnosticsManager.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.content.Context;
+import android.os.Binder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
@@ -544,6 +545,53 @@
};
}
+ /** @hide */
+ @VisibleForTesting
+ public static class ConnectivityDiagnosticsBinder
+ extends IConnectivityDiagnosticsCallback.Stub {
+ @NonNull private final ConnectivityDiagnosticsCallback mCb;
+ @NonNull private final Executor mExecutor;
+
+ /** @hide */
+ @VisibleForTesting
+ public ConnectivityDiagnosticsBinder(
+ @NonNull ConnectivityDiagnosticsCallback cb, @NonNull Executor executor) {
+ this.mCb = cb;
+ this.mExecutor = executor;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public void onConnectivityReport(@NonNull ConnectivityReport report) {
+ Binder.withCleanCallingIdentity(() -> {
+ mExecutor.execute(() -> {
+ mCb.onConnectivityReport(report);
+ });
+ });
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public void onDataStallSuspected(@NonNull DataStallReport report) {
+ Binder.withCleanCallingIdentity(() -> {
+ mExecutor.execute(() -> {
+ mCb.onDataStallSuspected(report);
+ });
+ });
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public void onNetworkConnectivityReported(
+ @NonNull Network network, boolean hasConnectivity) {
+ Binder.withCleanCallingIdentity(() -> {
+ mExecutor.execute(() -> {
+ mCb.onNetworkConnectivityReported(network, hasConnectivity);
+ });
+ });
+ }
+ }
+
/**
* Abstract base class for Connectivity Diagnostics callbacks. Used for notifications about
* network connectivity events. Must be extended by applications wanting notifications.
diff --git a/core/java/android/net/IConnectivityDiagnosticsCallback.aidl b/core/java/android/net/IConnectivityDiagnosticsCallback.aidl
new file mode 100644
index 0000000..3a161bf
--- /dev/null
+++ b/core/java/android/net/IConnectivityDiagnosticsCallback.aidl
@@ -0,0 +1,28 @@
+/**
+ *
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.Network;
+
+/** @hide */
+oneway interface IConnectivityDiagnosticsCallback {
+ void onConnectivityReport(in ConnectivityDiagnosticsManager.ConnectivityReport report);
+ void onDataStallSuspected(in ConnectivityDiagnosticsManager.DataStallReport report);
+ void onNetworkConnectivityReported(in Network n, boolean hasConnectivity);
+}
\ No newline at end of file
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 186196bd..3e9e7fa 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -18,6 +18,7 @@
import android.app.PendingIntent;
import android.net.ConnectionInfo;
+import android.net.IConnectivityDiagnosticsCallback;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkAgentConfig;
@@ -211,5 +212,9 @@
boolean isCallerCurrentAlwaysOnVpnApp();
boolean isCallerCurrentAlwaysOnVpnLockdownApp();
+ void registerConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback,
+ in NetworkRequest request);
+ void unregisterConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback);
+
IBinder startOrGetTestNetworkService();
}
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
index a94109d..4f4e27b 100644
--- a/core/java/android/net/NetworkCapabilities.java
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -26,6 +26,7 @@
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.Process;
import android.util.ArraySet;
import android.util.proto.ProtoOutputStream;
@@ -58,7 +59,6 @@
*/
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
- private static final int INVALID_UID = -1;
// Set to true when private DNS is broken.
private boolean mPrivateDnsBroken;
@@ -85,8 +85,8 @@
mTransportInfo = null;
mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
mUids = null;
- mEstablishingVpnAppUid = INVALID_UID;
mAdministratorUids.clear();
+ mOwnerUid = Process.INVALID_UID;
mSSID = null;
mPrivateDnsBroken = false;
}
@@ -104,8 +104,8 @@
mTransportInfo = nc.mTransportInfo;
mSignalStrength = nc.mSignalStrength;
setUids(nc.mUids); // Will make the defensive copy
- mEstablishingVpnAppUid = nc.mEstablishingVpnAppUid;
setAdministratorUids(nc.mAdministratorUids);
+ mOwnerUid = nc.mOwnerUid;
mUnwantedNetworkCapabilities = nc.mUnwantedNetworkCapabilities;
mSSID = nc.mSSID;
mPrivateDnsBroken = nc.mPrivateDnsBroken;
@@ -810,31 +810,26 @@
}
/**
- * UID of the app that manages this network, or INVALID_UID if none/unknown.
+ * UID of the app that owns this network, or INVALID_UID if none/unknown.
*
- * This field keeps track of the UID of the app that created this network and is in charge
- * of managing it. In the practice, it is used to store the UID of VPN apps so it is named
- * accordingly, but it may be renamed if other mechanisms are offered for third party apps
- * to create networks.
- *
- * Because this field is only used in the services side (and to avoid apps being able to
- * set this to whatever they want), this field is not parcelled and will not be conserved
- * across the IPC boundary.
- * @hide
+ * <p>This field keeps track of the UID of the app that created this network and is in charge of
+ * its lifecycle. This could be the UID of apps such as the Wifi network suggestor, the running
+ * VPN, or Carrier Service app managing a cellular data connection.
*/
- private int mEstablishingVpnAppUid = INVALID_UID;
+ private int mOwnerUid = Process.INVALID_UID;
/**
- * Set the UID of the managing app.
- * @hide
+ * Set the UID of the owner app.
*/
- public void setEstablishingVpnAppUid(final int uid) {
- mEstablishingVpnAppUid = uid;
+ public void setOwnerUid(final int uid) {
+ mOwnerUid = uid;
}
- /** @hide */
- public int getEstablishingVpnAppUid() {
- return mEstablishingVpnAppUid;
+ /**
+ * Retrieves the UID of the owner app.
+ */
+ public int getOwnerUid() {
+ return mOwnerUid;
}
/**
@@ -1157,7 +1152,7 @@
* member is null, then the network is not restricted by app UID. If it's an empty list, then
* it means nobody can use it.
* As a special exception, the app managing this network (as identified by its UID stored in
- * mEstablishingVpnAppUid) can always see this network. This is embodied by a special check in
+ * mOwnerUid) can always see this network. This is embodied by a special check in
* satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong>
* to the app that manages it as determined by #appliesToUid.
* <p>
@@ -1264,7 +1259,7 @@
* in the passed nc (representing the UIDs that this network is available to).
* <p>
* As a special exception, the UID that created the passed network (as represented by its
- * mEstablishingVpnAppUid field) always satisfies a NetworkRequest requiring it (of LISTEN
+ * mOwnerUid field) always satisfies a NetworkRequest requiring it (of LISTEN
* or REQUEST types alike), even if the network does not apply to it. That is so a VPN app
* can see its own network when it listens for it.
* <p>
@@ -1275,7 +1270,7 @@
public boolean satisfiedByUids(@NonNull NetworkCapabilities nc) {
if (null == nc.mUids || null == mUids) return true; // The network satisfies everything.
for (UidRange requiredRange : mUids) {
- if (requiredRange.contains(nc.mEstablishingVpnAppUid)) return true;
+ if (requiredRange.contains(nc.mOwnerUid)) return true;
if (!nc.appliesToUidRange(requiredRange)) {
return false;
}
@@ -1541,6 +1536,7 @@
dest.writeString(mSSID);
dest.writeBoolean(mPrivateDnsBroken);
dest.writeList(mAdministratorUids);
+ dest.writeInt(mOwnerUid);
}
public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -1562,6 +1558,7 @@
netCap.mSSID = in.readString();
netCap.mPrivateDnsBroken = in.readBoolean();
netCap.setAdministratorUids(in.readArrayList(null));
+ netCap.mOwnerUid = in.readInt();
return netCap;
}
@Override
@@ -1611,8 +1608,8 @@
sb.append(" Uids: <").append(mUids).append(">");
}
}
- if (mEstablishingVpnAppUid != INVALID_UID) {
- sb.append(" EstablishingAppUid: ").append(mEstablishingVpnAppUid);
+ if (mOwnerUid != Process.INVALID_UID) {
+ sb.append(" OwnerUid: ").append(mOwnerUid);
}
if (!mAdministratorUids.isEmpty()) {
diff --git a/core/java/android/os/BatteryStatsManager.java b/core/java/android/os/BatteryStatsManager.java
index f2e16b4..152141e 100644
--- a/core/java/android/os/BatteryStatsManager.java
+++ b/core/java/android/os/BatteryStatsManager.java
@@ -42,7 +42,7 @@
*/
@SystemApi
@SystemService(Context.BATTERY_STATS_SERVICE)
-public final class BatteryStatsManager {
+public class BatteryStatsManager {
/**
* Wifi states.
*
diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java
index 43a308c..2d1d0ed 100644
--- a/core/java/android/service/controls/Control.java
+++ b/core/java/android/service/controls/Control.java
@@ -19,12 +19,16 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Icon;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.controls.actions.ControlAction;
import android.service.controls.templates.ControlTemplate;
+import android.service.controls.templates.ControlTemplateWrapper;
import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -51,9 +55,8 @@
* <p>
* An {@link Intent} linking to the provider Activity that expands on this {@link Control} and
* allows for further actions should be provided.
- * @hide
*/
-public class Control implements Parcelable {
+public final class Control implements Parcelable {
private static final String TAG = "Control";
private static final int NUM_STATUS = 5;
@@ -99,6 +102,10 @@
private final @Nullable CharSequence mStructure;
private final @Nullable CharSequence mZone;
private final @NonNull PendingIntent mAppIntent;
+
+ private final @Nullable Icon mCustomIcon;
+ private final @Nullable ColorStateList mCustomColor;
+
private final @Status int mStatus;
private final @NonNull ControlTemplate mControlTemplate;
private final @NonNull CharSequence mStatusText;
@@ -113,14 +120,21 @@
* @param zone
* @param appIntent a {@link PendingIntent} linking to a page to interact with the
* corresponding device.
+ * @param customIcon
+ * @param customColor
+ * @param status
+ * @param controlTemplate
+ * @param statusText
*/
- public Control(@NonNull String controlId,
+ Control(@NonNull String controlId,
@DeviceTypes.DeviceType int deviceType,
@NonNull CharSequence title,
@NonNull CharSequence subtitle,
@Nullable CharSequence structure,
@Nullable CharSequence zone,
@NonNull PendingIntent appIntent,
+ @Nullable Icon customIcon,
+ @Nullable ColorStateList customColor,
@Status int status,
@NonNull ControlTemplate controlTemplate,
@NonNull CharSequence statusText) {
@@ -142,6 +156,10 @@
mStructure = structure;
mZone = zone;
mAppIntent = appIntent;
+
+ mCustomColor = customColor;
+ mCustomIcon = customIcon;
+
if (status < 0 || status >= NUM_STATUS) {
mStatus = STATUS_UNKNOWN;
Log.e(TAG, "Status unknown:" + status);
@@ -152,7 +170,11 @@
mStatusText = statusText;
}
- public Control(Parcel in) {
+ /**
+ * @param in
+ * @hide
+ */
+ Control(Parcel in) {
mControlId = in.readString();
mDeviceType = in.readInt();
mTitle = in.readCharSequence();
@@ -168,8 +190,22 @@
mZone = null;
}
mAppIntent = PendingIntent.CREATOR.createFromParcel(in);
+
+ if (in.readByte() == (byte) 1) {
+ mCustomIcon = Icon.CREATOR.createFromParcel(in);
+ } else {
+ mCustomIcon = null;
+ }
+
+ if (in.readByte() == (byte) 1) {
+ mCustomColor = ColorStateList.CREATOR.createFromParcel(in);
+ } else {
+ mCustomColor = null;
+ }
+
mStatus = in.readInt();
- mControlTemplate = ControlTemplate.CREATOR.createFromParcel(in);
+ ControlTemplateWrapper wrapper = ControlTemplateWrapper.CREATOR.createFromParcel(in);
+ mControlTemplate = wrapper.getWrappedTemplate();
mStatusText = in.readCharSequence();
}
@@ -208,6 +244,16 @@
return mAppIntent;
}
+ @Nullable
+ public Icon getCustomIcon() {
+ return mCustomIcon;
+ }
+
+ @Nullable
+ public ColorStateList getCustomColor() {
+ return mCustomColor;
+ }
+
@Status
public int getStatus() {
return mStatus;
@@ -229,7 +275,7 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mControlId);
dest.writeInt(mDeviceType);
dest.writeCharSequence(mTitle);
@@ -247,14 +293,27 @@
dest.writeByte((byte) 0);
}
mAppIntent.writeToParcel(dest, flags);
+ if (mCustomIcon != null) {
+ dest.writeByte((byte) 1);
+ mCustomIcon.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mCustomColor != null) {
+ dest.writeByte((byte) 1);
+ mCustomColor.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+
dest.writeInt(mStatus);
- mControlTemplate.writeToParcel(dest, flags);
+ new ControlTemplateWrapper(mControlTemplate).writeToParcel(dest, flags);
dest.writeCharSequence(mStatusText);
}
- public static final Creator<Control> CREATOR = new Creator<Control>() {
+ public static final @NonNull Creator<Control> CREATOR = new Creator<Control>() {
@Override
- public Control createFromParcel(Parcel source) {
+ public Control createFromParcel(@NonNull Parcel source) {
return new Control(source);
}
@@ -275,25 +334,25 @@
* <li> Subtitle: {@code ""}
* </ul>
* This fixes the values relating to state of the {@link Control} as required by
- * {@link ControlsProviderService#onLoad}:
+ * {@link ControlsProviderService#loadAvailableControls}:
* <ul>
* <li> Status: {@link Status#STATUS_UNKNOWN}
* <li> Control template: {@link ControlTemplate#NO_TEMPLATE}
* <li> Status text: {@code ""}
* </ul>
*/
- public static class StatelessBuilder {
+ @SuppressLint("MutableBareField")
+ public static final class StatelessBuilder {
private static final String TAG = "StatelessBuilder";
- protected @NonNull String mControlId;
- protected @DeviceTypes.DeviceType int mDeviceType = DeviceTypes.TYPE_UNKNOWN;
- protected @NonNull CharSequence mTitle = "";
- protected @NonNull CharSequence mSubtitle = "";
- protected @Nullable CharSequence mStructure;
- protected @Nullable CharSequence mZone;
- protected @NonNull PendingIntent mAppIntent;
- protected @Status int mStatus = STATUS_UNKNOWN;
- protected @NonNull ControlTemplate mControlTemplate = ControlTemplate.NO_TEMPLATE;
- protected @NonNull CharSequence mStatusText = "";
+ private @NonNull String mControlId;
+ private @DeviceTypes.DeviceType int mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ private @NonNull CharSequence mTitle = "";
+ private @NonNull CharSequence mSubtitle = "";
+ private @Nullable CharSequence mStructure;
+ private @Nullable CharSequence mZone;
+ private @NonNull PendingIntent mAppIntent;
+ private @Nullable Icon mCustomIcon;
+ private @Nullable ColorStateList mCustomColor;
/**
* @param controlId the identifier for the {@link Control}.
@@ -320,6 +379,8 @@
mStructure = control.mStructure;
mZone = control.mZone;
mAppIntent = control.mAppIntent;
+ mCustomIcon = control.mCustomIcon;
+ mCustomColor = control.mCustomColor;
}
/**
@@ -385,6 +446,18 @@
return this;
}
+ @NonNull
+ public StatelessBuilder setCustomIcon(@Nullable Icon customIcon) {
+ mCustomIcon = customIcon;
+ return this;
+ }
+
+ @NonNull
+ public StatelessBuilder setCustomColor(@Nullable ColorStateList customColor) {
+ mCustomColor = customColor;
+ return this;
+ }
+
/**
* Build a {@link Control}
* @return a valid {@link Control}
@@ -398,14 +471,42 @@
mStructure,
mZone,
mAppIntent,
- mStatus,
- mControlTemplate,
- mStatusText);
+ mCustomIcon,
+ mCustomColor,
+ STATUS_UNKNOWN,
+ ControlTemplate.NO_TEMPLATE,
+ "");
}
}
- public static class StatefulBuilder extends StatelessBuilder {
+ /**
+ * Builder class for {@link Control}.
+ *
+ * This class facilitates the creation of {@link Control}.
+ * It provides the following defaults for non-optional parameters:
+ * <ul>
+ * <li> Device type: {@link DeviceTypes#TYPE_UNKNOWN}
+ * <li> Title: {@code ""}
+ * <li> Subtitle: {@code ""}
+ * <li> Status: {@link Status#STATUS_UNKNOWN}
+ * <li> Control template: {@link ControlTemplate#NO_TEMPLATE}
+ * <li> Status text: {@code ""}
+ * </ul>
+ */
+ public static final class StatefulBuilder {
private static final String TAG = "StatefulBuilder";
+ private @NonNull String mControlId;
+ private @DeviceTypes.DeviceType int mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ private @NonNull CharSequence mTitle = "";
+ private @NonNull CharSequence mSubtitle = "";
+ private @Nullable CharSequence mStructure;
+ private @Nullable CharSequence mZone;
+ private @NonNull PendingIntent mAppIntent;
+ private @Nullable Icon mCustomIcon;
+ private @Nullable ColorStateList mCustomColor;
+ private @Status int mStatus = STATUS_UNKNOWN;
+ private @NonNull ControlTemplate mControlTemplate = ControlTemplate.NO_TEMPLATE;
+ private @NonNull CharSequence mStatusText = "";
/**
* @param controlId the identifier for the {@link Control}.
@@ -413,11 +514,27 @@
*/
public StatefulBuilder(@NonNull String controlId,
@NonNull PendingIntent appIntent) {
- super(controlId, appIntent);
+ Preconditions.checkNotNull(controlId);
+ Preconditions.checkNotNull(appIntent);
+ mControlId = controlId;
+ mAppIntent = appIntent;
}
+ /**
+ * Creates a {@link StatelessBuilder} using an existing {@link Control} as a base.
+ * @param control base for the builder.
+ */
public StatefulBuilder(@NonNull Control control) {
- super(control);
+ Preconditions.checkNotNull(control);
+ mControlId = control.mControlId;
+ mDeviceType = control.mDeviceType;
+ mTitle = control.mTitle;
+ mSubtitle = control.mSubtitle;
+ mStructure = control.mStructure;
+ mZone = control.mZone;
+ mAppIntent = control.mAppIntent;
+ mCustomIcon = control.mCustomIcon;
+ mCustomColor = control.mCustomColor;
mStatus = control.mStatus;
mControlTemplate = control.mControlTemplate;
mStatusText = control.mStatusText;
@@ -429,13 +546,19 @@
*/
@NonNull
public StatefulBuilder setControlId(@NonNull String controlId) {
- super.setControlId(controlId);
+ Preconditions.checkNotNull(controlId);
+ mControlId = controlId;
return this;
}
@NonNull
public StatefulBuilder setDeviceType(@DeviceTypes.DeviceType int deviceType) {
- super.setDeviceType(deviceType);
+ if (!DeviceTypes.validDeviceType(deviceType)) {
+ Log.e(TAG, "Invalid device type:" + deviceType);
+ mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ } else {
+ mDeviceType = deviceType;
+ }
return this;
}
@@ -445,25 +568,27 @@
*/
@NonNull
public StatefulBuilder setTitle(@NonNull CharSequence title) {
- super.setTitle(title);
+ Preconditions.checkNotNull(title);
+ mTitle = title;
return this;
}
@NonNull
public StatefulBuilder setSubtitle(@NonNull CharSequence subtitle) {
- super.setSubtitle(subtitle);
+ Preconditions.checkNotNull(subtitle);
+ mSubtitle = subtitle;
return this;
}
@NonNull
public StatefulBuilder setStructure(@Nullable CharSequence structure) {
- super.setStructure(structure);
+ mStructure = structure;
return this;
}
@NonNull
public StatefulBuilder setZone(@Nullable CharSequence zone) {
- super.setZone(zone);
+ mZone = zone;
return this;
}
@@ -473,7 +598,20 @@
*/
@NonNull
public StatefulBuilder setAppIntent(@NonNull PendingIntent appIntent) {
- super.setAppIntent(appIntent);
+ Preconditions.checkNotNull(appIntent);
+ mAppIntent = appIntent;
+ return this;
+ }
+
+ @NonNull
+ public StatefulBuilder setCustomIcon(@Nullable Icon customIcon) {
+ mCustomIcon = customIcon;
+ return this;
+ }
+
+ @NonNull
+ public StatefulBuilder setCustomColor(@Nullable ColorStateList customColor) {
+ mCustomColor = customColor;
return this;
}
@@ -501,5 +639,21 @@
mStatusText = statusText;
return this;
}
+
+ @NonNull
+ public Control build() {
+ return new Control(mControlId,
+ mDeviceType,
+ mTitle,
+ mSubtitle,
+ mStructure,
+ mZone,
+ mAppIntent,
+ mCustomIcon,
+ mCustomColor,
+ mStatus,
+ mControlTemplate,
+ mStatusText);
+ }
}
}
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index eca8541..bc65818 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -27,6 +27,7 @@
import android.os.Message;
import android.os.RemoteException;
import android.service.controls.actions.ControlAction;
+import android.service.controls.actions.ControlActionWrapper;
import android.service.controls.templates.ControlTemplate;
import android.text.TextUtils;
import android.util.Log;
@@ -35,150 +36,90 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.function.Consumer;
/**
* Service implementation allowing applications to contribute controls to the
* System UI.
- * @hide
*/
public abstract class ControlsProviderService extends Service {
@SdkConstant(SdkConstantType.SERVICE_ACTION)
- public static final String CONTROLS_ACTION = "android.service.controls.ControlsProviderService";
+ public static final String SERVICE_CONTROLS =
+ "android.service.controls.ControlsProviderService";
+ /**
+ * @hide
+ */
public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
- public static final String CALLBACK_BINDER = "CALLBACK_BINDER";
+
+ /**
+ * @hide
+ */
public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
- public final String TAG = getClass().getSimpleName();
+ public static final @NonNull String TAG = "ControlsProviderService";
- private IControlsProviderCallback mCallback;
private IBinder mToken;
private RequestHandler mHandler;
/**
- * Signal to retrieve all Controls. When complete, call
- * {@link IControlsProviderCallback#onLoad} to inform the caller.
+ * Retrieve all available controls, using the stateless builder
+ * {@link Control.StatelessBuilder} to build each Control, then use the
+ * provided consumer to callback to the call originator.
*/
- public abstract void load();
+ public abstract void loadAvailableControls(@NonNull Consumer<List<Control>> consumer);
/**
- * Informs the service that the caller is listening for updates to the given controlIds.
- * {@link IControlsProviderCallback#onRefreshState} should be called any time
- * there are Control updates to render.
+ * Return a valid Publisher for the given controlIds. This publisher will be asked
+ * to provide updates for the given list of controlIds as long as the Subscription
+ * is valid.
*/
- public abstract void subscribe(@NonNull List<String> controlIds);
-
- /**
- * Informs the service that the caller is done listening for updates,
- * and any calls to {@link IControlsProviderCallback#onRefreshState} will be ignored.
- */
- public abstract void unsubscribe();
+ @NonNull
+ public abstract Publisher<Control> publisherFor(@NonNull List<String> controlIds);
/**
* The user has interacted with a Control. The action is dictated by the type of
- * {@link ControlAction} that was sent.
+ * {@link ControlAction} that was sent. A response can be sent via
+ * {@link Consumer#accept}, with the Integer argument being one of the provided
+ * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
+ * was received successfully, or if additional prompts should be presented to
+ * the user. Any visual control updates should be sent via the Publisher.
*/
- public abstract void onAction(@NonNull String controlId, @NonNull ControlAction action);
-
- /**
- * Sends a list of the controls available from this service.
- *
- * The items in the list must not have state information (as created by
- * {@link Control.StatelessBuilder}).
- * @param controls
- */
- public final void onLoad(@NonNull List<Control> controls) {
- Preconditions.checkNotNull(controls);
- List<Control> list = new ArrayList<>();
- for (Control control: controls) {
- if (control == null) {
- Log.e(TAG, "onLoad: null control.");
- }
- if (isStateless(control)) {
- list.add(control);
- } else {
- Log.w(TAG, "onLoad: control is not stateless.");
- list.add(new Control.StatelessBuilder(control).build());
- }
- }
- try {
- mCallback.onLoad(mToken, list);
- } catch (RemoteException ex) {
- ex.rethrowAsRuntimeException();
- }
- }
-
- /**
- * Sends a list of the controls requested by {@link ControlsProviderService#subscribe} with
- * their state.
- * @param statefulControls
- */
- public final void onRefreshState(@NonNull List<Control> statefulControls) {
- Preconditions.checkNotNull(statefulControls);
- try {
- mCallback.onRefreshState(mToken, statefulControls);
- } catch (RemoteException ex) {
- ex.rethrowAsRuntimeException();
- }
- }
-
- /**
- * Sends the response of a command in the specified {@link Control}.
- * @param controlId
- * @param response
- */
- public final void onControlActionResponse(
- @NonNull String controlId, @ControlAction.ResponseResult int response) {
- Preconditions.checkNotNull(controlId);
- if (!ControlAction.isValidResponse(response)) {
- Log.e(TAG, "Not valid response result: " + response);
- response = ControlAction.RESPONSE_UNKNOWN;
- }
- try {
- mCallback.onControlActionResponse(mToken, controlId, response);
- } catch (RemoteException ex) {
- ex.rethrowAsRuntimeException();
- }
- }
-
- private boolean isStateless(Control control) {
- return (control.getStatus() == Control.STATUS_UNKNOWN
- && control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
- && TextUtils.isEmpty(control.getStatusText()));
- }
+ public abstract void performControlAction(@NonNull String controlId,
+ @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
@Override
- public IBinder onBind(Intent intent) {
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
mHandler = new RequestHandler(Looper.getMainLooper());
Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
- IBinder callbackBinder = bundle.getBinder(CALLBACK_BINDER);
mToken = bundle.getBinder(CALLBACK_TOKEN);
- mCallback = IControlsProviderCallback.Stub.asInterface(callbackBinder);
return new IControlsProvider.Stub() {
- public void load() {
- mHandler.sendEmptyMessage(RequestHandler.MSG_LOAD);
+ public void load(IControlsLoadCallback cb) {
+ mHandler.obtainMessage(RequestHandler.MSG_LOAD, cb).sendToTarget();
}
- public void subscribe(List<String> ids) {
- mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, ids).sendToTarget();
+ public void subscribe(List<String> controlIds,
+ IControlsSubscriber subscriber) {
+ SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
+ mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
}
- public void unsubscribe() {
- mHandler.sendEmptyMessage(RequestHandler.MSG_UNSUBSCRIBE);
- }
-
- public void onAction(String id, ControlAction action) {
- ActionMessage msg = new ActionMessage(id, action);
- mHandler.obtainMessage(RequestHandler.MSG_ON_ACTION, msg).sendToTarget();
+ public void action(String controlId, ControlActionWrapper action,
+ IControlsActionCallback cb) {
+ ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb);
+ mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget();
}
};
}
@Override
- public boolean onUnbind(Intent intent) {
- mCallback = null;
+ public boolean onUnbind(@NonNull Intent intent) {
mHandler = null;
return true;
}
@@ -186,8 +127,7 @@
private class RequestHandler extends Handler {
private static final int MSG_LOAD = 1;
private static final int MSG_SUBSCRIBE = 2;
- private static final int MSG_UNSUBSCRIBE = 3;
- private static final int MSG_ON_ACTION = 4;
+ private static final int MSG_ACTION = 3;
RequestHandler(Looper looper) {
super(looper);
@@ -196,30 +136,136 @@
public void handleMessage(Message msg) {
switch(msg.what) {
case MSG_LOAD:
- ControlsProviderService.this.load();
+ final IControlsLoadCallback cb = (IControlsLoadCallback) msg.obj;
+ ControlsProviderService.this.loadAvailableControls(consumerFor(cb));
break;
+
case MSG_SUBSCRIBE:
- List<String> ids = (List<String>) msg.obj;
- ControlsProviderService.this.subscribe(ids);
+ final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
+ final IControlsSubscriber cs = sMsg.mSubscriber;
+ Subscriber<Control> s = new Subscriber<Control>() {
+ public void onSubscribe(Subscription subscription) {
+ try {
+ cs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onNext(@NonNull Control statefulControl) {
+ Preconditions.checkNotNull(statefulControl);
+ try {
+ cs.onNext(mToken, statefulControl);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onError(Throwable t) {
+ try {
+ cs.onError(mToken, t.toString());
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onComplete() {
+ try {
+ cs.onComplete(mToken);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ };
+ ControlsProviderService.this.publisherFor(sMsg.mControlIds).subscribe(s);
break;
- case MSG_UNSUBSCRIBE:
- ControlsProviderService.this.unsubscribe();
- break;
- case MSG_ON_ACTION:
- ActionMessage aMsg = (ActionMessage) msg.obj;
- ControlsProviderService.this.onAction(aMsg.mId, aMsg.mAction);
+
+ case MSG_ACTION:
+ final ActionMessage aMsg = (ActionMessage) msg.obj;
+ ControlsProviderService.this.performControlAction(aMsg.mControlId,
+ aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
break;
}
}
+
+ private Consumer<Integer> consumerFor(final String controlId,
+ final IControlsActionCallback cb) {
+ return (@NonNull Integer response) -> {
+ Preconditions.checkNotNull(response);
+ if (!ControlAction.isValidResponse(response)) {
+ Log.e(TAG, "Not valid response result: " + response);
+ response = ControlAction.RESPONSE_UNKNOWN;
+ }
+ try {
+ cb.accept(mToken, controlId, response);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ };
+ }
+
+ private Consumer<List<Control>> consumerFor(IControlsLoadCallback cb) {
+ return (@NonNull List<Control> controls) -> {
+ Preconditions.checkNotNull(controls);
+ List<Control> list = new ArrayList<>();
+ for (Control control: controls) {
+ if (control == null) {
+ Log.e(TAG, "onLoad: null control.");
+ }
+ if (isStatelessControl(control)) {
+ list.add(control);
+ } else {
+ Log.w(TAG, "onLoad: control is not stateless.");
+ list.add(new Control.StatelessBuilder(control).build());
+ }
+ }
+ try {
+ cb.accept(mToken, list);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ };
+ }
+
+ private boolean isStatelessControl(Control control) {
+ return (control.getStatus() == Control.STATUS_UNKNOWN
+ && control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
+ && TextUtils.isEmpty(control.getStatusText()));
+ }
}
- private class ActionMessage {
- final String mId;
- final ControlAction mAction;
+ private static class SubscriptionAdapter extends IControlsSubscription.Stub {
+ final Subscription mSubscription;
- ActionMessage(String id, ControlAction action) {
- this.mId = id;
+ SubscriptionAdapter(Subscription s) {
+ this.mSubscription = s;
+ }
+
+ public void request(long n) {
+ mSubscription.request(n);
+ }
+
+ public void cancel() {
+ mSubscription.cancel();
+ }
+ }
+
+ private static class ActionMessage {
+ final String mControlId;
+ final ControlAction mAction;
+ final IControlsActionCallback mCb;
+
+ ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) {
+ this.mControlId = controlId;
this.mAction = action;
+ this.mCb = cb;
+ }
+ }
+
+ private static class SubscribeMessage {
+ final List<String> mControlIds;
+ final IControlsSubscriber mSubscriber;
+
+ SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) {
+ this.mControlIds = controlIds;
+ this.mSubscriber = subscriber;
}
}
}
diff --git a/core/java/android/service/controls/DeviceTypes.java b/core/java/android/service/controls/DeviceTypes.java
index b2d1c08..8dbb9cf 100644
--- a/core/java/android/service/controls/DeviceTypes.java
+++ b/core/java/android/service/controls/DeviceTypes.java
@@ -21,9 +21,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-/**
- * @hide
- */
public class DeviceTypes {
// Update this when adding new concrete types. Does not count TYPE_UNKNOWN
diff --git a/core/java/android/service/controls/templates/ControlTemplate.aidl b/core/java/android/service/controls/IControlsActionCallback.aidl
similarity index 71%
copy from core/java/android/service/controls/templates/ControlTemplate.aidl
copy to core/java/android/service/controls/IControlsActionCallback.aidl
index b6ab280..eab4c89 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.aidl
+++ b/core/java/android/service/controls/IControlsActionCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, The Android Open Source Project
+ * Copyright (c) 2020, 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.
@@ -14,6 +14,11 @@
* limitations under the License.
*/
-package android.service.controls.templates;
+package android.service.controls;
-parcelable ControlTemplate;
\ No newline at end of file
+/**
+ * @hide
+ */
+oneway interface IControlsActionCallback {
+ void accept(in IBinder token, in String controlId, int response);
+}
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/ControlTemplate.aidl b/core/java/android/service/controls/IControlsLoadCallback.aidl
similarity index 68%
copy from core/java/android/service/controls/templates/ControlTemplate.aidl
copy to core/java/android/service/controls/IControlsLoadCallback.aidl
index b6ab280..bfc61cd 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.aidl
+++ b/core/java/android/service/controls/IControlsLoadCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, The Android Open Source Project
+ * Copyright (c) 2020, 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.
@@ -14,6 +14,13 @@
* limitations under the License.
*/
-package android.service.controls.templates;
+package android.service.controls;
-parcelable ControlTemplate;
\ No newline at end of file
+import android.service.controls.Control;
+
+/**
+ * @hide
+ */
+oneway interface IControlsLoadCallback {
+ void accept(in IBinder token, in List<Control> controls);
+}
\ No newline at end of file
diff --git a/core/java/android/service/controls/IControlsProvider.aidl b/core/java/android/service/controls/IControlsProvider.aidl
index 6c105bb..4ce658e 100644
--- a/core/java/android/service/controls/IControlsProvider.aidl
+++ b/core/java/android/service/controls/IControlsProvider.aidl
@@ -16,15 +16,20 @@
package android.service.controls;
-import android.service.controls.actions.ControlAction;
+import android.service.controls.IControlsActionCallback;
+import android.service.controls.IControlsLoadCallback;
+import android.service.controls.IControlsSubscriber;
+import android.service.controls.actions.ControlActionWrapper;
-/** @hide */
+/**
+ * @hide
+ */
oneway interface IControlsProvider {
- void load();
+ void load(IControlsLoadCallback cb);
- void subscribe(in List<String> controlIds);
+ void subscribe(in List<String> controlIds,
+ IControlsSubscriber subscriber);
- void unsubscribe();
-
- void onAction(in String controlId, in ControlAction action);
+ void action(in String controlId, in ControlActionWrapper action,
+ IControlsActionCallback cb);
}
\ No newline at end of file
diff --git a/core/java/android/service/controls/IControlsProviderCallback.aidl b/core/java/android/service/controls/IControlsSubscriber.aidl
similarity index 63%
rename from core/java/android/service/controls/IControlsProviderCallback.aidl
rename to core/java/android/service/controls/IControlsSubscriber.aidl
index 91f6a79..75ce584 100644
--- a/core/java/android/service/controls/IControlsProviderCallback.aidl
+++ b/core/java/android/service/controls/IControlsSubscriber.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, The Android Open Source Project
+ * Copyright (c) 2020, 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.
@@ -17,12 +17,14 @@
package android.service.controls;
import android.service.controls.Control;
+import android.service.controls.IControlsSubscription;
-/** @hide */
-oneway interface IControlsProviderCallback {
- void onLoad(in IBinder token, in List<Control> controls);
-
- void onRefreshState(in IBinder token, in List<Control> statefulControls);
-
- void onControlActionResponse(in IBinder token, in String controlId, int response);
+/**
+ * @hide
+ */
+oneway interface IControlsSubscriber {
+ void onSubscribe(in IBinder token, in IControlsSubscription cs);
+ void onNext(in IBinder token, in Control c);
+ void onError(in IBinder token, in String s);
+ void onComplete(in IBinder token);
}
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/ControlTemplate.aidl b/core/java/android/service/controls/IControlsSubscription.aidl
similarity index 74%
rename from core/java/android/service/controls/templates/ControlTemplate.aidl
rename to core/java/android/service/controls/IControlsSubscription.aidl
index b6ab280..0af575e 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.aidl
+++ b/core/java/android/service/controls/IControlsSubscription.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, The Android Open Source Project
+ * Copyright (c) 2020, 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.
@@ -14,6 +14,12 @@
* limitations under the License.
*/
-package android.service.controls.templates;
+package android.service.controls;
-parcelable ControlTemplate;
\ No newline at end of file
+/**
+ * @hide
+ */
+oneway interface IControlsSubscription {
+ void request(long n);
+ void cancel();
+}
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/BooleanAction.java b/core/java/android/service/controls/actions/BooleanAction.java
index fb2c5ad..0259335 100644
--- a/core/java/android/service/controls/actions/BooleanAction.java
+++ b/core/java/android/service/controls/actions/BooleanAction.java
@@ -19,12 +19,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.controls.templates.ToggleTemplate;
/**
* Action sent by a {@link ToggleTemplate}
- * @hide
*/
public final class BooleanAction extends ControlAction {
@@ -54,6 +52,10 @@
mNewState = newState;
}
+ /**
+ * @param b
+ * @hide
+ */
BooleanAction(Bundle b) {
super(b);
mNewState = b.getBoolean(KEY_NEW_STATE);
@@ -77,24 +79,15 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putBoolean(KEY_NEW_STATE, mNewState);
return b;
}
-
- public static final @NonNull Creator<BooleanAction> CREATOR = new Creator<BooleanAction>() {
- @Override
- public BooleanAction createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new BooleanAction(source.readBundle());
- }
-
- @Override
- public BooleanAction[] newArray(int size) {
- return new BooleanAction[size];
- }
- };
}
diff --git a/core/java/android/service/controls/actions/CommandAction.aidl b/core/java/android/service/controls/actions/CommandAction.aidl
deleted file mode 100644
index 7c1ee41..0000000
--- a/core/java/android/service/controls/actions/CommandAction.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.service.controls.actions;
-
-parcelable CommandAction;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/CommandAction.java b/core/java/android/service/controls/actions/CommandAction.java
index c69c539..84d6080 100644
--- a/core/java/android/service/controls/actions/CommandAction.java
+++ b/core/java/android/service/controls/actions/CommandAction.java
@@ -19,11 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
-/**
- * @hide
- */
public final class CommandAction extends ControlAction {
private static final @ActionType int TYPE = TYPE_COMMAND;
@@ -36,7 +32,11 @@
this(templateId, null);
}
- public CommandAction(Bundle b) {
+ /**
+ * @param b
+ * @hide
+ */
+ CommandAction(Bundle b) {
super(b);
}
@@ -44,18 +44,4 @@
public int getActionType() {
return TYPE;
}
-
- public static final Creator<CommandAction> CREATOR = new Creator<CommandAction>() {
- @Override
- public CommandAction createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new CommandAction(source.readBundle());
- }
-
- @Override
- public CommandAction[] newArray(int size) {
- return new CommandAction[size];
- }
- };
}
diff --git a/core/java/android/service/controls/actions/ControlAction.aidl b/core/java/android/service/controls/actions/ControlAction.aidl
deleted file mode 100644
index b012521..0000000
--- a/core/java/android/service/controls/actions/ControlAction.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.actions;
-
-parcelable ControlAction;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/ControlAction.java b/core/java/android/service/controls/actions/ControlAction.java
index 83d1cf8..4141da8 100644
--- a/core/java/android/service/controls/actions/ControlAction.java
+++ b/core/java/android/service/controls/actions/ControlAction.java
@@ -21,10 +21,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.service.controls.IControlsProviderCallback;
+import android.service.controls.IControlsActionCallback;
import android.service.controls.templates.ControlTemplate;
+import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -36,20 +35,21 @@
*
* The action may have a value to authenticate the input, when the provider has requested it to
* complete the action.
- * @hide
*/
-public abstract class ControlAction implements Parcelable {
+public abstract class ControlAction {
+ private static final String TAG = "ControlAction";
+
+ private static final String KEY_ACTION_TYPE = "key_action_type";
private static final String KEY_TEMPLATE_ID = "key_template_id";
private static final String KEY_CHALLENGE_VALUE = "key_challenge_value";
-
/**
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
- TYPE_UNKNOWN,
+ TYPE_ERROR,
TYPE_BOOLEAN,
TYPE_FLOAT,
TYPE_MULTI_FLOAT,
@@ -57,15 +57,16 @@
TYPE_COMMAND
})
public @interface ActionType {};
- public static final ControlAction UNKNOWN_ACTION = new ControlAction() {
+ public static final @NonNull ControlAction ERROR_ACTION = new ControlAction() {
@Override
public int getActionType() {
- return TYPE_UNKNOWN;
+ return TYPE_ERROR;
}
};
- public static final @ActionType int TYPE_UNKNOWN = 0;
+ public static final @ActionType int TYPE_ERROR = -1;
+
/**
* The identifier of {@link BooleanAction}.
*/
@@ -104,27 +105,27 @@
public static final @ResponseResult int RESPONSE_UNKNOWN = 0;
/**
- * Response code for {@link IControlsProviderCallback#onControlActionResponse} indicating that
+ * Response code for {@link IControlsActionCallback#accept} indicating that
* the action has been performed. The action may still fail later and the state may not change.
*/
public static final @ResponseResult int RESPONSE_OK = 1;
/**
- * Response code for {@link IControlsProviderCallback#onControlActionResponse} indicating that
+ * Response code for {@link IControlsActionCallback#accept} indicating that
* the action has failed.
*/
public static final @ResponseResult int RESPONSE_FAIL = 2;
/**
- * Response code for {@link IControlsProviderCallback#onControlActionResponse} indicating that
+ * Response code for {@link IControlsActionCallback#accept} indicating that
* in order for the action to be performed, acknowledgment from the user is required.
*/
public static final @ResponseResult int RESPONSE_CHALLENGE_ACK = 3;
/**
- * Response code for {@link IControlsProviderCallback#onControlActionResponse} indicating that
+ * Response code for {@link IControlsActionCallback#accept} indicating that
* in order for the action to be performed, a PIN is required.
*/
public static final @ResponseResult int RESPONSE_CHALLENGE_PIN = 4;
/**
- * Response code for {@link IControlsProviderCallback#onControlActionResponse} indicating that
+ * Response code for {@link IControlsActionCallback#accept} indicating that
* in order for the action to be performed, an alphanumeric passphrase is required.
*/
public static final @ResponseResult int RESPONSE_CHALLENGE_PASSPHRASE = 5;
@@ -175,68 +176,55 @@
return mChallengeValue;
}
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public final void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(getActionType());
- dest.writeBundle(getDataBundle());
- }
-
/**
* Obtain a {@link Bundle} describing this object populated with data.
*
* Implementations in subclasses should populate the {@link Bundle} returned by
* {@link ControlAction}.
* @return a {@link Bundle} containing the data that represents this object.
+ * @hide
*/
@CallSuper
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = new Bundle();
+ b.putInt(KEY_ACTION_TYPE, getActionType());
b.putString(KEY_TEMPLATE_ID, mTemplateId);
b.putString(KEY_CHALLENGE_VALUE, mChallengeValue);
return b;
}
- public static final @NonNull Creator<ControlAction> CREATOR = new Creator<ControlAction>() {
- @Override
- public ControlAction createFromParcel(Parcel source) {
- int type = source.readInt();
- return createActionFromType(type, source);
+ /**
+ * @param bundle
+ * @return
+ * @hide
+ */
+ @NonNull
+ static ControlAction createActionFromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ Log.e(TAG, "Null bundle");
+ return ERROR_ACTION;
}
-
- @Override
- public ControlAction[] newArray(int size) {
- return new ControlAction[size];
- }
- };
-
-
- private static ControlAction createActionFromType(@ActionType int type, Parcel source) {
- switch(type) {
- case TYPE_BOOLEAN:
- return new BooleanAction(source.readBundle());
- case TYPE_FLOAT:
- return new FloatAction(source.readBundle());
- case TYPE_MULTI_FLOAT:
- return new MultiFloatAction(source.readBundle());
- case TYPE_MODE:
- return new ModeAction(source.readBundle());
- case TYPE_COMMAND:
- return new CommandAction(source.readBundle());
- default:
- source.readBundle();
- return UNKNOWN_ACTION;
+ int type = bundle.getInt(KEY_ACTION_TYPE, TYPE_ERROR);
+ try {
+ switch (type) {
+ case TYPE_BOOLEAN:
+ return new BooleanAction(bundle);
+ case TYPE_FLOAT:
+ return new FloatAction(bundle);
+ case TYPE_MULTI_FLOAT:
+ return new MultiFloatAction(bundle);
+ case TYPE_MODE:
+ return new ModeAction(bundle);
+ case TYPE_COMMAND:
+ return new CommandAction(bundle);
+ case TYPE_ERROR:
+ default:
+ return ERROR_ACTION;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error creating action", e);
+ return ERROR_ACTION;
}
}
-
- protected static void verifyType(@ActionType int type, @ActionType int thisType) {
- if (type != thisType) {
- throw new IllegalStateException("The type " + type + "does not match " + thisType);
- }
- }
-
}
diff --git a/core/java/android/service/controls/actions/BooleanAction.aidl b/core/java/android/service/controls/actions/ControlActionWrapper.aidl
similarity index 87%
rename from core/java/android/service/controls/actions/BooleanAction.aidl
rename to core/java/android/service/controls/actions/ControlActionWrapper.aidl
index d1e7e02..5ba962d 100644
--- a/core/java/android/service/controls/actions/BooleanAction.aidl
+++ b/core/java/android/service/controls/actions/ControlActionWrapper.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -16,4 +16,4 @@
package android.service.controls.actions;
-parcelable BooleanAction;
\ No newline at end of file
+parcelable ControlActionWrapper;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/ControlActionWrapper.java b/core/java/android/service/controls/actions/ControlActionWrapper.java
new file mode 100644
index 0000000..6a3ec86
--- /dev/null
+++ b/core/java/android/service/controls/actions/ControlActionWrapper.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.controls.actions;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Wrapper for parceling/unparceling {@link ControlAction}.
+ * @hide
+ */
+public final class ControlActionWrapper implements Parcelable {
+
+ private final @NonNull ControlAction mControlAction;
+
+ public ControlActionWrapper(@NonNull ControlAction controlAction) {
+ Preconditions.checkNotNull(controlAction);
+
+ mControlAction = controlAction;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBundle(mControlAction.getDataBundle());
+ }
+
+ @NonNull
+ public ControlAction getWrappedAction() {
+ return mControlAction;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<ControlActionWrapper> CREATOR =
+ new Creator<ControlActionWrapper>() {
+ @Override
+ public ControlActionWrapper createFromParcel(@NonNull Parcel in) {
+ return new ControlActionWrapper(
+ ControlAction.createActionFromBundle(in.readBundle()));
+ }
+
+ @Override
+ public ControlActionWrapper[] newArray(int size) {
+ return new ControlActionWrapper[size];
+ }
+ };
+}
diff --git a/core/java/android/service/controls/actions/FloatAction.aidl b/core/java/android/service/controls/actions/FloatAction.aidl
deleted file mode 100644
index 2c1e76d..0000000
--- a/core/java/android/service/controls/actions/FloatAction.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.actions;
-
-parcelable FloatAction;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/FloatAction.java b/core/java/android/service/controls/actions/FloatAction.java
index 1c3fb4d..5b271ce 100644
--- a/core/java/android/service/controls/actions/FloatAction.java
+++ b/core/java/android/service/controls/actions/FloatAction.java
@@ -19,13 +19,11 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.controls.templates.RangeTemplate;
import android.service.controls.templates.ToggleRangeTemplate;
/**
* Action sent by a {@link RangeTemplate}, {@link ToggleRangeTemplate}.
- * @hide
*/
public final class FloatAction extends ControlAction {
@@ -56,7 +54,11 @@
mNewValue = newValue;
}
- public FloatAction(Bundle b) {
+ /**
+ * @param b
+ * @hide
+ */
+ FloatAction(Bundle b) {
super(b);
mNewValue = b.getFloat(KEY_NEW_VALUE);
}
@@ -76,24 +78,15 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putFloat(KEY_NEW_VALUE, mNewValue);
return b;
}
-
- public static final @NonNull Creator<FloatAction> CREATOR = new Creator<FloatAction>() {
- @Override
- public FloatAction createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new FloatAction(source.readBundle());
- }
-
- @Override
- public FloatAction[] newArray(int size) {
- return new FloatAction[size];
- }
- };
}
diff --git a/core/java/android/service/controls/actions/ModeAction.aidl b/core/java/android/service/controls/actions/ModeAction.aidl
deleted file mode 100644
index 3ef89e0..0000000
--- a/core/java/android/service/controls/actions/ModeAction.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.actions;
-
-parcelable ModeAction;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/ModeAction.java b/core/java/android/service/controls/actions/ModeAction.java
index 0bd1d24..ca40974 100644
--- a/core/java/android/service/controls/actions/ModeAction.java
+++ b/core/java/android/service/controls/actions/ModeAction.java
@@ -19,11 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
-/**
- * @hide
- */
public final class ModeAction extends ControlAction {
private static final @ActionType int TYPE = TYPE_MODE;
@@ -45,13 +41,22 @@
this(templateId, newMode, null);
}
+ /**
+ * @param b
+ * @hide
+ */
ModeAction(Bundle b) {
super(b);
mNewMode = b.getInt(KEY_MODE);
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putInt(KEY_MODE, mNewMode);
return b;
@@ -60,18 +65,4 @@
public int getNewMode() {
return mNewMode;
}
-
- public static final Creator<ModeAction> CREATOR = new Creator<ModeAction>() {
- @Override
- public ModeAction createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new ModeAction(source.readBundle());
- }
-
- @Override
- public ModeAction[] newArray(int size) {
- return new ModeAction[size];
- }
- };
}
diff --git a/core/java/android/service/controls/actions/MultiFloatAction.aidl b/core/java/android/service/controls/actions/MultiFloatAction.aidl
deleted file mode 100644
index bcba758..0000000
--- a/core/java/android/service/controls/actions/MultiFloatAction.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.service.controls.actions;
-
-parcelable MultiFloatAction;
\ No newline at end of file
diff --git a/core/java/android/service/controls/actions/MultiFloatAction.java b/core/java/android/service/controls/actions/MultiFloatAction.java
index aef8a78..e574079 100644
--- a/core/java/android/service/controls/actions/MultiFloatAction.java
+++ b/core/java/android/service/controls/actions/MultiFloatAction.java
@@ -19,14 +19,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
import android.util.Log;
import com.android.internal.util.Preconditions;
-/**
- * @hide
- */
public final class MultiFloatAction extends ControlAction {
private static final String TAG = "MultiFloatAction";
@@ -58,6 +54,10 @@
this(templateId, newValues, null);
}
+ /**
+ * @param b
+ * @hide
+ */
MultiFloatAction(Bundle b) {
super(b);
mNewValues = b.getFloatArray(KEY_VALUES);
@@ -68,24 +68,15 @@
return mNewValues.clone();
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putFloatArray(KEY_VALUES, mNewValues);
return b;
}
-
- public static final Creator<MultiFloatAction> CREATOR = new Creator<MultiFloatAction>() {
- @Override
- public MultiFloatAction createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new MultiFloatAction(source.readBundle());
- }
-
- @Override
- public MultiFloatAction[] newArray(int size) {
- return new MultiFloatAction[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/ControlButton.java b/core/java/android/service/controls/templates/ControlButton.java
index e03ac6f..157e231 100644
--- a/core/java/android/service/controls/templates/ControlButton.java
+++ b/core/java/android/service/controls/templates/ControlButton.java
@@ -24,7 +24,6 @@
/**
* Button element for {@link ControlTemplate}.
- * @hide
*/
public final class ControlButton implements Parcelable {
@@ -64,7 +63,8 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ @NonNull
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeByte(mChecked ? (byte) 1 : (byte) 0);
dest.writeCharSequence(mActionDescription);
}
@@ -74,7 +74,7 @@
mActionDescription = in.readCharSequence();
}
- public static final Creator<ControlButton> CREATOR = new Creator<ControlButton>() {
+ public static final @NonNull Creator<ControlButton> CREATOR = new Creator<ControlButton>() {
@Override
public ControlButton createFromParcel(Parcel source) {
return new ControlButton(source);
diff --git a/core/java/android/service/controls/templates/ControlTemplate.java b/core/java/android/service/controls/templates/ControlTemplate.java
index bf194f8..d2c0f76 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.java
+++ b/core/java/android/service/controls/templates/ControlTemplate.java
@@ -19,11 +19,11 @@
import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.service.controls.Control;
import android.service.controls.actions.ControlAction;
+import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -39,27 +39,37 @@
* associated state. The actions available to a given {@link Control} are determined by its
* {@link ControlTemplate}.
* @see ControlAction
- * @hide
*/
-public abstract class ControlTemplate implements Parcelable {
+public abstract class ControlTemplate {
+
+ private static final String TAG = "ControlTemplate";
private static final String KEY_TEMPLATE_ID = "key_template_id";
+ private static final String KEY_TEMPLATE_TYPE = "key_template_type";
/**
* Singleton representing a {@link Control} with no input.
*/
- public static final ControlTemplate NO_TEMPLATE = new ControlTemplate("") {
+ public static final @NonNull ControlTemplate NO_TEMPLATE = new ControlTemplate("") {
@Override
public int getTemplateType() {
return TYPE_NONE;
}
};
+ public static final @NonNull ControlTemplate ERROR_TEMPLATE = new ControlTemplate("") {
+ @Override
+ public int getTemplateType() {
+ return TYPE_ERROR;
+ }
+ };
+
/**
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
+ TYPE_ERROR,
TYPE_NONE,
TYPE_TOGGLE,
TYPE_RANGE,
@@ -72,47 +82,50 @@
})
public @interface TemplateType {}
+ public static final @TemplateType int TYPE_ERROR = -1;
+
/**
* Type identifier of {@link ControlTemplate#NO_TEMPLATE}.
*/
- public static final int TYPE_NONE = 0;
+ public static final @TemplateType int TYPE_NONE = 0;
/**
* Type identifier of {@link ToggleTemplate}.
*/
- public static final int TYPE_TOGGLE = 1;
+ public static final @TemplateType int TYPE_TOGGLE = 1;
/**
* Type identifier of {@link RangeTemplate}.
*/
- public static final int TYPE_RANGE = 2;
+ public static final @TemplateType int TYPE_RANGE = 2;
/**
* Type identifier of {@link ThumbnailTemplate}.
*/
- public static final int TYPE_THUMBNAIL = 3;
+ public static final @TemplateType int TYPE_THUMBNAIL = 3;
/**
* Type identifier of {@link DiscreteToggleTemplate}.
*/
- public static final int TYPE_DISCRETE_TOGGLE = 4;
+ public static final @TemplateType int TYPE_DISCRETE_TOGGLE = 4;
/**
* @hide
*/
- public static final int TYPE_COORD_RANGE = 5;
+ public static final @TemplateType int TYPE_COORD_RANGE = 5;
- public static final int TYPE_TOGGLE_RANGE = 6;
+ public static final @TemplateType int TYPE_TOGGLE_RANGE = 6;
- public static final int TYPE_TEMPERATURE = 7;
+ public static final @TemplateType int TYPE_TEMPERATURE = 7;
- public static final int TYPE_STATELESS = 8;
+ public static final @TemplateType int TYPE_STATELESS = 8;
private @NonNull final String mTemplateId;
/**
* @return the identifier for this object.
*/
+ @NonNull
public String getTemplateId() {
return mTemplateId;
}
@@ -122,24 +135,16 @@
*/
public abstract @TemplateType int getTemplateType();
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public final void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(getTemplateType());
- dest.writeBundle(getDataBundle());
- }
-
/**
* Obtain a {@link Bundle} describing this object populated with data.
* @return a {@link Bundle} containing the data that represents this object.
+ * @hide
*/
@CallSuper
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = new Bundle();
+ b.putInt(KEY_TEMPLATE_TYPE, getTemplateType());
b.putString(KEY_TEMPLATE_ID, mTemplateId);
return b;
}
@@ -148,6 +153,10 @@
mTemplateId = "";
}
+ /**
+ * @param b
+ * @hide
+ */
ControlTemplate(@NonNull Bundle b) {
mTemplateId = b.getString(KEY_TEMPLATE_ID);
}
@@ -160,48 +169,46 @@
mTemplateId = templateId;
}
- public static final Creator<ControlTemplate> CREATOR = new Creator<ControlTemplate>() {
- @Override
- public ControlTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- return createTemplateFromType(type, source);
+ /**
+ *
+ * @param bundle
+ * @return
+ * @hide
+ */
+ @NonNull
+ static ControlTemplate createTemplateFromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ Log.e(TAG, "Null bundle");
+ return ERROR_TEMPLATE;
}
-
- @Override
- public ControlTemplate[] newArray(int size) {
- return new ControlTemplate[size];
- }
- };
-
-
- private static ControlTemplate createTemplateFromType(@TemplateType int type, Parcel source) {
- switch(type) {
- case TYPE_TOGGLE:
- return new ToggleTemplate(source.readBundle());
- case TYPE_RANGE:
- return new RangeTemplate(source.readBundle());
- case TYPE_THUMBNAIL:
- return new ThumbnailTemplate(source.readBundle());
- case TYPE_DISCRETE_TOGGLE:
- return new DiscreteToggleTemplate(source.readBundle());
- case TYPE_COORD_RANGE:
- return new CoordinatedRangeTemplate(source.readBundle());
- case TYPE_TOGGLE_RANGE:
- return new ToggleRangeTemplate(source.readBundle());
- case TYPE_TEMPERATURE:
- return new TemperatureControlTemplate(source.readBundle());
- case TYPE_STATELESS:
- return new StatelessTemplate(source.readBundle());
- case TYPE_NONE:
- default:
- source.readBundle();
- return NO_TEMPLATE;
- }
- }
-
- protected static void verifyType(@TemplateType int type, @TemplateType int thisType) {
- if (type != thisType) {
- throw new IllegalStateException("The type " + type + "does not match " + thisType);
+ int type = bundle.getInt(KEY_TEMPLATE_TYPE, TYPE_ERROR);
+ try {
+ switch (type) {
+ case TYPE_TOGGLE:
+ return new ToggleTemplate(bundle);
+ case TYPE_RANGE:
+ return new RangeTemplate(bundle);
+ case TYPE_THUMBNAIL:
+ return new ThumbnailTemplate(bundle);
+ case TYPE_DISCRETE_TOGGLE:
+ return new DiscreteToggleTemplate(bundle);
+ case TYPE_COORD_RANGE:
+ return new CoordinatedRangeTemplate(bundle);
+ case TYPE_TOGGLE_RANGE:
+ return new ToggleRangeTemplate(bundle);
+ case TYPE_TEMPERATURE:
+ return new TemperatureControlTemplate(bundle);
+ case TYPE_STATELESS:
+ return new StatelessTemplate(bundle);
+ case TYPE_NONE:
+ return NO_TEMPLATE;
+ case TYPE_ERROR:
+ default:
+ return ERROR_TEMPLATE;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error creating template", e);
+ return ERROR_TEMPLATE;
}
}
}
diff --git a/core/java/android/service/controls/templates/ToggleRangeTemplate.aidl b/core/java/android/service/controls/templates/ControlTemplateWrapper.aidl
similarity index 87%
rename from core/java/android/service/controls/templates/ToggleRangeTemplate.aidl
rename to core/java/android/service/controls/templates/ControlTemplateWrapper.aidl
index 2611284..208ca4e 100644
--- a/core/java/android/service/controls/templates/ToggleRangeTemplate.aidl
+++ b/core/java/android/service/controls/templates/ControlTemplateWrapper.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -16,4 +16,4 @@
package android.service.controls.templates;
-parcelable ToggleRangeTemplate;
\ No newline at end of file
+parcelable ControlTemplateWrapper;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/ControlTemplateWrapper.java b/core/java/android/service/controls/templates/ControlTemplateWrapper.java
new file mode 100644
index 0000000..7957260
--- /dev/null
+++ b/core/java/android/service/controls/templates/ControlTemplateWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Wrapper for parceling/unparceling {@link ControlTemplate}.
+ * @hide
+ */
+public final class ControlTemplateWrapper implements Parcelable {
+
+ private final @NonNull ControlTemplate mControlTemplate;
+
+ public ControlTemplateWrapper(@NonNull ControlTemplate template) {
+ Preconditions.checkNotNull(template);
+ mControlTemplate = template;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public ControlTemplate getWrappedTemplate() {
+ return mControlTemplate;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBundle(mControlTemplate.getDataBundle());
+ }
+
+ public static final @NonNull Creator<ControlTemplateWrapper> CREATOR =
+ new Creator<ControlTemplateWrapper>() {
+ @Override
+ public ControlTemplateWrapper createFromParcel(@NonNull Parcel source) {
+ return new ControlTemplateWrapper(
+ ControlTemplate.createTemplateFromBundle(source.readBundle()));
+ }
+
+ @Override
+ public ControlTemplateWrapper[] newArray(int size) {
+ return new ControlTemplateWrapper[size];
+ }
+ };
+}
diff --git a/core/java/android/service/controls/templates/CoordinatedRangeTemplate.aidl b/core/java/android/service/controls/templates/CoordinatedRangeTemplate.aidl
deleted file mode 100644
index 972142c..0000000
--- a/core/java/android/service/controls/templates/CoordinatedRangeTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable CoordinatedRangeTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/CoordinatedRangeTemplate.java b/core/java/android/service/controls/templates/CoordinatedRangeTemplate.java
index 3d820c4..6aa5480 100644
--- a/core/java/android/service/controls/templates/CoordinatedRangeTemplate.java
+++ b/core/java/android/service/controls/templates/CoordinatedRangeTemplate.java
@@ -19,12 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
-import android.os.Parcel;
import android.util.Log;
-/**
- * @hide
- */
public final class CoordinatedRangeTemplate extends ControlTemplate {
private static final String TAG = "CoordinatedRangeTemplate";
@@ -74,10 +70,14 @@
minValueHigh, maxValueHigh, currentValueHigh, stepValue, formatString));
}
+ /**
+ * @param b
+ * @hide
+ */
CoordinatedRangeTemplate(Bundle b) {
super(b);
- mRangeLow = b.getParcelable(KEY_RANGE_LOW);
- mRangeHigh = b.getParcelable(KEY_RANGE_HIGH);
+ mRangeLow = new RangeTemplate(b.getBundle(KEY_RANGE_LOW));
+ mRangeHigh = new RangeTemplate(b.getBundle(KEY_RANGE_HIGH));
mMinGap = b.getFloat(KEY_MIN_GAP);
validateRanges();
}
@@ -134,11 +134,16 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
- b.putParcelable(KEY_RANGE_LOW, mRangeLow);
- b.putParcelable(KEY_RANGE_HIGH, mRangeHigh);
+ b.putBundle(KEY_RANGE_LOW, mRangeLow.getDataBundle());
+ b.putBundle(KEY_RANGE_HIGH, mRangeHigh.getDataBundle());
return b;
}
@@ -160,18 +165,4 @@
}
}
- public static final Creator<CoordinatedRangeTemplate> CREATOR =
- new Creator<CoordinatedRangeTemplate>() {
- @Override
- public CoordinatedRangeTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new CoordinatedRangeTemplate(source.readBundle());
- }
-
- @Override
- public CoordinatedRangeTemplate[] newArray(int size) {
- return new CoordinatedRangeTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/DiscreteToggleTemplate.aidl b/core/java/android/service/controls/templates/DiscreteToggleTemplate.aidl
deleted file mode 100644
index d22e375..0000000
--- a/core/java/android/service/controls/templates/DiscreteToggleTemplate.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.service.controls.templates;
-
-parcelable DiscreteToggleTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/DiscreteToggleTemplate.java b/core/java/android/service/controls/templates/DiscreteToggleTemplate.java
index a8c193c..7a1331a 100644
--- a/core/java/android/service/controls/templates/DiscreteToggleTemplate.java
+++ b/core/java/android/service/controls/templates/DiscreteToggleTemplate.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.controls.Control;
import android.service.controls.actions.BooleanAction;
@@ -33,9 +32,8 @@
* {@link BooleanAction#getNewState} will be {@code false} if the button was
* {@link DiscreteToggleTemplate#getNegativeButton} and {@code true} if the button was
* {@link DiscreteToggleTemplate#getPositiveButton}.
- * @hide
*/
-public class DiscreteToggleTemplate extends ControlTemplate {
+public final class DiscreteToggleTemplate extends ControlTemplate {
private static final @TemplateType int TYPE = TYPE_DISCRETE_TOGGLE;
private static final String KEY_NEGATIVE_BUTTON = "key_negative_button";
@@ -46,8 +44,8 @@
/**
* @param templateId the identifier for this template object
- * @param negativeButton a {@ControlButton} for the <i>Negative</i> input
- * @param positiveButton a {@ControlButton} for the <i>Positive</i> input
+ * @param negativeButton a {@link ControlButton} for the <i>Negative</i> input
+ * @param positiveButton a {@link ControlButton} for the <i>Positive</i> input
*/
public DiscreteToggleTemplate(@NonNull String templateId,
@NonNull ControlButton negativeButton,
@@ -59,6 +57,10 @@
mPositiveButton = positiveButton;
}
+ /**
+ * @param b
+ * @hide
+ */
DiscreteToggleTemplate(Bundle b) {
super(b);
mNegativeButton = b.getParcelable(KEY_NEGATIVE_BUTTON);
@@ -89,32 +91,17 @@
return TYPE;
}
-
+ /**
+ * @return
+ * @hide
+ */
@Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putParcelable(KEY_NEGATIVE_BUTTON, mNegativeButton);
b.putParcelable(KEY_POSITIVE_BUTTON, mPositiveButton);
return b;
}
- public static final Creator<DiscreteToggleTemplate> CREATOR =
- new Creator<DiscreteToggleTemplate>() {
- @Override
- public DiscreteToggleTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new DiscreteToggleTemplate(source.readBundle());
- }
-
- @Override
- public DiscreteToggleTemplate[] newArray(int size) {
- return new DiscreteToggleTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/RangeTemplate.aidl b/core/java/android/service/controls/templates/RangeTemplate.aidl
deleted file mode 100644
index 9928815..0000000
--- a/core/java/android/service/controls/templates/RangeTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2019, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable RangeTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/RangeTemplate.java b/core/java/android/service/controls/templates/RangeTemplate.java
index bb79d83..fe0d167 100644
--- a/core/java/android/service/controls/templates/RangeTemplate.java
+++ b/core/java/android/service/controls/templates/RangeTemplate.java
@@ -27,7 +27,6 @@
* A template for a {@link Control} with inputs in a "continuous" range of values.
*
* @see FloatAction
- * @hide
*/
public final class RangeTemplate extends ControlTemplate {
@@ -148,8 +147,13 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putFloat(KEY_MIN_VALUE, mMinValue);
b.putFloat(KEY_MAX_VALUE, mMaxValue);
@@ -181,18 +185,4 @@
throw new IllegalArgumentException(String.format("stepValue=%f <= 0", mStepValue));
}
}
-
- public static final Creator<RangeTemplate> CREATOR = new Creator<RangeTemplate>() {
- @Override
- public RangeTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new RangeTemplate(source.readBundle());
- }
-
- @Override
- public RangeTemplate[] newArray(int size) {
- return new RangeTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/StatelessTemplate.aidl b/core/java/android/service/controls/templates/StatelessTemplate.aidl
deleted file mode 100644
index 02e18d9..0000000
--- a/core/java/android/service/controls/templates/StatelessTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable StatelessTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/StatelessTemplate.java b/core/java/android/service/controls/templates/StatelessTemplate.java
index 12ab9bc..3f98bea 100644
--- a/core/java/android/service/controls/templates/StatelessTemplate.java
+++ b/core/java/android/service/controls/templates/StatelessTemplate.java
@@ -18,11 +18,7 @@
import android.annotation.NonNull;
import android.os.Bundle;
-import android.os.Parcel;
-/**
- * @hide
- */
public final class StatelessTemplate extends ControlTemplate {
@Override
@@ -30,23 +26,15 @@
return TYPE_STATELESS;
}
- public StatelessTemplate(@NonNull Bundle b) {
+ /**
+ * @param b
+ * @hide
+ */
+ StatelessTemplate(@NonNull Bundle b) {
super(b);
}
public StatelessTemplate(@NonNull String templateId) {
super(templateId);
}
-
- public static final Creator<StatelessTemplate> CREATOR = new Creator<StatelessTemplate>() {
- @Override
- public StatelessTemplate createFromParcel(Parcel source) {
- return new StatelessTemplate(source.readBundle());
- }
-
- @Override
- public StatelessTemplate[] newArray(int size) {
- return new StatelessTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/TemperatureControlTemplate.aidl b/core/java/android/service/controls/templates/TemperatureControlTemplate.aidl
deleted file mode 100644
index 7994d26..0000000
--- a/core/java/android/service/controls/templates/TemperatureControlTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable TemperatureControlTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/TemperatureControlTemplate.java b/core/java/android/service/controls/templates/TemperatureControlTemplate.java
index 987621e..9d8dca62 100644
--- a/core/java/android/service/controls/templates/TemperatureControlTemplate.java
+++ b/core/java/android/service/controls/templates/TemperatureControlTemplate.java
@@ -19,7 +19,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.os.Bundle;
-import android.os.Parcel;
import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -27,9 +26,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-/**
- * @hide
- */
public final class TemperatureControlTemplate extends ControlTemplate {
private static final String TAG = "ThermostatTemplate";
@@ -67,6 +63,9 @@
public static final @Mode int MODE_ECO = 5;
+ /**
+ * @hide
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
FLAG_MODE_OFF,
@@ -136,18 +135,27 @@
}
}
+ /**
+ * @param b
+ * @hide
+ */
TemperatureControlTemplate(@NonNull Bundle b) {
super(b);
- mTemplate = b.getParcelable(KEY_TEMPLATE);
+ mTemplate = ControlTemplate.createTemplateFromBundle(b.getBundle(KEY_TEMPLATE));
mCurrentMode = b.getInt(KEY_CURRENT_MODE);
mCurrentActiveMode = b.getInt(KEY_CURRENT_ACTIVE_MODE);
mModes = b.getInt(KEY_MODES);
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
- b.putParcelable(KEY_TEMPLATE, mTemplate);
+ b.putBundle(KEY_TEMPLATE, mTemplate.getDataBundle());
b.putInt(KEY_CURRENT_MODE, mCurrentMode);
b.putInt(KEY_CURRENT_ACTIVE_MODE, mCurrentActiveMode);
b.putInt(KEY_MODES, mModes);
@@ -175,18 +183,4 @@
public int getTemplateType() {
return TYPE;
}
-
- public static final Creator<TemperatureControlTemplate> CREATOR = new Creator<TemperatureControlTemplate>() {
- @Override
- public TemperatureControlTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new TemperatureControlTemplate(source.readBundle());
- }
-
- @Override
- public TemperatureControlTemplate[] newArray(int size) {
- return new TemperatureControlTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/ThumbnailTemplate.aidl b/core/java/android/service/controls/templates/ThumbnailTemplate.aidl
deleted file mode 100644
index 81c879b..0000000
--- a/core/java/android/service/controls/templates/ThumbnailTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2019, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable ThumbnailTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/ThumbnailTemplate.java b/core/java/android/service/controls/templates/ThumbnailTemplate.java
index 111d60d..72179f4 100644
--- a/core/java/android/service/controls/templates/ThumbnailTemplate.java
+++ b/core/java/android/service/controls/templates/ThumbnailTemplate.java
@@ -19,15 +19,12 @@
import android.annotation.NonNull;
import android.graphics.drawable.Icon;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.controls.Control;
import com.android.internal.util.Preconditions;
/**
* A template for a {@link Control} that displays an image.
- *
- * @hide
*/
public final class ThumbnailTemplate extends ControlTemplate {
@@ -52,6 +49,10 @@
mContentDescription = contentDescription;
}
+ /**
+ * @param b
+ * @hide
+ */
ThumbnailTemplate(Bundle b) {
super(b);
mThumbnail = b.getParcelable(KEY_ICON);
@@ -82,25 +83,16 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putObject(KEY_ICON, mThumbnail);
b.putObject(KEY_CONTENT_DESCRIPTION, mContentDescription);
return b;
}
-
- public static final Creator<ThumbnailTemplate> CREATOR = new Creator<ThumbnailTemplate>() {
- @Override
- public ThumbnailTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new ThumbnailTemplate(source.readBundle());
- }
-
- @Override
- public ThumbnailTemplate[] newArray(int size) {
- return new ThumbnailTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/ToggleRangeTemplate.java b/core/java/android/service/controls/templates/ToggleRangeTemplate.java
index aa6f6fb..af43b94 100644
--- a/core/java/android/service/controls/templates/ToggleRangeTemplate.java
+++ b/core/java/android/service/controls/templates/ToggleRangeTemplate.java
@@ -18,13 +18,9 @@
import android.annotation.NonNull;
import android.os.Bundle;
-import android.os.Parcel;
import com.android.internal.util.Preconditions;
-/**
- * @hide
- */
public final class ToggleRangeTemplate extends ControlTemplate {
private static final @TemplateType int TYPE = TYPE_TOGGLE_RANGE;
@@ -34,11 +30,14 @@
private @NonNull final ControlButton mControlButton;
private @NonNull final RangeTemplate mRangeTemplate;
-
+ /**
+ * @param b
+ * @hide
+ */
ToggleRangeTemplate(@NonNull Bundle b) {
super(b);
mControlButton = b.getParcelable(KEY_BUTTON);
- mRangeTemplate = b.getParcelable(KEY_RANGE);
+ mRangeTemplate = new RangeTemplate(b.getBundle(KEY_RANGE));
}
public ToggleRangeTemplate(@NonNull String templateId,
@@ -60,11 +59,16 @@
range);
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putParcelable(KEY_BUTTON, mControlButton);
- b.putParcelable(KEY_RANGE, mRangeTemplate);
+ b.putBundle(KEY_RANGE, mRangeTemplate.getDataBundle());
return b;
}
@@ -86,19 +90,4 @@
public int getTemplateType() {
return TYPE;
}
-
- public static final Creator<ToggleRangeTemplate> CREATOR = new Creator<ToggleRangeTemplate>() {
-
- @Override
- public ToggleRangeTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new ToggleRangeTemplate(source.readBundle());
- }
-
- @Override
- public ToggleRangeTemplate[] newArray(int size) {
- return new ToggleRangeTemplate[size];
- }
- };
}
diff --git a/core/java/android/service/controls/templates/ToggleTemplate.aidl b/core/java/android/service/controls/templates/ToggleTemplate.aidl
deleted file mode 100644
index 98a9e49..0000000
--- a/core/java/android/service/controls/templates/ToggleTemplate.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2019, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.controls.templates;
-
-parcelable ToggleTemplate;
\ No newline at end of file
diff --git a/core/java/android/service/controls/templates/ToggleTemplate.java b/core/java/android/service/controls/templates/ToggleTemplate.java
index 0e5fd33..e4aa6b0 100644
--- a/core/java/android/service/controls/templates/ToggleTemplate.java
+++ b/core/java/android/service/controls/templates/ToggleTemplate.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.controls.Control;
import android.service.controls.actions.BooleanAction;
@@ -31,7 +30,6 @@
* An action on this template will originate a {@link BooleanAction} to change that state.
*
* @see BooleanAction
- * @hide
*/
public final class ToggleTemplate extends ControlTemplate {
@@ -41,7 +39,7 @@
/**
* @param templateId the identifier for this template object
- * @param button a {@ControlButton} that can show the current state and toggle it
+ * @param button a {@link ControlButton} that can show the current state and toggle it
*/
public ToggleTemplate(@NonNull String templateId, @NonNull ControlButton button) {
super(templateId);
@@ -49,6 +47,10 @@
mButton = button;
}
+ /**
+ * @param b
+ * @hide
+ */
ToggleTemplate(Bundle b) {
super(b);
mButton = b.getParcelable(KEY_BUTTON);
@@ -58,6 +60,7 @@
return mButton.isChecked();
}
+ @NonNull
public CharSequence getContentDescription() {
return mButton.getActionDescription();
}
@@ -70,25 +73,15 @@
return TYPE;
}
+ /**
+ * @return
+ * @hide
+ */
@Override
- protected Bundle getDataBundle() {
+ @NonNull
+ Bundle getDataBundle() {
Bundle b = super.getDataBundle();
b.putParcelable(KEY_BUTTON, mButton);
return b;
}
-
- public static final Creator<ToggleTemplate> CREATOR = new Creator<ToggleTemplate>() {
- @Override
- public ToggleTemplate createFromParcel(Parcel source) {
- int type = source.readInt();
- verifyType(type, TYPE);
- return new ToggleTemplate(source.readBundle());
- }
-
- @Override
- public ToggleTemplate[] newArray(int size) {
- return new ToggleTemplate[size];
- }
- };
-
}
diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java
index 4f400a8..c5d97b7 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -17,7 +17,8 @@
package android.service.notification;
import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
-import static android.util.FeatureFlagUtils.*;
+import static android.util.FeatureFlagUtils.NOTIF_CONVO_BYPASS_SHORTCUT_REQ;
+import static android.util.FeatureFlagUtils.isEnabled;
import android.annotation.NonNull;
import android.app.Notification;
@@ -33,7 +34,6 @@
import android.os.Parcelable;
import android.os.UserHandle;
import android.text.TextUtils;
-import android.util.FeatureFlagUtils;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -454,11 +454,23 @@
return conversationId;
}
- private String getGroupLogTag() {
+ /**
+ * Returns a probably-unique string based on the notification's group name,
+ * with no more than MAX_LOG_TAG_LENGTH characters.
+ * @return String based on group name of notification.
+ * @hide
+ */
+ public String getGroupLogTag() {
return shortenTag(getGroup());
}
- private String getChannelIdLogTag() {
+ /**
+ * Returns a probably-unique string based on the notification's channel ID,
+ * with no more than MAX_LOG_TAG_LENGTH characters.
+ * @return String based on channel ID of notification.
+ * @hide
+ */
+ public String getChannelIdLogTag() {
if (notification.getChannelId() == null) {
return null;
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 68d6e03..96ab0bb 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1101,6 +1101,9 @@
}
if (target != null) {
+ if (intent != null) {
+ intent.fixUris(UserHandle.myUserId());
+ }
safelyStartActivity(target);
// Rely on the ActivityManager to pop up a dialog regarding app suspension
diff --git a/core/jni/android/graphics/Bitmap.cpp b/core/jni/android/graphics/Bitmap.cpp
index 32a7cf3..eb7d432 100755
--- a/core/jni/android/graphics/Bitmap.cpp
+++ b/core/jni/android/graphics/Bitmap.cpp
@@ -475,8 +475,7 @@
}
auto fm = static_cast<Bitmap::JavaCompressFormat>(format);
- auto result = bitmap->bitmap().compress(fm, quality, strm.get());
- return result == Bitmap::CompressResult::Success ? JNI_TRUE : JNI_FALSE;
+ return bitmap->bitmap().compress(fm, quality, strm.get()) ? JNI_TRUE : JNI_FALSE;
}
static inline void bitmapErase(SkBitmap bitmap, const SkColor4f& color,
diff --git a/core/jni/android/graphics/apex/android_bitmap.cpp b/core/jni/android/graphics/apex/android_bitmap.cpp
index 6628529..decd190 100644
--- a/core/jni/android/graphics/apex/android_bitmap.cpp
+++ b/core/jni/android/graphics/apex/android_bitmap.cpp
@@ -290,14 +290,8 @@
}
CompressWriter stream(userContext, fn);
- switch (Bitmap::compress(bitmap, format, quality, &stream)) {
- case Bitmap::CompressResult::Success:
- return ANDROID_BITMAP_RESULT_SUCCESS;
- case Bitmap::CompressResult::AllocationFailed:
- return ANDROID_BITMAP_RESULT_ALLOCATION_FAILED;
- case Bitmap::CompressResult::Error:
- return ANDROID_BITMAP_RESULT_JNI_EXCEPTION;
- }
+ return Bitmap::compress(bitmap, format, quality, &stream) ? ANDROID_BITMAP_RESULT_SUCCESS
+ : ANDROID_BITMAP_RESULT_JNI_EXCEPTION;
}
AHardwareBuffer* ABitmap_getHardwareBuffer(ABitmap* bitmapHandle) {
diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
new file mode 100644
index 0000000..2648a06
--- /dev/null
+++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.controls;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.controls.actions.CommandAction;
+import android.service.controls.actions.ControlAction;
+import android.service.controls.actions.ControlActionWrapper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ControlProviderServiceTest {
+
+ private IBinder mToken = new Binder();
+ @Mock
+ private IControlsActionCallback.Stub mActionCallback;
+ @Mock
+ private IControlsLoadCallback.Stub mLoadCallback;
+ @Mock
+ private IControlsSubscriber.Stub mSubscriber;
+ @Mock
+ private IIntentSender mIIntentSender;
+
+ private PendingIntent mPendingIntent;
+ private FakeControlsProviderService mControlsProviderService;
+
+ private IControlsProvider mControlsProvider;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mActionCallback.asBinder()).thenCallRealMethod();
+ when(mActionCallback.queryLocalInterface(any())).thenReturn(mActionCallback);
+ when(mLoadCallback.asBinder()).thenCallRealMethod();
+ when(mLoadCallback.queryLocalInterface(any())).thenReturn(mLoadCallback);
+ when(mSubscriber.asBinder()).thenCallRealMethod();
+ when(mSubscriber.queryLocalInterface(any())).thenReturn(mSubscriber);
+
+ Bundle b = new Bundle();
+ b.putBinder(ControlsProviderService.CALLBACK_TOKEN, mToken);
+ Intent intent = new Intent();
+ intent.putExtra(ControlsProviderService.CALLBACK_BUNDLE, b);
+
+ mPendingIntent = new PendingIntent(mIIntentSender);
+
+ mControlsProviderService = new FakeControlsProviderService();
+ mControlsProvider = IControlsProvider.Stub.asInterface(
+ mControlsProviderService.onBind(intent));
+ }
+
+ @Test
+ public void testOnLoad_allStateless() throws RemoteException {
+ Control control1 = new Control.StatelessBuilder("TEST_ID", mPendingIntent).build();
+ Control control2 = new Control.StatelessBuilder("TEST_ID_2", mPendingIntent)
+ .setDeviceType(DeviceTypes.TYPE_AIR_FRESHENER).build();
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<List<Control>> captor = ArgumentCaptor.forClass(List.class);
+
+ ArrayList<Control> list = new ArrayList<>();
+ list.add(control1);
+ list.add(control2);
+
+ mControlsProviderService.setControls(list);
+ mControlsProvider.load(mLoadCallback);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mLoadCallback).accept(eq(mToken), captor.capture());
+ List<Control> l = captor.getValue();
+ assertEquals(2, l.size());
+ assertTrue(equals(control1, l.get(0)));
+ assertTrue(equals(control2, l.get(1)));
+ }
+
+ @Test
+ public void testOnLoad_statefulConvertedToStateless() throws RemoteException {
+ Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
+ .setTitle("TEST_TITLE")
+ .setStatus(Control.STATUS_OK)
+ .build();
+ Control statelessControl = new Control.StatelessBuilder(control).build();
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<List<Control>> captor = ArgumentCaptor.forClass(List.class);
+
+ ArrayList<Control> list = new ArrayList<>();
+ list.add(control);
+
+ mControlsProviderService.setControls(list);
+ mControlsProvider.load(mLoadCallback);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mLoadCallback).accept(eq(mToken), captor.capture());
+ List<Control> l = captor.getValue();
+ assertEquals(1, l.size());
+ assertFalse(equals(control, l.get(0)));
+ assertTrue(equals(statelessControl, l.get(0)));
+ assertEquals(Control.STATUS_UNKNOWN, l.get(0).getStatus());
+ }
+
+ @Test
+ public void testSubscribe() throws RemoteException {
+ Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
+ .setTitle("TEST_TITLE")
+ .setStatus(Control.STATUS_OK)
+ .build();
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<Control> controlCaptor =
+ ArgumentCaptor.forClass(Control.class);
+ ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
+ ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
+
+ ArrayList<Control> list = new ArrayList<>();
+ list.add(control);
+
+ mControlsProviderService.setControls(list);
+
+ mControlsProvider.subscribe(new ArrayList<String>(), mSubscriber);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
+ subscriptionCaptor.getValue().request(1);
+
+ verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
+ Control c = controlCaptor.getValue();
+ assertTrue(equals(c, list.get(0)));
+ }
+
+ @Test
+ public void testOnAction() throws RemoteException {
+ mControlsProvider.action("TEST_ID", new ControlActionWrapper(
+ new CommandAction("", null)), mActionCallback);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mActionCallback).accept(mToken, "TEST_ID",
+ ControlAction.RESPONSE_OK);
+ }
+
+ private static boolean equals(Control c1, Control c2) {
+ if (c1 == c2) return true;
+ if (c1 == null || c2 == null) return false;
+ return Objects.equals(c1.getControlId(), c2.getControlId())
+ && c1.getDeviceType() == c2.getDeviceType()
+ && Objects.equals(c1.getTitle(), c2.getTitle())
+ && Objects.equals(c1.getSubtitle(), c2.getSubtitle())
+ && Objects.equals(c1.getStructure(), c2.getStructure())
+ && Objects.equals(c1.getZone(), c2.getZone())
+ && Objects.equals(c1.getAppIntent(), c2.getAppIntent())
+ && Objects.equals(c1.getCustomIcon(), c2.getCustomIcon())
+ && Objects.equals(c1.getCustomColor(), c2.getCustomColor())
+ && c1.getStatus() == c2.getStatus()
+ && Objects.equals(c1.getControlTemplate(), c2.getControlTemplate())
+ && Objects.equals(c1.getStatusText(), c2.getStatusText());
+ }
+
+ static class FakeControlsProviderService extends ControlsProviderService {
+
+ private List<Control> mControls;
+
+ public void setControls(List<Control> controls) {
+ mControls = controls;
+ }
+
+ @Override
+ public void loadAvailableControls(Consumer<List<Control>> cb) {
+ cb.accept(mControls);
+ }
+
+ @Override
+ public Publisher<Control> publisherFor(List<String> ids) {
+ return new Publisher<Control>() {
+ public void subscribe(final Subscriber s) {
+ s.onSubscribe(new Subscription() {
+ public void request(long n) {
+ for (Control c : mControls) {
+ s.onNext(c);
+ }
+ }
+ public void cancel() {}
+ });
+ }
+ };
+ }
+
+ @Override
+ public void performControlAction(String controlId, ControlAction action,
+ Consumer<Integer> cb) {
+ cb.accept(ControlAction.RESPONSE_OK);
+ }
+ }
+}
+
+
diff --git a/core/tests/coretests/src/android/service/controls/ControlActionTest.java b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
similarity index 84%
rename from core/tests/coretests/src/android/service/controls/ControlActionTest.java
rename to core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
index d0264da..10a7b76 100644
--- a/core/tests/coretests/src/android/service/controls/ControlActionTest.java
+++ b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.service.controls;
+package android.service.controls.actions;
import static junit.framework.Assert.assertTrue;
@@ -22,12 +22,6 @@
import static org.junit.Assert.assertNotNull;
import android.os.Parcel;
-import android.service.controls.actions.BooleanAction;
-import android.service.controls.actions.CommandAction;
-import android.service.controls.actions.ControlAction;
-import android.service.controls.actions.FloatAction;
-import android.service.controls.actions.ModeAction;
-import android.service.controls.actions.MultiFloatAction;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -97,9 +91,9 @@
assertNotNull(parcel);
parcel.setDataPosition(0);
- toParcel.writeToParcel(parcel, 0);
+ new ControlActionWrapper(toParcel).writeToParcel(parcel, 0);
parcel.setDataPosition(0);
- return ControlAction.CREATOR.createFromParcel(parcel);
+ return ControlActionWrapper.CREATOR.createFromParcel(parcel).getWrappedAction();
}
}
diff --git a/core/tests/coretests/src/android/service/controls/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
similarity index 71%
rename from core/tests/coretests/src/android/service/controls/ControlTemplateTest.java
rename to core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
index 2756891..c9b5eec 100644
--- a/core/tests/coretests/src/android/service/controls/ControlTemplateTest.java
+++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.service.controls;
+package android.service.controls.templates;
import static junit.framework.Assert.assertTrue;
@@ -24,16 +24,6 @@
import android.annotation.DrawableRes;
import android.graphics.drawable.Icon;
import android.os.Parcel;
-import android.service.controls.templates.ControlButton;
-import android.service.controls.templates.ControlTemplate;
-import android.service.controls.templates.CoordinatedRangeTemplate;
-import android.service.controls.templates.DiscreteToggleTemplate;
-import android.service.controls.templates.RangeTemplate;
-import android.service.controls.templates.StatelessTemplate;
-import android.service.controls.templates.TemperatureControlTemplate;
-import android.service.controls.templates.ThumbnailTemplate;
-import android.service.controls.templates.ToggleRangeTemplate;
-import android.service.controls.templates.ToggleTemplate;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -65,8 +55,7 @@
@Test
public void testUnparcelingCorrectClass_none() {
- ControlTemplate
- toParcel = ControlTemplate.NO_TEMPLATE;
+ ControlTemplate toParcel = ControlTemplate.NO_TEMPLATE;
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -75,8 +64,7 @@
@Test
public void testUnparcelingCorrectClass_toggle() {
- ControlTemplate
- toParcel = new android.service.controls.templates.ToggleTemplate(TEST_ID, mControlButton);
+ ControlTemplate toParcel = new ToggleTemplate(TEST_ID, mControlButton);
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -86,8 +74,7 @@
@Test
public void testUnparcelingCorrectClass_range() {
- ControlTemplate
- toParcel = new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f");
+ ControlTemplate toParcel = new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f");
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -117,8 +104,7 @@
@Test
public void testUnparcelingCorrectClass_thumbnail() {
- ControlTemplate
- toParcel = new ThumbnailTemplate(TEST_ID, mIcon, TEST_ACTION_DESCRIPTION);
+ ControlTemplate toParcel = new ThumbnailTemplate(TEST_ID, mIcon, TEST_ACTION_DESCRIPTION);
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -140,7 +126,7 @@
@Test
public void testUnparcelingCorrectClass_coordRange() {
ControlTemplate toParcel =
- new CoordinatedRangeTemplate(TEST_ID,0.1f, 0, 1, 0.5f, 1, 2, 1.5f, 0.1f, "%f");
+ new CoordinatedRangeTemplate(TEST_ID, 0.1f, 0, 1, 0.5f, 1, 2, 1.5f, 0.1f, "%f");
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
assertEquals(ControlTemplate.TYPE_COORD_RANGE, fromParcel.getTemplateType());
assertTrue(fromParcel instanceof CoordinatedRangeTemplate);
@@ -149,7 +135,7 @@
@Test
public void testCoordRangeParameters_negativeMinGap() {
CoordinatedRangeTemplate template =
- new CoordinatedRangeTemplate(TEST_ID,-0.1f, 0, 1, 0.5f, 1, 2, 1.5f, 0.1f, "%f");
+ new CoordinatedRangeTemplate(TEST_ID, -0.1f, 0, 1, 0.5f, 1, 2, 1.5f, 0.1f, "%f");
assertEquals(0, template.getMinGap(), 0);
}
@@ -176,9 +162,8 @@
@Test
public void testUnparcelingCorrectClass_toggleRange() {
- ControlTemplate toParcel =
- new ToggleRangeTemplate(TEST_ID, mControlButton,
- new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f"));
+ ControlTemplate toParcel = new ToggleRangeTemplate(TEST_ID, mControlButton,
+ new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f"));
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -198,11 +183,12 @@
@Test
public void testUnparcelingCorrectClass_thermostat() {
- ControlTemplate toParcel = new TemperatureControlTemplate(TEST_ID,
- new ToggleTemplate("", mControlButton),
- TemperatureControlTemplate.MODE_OFF,
- TemperatureControlTemplate.MODE_OFF,
- TemperatureControlTemplate.FLAG_MODE_OFF);
+ ControlTemplate toParcel = new TemperatureControlTemplate(
+ TEST_ID,
+ new ToggleTemplate("", mControlButton),
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
@@ -212,48 +198,70 @@
@Test
public void testThermostatParams_wrongMode() {
- TemperatureControlTemplate thermostat = new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE, -1,
- TemperatureControlTemplate.MODE_OFF, TemperatureControlTemplate.FLAG_MODE_OFF);
+ TemperatureControlTemplate thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ -1,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentMode());
- thermostat = new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE, 100,
- TemperatureControlTemplate.MODE_OFF, TemperatureControlTemplate.FLAG_MODE_OFF);
+ thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ 100,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentMode());
}
@Test
public void testThermostatParams_wrongActiveMode() {
- TemperatureControlTemplate thermostat = new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE,
- TemperatureControlTemplate.MODE_OFF,-1, TemperatureControlTemplate.FLAG_MODE_OFF);
+ TemperatureControlTemplate thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ TemperatureControlTemplate.MODE_OFF,
+ -1,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentActiveMode());
- thermostat = new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE,
- TemperatureControlTemplate.MODE_OFF,100, TemperatureControlTemplate.FLAG_MODE_OFF);
+ thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ TemperatureControlTemplate.MODE_OFF,
+ 100,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentActiveMode());
}
@Test(expected = IllegalArgumentException.class)
public void testThermostatParams_wrongFlags_currentMode() {
- new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE, TemperatureControlTemplate.MODE_HEAT,
- TemperatureControlTemplate.MODE_OFF, TemperatureControlTemplate.FLAG_MODE_OFF);
+ new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ TemperatureControlTemplate.MODE_HEAT,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
}
@Test(expected = IllegalArgumentException.class)
public void testThermostatParams_wrongFlags_currentActiveMode() {
- new TemperatureControlTemplate(TEST_ID, ControlTemplate.NO_TEMPLATE, TemperatureControlTemplate.MODE_HEAT,
- TemperatureControlTemplate.MODE_OFF, TemperatureControlTemplate.FLAG_MODE_HEAT);
+ new TemperatureControlTemplate(TEST_ID,
+ ControlTemplate.NO_TEMPLATE,
+ TemperatureControlTemplate.MODE_HEAT,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_HEAT);
}
- private ControlTemplate parcelAndUnparcel(
- ControlTemplate toParcel) {
+ private ControlTemplate parcelAndUnparcel(ControlTemplate toParcel) {
Parcel parcel = Parcel.obtain();
assertNotNull(parcel);
parcel.setDataPosition(0);
- toParcel.writeToParcel(parcel, 0);
+ new ControlTemplateWrapper(toParcel).writeToParcel(parcel, 0);
parcel.setDataPosition(0);
- return ControlTemplate.CREATOR.createFromParcel(parcel);
+ return ControlTemplateWrapper.CREATOR.createFromParcel(parcel).getWrappedTemplate();
}
}
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 3c402e9..7c0efe1 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -471,36 +471,14 @@
return BitmapPalette::Unknown;
}
-Bitmap::CompressResult Bitmap::compress(JavaCompressFormat format, int32_t quality,
- SkWStream* stream) {
+bool Bitmap::compress(JavaCompressFormat format, int32_t quality, SkWStream* stream) {
SkBitmap skbitmap;
getSkBitmap(&skbitmap);
return compress(skbitmap, format, quality, stream);
}
-Bitmap::CompressResult Bitmap::compress(const SkBitmap& bitmap, JavaCompressFormat format,
- int32_t quality, SkWStream* stream) {
- SkBitmap skbitmap = bitmap;
- if (skbitmap.colorType() == kRGBA_F16_SkColorType) {
- // Convert to P3 before encoding. This matches
- // SkAndroidCodec::computeOutputColorSpace for wide gamuts. Now that F16
- // could already be P3, we still want to convert to 8888.
- auto cs = SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDCIP3);
- auto info = skbitmap.info().makeColorType(kRGBA_8888_SkColorType)
- .makeColorSpace(std::move(cs));
- SkBitmap p3;
- if (!p3.tryAllocPixels(info)) {
- return CompressResult::AllocationFailed;
- }
-
- SkPixmap pm;
- SkAssertResult(p3.peekPixels(&pm)); // should always work if tryAllocPixels() did.
- if (!skbitmap.readPixels(pm)) {
- return CompressResult::Error;
- }
- skbitmap = p3;
- }
-
+bool Bitmap::compress(const SkBitmap& bitmap, JavaCompressFormat format,
+ int32_t quality, SkWStream* stream) {
SkEncodedImageFormat fm;
switch (format) {
case JavaCompressFormat::Jpeg:
@@ -518,12 +496,10 @@
options.fQuality = quality;
options.fCompression = format == JavaCompressFormat::WebpLossy ?
SkWebpEncoder::Compression::kLossy : SkWebpEncoder::Compression::kLossless;
- return SkWebpEncoder::Encode(stream, skbitmap.pixmap(), options)
- ? CompressResult::Success : CompressResult::Error;
+ return SkWebpEncoder::Encode(stream, bitmap.pixmap(), options);
}
}
- return SkEncodeImage(stream, skbitmap, fm, quality)
- ? CompressResult::Success : CompressResult::Error;
+ return SkEncodeImage(stream, bitmap, fm, quality);
}
} // namespace android
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index ee365af..3bfb780 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -154,16 +154,10 @@
WebpLossless = 4,
};
- enum class CompressResult {
- Success,
- AllocationFailed,
- Error,
- };
+ bool compress(JavaCompressFormat format, int32_t quality, SkWStream* stream);
- CompressResult compress(JavaCompressFormat format, int32_t quality, SkWStream* stream);
-
- static CompressResult compress(const SkBitmap& bitmap, JavaCompressFormat format,
- int32_t quality, SkWStream* stream);
+ static bool compress(const SkBitmap& bitmap, JavaCompressFormat format,
+ int32_t quality, SkWStream* stream);
private:
static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index cbc9683..197786f 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -763,7 +763,13 @@
int maxLevel = 0;
for (CodecProfileLevel pl : profileLevels) {
if (pl.profile == profile && pl.level > maxLevel) {
- maxLevel = pl.level;
+ // H.263 levels are not completely ordered:
+ // Level45 support only implies Level10 support
+ if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)
+ || pl.level != CodecProfileLevel.H263Level45
+ || maxLevel == CodecProfileLevel.H263Level10) {
+ maxLevel = pl.level;
+ }
}
}
levelCaps = createFromProfileLevel(mMime, profile, maxLevel);
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index f408ac3..f61d55e 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -20,6 +20,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
+import android.media.MediaCodec;
+import android.media.MediaParser;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -45,6 +47,7 @@
* <table>
* <tr><th>Name</th><th>Value Type</th><th>Description</th></tr>
* <tr><td>{@link #KEY_MIME}</td><td>String</td><td>The type of the format.</td></tr>
+ * <tr><td>{@link #KEY_CODECS_STRING}</td><td>String</td><td>optional, the RFC 6381 codecs string of the MediaFormat</td></tr>
* <tr><td>{@link #KEY_MAX_INPUT_SIZE}</td><td>Integer</td><td>optional, maximum size of a buffer of input data</td></tr>
* <tr><td>{@link #KEY_PIXEL_ASPECT_RATIO_WIDTH}</td><td>Integer</td><td>optional, the pixel aspect ratio width</td></tr>
* <tr><td>{@link #KEY_PIXEL_ASPECT_RATIO_HEIGHT}</td><td>Integer</td><td>optional, the pixel aspect ratio height</td></tr>
@@ -217,6 +220,15 @@
public static final String KEY_MIME = "mime";
/**
+ * A key describing the codecs string of the MediaFormat. See RFC 6381 section 3.2 for the
+ * syntax of the value. The value does not hold {@link MediaCodec}-exposed codec names.
+ * The associated value is a string.
+ *
+ * @see MediaParser.TrackData#mediaFormat
+ */
+ public static final String KEY_CODECS_STRING = "codecs-string";
+
+ /**
* An optional key describing the low latency decoding mode. This is an optional parameter
* that applies only to decoders. If enabled, the decoder doesn't hold input and output
* data more than required by the codec standards.
diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java
index 4cd581b..1617b87 100644
--- a/media/java/android/media/MediaMetadataRetriever.java
+++ b/media/java/android/media/MediaMetadataRetriever.java
@@ -28,6 +28,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
+import android.text.TextUtils;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -44,6 +45,162 @@
* frame and meta data from an input media file.
*/
public class MediaMetadataRetriever implements AutoCloseable {
+
+ // borrowed from ExoPlayer
+ private static final String[] STANDARD_GENRES = new String[] {
+ // These are the official ID3v1 genres.
+ "Blues",
+ "Classic Rock",
+ "Country",
+ "Dance",
+ "Disco",
+ "Funk",
+ "Grunge",
+ "Hip-Hop",
+ "Jazz",
+ "Metal",
+ "New Age",
+ "Oldies",
+ "Other",
+ "Pop",
+ "R&B",
+ "Rap",
+ "Reggae",
+ "Rock",
+ "Techno",
+ "Industrial",
+ "Alternative",
+ "Ska",
+ "Death Metal",
+ "Pranks",
+ "Soundtrack",
+ "Euro-Techno",
+ "Ambient",
+ "Trip-Hop",
+ "Vocal",
+ "Jazz+Funk",
+ "Fusion",
+ "Trance",
+ "Classical",
+ "Instrumental",
+ "Acid",
+ "House",
+ "Game",
+ "Sound Clip",
+ "Gospel",
+ "Noise",
+ "AlternRock",
+ "Bass",
+ "Soul",
+ "Punk",
+ "Space",
+ "Meditative",
+ "Instrumental Pop",
+ "Instrumental Rock",
+ "Ethnic",
+ "Gothic",
+ "Darkwave",
+ "Techno-Industrial",
+ "Electronic",
+ "Pop-Folk",
+ "Eurodance",
+ "Dream",
+ "Southern Rock",
+ "Comedy",
+ "Cult",
+ "Gangsta",
+ "Top 40",
+ "Christian Rap",
+ "Pop/Funk",
+ "Jungle",
+ "Native American",
+ "Cabaret",
+ "New Wave",
+ "Psychadelic",
+ "Rave",
+ "Showtunes",
+ "Trailer",
+ "Lo-Fi",
+ "Tribal",
+ "Acid Punk",
+ "Acid Jazz",
+ "Polka",
+ "Retro",
+ "Musical",
+ "Rock & Roll",
+ "Hard Rock",
+ // These were made up by the authors of Winamp and later added to the ID3 spec.
+ "Folk",
+ "Folk-Rock",
+ "National Folk",
+ "Swing",
+ "Fast Fusion",
+ "Bebob",
+ "Latin",
+ "Revival",
+ "Celtic",
+ "Bluegrass",
+ "Avantgarde",
+ "Gothic Rock",
+ "Progressive Rock",
+ "Psychedelic Rock",
+ "Symphonic Rock",
+ "Slow Rock",
+ "Big Band",
+ "Chorus",
+ "Easy Listening",
+ "Acoustic",
+ "Humour",
+ "Speech",
+ "Chanson",
+ "Opera",
+ "Chamber Music",
+ "Sonata",
+ "Symphony",
+ "Booty Bass",
+ "Primus",
+ "Porn Groove",
+ "Satire",
+ "Slow Jam",
+ "Club",
+ "Tango",
+ "Samba",
+ "Folklore",
+ "Ballad",
+ "Power Ballad",
+ "Rhythmic Soul",
+ "Freestyle",
+ "Duet",
+ "Punk Rock",
+ "Drum Solo",
+ "A capella",
+ "Euro-House",
+ "Dance Hall",
+ // These were made up by the authors of Winamp but have not been added to the ID3 spec.
+ "Goa",
+ "Drum & Bass",
+ "Club-House",
+ "Hardcore",
+ "Terror",
+ "Indie",
+ "BritPop",
+ "Afro-Punk",
+ "Polsk Punk",
+ "Beat",
+ "Christian Gangsta Rap",
+ "Heavy Metal",
+ "Black Metal",
+ "Crossover",
+ "Contemporary Christian",
+ "Christian Rock",
+ "Merengue",
+ "Salsa",
+ "Thrash Metal",
+ "Anime",
+ "Jpop",
+ "Synthpop"
+ };
+
static {
System.loadLibrary("media_jni");
native_init();
@@ -225,6 +382,8 @@
private native void _setDataSource(MediaDataSource dataSource)
throws IllegalArgumentException;
+ private native @Nullable String nativeExtractMetadata(int keyCode);
+
/**
* Call this method after setDataSource(). This method retrieves the
* meta data value associated with the keyCode.
@@ -236,7 +395,118 @@
* @return The meta data value associate with the given keyCode on success;
* null on failure.
*/
- public native @Nullable String extractMetadata(int keyCode);
+ public @Nullable String extractMetadata(int keyCode) {
+ String meta = nativeExtractMetadata(keyCode);
+ if (keyCode == METADATA_KEY_GENRE) {
+ // translate numeric genre code(s) to human readable
+ meta = convertGenreTag(meta);
+ }
+ return meta;
+ }
+
+ /*
+ * The id3v2 spec doesn't specify the syntax of the genre tag very precisely, so
+ * some assumptions are made. Using one possible interpretation of the id3v2
+ * spec, this method converts an id3 genre tag string to a human readable string,
+ * as follows:
+ * - if the first character of the tag is a digit, the entire tag is assumed to
+ * be an id3v1 numeric genre code. If the tag does not parse to a number, or
+ * the number is outside the range of defined standard genres, it is ignored.
+ * - if the tag does not start with a digit, it is assumed to be an id3v2 style
+ * tag consisting of one or more genres, with each genre being either a parenthesized
+ * integer referring to an id3v1 numeric genre code, the special indicators "(CR)" or
+ * "(RX)" (for "Cover" or "Remix", respectively), or a custom genre string. When
+ * a custom genre string is encountered, it is assumed to continue until the end
+ * of the tag, unless it starts with "((" in which case it is assumed to continue
+ * until the next close-parenthesis or the end of the tag. Any parse error in the tag
+ * causes it to be ignored.
+ * The human-readable genre string is not localized, and uses the English genre names
+ * from the spec.
+ */
+ private String convertGenreTag(String meta) {
+ if (TextUtils.isEmpty(meta)) {
+ return null;
+ }
+
+ if (Character.isDigit(meta.charAt(0))) {
+ // assume a single id3v1-style bare number without any extra characters
+ try {
+ int genreIndex = Integer.parseInt(meta);
+ if (genreIndex >= 0 && genreIndex < STANDARD_GENRES.length) {
+ return STANDARD_GENRES[genreIndex];
+ }
+ } catch (NumberFormatException e) {
+ // ignore and fall through
+ }
+ return null;
+ } else {
+ // assume id3v2-style genre tag, with parenthesized numeric genres
+ // and/or literal genre strings, possibly more than one per tag.
+ StringBuilder genres = null;
+ String nextGenre = null;
+ while (true) {
+ if (!TextUtils.isEmpty(nextGenre)) {
+ if (genres == null) {
+ genres = new StringBuilder();
+ }
+ if (genres.length() != 0) {
+ genres.append(", ");
+ }
+ genres.append(nextGenre);
+ nextGenre = null;
+ }
+ if (TextUtils.isEmpty(meta)) {
+ // entire tag has been processed.
+ break;
+ }
+ if (meta.startsWith("(RX)")) {
+ nextGenre = "Remix";
+ meta = meta.substring(4);
+ } else if (meta.startsWith("(CR)")) {
+ nextGenre = "Cover";
+ meta = meta.substring(4);
+ } else if (meta.startsWith("((")) {
+ // the id3v2 spec says that custom genres that start with a parenthesis
+ // should be "escaped" with another parenthesis, however the spec doesn't
+ // specify escaping parentheses inside the custom string. We'll parse any
+ // such strings until a closing parenthesis is found, or the end of
+ // the tag is reached.
+ int closeParenOffset = meta.indexOf(')');
+ if (closeParenOffset == -1) {
+ // string continues to end of tag
+ nextGenre = meta.substring(1);
+ meta = "";
+ } else {
+ nextGenre = meta.substring(1, closeParenOffset + 1);
+ meta = meta.substring(closeParenOffset + 1);
+ }
+ } else if (meta.startsWith("(")) {
+ // should be a parenthesized numeric genre
+ int closeParenOffset = meta.indexOf(')');
+ if (closeParenOffset == -1) {
+ return null;
+ }
+ String genreNumString = meta.substring(1, closeParenOffset);
+ try {
+ int genreIndex = Integer.parseInt(genreNumString.toString());
+ if (genreIndex >= 0 && genreIndex < STANDARD_GENRES.length) {
+ nextGenre = STANDARD_GENRES[genreIndex];
+ } else {
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ meta = meta.substring(closeParenOffset + 1);
+ } else {
+ // custom genre
+ nextGenre = meta;
+ meta = "";
+ }
+ }
+ return genres == null || genres.length() == 0 ? null : genres.toString();
+ }
+ }
/**
* This method is similar to {@link #getFrameAtTime(long, int, BitmapParams)}
diff --git a/media/jni/android_media_MediaMetadataRetriever.cpp b/media/jni/android_media_MediaMetadataRetriever.cpp
index a5c62cb..1c9b349 100644
--- a/media/jni/android_media_MediaMetadataRetriever.cpp
+++ b/media/jni/android_media_MediaMetadataRetriever.cpp
@@ -728,7 +728,7 @@
(void *)android_media_MediaMetadataRetriever_getFrameAtIndex
},
- {"extractMetadata", "(I)Ljava/lang/String;",
+ {"nativeExtractMetadata", "(I)Ljava/lang/String;",
(void *)android_media_MediaMetadataRetriever_extractMetadata},
{"getEmbeddedPicture", "(I)[B",
(void *)android_media_MediaMetadataRetriever_getEmbeddedPicture},
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 486386f..1072076 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -4488,7 +4488,7 @@
try {
overlayManager.setEnabledExclusiveInCategory(
NAV_BAR_MODE_2BUTTON_OVERLAY, UserHandle.USER_CURRENT);
- } catch (RemoteException e) {
+ } catch (SecurityException | IllegalStateException | RemoteException e) {
throw new IllegalStateException(
"Failed to set nav bar interaction mode overlay");
}
diff --git a/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml b/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml
new file mode 100644
index 0000000..64b57c5
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_bubble_overflow_button.xml
@@ -0,0 +1,23 @@
+<!--
+Copyright (C) 2020 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:height="52dp"
+ android:width="52dp">
+ <path android:fillColor="#1A73E8"
+ android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bubble_overflow_activity.xml b/packages/SystemUI/res/layout/bubble_overflow_activity.xml
index 4cee746..95f205a 100644
--- a/packages/SystemUI/res/layout/bubble_overflow_activity.xml
+++ b/packages/SystemUI/res/layout/bubble_overflow_activity.xml
@@ -13,8 +13,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
+
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bubble_overflow_recycler"
- android:scrollbars="vertical"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"/>
+ android:layout_gravity="center_horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
diff --git a/packages/SystemUI/res/layout/bubble_overflow_button.xml b/packages/SystemUI/res/layout/bubble_overflow_button.xml
new file mode 100644
index 0000000..eb5dc9b
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_overflow_button.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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
+ -->
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_overflow_button"
+ android:layout_width="@dimen/individual_bubble_size"
+ android:layout_height="@dimen/individual_bubble_size"
+ android:src="@drawable/ic_bubble_overflow_button"
+ android:scaleType="center"
+ android:layout_gravity="end"/>
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index c1cf7b4..4171cd9 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -27,6 +27,12 @@
performance issues arise. -->
<integer name="bubbles_max_rendered">5</integer>
+ <!-- Number of columns in bubble overflow. -->
+ <integer name="bubbles_overflow_columns">4</integer>
+
+ <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. -->
+ <integer name="bubbles_max_overflow">16</integer>
+
<!-- Ratio of "left" end of status bar that will swipe to QQS. -->
<integer name="qqs_split_fraction">3</integer>
<!-- Ratio of "right" end of status bar that will swipe to QS. -->
diff --git a/packages/SystemUI/src/com/android/systemui/DumpController.kt b/packages/SystemUI/src/com/android/systemui/DumpController.kt
index f14c4cd..7a83a89 100644
--- a/packages/SystemUI/src/com/android/systemui/DumpController.kt
+++ b/packages/SystemUI/src/com/android/systemui/DumpController.kt
@@ -19,10 +19,10 @@
import android.util.ArraySet
import android.util.Log
import androidx.annotation.GuardedBy
-import com.android.internal.util.Preconditions
import java.io.FileDescriptor
import java.io.PrintWriter
import java.lang.ref.WeakReference
+import java.util.Objects.requireNonNull
import javax.inject.Inject
import javax.inject.Singleton
@@ -58,7 +58,7 @@
* @param dumpable the [Dumpable] to be added
*/
fun registerDumpable(dumpable: Dumpable) {
- Preconditions.checkNotNull(dumpable, "The dumpable to be added cannot be null")
+ requireNonNull(dumpable, "The dumpable to be added cannot be null")
registerDumpable(dumpable.javaClass.simpleName, dumpable)
}
@@ -71,7 +71,7 @@
* @param dumpable the [Dumpable] to be added
*/
fun registerDumpable(tag: String, dumpable: Dumpable) {
- Preconditions.checkNotNull(dumpable, "The dumpable to be added cannot be null")
+ requireNonNull(dumpable, "The dumpable to be added cannot be null")
if (DEBUG) Log.v(TAG, "*** register callback for $dumpable")
synchronized<Unit>(listeners) {
if (listeners.any { it.tag == tag }) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index dc24996..601bae2 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -89,12 +89,12 @@
/**
* Updates the view with provided info.
*/
- public void update(Bubble bubble, Bitmap bubbleImage, int dotColor, Path dotPath) {
+ public void update(Bubble bubble) {
mBubble = bubble;
- setImageBitmap(bubbleImage);
+ setImageBitmap(bubble.getBadgedImage());
setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
- mDotColor = dotColor;
- drawDot(dotPath);
+ mDotColor = bubble.getDotColor();
+ drawDot(bubble.getDotPath());
animateDot();
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 2d9775d..ccce85c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -31,6 +31,8 @@
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -93,6 +95,9 @@
}
private FlyoutMessage mFlyoutMessage;
+ private Bitmap mBadgedImage;
+ private int mDotColor;
+ private Path mDotPath;
public static String groupId(NotificationEntry entry) {
UserHandle user = entry.getSbn().getUser();
@@ -124,6 +129,18 @@
return mEntry.getSbn().getPackageName();
}
+ public Bitmap getBadgedImage() {
+ return mBadgedImage;
+ }
+
+ public int getDotColor() {
+ return mDotColor;
+ }
+
+ public Path getDotPath() {
+ return mDotPath;
+ }
+
@Nullable
public String getAppName() {
return mAppName;
@@ -205,8 +222,12 @@
mAppName = info.appName;
mFlyoutMessage = info.flyoutMessage;
+ mBadgedImage = info.badgedBubbleImage;
+ mDotColor = info.dotColor;
+ mDotPath = info.dotPath;
+
mExpandedView.update(this);
- mIconView.update(this, info.badgedBubbleImage, info.dotColor, info.dotPath);
+ mIconView.update(this);
}
/**
@@ -262,6 +283,13 @@
}
/**
+ * Should be invoked whenever a Bubble is promoted from overflow.
+ */
+ void markUpdatedAt(long lastAccessedMillis) {
+ mLastUpdated = lastAccessedMillis;
+ }
+
+ /**
* Whether this notification should be shown in the shade when it is also displayed as a
* bubble.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index e642d4e..8c9946f 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -103,6 +103,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -151,6 +152,7 @@
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
private BubbleIconFactory mBubbleIconFactory;
+ private int mMaxBubbles;
// Tracks the id of the current (foreground) user.
private int mCurrentUserId;
@@ -171,6 +173,8 @@
private StatusBarStateListener mStatusBarStateListener;
private final ScreenshotHelper mScreenshotHelper;
+ // Callback that updates BubbleOverflowActivity on data change.
+ @Nullable private Runnable mOverflowCallback = null;
private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
private IStatusBarService mBarService;
@@ -301,6 +305,7 @@
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
+ mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
mNotificationEntryManager = entryManager;
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
@@ -370,6 +375,18 @@
mInflateSynchronously = inflateSynchronously;
}
+ void setOverflowCallback(Runnable updateOverflow) {
+ mOverflowCallback = updateOverflow;
+ }
+
+ /**
+ * @return Bubbles for updating overflow.
+ */
+ List<Bubble> getOverflowBubbles() {
+ return mBubbleData.getOverflowBubbles();
+ }
+
+
/**
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
* method initializes the stack view and adds it to the StatusBar just above the scrim.
@@ -537,6 +554,10 @@
mBubbleData.setSelectedBubble(bubble);
}
+ void promoteBubbleFromOverflow(Bubble bubble) {
+ mBubbleData.promoteBubbleFromOverflow(bubble);
+ }
+
/**
* Request the stack expand if needed, then select the specified Bubble as current.
*
@@ -817,6 +838,11 @@
@Override
public void applyUpdate(BubbleData.Update update) {
+ // Update bubbles in overflow.
+ if (mOverflowCallback != null) {
+ mOverflowCallback.run();
+ }
+
if (update.addedBubble != null) {
mStackView.addBubble(update.addedBubble);
}
@@ -890,6 +916,8 @@
mStackView.updateBubble(update.updatedBubble);
}
+ // At this point, the correct bubbles are inflated in the stack.
+ // Make sure the order in bubble data is reflected in bubble row.
if (update.orderChanged) {
mStackView.updateBubbleOrder(update.bubbles);
}
@@ -912,15 +940,18 @@
updateStack();
if (DEBUG_BUBBLE_CONTROLLER) {
- Log.d(TAG, "[BubbleData]");
+ Log.d(TAG, "\n[BubbleData] bubbles:");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getBubbles(),
mBubbleData.getSelectedBubble()));
if (mStackView != null) {
- Log.d(TAG, "[BubbleStackView]");
+ Log.d(TAG, "\n[BubbleStackView]");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(mStackView.getBubblesOnScreen(),
mStackView.getExpandedBubble()));
}
+ Log.d(TAG, "\n[BubbleData] overflow:");
+ Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getOverflowBubbles(),
+ null));
}
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index cc0824e..8b687e7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -78,9 +78,11 @@
// A read-only view of the bubbles list, changes there will be reflected here.
final List<Bubble> bubbles;
+ final List<Bubble> overflowBubbles;
- private Update(List<Bubble> bubbleOrder) {
- bubbles = Collections.unmodifiableList(bubbleOrder);
+ private Update(List<Bubble> row, List<Bubble> overflow) {
+ bubbles = Collections.unmodifiableList(row);
+ overflowBubbles = Collections.unmodifiableList(overflow);
}
boolean anythingChanged() {
@@ -113,11 +115,14 @@
private final Context mContext;
/** Bubbles that are actively in the stack. */
private final List<Bubble> mBubbles;
+ /** Bubbles that aged out to overflow. */
+ private final List<Bubble> mOverflowBubbles;
/** Bubbles that are being loaded but haven't been added to the stack just yet. */
private final List<Bubble> mPendingBubbles;
private Bubble mSelectedBubble;
private boolean mExpanded;
private final int mMaxBubbles;
+ private final int mMaxOverflowBubbles;
// State tracked during an operation -- keeps track of what listener events to dispatch.
private Update mStateChange;
@@ -146,9 +151,11 @@
public BubbleData(Context context) {
mContext = context;
mBubbles = new ArrayList<>();
+ mOverflowBubbles = new ArrayList<>();
mPendingBubbles = new ArrayList<>();
- mStateChange = new Update(mBubbles);
+ mStateChange = new Update(mBubbles, mOverflowBubbles);
mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
+ mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
}
public boolean hasBubbles() {
@@ -184,6 +191,19 @@
dispatchPendingChanges();
}
+ public void promoteBubbleFromOverflow(Bubble bubble) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "promoteBubbleFromOverflow: " + bubble);
+ }
+ mOverflowBubbles.remove(bubble);
+ doAdd(bubble);
+ setSelectedBubbleInternal(bubble);
+ // Preserve new order for next repack, which sorts by last updated time.
+ bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
+ trim();
+ dispatchPendingChanges();
+ }
+
/**
* Constructs a new bubble or returns an existing one. Does not add new bubbles to
* bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
@@ -343,6 +363,7 @@
mStateChange.orderChanged = true;
}
mStateChange.addedBubble = bubble;
+
if (!isExpanded()) {
mStateChange.orderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId()));
// Top bubble becomes selected.
@@ -407,6 +428,17 @@
mStateChange.orderChanged |= repackAll();
}
+ if (reason == BubbleController.DISMISS_AGED) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "overflowing bubble: " + bubbleToRemove);
+ }
+ mOverflowBubbles.add(0, bubbleToRemove);
+ if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
+ // Remove oldest bubble.
+ mOverflowBubbles.remove(mOverflowBubbles.size() - 1);
+ }
+ }
+
// Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
// Move selection to the new bubble at the same position.
@@ -454,7 +486,7 @@
if (mListener != null && mStateChange.anythingChanged()) {
mListener.applyUpdate(mStateChange);
}
- mStateChange = new Update(mBubbles);
+ mStateChange = new Update(mBubbles, mOverflowBubbles);
}
/**
@@ -689,12 +721,19 @@
}
/**
- * The set of bubbles.
+ * The set of bubbles in row.
*/
@VisibleForTesting(visibility = PRIVATE)
public List<Bubble> getBubbles() {
return Collections.unmodifiableList(mBubbles);
}
+ /**
+ * The set of bubbles in overflow.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public List<Bubble> getOverflowBubbles() {
+ return Collections.unmodifiableList(mOverflowBubbles);
+ }
@VisibleForTesting(visibility = PRIVATE)
Bubble getBubbleWithKey(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 48ce4e9..cf8b2be 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -24,6 +24,7 @@
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.ActivityView;
@@ -83,7 +84,7 @@
private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING;
private int mTaskId = -1;
- private PendingIntent mBubbleIntent;
+ private PendingIntent mPendingIntent;
private boolean mKeyboardVisible;
private boolean mNeedsNewHeight;
@@ -98,7 +99,9 @@
private int[] mTempLoc = new int[2];
private int mExpandedViewTouchSlop;
- private Bubble mBubble;
+ @Nullable private Bubble mBubble;
+
+ private boolean mIsOverflow;
private BubbleController mBubbleController = Dependency.get(BubbleController.class);
private WindowManager mWindowManager;
@@ -125,7 +128,7 @@
+ "bubble=" + getBubbleKey());
}
try {
- if (mBubble.usingShortcutInfo()) {
+ if (!mIsOverflow && mBubble.usingShortcutInfo()) {
mActivityView.startShortcutActivity(mBubble.getShortcutInfo(),
options, null /* sourceBounds */);
} else {
@@ -133,7 +136,7 @@
// Apply flags to make behaviour match documentLaunchMode=always.
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
- mActivityView.startActivity(mBubbleIntent, fillInIntent, options);
+ mActivityView.startActivity(mPendingIntent, fillInIntent, options);
}
} catch (RuntimeException e) {
// If there's a runtime exception here then there's something
@@ -141,7 +144,7 @@
// the bubble again so we'll just remove it.
Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
+ ", " + e.getMessage() + "; removing bubble");
- mBubbleController.removeBubble(mBubble.getKey(),
+ mBubbleController.removeBubble(getBubbleKey(),
BubbleController.DISMISS_INVALID_INTENT);
}
});
@@ -241,6 +244,7 @@
mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
true /* singleTaskInstance */);
+
// Set ActivityView's alpha value as zero, since there is no view content to be shown.
setContentVisibility(false);
addView(mActivityView);
@@ -342,6 +346,15 @@
mStackView = stackView;
}
+ public void setOverflow(boolean overflow) {
+ mIsOverflow = overflow;
+
+ Intent target = new Intent(mContext, BubbleOverflowActivity.class);
+ mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0,
+ target, PendingIntent.FLAG_UPDATE_CURRENT);
+ mSettingsIcon.setVisibility(GONE);
+ }
+
/**
* Sets the bubble used to populate this view.
*/
@@ -350,14 +363,14 @@
Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null"));
}
boolean isNew = mBubble == null;
- if (isNew || bubble.getKey().equals(mBubble.getKey())) {
+ if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
mBubble = bubble;
mSettingsIcon.setContentDescription(getResources().getString(
R.string.bubbles_settings_button_description, bubble.getAppName()));
if (isNew) {
- mBubbleIntent = mBubble.getBubbleIntent();
- if (mBubbleIntent != null || mBubble.getShortcutInfo() != null) {
+ mPendingIntent = mBubble.getBubbleIntent();
+ if (mPendingIntent != null || mBubble.getShortcutInfo() != null) {
setContentVisibility(false);
mActivityView.setVisibility(VISIBLE);
}
@@ -393,12 +406,16 @@
return true;
}
+ // TODO(138116789) Fix overflow height.
void updateHeight() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
}
if (usingActivityView()) {
- float desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
+ float desiredHeight = mMinHeight;
+ if (!mIsOverflow) {
+ desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
+ }
float height = Math.min(desiredHeight, getMaxExpandedHeight());
height = Math.max(height, mMinHeight);
LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
@@ -423,8 +440,12 @@
int bottomInset = getRootWindowInsets() != null
? getRootWindowInsets().getStableInsetBottom()
: 0;
- return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
+ int mh = mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
- mPointerMargin - bottomInset;
+ Log.i(TAG, "max exp height: " + mh);
+// return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
+// - mPointerMargin - bottomInset;
+ return mh;
}
/**
@@ -543,7 +564,7 @@
}
private boolean usingActivityView() {
- return (mBubbleIntent != null || mBubble.getShortcutInfo() != null)
+ return (mPendingIntent != null || mBubble.getShortcutInfo() != null)
&& mActivityView != null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index 4252f72..006de84 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -76,6 +76,9 @@
private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu";
private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false;
+ private static final String ALLOW_BUBBLE_OVERFLOW = "allow_bubble_overflow";
+ private static final boolean ALLOW_BUBBLE_OVERFLOW_DEFAULT = false;
+
/**
* When true, if a notification has the information necessary to bubble (i.e. valid
* contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -141,6 +144,16 @@
}
/**
+ * When true, show a menu when a bubble is long-pressed, which will allow the user to take
+ * actions on that bubble.
+ */
+ static boolean allowBubbleOverflow(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ ALLOW_BUBBLE_OVERFLOW,
+ ALLOW_BUBBLE_OVERFLOW_DEFAULT ? 1 : 0) != 0;
+ }
+
+ /**
* If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
* {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
* the notification has necessary info for BubbleMetadata.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
index d99607f..bea55c8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java
@@ -1,15 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.systemui.bubbles;
+import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_OVERFLOW;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+
import android.app.Activity;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.systemui.R;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
import javax.inject.Inject;
/**
@@ -17,9 +44,13 @@
* Must be public to be accessible to androidx...AppComponentFactory
*/
public class BubbleOverflowActivity extends Activity {
- private RecyclerView mRecyclerView;
- private int mMaxBubbles;
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES;
+
private BubbleController mBubbleController;
+ private BubbleOverflowAdapter mAdapter;
+ private RecyclerView mRecyclerView;
+ private List<Bubble> mOverflowBubbles = new ArrayList<>();
+ private int mMaxBubbles;
@Inject
public BubbleOverflowActivity(BubbleController controller) {
@@ -35,17 +66,42 @@
mMaxBubbles = getResources().getInteger(R.integer.bubbles_max_rendered);
mRecyclerView = findViewById(R.id.bubble_overflow_recycler);
mRecyclerView.setLayoutManager(
- new GridLayoutManager(getApplicationContext(), /* numberOfColumns */ mMaxBubbles));
+ new GridLayoutManager(getApplicationContext(),
+ getResources().getInteger(R.integer.bubbles_overflow_columns)));
+
+ mAdapter = new BubbleOverflowAdapter(mOverflowBubbles,
+ mBubbleController::promoteBubbleFromOverflow);
+ mRecyclerView.setAdapter(mAdapter);
+
+ updateData(mBubbleController.getOverflowBubbles());
+ mBubbleController.setOverflowCallback(() -> {
+ updateData(mBubbleController.getOverflowBubbles());
+ });
}
void setBackgroundColor() {
final TypedArray ta = getApplicationContext().obtainStyledAttributes(
- new int[] {android.R.attr.colorBackgroundFloating});
+ new int[]{android.R.attr.colorBackgroundFloating});
int bgColor = ta.getColor(0, Color.WHITE);
ta.recycle();
findViewById(android.R.id.content).setBackgroundColor(bgColor);
}
+ void updateData(List<Bubble> bubbles) {
+ mOverflowBubbles.clear();
+ if (bubbles.size() > mMaxBubbles) {
+ mOverflowBubbles.addAll(bubbles.subList(mMaxBubbles, bubbles.size()));
+ } else {
+ mOverflowBubbles.addAll(bubbles);
+ }
+ mAdapter.notifyDataSetChanged();
+
+ if (DEBUG_OVERFLOW) {
+ Log.d(TAG, "Updated overflow bubbles:\n" + BubbleDebugConfig.formatBubblesString(
+ mOverflowBubbles, /*selected*/ null));
+ }
+ }
+
@Override
public void onStart() {
super.onStart();
@@ -75,3 +131,48 @@
super.onDestroy();
}
}
+
+class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> {
+ private Consumer<Bubble> mPromoteBubbleFromOverflow;
+ private List<Bubble> mBubbles;
+
+ public BubbleOverflowAdapter(List<Bubble> list, Consumer<Bubble> promoteBubble) {
+ mBubbles = list;
+ mPromoteBubbleFromOverflow = promoteBubble;
+ }
+
+ @Override
+ public BubbleOverflowAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ BadgedImageView view = (BadgedImageView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.bubble_view, parent, false);
+ view.setPadding(15, 15, 15, 15);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder vh, int index) {
+ Bubble bubble = mBubbles.get(index);
+
+ vh.mBadgedImageView.update(bubble);
+ vh.mBadgedImageView.setOnClickListener(view -> {
+ mBubbles.remove(bubble);
+ notifyDataSetChanged();
+ mPromoteBubbleFromOverflow.accept(bubble);
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return mBubbles.size();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public BadgedImageView mBadgedImageView;
+
+ public ViewHolder(BadgedImageView v) {
+ super(v);
+ mBadgedImageView = v;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 54a42a6..fe4c915 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -33,6 +33,8 @@
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
@@ -40,6 +42,9 @@
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -58,6 +63,7 @@
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -195,8 +201,6 @@
private int mPointerHeight;
private int mStatusBarHeight;
private int mImeOffset;
- private int mBubbleMenuOffset = 252;
- private BubbleIconFactory mBubbleIconFactory;
private Bubble mExpandedBubble;
private boolean mIsExpanded;
@@ -320,6 +324,8 @@
private Runnable mAfterMagnet;
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
+ private BubbleExpandedView mOverflowExpandedView;
+ private ImageView mOverflowBtn;
public BubbleStackView(Context context, BubbleData data,
@Nullable SurfaceSynchronizer synchronizer) {
@@ -369,8 +375,6 @@
mBubbleContainer.setClipChildren(false);
addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- mBubbleIconFactory = new BubbleIconFactory(context);
-
mExpandedViewContainer = new FrameLayout(context);
mExpandedViewContainer.setElevation(elevation);
mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
@@ -414,6 +418,10 @@
setFocusable(true);
mBubbleContainer.bringToFront();
+ if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
+ setUpOverflow();
+ }
+
setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
if (!mIsExpanded || mIsExpansionAnimating) {
return view.onApplyWindowInsets(insets);
@@ -421,7 +429,13 @@
mExpandedAnimationController.updateYPosition(
// Update the insets after we're done translating otherwise position
// calculation for them won't be correct.
- () -> mExpandedBubble.getExpandedView().updateInsets(insets));
+ () -> {
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateInsets(insets);
+ } else {
+ mExpandedBubble.getExpandedView().updateInsets(insets);
+ }
+ });
return view.onApplyWindowInsets(insets);
});
@@ -433,7 +447,11 @@
// Reposition & adjust the height for new orientation
if (mIsExpanded) {
mExpandedViewContainer.setTranslationY(getExpandedViewY());
- mExpandedBubble.getExpandedView().updateView();
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateView();
+ } else {
+ mExpandedBubble.getExpandedView().updateView();
+ }
}
// Need to update the padding around the view
@@ -499,6 +517,51 @@
mBubbleMenuView = findViewById(R.id.bubble_menu_container);
}
+ private void setUpOverflow() {
+ mOverflowExpandedView = (BubbleExpandedView) mInflater.inflate(
+ R.layout.bubble_expanded_view, this /* root */, false /* attachToRoot */);
+ mOverflowExpandedView.setOverflow(true);
+
+ mInflater.inflate(R.layout.bubble_overflow_button, this);
+ mOverflowBtn = findViewById(R.id.bubble_overflow_button);
+ mOverflowBtn.setOnClickListener(v -> {
+ showOverflow();
+ });
+
+ TypedArray ta = mContext.obtainStyledAttributes(
+ new int[]{android.R.attr.colorBackgroundFloating});
+ int bgColor = ta.getColor(0, Color.WHITE /* default */);
+ ta.recycle();
+
+ InsetDrawable fg = new InsetDrawable(mOverflowBtn.getDrawable(), 28);
+ ColorDrawable bg = new ColorDrawable(bgColor);
+ AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(bg, fg);
+ mOverflowBtn.setImageDrawable(adaptiveIcon);
+ mOverflowBtn.setVisibility(GONE);
+ }
+
+ void showOverflow() {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Show overflow.");
+ }
+ mExpandedViewContainer.setAlpha(0.0f);
+ mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
+ if (mExpandedBubble != null) {
+ mExpandedBubble.setContentVisibility(false);
+ mExpandedBubble = null;
+ }
+ mExpandedViewContainer.removeAllViews();
+ if (mIsExpanded) {
+ mExpandedViewContainer.addView(mOverflowExpandedView);
+ mOverflowExpandedView.populateExpandedView();
+ mExpandedViewContainer.setVisibility(VISIBLE);
+ mExpandedViewContainer.setAlpha(1.0f);
+ mOverflowExpandedView.setContentVisibility(true);
+ }
+ requestUpdate();
+ });
+ }
+
private void setUpFlyout() {
if (mFlyout != null) {
removeView(mFlyout);
@@ -734,9 +797,10 @@
new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
animateInFlyoutForBubble(bubble);
+ updatePointerPosition();
+ updateOverflowBtnVisibility( /*apply */ true);
requestUpdate();
logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
- updatePointerPosition();
}
// via BubbleData.Listener
@@ -753,7 +817,32 @@
} else {
Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
}
- updatePointerPosition();
+ updateOverflowBtnVisibility(/* apply */ true);
+ }
+
+ private void updateOverflowBtnVisibility(boolean apply) {
+ if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
+ return;
+ }
+ if (mIsExpanded) {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Expanded && overflow > 0. Show overflow button at");
+ Log.d(TAG, "x: " + mExpandedAnimationController.getOverflowBtnLeft());
+ Log.d(TAG, "y: " + mExpandedAnimationController.getExpandedY());
+ }
+ mOverflowBtn.setX(mExpandedAnimationController.getOverflowBtnLeft());
+ mOverflowBtn.setY(mExpandedAnimationController.getExpandedY());
+ mOverflowBtn.setVisibility(VISIBLE);
+ mExpandedAnimationController.setShowOverflowBtn(true);
+ if (apply) {
+ mExpandedAnimationController.expandFromStack(null /* after */);
+ }
+ } else {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "Collapsed. Hide overflow button.");
+ }
+ mOverflowBtn.setVisibility(GONE);
+ }
}
// via BubbleData.Listener
@@ -953,6 +1042,12 @@
final Bubble previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "animateCollapse");
+ Log.d(TAG, BubbleDebugConfig.formatBubblesString(this.getBubblesOnScreen(),
+ this.getExpandedBubble()));
+ }
+ updateOverflowBtnVisibility(/* apply */ false);
mBubbleContainer.cancelAllAnimations();
mExpandedAnimationController.collapseBackToStack(
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
@@ -960,7 +1055,9 @@
() -> {
mBubbleContainer.setActiveController(mStackAnimationController);
afterExpandedViewAnimation();
- previouslySelected.setContentVisibility(false);
+ if (previouslySelected != null) {
+ previouslySelected.setContentVisibility(false);
+ }
});
mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
@@ -975,12 +1072,12 @@
beforeExpandedViewAnimation();
mBubbleContainer.setActiveController(mExpandedAnimationController);
+ updateOverflowBtnVisibility(/* apply */ false);
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition();
afterExpandedViewAnimation();
} /* after */);
-
mExpandedViewContainer.setTranslationX(getCollapsedX());
mExpandedViewContainer.setTranslationY(getCollapsedY());
mExpandedViewContainer.setAlpha(0f);
@@ -1063,9 +1160,11 @@
Log.d(TAG, "onDragStart()");
}
if (mIsExpanded || mIsExpansionAnimating) {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "mIsExpanded or mIsExpansionAnimating");
+ }
return;
}
-
hideBubbleMenu();
mStackAnimationController.cancelStackPositionAnimations();
mBubbleContainer.setActiveController(mStackAnimationController);
@@ -1545,7 +1644,11 @@
if (!mExpandedViewYAnim.isRunning()) {
// We're not animating so set the value
mExpandedViewContainer.setTranslationY(y);
- mExpandedBubble.getExpandedView().updateView();
+ if (mExpandedBubble == null) {
+ mOverflowExpandedView.updateView();
+ } else {
+ mExpandedBubble.getExpandedView().updateView();
+ }
} else {
// We are animating so update the value; there is an end listener on the animator
// that will ensure expandedeView.updateView gets called.
@@ -1571,23 +1674,28 @@
}
private void updatePointerPosition() {
- if (DEBUG_BUBBLE_STACK_VIEW) {
- Log.d(TAG, "updatePointerPosition()");
- }
-
Bubble expandedBubble = getExpandedBubble();
if (expandedBubble == null) {
return;
}
int index = getBubbleIndex(expandedBubble);
+ if (index >= mMaxBubbles) {
+ // In between state, where extra bubble will be overflowed, and new bubble added
+ index = 0;
+ }
float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
float halfBubble = mBubbleSize / 2f;
float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
// Padding might be adjusted for insets, so get it directly from the view
bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
- expandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
+ if (index >= mMaxBubbles) {
+ Bubble first = mBubbleData.getBubbles().get(0);
+ first.getExpandedView().setPointerPosition(bubbleCenter);
+ } else {
+ expandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
+ }
}
/**
@@ -1680,7 +1788,11 @@
if (!isExpanded()) {
return false;
}
- return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
+ if (mExpandedBubble == null) {
+ return mOverflowExpandedView.performBackPressIfNeeded();
+ } else {
+ return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
+ }
}
/** For debugging only */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 6528f37..6d6969d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -67,6 +67,8 @@
private float mBubblePaddingTop;
/** Size of each bubble. */
private float mBubbleSizePx;
+ /** Width of the overflow button. */
+ private float mOverflowBtnWidth;
/** Height of the status bar. */
private float mStatusBarHeight;
/** Size of display. */
@@ -81,7 +83,7 @@
private boolean mAnimatingExpand = false;
private boolean mAnimatingCollapse = false;
- private Runnable mAfterExpand;
+ private @Nullable Runnable mAfterExpand;
private Runnable mAfterCollapse;
private PointF mCollapsePoint;
@@ -97,6 +99,7 @@
private boolean mSpringingBubbleToTouch = false;
private int mExpandedViewPadding;
+ private boolean mShowOverflowBtn;
public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
int orientation) {
@@ -116,7 +119,7 @@
/**
* Animates expanding the bubbles into a row along the top of the screen.
*/
- public void expandFromStack(Runnable after) {
+ public void expandFromStack(@Nullable Runnable after) {
mAnimatingCollapse = false;
mAnimatingExpand = true;
mAfterExpand = after;
@@ -150,6 +153,14 @@
}
}
+ public void setShowOverflowBtn(boolean showBtn) {
+ mShowOverflowBtn = showBtn;
+ }
+
+ public boolean getShowOverflowBtn() {
+ return mShowOverflowBtn;
+ }
+
/**
* Animates the bubbles along a curved path, either to expand them along the top or collapse
* them back into a stack.
@@ -380,6 +391,7 @@
mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
+ mOverflowBtnWidth = mBubbleSizePx;
mStatusBarHeight =
res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
@@ -498,6 +510,14 @@
return getRowLeft() + bubbleFromRowLeft;
}
+ public float getOverflowBtnLeft() {
+ if (mLayout == null || mLayout.getChildCount() == 0) {
+ return 0;
+ }
+ return getBubbleLeft(mLayout.getChildCount() - 1) + mBubbleSizePx
+ + getSpaceBetweenBubbles();
+ }
+
/**
* When expanded, the bubbles are centered in the screen. In portrait, all available space is
* used. In landscape we have too much space so the value is restricted. This method accounts
@@ -505,7 +525,7 @@
*
* @return the desired width to display the expanded bubbles in.
*/
- private float getWidthForDisplayingBubbles() {
+ public float getWidthForDisplayingBubbles() {
final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
// display size y in landscape will be the smaller dimension of the screen
@@ -551,7 +571,11 @@
final float totalBubbleWidth = bubbleCount * mBubbleSizePx;
final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
- final float rowWidth = totalGapWidth + totalBubbleWidth;
+ float rowWidth = totalGapWidth + totalBubbleWidth;
+ if (mShowOverflowBtn) {
+ rowWidth += getSpaceBetweenBubbles();
+ rowWidth += mOverflowBtnWidth;
+ }
// This display size we're using includes the size of the insets, we want the true
// center of the display minus the notch here, which means we should include the
@@ -559,7 +583,6 @@
final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
final float halfRow = rowWidth / 2f;
final float rowLeft = trueCenter - halfRow;
-
return rowLeft;
}
@@ -567,12 +590,12 @@
* @return Space between bubbles in row above expanded view.
*/
private float getSpaceBetweenBubbles() {
- final float rowMargins = mExpandedViewPadding * 2;
- final float maxRowWidth = getWidthForDisplayingBubbles() - rowMargins;
-
final float totalBubbleWidth = mBubblesMaxRendered * mBubbleSizePx;
- final float totalGapWidth = maxRowWidth - totalBubbleWidth;
-
+ final float rowMargins = mExpandedViewPadding * 2;
+ float totalGapWidth = getWidthForDisplayingBubbles() - rowMargins - totalBubbleWidth;
+ if (mShowOverflowBtn) {
+ totalGapWidth -= mBubbleSizePx;
+ }
final int gapCount = mBubblesMaxRendered - 1;
final float gapWidth = totalGapWidth / gapCount;
return gapWidth;
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 80e48b9..2db2cf1 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -20,7 +20,10 @@
import android.content.Context
import android.os.IBinder
import android.service.controls.Control
-import android.service.controls.IControlsProviderCallback
+import android.service.controls.IControlsActionCallback
+import android.service.controls.IControlsLoadCallback
+import android.service.controls.IControlsSubscriber
+import android.service.controls.IControlsSubscription
import android.service.controls.actions.ControlAction
import android.util.ArrayMap
import android.util.Log
@@ -54,20 +57,14 @@
private val componentMap: MutableMap<ComponentName, ControlsProviderLifecycleManager> =
ArrayMap<ComponentName, ControlsProviderLifecycleManager>()
- private val serviceCallback = object : IControlsProviderCallback.Stub() {
- override fun onLoad(token: IBinder, controls: MutableList<Control>) {
+ private val loadCallbackService = object : IControlsLoadCallback.Stub() {
+ override fun accept(token: IBinder, controls: MutableList<Control>) {
backgroundExecutor.execute(OnLoadRunnable(token, controls))
}
+ }
- override fun onRefreshState(token: IBinder, controlStates: List<Control>) {
- if (!refreshing.get()) {
- Log.d(TAG, "Refresh outside of window for token:$token")
- } else {
- backgroundExecutor.execute(OnRefreshStateRunnable(token, controlStates))
- }
- }
-
- override fun onControlActionResponse(
+ private val actionCallbackService = object : IControlsActionCallback.Stub() {
+ override fun accept(
token: IBinder,
controlId: String,
@ControlAction.ResponseResult response: Int
@@ -76,13 +73,36 @@
}
}
+ private val subscriberService = object : IControlsSubscriber.Stub() {
+ override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
+ backgroundExecutor.execute(OnSubscribeRunnable(token, subs))
+ }
+
+ override fun onNext(token: IBinder, c: Control) {
+ if (!refreshing.get()) {
+ Log.d(TAG, "Refresh outside of window for token:$token")
+ } else {
+ backgroundExecutor.execute(OnNextRunnable(token, c))
+ }
+ }
+ override fun onError(token: IBinder, s: String) {
+ backgroundExecutor.execute(OnErrorRunnable(token, s))
+ }
+
+ override fun onComplete(token: IBinder) {
+ backgroundExecutor.execute(OnCompleteRunnable(token))
+ }
+ }
+
@VisibleForTesting
internal open fun createProviderManager(component: ComponentName):
ControlsProviderLifecycleManager {
return ControlsProviderLifecycleManager(
context,
backgroundExecutor,
- serviceCallback,
+ loadCallbackService,
+ actionCallbackService,
+ subscriberService,
component
)
}
@@ -176,16 +196,51 @@
}
}
- private inner class OnRefreshStateRunnable(
+ private inner class OnNextRunnable(
token: IBinder,
- val list: List<Control>
+ val control: Control
) : CallbackRunnable(token) {
override fun run() {
if (!refreshing.get()) {
Log.d(TAG, "onRefresh outside of window from:${provider?.componentName}")
}
provider?.let {
- lazyController.get().refreshStatus(it.componentName, list)
+ lazyController.get().refreshStatus(it.componentName, control)
+ }
+ }
+ }
+
+ private inner class OnSubscribeRunnable(
+ token: IBinder,
+ val subscription: IControlsSubscription
+ ) : CallbackRunnable(token) {
+ override fun run() {
+ if (!refreshing.get()) {
+ Log.d(TAG, "onRefresh outside of window from '${provider?.componentName}'")
+ }
+ provider?.let {
+ it.startSubscription(subscription)
+ }
+ }
+ }
+
+ private inner class OnCompleteRunnable(
+ token: IBinder
+ ) : CallbackRunnable(token) {
+ override fun run() {
+ provider?.let {
+ Log.i(TAG, "onComplete receive from '${provider?.componentName}'")
+ }
+ }
+ }
+
+ private inner class OnErrorRunnable(
+ token: IBinder,
+ val error: String
+ ) : CallbackRunnable(token) {
+ override fun run() {
+ provider?.let {
+ Log.e(TAG, "onError receive from '${provider?.componentName}': $error")
}
}
}
@@ -201,4 +256,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 4d95822..e098faa 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -30,11 +30,11 @@
fun changeFavoriteStatus(controlInfo: ControlInfo, state: Boolean)
fun unsubscribe()
fun action(controlInfo: ControlInfo, action: ControlAction)
- fun refreshStatus(componentName: ComponentName, controls: List<Control>)
+ fun refreshStatus(componentName: ComponentName, control: Control)
fun onActionResponse(
componentName: ComponentName,
controlId: String,
@ControlAction.ResponseResult response: Int
)
fun clearFavorites()
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 7e328e4..d5b5b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -210,20 +210,20 @@
}
}
- override fun refreshStatus(componentName: ComponentName, controls: List<Control>) {
+ override fun refreshStatus(componentName: ComponentName, control: Control) {
if (!available) {
Log.d(TAG, "Controls not available")
return
}
executor.execute {
synchronized(currentFavorites) {
- val changed = updateFavoritesLocked(componentName, controls)
+ val changed = updateFavoritesLocked(componentName, listOf(control))
if (changed) {
persistenceWrapper.storeFavorites(favoritesAsListLocked())
}
}
}
- uiController.onRefreshState(componentName, controls)
+ uiController.onRefreshState(componentName, listOf(control))
}
override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
@@ -270,4 +270,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 79057ad..99aa360 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -25,11 +25,13 @@
import android.os.IBinder
import android.os.RemoteException
import android.service.controls.Control
-import android.service.controls.ControlsProviderService.CALLBACK_BINDER
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
+import android.service.controls.IControlsActionCallback
+import android.service.controls.IControlsLoadCallback
import android.service.controls.IControlsProvider
-import android.service.controls.IControlsProviderCallback
+import android.service.controls.IControlsSubscriber
+import android.service.controls.IControlsSubscription
import android.service.controls.actions.ControlAction
import android.util.ArraySet
import android.util.Log
@@ -41,19 +43,23 @@
class ControlsProviderLifecycleManager(
private val context: Context,
private val executor: DelayableExecutor,
- private val serviceCallback: IControlsProviderCallback.Stub,
+ private val loadCallbackService: IControlsLoadCallback.Stub,
+ private val actionCallbackService: IControlsActionCallback.Stub,
+ private val subscriberService: IControlsSubscriber.Stub,
val componentName: ComponentName
) : IBinder.DeathRecipient {
var lastLoadCallback: LoadCallback? = null
private set
val token: IBinder = Binder()
+ @GuardedBy("subscriptions")
+ private val subscriptions = mutableListOf<IControlsSubscription>()
private var unbindImmediate = false
private var requiresBound = false
private var isBound = false
@GuardedBy("queuedMessages")
private val queuedMessages: MutableSet<Message> = ArraySet()
- private var wrapper: ControlsProviderServiceWrapper? = null
+ private var wrapper: ServiceWrapper? = null
private var bindTryCount = 0
private val TAG = javaClass.simpleName
private var onLoadCanceller: Runnable? = null
@@ -61,12 +67,12 @@
companion object {
private const val MSG_LOAD = 0
private const val MSG_SUBSCRIBE = 1
- private const val MSG_UNSUBSCRIBE = 2
- private const val MSG_ON_ACTION = 3
- private const val MSG_UNBIND = 4
+ private const val MSG_ACTION = 2
+ private const val MSG_UNBIND = 3
private const val BIND_RETRY_DELAY = 1000L // ms
private const val LOAD_TIMEOUT = 5000L // ms
private const val MAX_BIND_RETRIES = 5
+ private const val MAX_CONTROLS_REQUEST = 100000L
private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
Context.BIND_WAIVE_PRIORITY
@@ -75,7 +81,6 @@
private val intent = Intent().apply {
component = componentName
putExtra(CALLBACK_BUNDLE, Bundle().apply {
- putBinder(CALLBACK_BINDER, serviceCallback)
putBinder(CALLBACK_TOKEN, token)
})
}
@@ -119,7 +124,7 @@
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (DEBUG) Log.d(TAG, "onServiceConnected $name")
bindTryCount = 0
- wrapper = ControlsProviderServiceWrapper(IControlsProvider.Stub.asInterface(service))
+ wrapper = ServiceWrapper(IControlsProvider.Stub.asInterface(service))
try {
service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
} catch (_: RemoteException) {}
@@ -151,7 +156,7 @@
}
queue.filter { it is Message.Action }.forEach {
val msg = it as Message.Action
- onAction(msg.id, msg.action)
+ action(msg.id, msg.action)
}
}
@@ -182,7 +187,7 @@
if (DEBUG) {
Log.d(TAG, "load $componentName")
}
- if (!(wrapper?.load() ?: false)) {
+ if (!(wrapper?.load(loadCallbackService) ?: false)) {
queueMessage(Message.Load)
binderDied()
}
@@ -194,7 +199,7 @@
onLoadCanceller = executor.executeDelayed({
// Didn't receive a response in time, log and send back empty list
Log.d(TAG, "Timeout waiting onLoad for $componentName")
- serviceCallback.onLoad(token, emptyList())
+ loadCallbackService.accept(token, emptyList())
}, LOAD_TIMEOUT, TimeUnit.MILLISECONDS)
if (isBound) {
load()
@@ -218,7 +223,7 @@
if (DEBUG) {
Log.d(TAG, "subscribe $componentName - $controlIds")
}
- if (!(wrapper?.subscribe(controlIds) ?: false)) {
+ if (!(wrapper?.subscribe(controlIds, subscriberService) ?: false)) {
queueMessage(Message.Subscribe(controlIds))
binderDied()
}
@@ -226,29 +231,45 @@
fun maybeBindAndSendAction(controlId: String, action: ControlAction) {
if (isBound) {
- onAction(controlId, action)
+ action(controlId, action)
} else {
queueMessage(Message.Action(controlId, action))
bindService(true)
}
}
- private fun onAction(controlId: String, action: ControlAction) {
+ private fun action(controlId: String, action: ControlAction) {
if (DEBUG) {
Log.d(TAG, "onAction $componentName - $controlId")
}
- if (!(wrapper?.onAction(controlId, action) ?: false)) {
+ if (!(wrapper?.action(controlId, action, actionCallbackService) ?: false)) {
queueMessage(Message.Action(controlId, action))
binderDied()
}
}
+ fun startSubscription(subscription: IControlsSubscription) {
+ synchronized(subscriptions) {
+ subscriptions.add(subscription)
+ }
+ wrapper?.request(subscription, MAX_CONTROLS_REQUEST)
+ }
+
fun unsubscribe() {
if (DEBUG) {
Log.d(TAG, "unsubscribe $componentName")
}
unqueueMessage(Message.Subscribe(emptyList())) // Removes all subscribe messages
- wrapper?.unsubscribe()
+
+ val subs = synchronized(subscriptions) {
+ ArrayList(subscriptions).also {
+ subscriptions.clear()
+ }
+ }
+
+ subs.forEach {
+ wrapper?.cancel(it)
+ }
}
fun maybeUnbindAndRemoveCallback() {
@@ -277,7 +298,7 @@
override val type = MSG_SUBSCRIBE
}
class Action(val id: String, val action: ControlAction) : Message() {
- override val type = MSG_ON_ACTION
+ override val type = MSG_ACTION
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt
deleted file mode 100644
index 882a10d..0000000
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapper.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.controls.controller
-
-import android.service.controls.actions.ControlAction
-import android.service.controls.IControlsProvider
-import android.util.Log
-
-class ControlsProviderServiceWrapper(val service: IControlsProvider) {
- companion object {
- private const val TAG = "ControlsProviderServiceWrapper"
- }
-
- private fun callThroughService(block: () -> Unit): Boolean {
- try {
- block()
- return true
- } catch (ex: Exception) {
- Log.d(TAG, "Caught exception from ControlsProviderService", ex)
- return false
- }
- }
-
- fun load(): Boolean {
- return callThroughService {
- service.load()
- }
- }
-
- fun subscribe(controlIds: List<String>): Boolean {
- return callThroughService {
- service.subscribe(controlIds)
- }
- }
-
- fun unsubscribe(): Boolean {
- return callThroughService {
- service.unsubscribe()
- }
- }
-
- fun onAction(controlId: String, action: ControlAction): Boolean {
- return callThroughService {
- service.onAction(controlId, action)
- }
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt
new file mode 100644
index 0000000..5c812b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.controller
+
+import android.service.controls.actions.ControlAction
+import android.service.controls.IControlsActionCallback
+import android.service.controls.IControlsLoadCallback
+import android.service.controls.IControlsProvider
+import android.service.controls.IControlsSubscriber
+import android.service.controls.IControlsSubscription
+import android.service.controls.actions.ControlActionWrapper
+import android.util.Log
+
+class ServiceWrapper(val service: IControlsProvider) {
+ companion object {
+ private const val TAG = "ServiceWrapper"
+ }
+
+ private fun callThroughService(block: () -> Unit): Boolean {
+ try {
+ block()
+ return true
+ } catch (ex: Exception) {
+ Log.e(TAG, "Caught exception from ControlsProviderService", ex)
+ return false
+ }
+ }
+
+ fun load(cb: IControlsLoadCallback): Boolean {
+ return callThroughService {
+ service.load(cb)
+ }
+ }
+
+ fun subscribe(controlIds: List<String>, subscriber: IControlsSubscriber): Boolean {
+ return callThroughService {
+ service.subscribe(controlIds, subscriber)
+ }
+ }
+
+ fun request(subscription: IControlsSubscription, num: Long): Boolean {
+ return callThroughService {
+ subscription.request(num)
+ }
+ }
+
+ fun cancel(subscription: IControlsSubscription): Boolean {
+ return callThroughService {
+ subscription.cancel()
+ }
+ }
+
+ fun action(
+ controlId: String,
+ action: ControlAction,
+ cb: IControlsActionCallback
+ ): Boolean {
+ return callThroughService {
+ service.action(controlId, ControlActionWrapper(action), cb)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
index 9372162..3949c59 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
@@ -51,7 +51,7 @@
context,
executor,
ServiceListing.Builder(context)
- .setIntentAction(ControlsProviderService.CONTROLS_ACTION)
+ .setIntentAction(ControlsProviderService.SERVICE_CONTROLS)
.setPermission("android.permission.BIND_CONTROLS")
.setNoun("Controls Provider")
.setSetting("controls_providers")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
index d6336ed..826af66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java
@@ -387,7 +387,7 @@
Log.d(TAG, "setModeOverlay: overlayPackage=" + overlayPkg
+ " userId=" + userId);
}
- } catch (RemoteException e) {
+ } catch (SecurityException | IllegalStateException | RemoteException e) {
Log.e(TAG, "Failed to enable overlay " + overlayPkg + " for user " + userId);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java
index 41e026a..665cb63 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayManager.java
@@ -178,7 +178,7 @@
} else {
mOverlayManager.setEnabled(pkg, false, userHandle);
}
- } catch (IllegalStateException e) {
+ } catch (SecurityException | IllegalStateException e) {
Log.e(TAG,
String.format("setEnabled failed: %s %s %b", pkg, userHandle, enabled), e);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index b09603d..1a2e237 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -257,7 +257,7 @@
* enforced by expiring the bubble which was least recently updated (lowest timestamp).
*/
@Test
- public void test_collapsed_addBubble_atMaxBubbles_expiresOldest() {
+ public void test_collapsed_addBubble_atMaxBubbles_overflowsOldest() {
// Setup
sendUpdatedEntryAtTime(mEntryA1, 1000);
sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -269,7 +269,10 @@
// Test
sendUpdatedEntryAtTime(mEntryC1, 6000);
verifyUpdateReceived();
+
+ // Verify
assertBubbleRemoved(mBubbleA1, BubbleController.DISMISS_AGED);
+ assertThat(mBubbleData.getOverflowBubbles()).isEqualTo(ImmutableList.of(mBubbleA1));
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index a19c299..be86a9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -224,8 +224,9 @@
@Test
fun testRefreshStatus() {
- val list = listOf(Control.StatefulBuilder(TEST_CONTROL_ID, pendingIntent).build())
- controller.refreshStatus(TEST_COMPONENT, list)
+ val control = Control.StatefulBuilder(TEST_CONTROL_ID, pendingIntent).build()
+ val list = listOf(control)
+ controller.refreshStatus(TEST_COMPONENT, control)
verify(uiController).onRefreshState(TEST_COMPONENT, list)
}
@@ -340,7 +341,7 @@
val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2)
val control = builderFromInfo(newControlInfo).build()
- controller.refreshStatus(TEST_COMPONENT, listOf(control))
+ controller.refreshStatus(TEST_COMPONENT, control)
delayableExecutor.runAllReady()
@@ -357,4 +358,4 @@
controller.clearFavorites()
assertTrue(controller.getFavoriteControls().isEmpty())
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
index 556bb40..4fc1cca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -18,9 +18,12 @@
import android.content.ComponentName
import android.service.controls.Control
+import android.service.controls.IControlsActionCallback
+import android.service.controls.IControlsLoadCallback
import android.service.controls.IControlsProvider
-import android.service.controls.IControlsProviderCallback
+import android.service.controls.IControlsSubscriber
import android.service.controls.actions.ControlAction
+import android.service.controls.actions.ControlActionWrapper
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -35,7 +38,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
@@ -46,14 +52,25 @@
class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
@Mock
- private lateinit var serviceCallback: IControlsProviderCallback.Stub
+ private lateinit var actionCallback: IControlsActionCallback.Stub
+ @Mock
+ private lateinit var loadCallback: IControlsLoadCallback.Stub
+ @Mock
+ private lateinit var subscriber: IControlsSubscriber.Stub
@Mock
private lateinit var service: IControlsProvider.Stub
+ @Captor
+ private lateinit var wrapperCaptor: ArgumentCaptor<ControlActionWrapper>
+
private val componentName = ComponentName("test.pkg", "test.cls")
private lateinit var manager: ControlsProviderLifecycleManager
private lateinit var executor: DelayableExecutor
+ companion object {
+ fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+ }
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -66,7 +83,9 @@
manager = ControlsProviderLifecycleManager(
context,
executor,
- serviceCallback,
+ loadCallback,
+ actionCallback,
+ subscriber,
componentName
)
}
@@ -94,7 +113,7 @@
val callback: (List<Control>) -> Unit = {}
manager.maybeBindAndLoad(callback)
- verify(service).load()
+ verify(service).load(loadCallback)
assertTrue(mContext.isBound(componentName))
assertEquals(callback, manager.lastLoadCallback)
@@ -110,29 +129,23 @@
}
@Test
- fun testUnsubscribe() {
- manager.bindPermanently()
- manager.unsubscribe()
-
- verify(service).unsubscribe()
- }
-
- @Test
fun testMaybeBindAndSubscribe() {
val list = listOf("TEST_ID")
manager.maybeBindAndSubscribe(list)
assertTrue(mContext.isBound(componentName))
- verify(service).subscribe(list)
+ verify(service).subscribe(list, subscriber)
}
@Test
fun testMaybeBindAndAction() {
val controlId = "TEST_ID"
- val action = ControlAction.UNKNOWN_ACTION
+ val action = ControlAction.ERROR_ACTION
manager.maybeBindAndSendAction(controlId, action)
assertTrue(mContext.isBound(componentName))
- verify(service).onAction(controlId, action)
+ verify(service).action(eq(controlId), capture(wrapperCaptor),
+ eq(actionCallback))
+ assertEquals(action, wrapperCaptor.getValue().getWrappedAction())
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt
deleted file mode 100644
index d6993c0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderServiceWrapperTest.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.controls.controller
-
-import android.os.RemoteException
-import android.service.controls.IControlsProvider
-import android.service.controls.actions.ControlAction
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.any
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class ControlsProviderServiceWrapperTest : SysuiTestCase() {
-
- @Mock
- private lateinit var service: IControlsProvider
-
- private val exception = RemoteException()
-
- private lateinit var wrapper: ControlsProviderServiceWrapper
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- wrapper = ControlsProviderServiceWrapper(service)
- }
-
- @Test
- fun testLoad_happyPath() {
- val result = wrapper.load()
-
- assertTrue(result)
- verify(service).load()
- }
-
- @Test
- fun testLoad_error() {
- `when`(service.load()).thenThrow(exception)
- val result = wrapper.load()
-
- assertFalse(result)
- }
-
- @Test
- fun testSubscribe_happyPath() {
- val list = listOf("TEST_ID")
- val result = wrapper.subscribe(list)
-
- assertTrue(result)
- verify(service).subscribe(list)
- }
-
- @Test
- fun testSubscribe_error() {
- `when`(service.subscribe(any())).thenThrow(exception)
-
- val list = listOf("TEST_ID")
- val result = wrapper.subscribe(list)
-
- assertFalse(result)
- }
-
- @Test
- fun testUnsubscribe_happyPath() {
- val result = wrapper.unsubscribe()
-
- assertTrue(result)
- verify(service).unsubscribe()
- }
-
- @Test
- fun testUnsubscribe_error() {
- `when`(service.unsubscribe()).thenThrow(exception)
- val result = wrapper.unsubscribe()
-
- assertFalse(result)
- }
-
- @Test
- fun testOnAction_happyPath() {
- val id = "TEST_ID"
- val action = ControlAction.UNKNOWN_ACTION
-
- val result = wrapper.onAction(id, action)
-
- assertTrue(result)
- verify(service).onAction(id, action)
- }
-
- @Test
- fun testOnAction_error() {
- `when`(service.onAction(any(), any())).thenThrow(exception)
-
- val id = "TEST_ID"
- val action = ControlAction.UNKNOWN_ACTION
-
- val result = wrapper.onAction(id, action)
-
- assertFalse(result)
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt
new file mode 100644
index 0000000..9e7ce06
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.controller
+
+import android.os.RemoteException
+import android.service.controls.IControlsActionCallback
+import android.service.controls.IControlsLoadCallback
+import android.service.controls.IControlsProvider
+import android.service.controls.IControlsSubscriber
+import android.service.controls.IControlsSubscription
+import android.service.controls.actions.ControlAction
+import android.service.controls.actions.ControlActionWrapper
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ServiceWrapperTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var service: IControlsProvider
+
+ @Mock
+ private lateinit var subscription: IControlsSubscription
+
+ @Mock
+ private lateinit var subscriber: IControlsSubscriber
+
+ @Mock
+ private lateinit var loadCallback: IControlsLoadCallback
+
+ @Mock
+ private lateinit var actionCallback: IControlsActionCallback
+
+ @Captor
+ private lateinit var wrapperCaptor: ArgumentCaptor<ControlActionWrapper>
+
+ private val exception = RemoteException()
+
+ private lateinit var wrapper: ServiceWrapper
+
+ companion object {
+ fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ wrapper = ServiceWrapper(service)
+ }
+
+ @Test
+ fun testLoad_happyPath() {
+ val result = wrapper.load(loadCallback)
+
+ assertTrue(result)
+ verify(service).load(loadCallback)
+ }
+
+ @Test
+ fun testLoad_error() {
+ `when`(service.load(any())).thenThrow(exception)
+ val result = wrapper.load(loadCallback)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun testSubscribe_happyPath() {
+ val list = listOf("TEST_ID")
+ val result = wrapper.subscribe(list, subscriber)
+
+ assertTrue(result)
+ verify(service).subscribe(list, subscriber)
+ }
+
+ @Test
+ fun testSubscribe_error() {
+ `when`(service.subscribe(any(), any())).thenThrow(exception)
+
+ val list = listOf("TEST_ID")
+ val result = wrapper.subscribe(list, subscriber)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun testCancel_happyPath() {
+ val result = wrapper.cancel(subscription)
+
+ assertTrue(result)
+ verify(subscription).cancel()
+ }
+
+ @Test
+ fun testCancel_error() {
+ `when`(subscription.cancel()).thenThrow(exception)
+ val result = wrapper.cancel(subscription)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun testOnAction_happyPath() {
+ val id = "TEST_ID"
+ val action = ControlAction.ERROR_ACTION
+
+ val result = wrapper.action(id, action, actionCallback)
+
+ assertTrue(result)
+ verify(service).action(eq(id), capture(wrapperCaptor),
+ eq(actionCallback))
+ assertEquals(action, wrapperCaptor.getValue().getWrappedAction())
+ }
+
+ @Test
+ fun testOnAction_error() {
+ `when`(service.action(any(), any(), any())).thenThrow(exception)
+
+ val id = "TEST_ID"
+ val action = ControlAction.ERROR_ACTION
+
+ val result = wrapper.action(id, action, actionCallback)
+
+ assertFalse(result)
+ }
+}
diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 8dacecc..79c6930 100644
--- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -130,6 +130,12 @@
*/
public static final int TETHERING_WIFI_P2P = 3;
+ /**
+ * Ncm local tethering type.
+ * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ */
+ public static final int TETHERING_NCM = 4;
+
public static final int TETHER_ERROR_NO_ERROR = 0;
public static final int TETHER_ERROR_UNKNOWN_IFACE = 1;
public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2;
diff --git a/packages/Tethering/res/values/config.xml b/packages/Tethering/res/values/config.xml
index 19e8d69..c489cbc 100644
--- a/packages/Tethering/res/values/config.xml
+++ b/packages/Tethering/res/values/config.xml
@@ -29,6 +29,12 @@
</string-array>
<!-- List of regexpressions describing the interface (if any) that represent tetherable
+ NCM interfaces. If the device doesn't want to support tethering over NCM this should
+ be empty. -->
+ <string-array translatable="false" name="config_tether_ncm_regexs">
+ </string-array>
+
+ <!-- List of regexpressions describing the interface (if any) that represent tetherable
Wifi interfaces. If the device doesn't want to support tethering over Wifi this
should be empty. An example would be "softap.*" -->
<string-array translatable="false" name="config_tether_wifi_regexs">
diff --git a/packages/Tethering/res/values/overlayable.xml b/packages/Tethering/res/values/overlayable.xml
index e089d9d..fe025c7 100644
--- a/packages/Tethering/res/values/overlayable.xml
+++ b/packages/Tethering/res/values/overlayable.xml
@@ -17,6 +17,7 @@
<overlayable name="TetheringConfig">
<policy type="product|system|vendor">
<item type="array" name="config_tether_usb_regexs"/>
+ <item type="array" name="config_tether_ncm_regexs" />
<item type="array" name="config_tether_wifi_regexs"/>
<item type="array" name="config_tether_wifi_p2p_regexs"/>
<item type="array" name="config_tether_bluetooth_regexs"/>
diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java
index 0491ad7..57cc4dd 100644
--- a/packages/Tethering/src/android/net/ip/IpServer.java
+++ b/packages/Tethering/src/android/net/ip/IpServer.java
@@ -416,7 +416,8 @@
final Inet4Address srvAddr;
int prefixLen = 0;
try {
- if (mInterfaceType == TetheringManager.TETHERING_USB) {
+ if (mInterfaceType == TetheringManager.TETHERING_USB
+ || mInterfaceType == TetheringManager.TETHERING_NCM) {
srvAddr = (Inet4Address) parseNumericAddress(USB_NEAR_IFACE_ADDR);
prefixLen = USB_PREFIX_LENGTH;
} else if (mInterfaceType == TetheringManager.TETHERING_WIFI) {
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
index c47f2d6..02ba17e 100644
--- a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.hardware.usb.UsbManager.USB_CONFIGURED;
import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM;
import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
@@ -30,6 +31,7 @@
import static android.net.TetheringManager.EXTRA_ERRORED_TETHER;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_INVALID;
+import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -408,6 +410,8 @@
return TETHERING_USB;
} else if (cfg.isBluetooth(iface)) {
return TETHERING_BLUETOOTH;
+ } else if (cfg.isNcm(iface)) {
+ return TETHERING_NCM;
}
return TETHERING_INVALID;
}
@@ -456,6 +460,10 @@
case TETHERING_BLUETOOTH:
setBluetoothTethering(enable, listener);
break;
+ case TETHERING_NCM:
+ result = setNcmTethering(enable);
+ sendTetherResult(listener, result);
+ break;
default:
Log.w(TAG, "Invalid tether type.");
sendTetherResult(listener, TETHER_ERROR_UNKNOWN_IFACE);
@@ -805,6 +813,7 @@
final boolean usbConnected = intent.getBooleanExtra(USB_CONNECTED, false);
final boolean usbConfigured = intent.getBooleanExtra(USB_CONFIGURED, false);
final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
+ final boolean ncmEnabled = intent.getBooleanExtra(USB_FUNCTION_NCM, false);
mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s",
usbConnected, usbConfigured, rndisEnabled));
@@ -832,6 +841,8 @@
} else if (usbConfigured && rndisEnabled) {
// Tether if rndis is enabled and usb is configured.
tetherMatchingInterfaces(IpServer.STATE_TETHERED, TETHERING_USB);
+ } else if (usbConnected && ncmEnabled) {
+ tetherMatchingInterfaces(IpServer.STATE_LOCAL_ONLY, TETHERING_NCM);
}
mRndisEnabled = usbConfigured && rndisEnabled;
}
@@ -1133,6 +1144,16 @@
return TETHER_ERROR_NO_ERROR;
}
+ private int setNcmTethering(boolean enable) {
+ if (VDBG) Log.d(TAG, "setNcmTethering(" + enable + ")");
+ UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+ synchronized (mPublicSync) {
+ usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM
+ : UsbManager.FUNCTION_NONE);
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
// TODO review API - figure out how to delete these entirely.
String[] getTetheredIfaces() {
ArrayList<String> list = new ArrayList<String>();
diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java b/packages/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
index 068c346..7e9e26f 100644
--- a/packages/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/packages/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -83,6 +83,7 @@
public final String[] tetherableWifiRegexs;
public final String[] tetherableWifiP2pRegexs;
public final String[] tetherableBluetoothRegexs;
+ public final String[] tetherableNcmRegexs;
public final boolean isDunRequired;
public final boolean chooseUpstreamAutomatically;
public final Collection<Integer> preferredUpstreamIfaceTypes;
@@ -103,6 +104,7 @@
Resources res = getResources(ctx, activeDataSubId);
tetherableUsbRegexs = getResourceStringArray(res, R.array.config_tether_usb_regexs);
+ tetherableNcmRegexs = getResourceStringArray(res, R.array.config_tether_ncm_regexs);
// TODO: Evaluate deleting this altogether now that Wi-Fi always passes
// us an interface name. Careful consideration needs to be given to
// implications for Settings and for provisioning checks.
@@ -156,6 +158,11 @@
return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs);
}
+ /** Check if interface is ncm */
+ public boolean isNcm(String iface) {
+ return matchesDownstreamRegexs(iface, tetherableNcmRegexs);
+ }
+
/** Check whether no ui entitlement application is available.*/
public boolean hasMobileHotspotProvisionApp() {
return !TextUtils.isEmpty(provisioningAppNoUi);
@@ -170,6 +177,7 @@
dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs);
dumpStringArray(pw, "tetherableWifiP2pRegexs", tetherableWifiP2pRegexs);
dumpStringArray(pw, "tetherableBluetoothRegexs", tetherableBluetoothRegexs);
+ dumpStringArray(pw, "tetherableNcmRegexs", tetherableNcmRegexs);
pw.print("isDunRequired: ");
pw.println(isDunRequired);
diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
index 6fc0c65..4710287 100644
--- a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
+++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -18,6 +18,7 @@
import static android.hardware.usb.UsbManager.USB_CONFIGURED;
import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM;
import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
@@ -27,6 +28,7 @@
import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
@@ -151,6 +153,7 @@
private static final String TEST_USB_IFNAME = "test_rndis0";
private static final String TEST_WLAN_IFNAME = "test_wlan0";
private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
+ private static final String TEST_NCM_IFNAME = "test_ncm0";
private static final String TETHERING_NAME = "Tethering";
private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
@@ -252,9 +255,11 @@
ifName.equals(TEST_USB_IFNAME)
|| ifName.equals(TEST_WLAN_IFNAME)
|| ifName.equals(TEST_MOBILE_IFNAME)
- || ifName.equals(TEST_P2P_IFNAME));
+ || ifName.equals(TEST_P2P_IFNAME)
+ || ifName.equals(TEST_NCM_IFNAME));
final String[] ifaces = new String[] {
- TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME, TEST_P2P_IFNAME};
+ TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME, TEST_P2P_IFNAME,
+ TEST_NCM_IFNAME};
return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
MacAddress.ALL_ZEROS_ADDRESS);
}
@@ -428,13 +433,16 @@
.thenReturn(new String[]{ "test_p2p-p2p\\d-.*" });
when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
.thenReturn(new String[0]);
+ when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
+ .thenReturn(new String[] { "test_ncm\\d" });
when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[0]);
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(false);
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
false);
when(mNetd.interfaceGetList())
.thenReturn(new String[] {
- TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME});
+ TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
+ TEST_NCM_IFNAME});
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
mInterfaceConfiguration = new InterfaceConfigurationParcel();
mInterfaceConfiguration.flags = new String[0];
@@ -524,11 +532,16 @@
P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST);
}
- private void sendUsbBroadcast(boolean connected, boolean configured, boolean rndisFunction) {
+ private void sendUsbBroadcast(boolean connected, boolean configured, boolean function,
+ int type) {
final Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
intent.putExtra(USB_CONNECTED, connected);
intent.putExtra(USB_CONFIGURED, configured);
- intent.putExtra(USB_FUNCTION_RNDIS, rndisFunction);
+ if (type == TETHERING_USB) {
+ intent.putExtra(USB_FUNCTION_RNDIS, function);
+ } else {
+ intent.putExtra(USB_FUNCTION_NCM, function);
+ }
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
@@ -578,6 +591,15 @@
verifyNoMoreInteractions(mWifiManager);
}
+ private void prepareNcmTethering() {
+ // Emulate startTethering(TETHERING_NCM) called
+ mTethering.startTethering(createTetheringRquestParcel(TETHERING_NCM), null);
+ mLooper.dispatchAll();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
+
+ mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
+ }
+
private void prepareUsbTethering(UpstreamNetworkState upstreamState) {
when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
@@ -600,7 +622,7 @@
verifyNoMoreInteractions(mNetd);
// Pretend we then receive USB configured broadcast.
- sendUsbBroadcast(true, true, true);
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
mLooper.dispatchAll();
// Now we should see the start of tethering mechanics (in this case:
// tetherMatchingInterfaces() which starts by fetching all interfaces).
@@ -691,7 +713,7 @@
private void runUsbTethering(UpstreamNetworkState upstreamState) {
prepareUsbTethering(upstreamState);
- sendUsbBroadcast(true, true, true);
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
mLooper.dispatchAll();
}
@@ -814,6 +836,29 @@
verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
}
+ private void runNcmTethering() {
+ prepareNcmTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_NCM);
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void workingNcmTethering() throws Exception {
+ runNcmTethering();
+
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ }
+
+ @Test
+ public void workingNcmTethering_LegacyDhcp() {
+ when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
+ true);
+ sendConfigurationChanged();
+ runNcmTethering();
+
+ verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
@Test
public void workingLocalOnlyHotspotEnrichedApBroadcastWithIfaceChanged() throws Exception {
workingLocalOnlyHotspotEnrichedApBroadcast(true);
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 882b417dd6..dd33566 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -64,6 +64,7 @@
import android.net.ConnectionInfo;
import android.net.ConnectivityManager;
import android.net.ICaptivePortal;
+import android.net.IConnectivityDiagnosticsCallback;
import android.net.IConnectivityManager;
import android.net.IDnsResolver;
import android.net.IIpConnectivityMetrics;
@@ -1623,7 +1624,8 @@
return getNetworkCapabilitiesInternal(getNetworkAgentInfoForNetwork(network));
}
- private NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
+ @VisibleForTesting
+ NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
NetworkCapabilities nc, int callerPid, int callerUid) {
final NetworkCapabilities newNc = new NetworkCapabilities(nc);
if (!checkSettingsPermission(callerPid, callerUid)) {
@@ -1634,9 +1636,23 @@
newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
}
newNc.setAdministratorUids(Collections.EMPTY_LIST);
+
+ maybeSanitizeLocationInfoForCaller(newNc, callerUid);
+
return newNc;
}
+ private void maybeSanitizeLocationInfoForCaller(
+ NetworkCapabilities nc, int callerUid) {
+ // TODO(b/142072839): Conditionally reset the owner UID if the following
+ // conditions are not met:
+ // 1. The destination app is the network owner
+ // 2. The destination app has the ACCESS_COARSE_LOCATION permission granted
+ // if target SDK<29 or otherwise has the ACCESS_FINE_LOCATION permission granted
+ // 3. The user's location toggle is on
+ nc.setOwnerUid(INVALID_UID);
+ }
+
private LinkProperties linkPropertiesRestrictedForCallerPermissions(
LinkProperties lp, int callerPid, int callerUid) {
if (lp == null) return new LinkProperties();
@@ -1665,6 +1681,9 @@
nc.setSingleUid(Binder.getCallingUid());
}
nc.setAdministratorUids(Collections.EMPTY_LIST);
+
+ // Clear owner UID; this can never come from an app.
+ nc.setOwnerUid(INVALID_UID);
}
private void restrictBackgroundRequestForCaller(NetworkCapabilities nc) {
@@ -5801,7 +5820,7 @@
}
final Set<UidRange> ranges = nai.networkCapabilities.getUids();
- final int vpnAppUid = nai.networkCapabilities.getEstablishingVpnAppUid();
+ final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
// TODO: this create a window of opportunity for apps to receive traffic between the time
// when the old rules are removed and the time when new rules are added. To fix this,
// make eBPF support two whitelisted interfaces so here new rules can be added before the
@@ -6000,7 +6019,7 @@
if (nc == null || lp == null) return false;
return nai.isVPN()
&& !nai.networkAgentConfig.allowBypass
- && nc.getEstablishingVpnAppUid() != Process.SYSTEM_UID
+ && nc.getOwnerUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIPv4DefaultRoute() || lp.hasIPv6DefaultRoute());
}
@@ -6048,12 +6067,10 @@
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
// to be removed will never overlap with the new range to be added.
if (wasFiltering && !prevRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges,
- prevNc.getEstablishingVpnAppUid());
+ mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges,
- newNc.getEstablishingVpnAppUid());
+ mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
}
} catch (Exception e) {
// Never crash!
@@ -7314,4 +7331,20 @@
return mTNS;
}
}
+
+ @Override
+ public void registerConnectivityDiagnosticsCallback(
+ @NonNull IConnectivityDiagnosticsCallback callback, @NonNull NetworkRequest request) {
+ // TODO(b/146444622): implement register IConnectivityDiagnosticsCallback functionality
+ throw new UnsupportedOperationException(
+ "registerConnectivityDiagnosticsCallback not yet implemented");
+ }
+
+ @Override
+ public void unregisterConnectivityDiagnosticsCallback(
+ @NonNull IConnectivityDiagnosticsCallback callback) {
+ // TODO(b/146444622): implement register IConnectivityDiagnosticsCallback functionality
+ throw new UnsupportedOperationException(
+ "unregisterConnectivityDiagnosticsCallback not yet implemented");
+ }
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 0c9abae..423e021 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -954,7 +954,7 @@
NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
networkAgentConfig.allowBypass = mConfig.allowBypass && !mLockdown;
- mNetworkCapabilities.setEstablishingVpnAppUid(Binder.getCallingUid());
+ mNetworkCapabilities.setOwnerUid(Binder.getCallingUid());
mNetworkCapabilities.setUids(createUserAndRestrictedProfilesRanges(mUserHandle,
mConfig.allowedApplications, mConfig.disallowedApplications));
long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index eaafc3f..214dcc0 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -48,6 +48,7 @@
import android.os.HandlerThread;
import android.os.RemoteException;
import android.util.Slog;
+import android.util.StatsLog;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -171,6 +172,8 @@
success = false;
}
+ StatsLog.write(StatsLog.INTEGRITY_RULES_PUSHED, success, ruleProvider, version);
+
Intent intent = new Intent();
intent.putExtra(EXTRA_STATUS, success ? STATUS_SUCCESS : STATUS_FAILURE);
try {
@@ -268,6 +271,16 @@
+ result.getEffect()
+ " due to "
+ result.getRule());
+
+ StatsLog.write(
+ StatsLog.INTEGRITY_CHECK_RESULT_REPORTED,
+ packageName,
+ appCert,
+ appInstallMetadata.getVersionCode(),
+ installerPackageName,
+ getLoggingResponse(result),
+ isCausedByAppCertRule(result),
+ isCausedByInstallerRule(result));
mPackageManagerInternal.setIntegrityVerificationResult(
verificationId,
result.getEffect() == IntegrityCheckResult.Effect.ALLOW
@@ -580,6 +593,26 @@
}
}
+ private static int getLoggingResponse(IntegrityCheckResult result) {
+ if (result.getEffect() == IntegrityCheckResult.Effect.DENY) {
+ return StatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__REJECTED;
+ } else if (result.getRule() != null) {
+ return StatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__FORCE_ALLOWED;
+ } else {
+ return StatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__ALLOWED;
+ }
+ }
+
+ private static boolean isCausedByAppCertRule(IntegrityCheckResult result) {
+ // TODO(b/147095027): implement this.
+ return true;
+ }
+
+ private static boolean isCausedByInstallerRule(IntegrityCheckResult result) {
+ // TODO(b/147095027): implement this.
+ return true;
+ }
+
private List<String> getAllowedRuleProviders() {
return Arrays.asList(mContext.getResources().getStringArray(
R.array.config_integrityRuleProviderPackages));
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7cc6732..e92f3ec 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -513,6 +513,7 @@
private TriPredicate<String, Integer, String> mAllowedManagedServicePackages;
private final SavePolicyFileRunnable mSavePolicyFile = new SavePolicyFileRunnable();
+ private NotificationRecordLogger mNotificationRecordLogger;
private static class Archive {
final int mBufferSize;
@@ -1727,7 +1728,14 @@
}
public NotificationManagerService(Context context) {
+ this(context, new NotificationRecordLoggerImpl());
+ }
+
+ @VisibleForTesting
+ public NotificationManagerService(Context context,
+ NotificationRecordLogger notificationRecordLogger) {
super(context);
+ mNotificationRecordLogger = notificationRecordLogger;
Notification.processWhitelistToken = WHITELIST_TOKEN;
}
@@ -6304,9 +6312,11 @@
mRankingHelper.extractSignals(r);
mRankingHelper.sort(mNotificationList);
+ final int position = mRankingHelper.indexOf(mNotificationList, r);
+ int buzzBeepBlinkLoggingCode = 0;
if (!r.isHidden()) {
- buzzBeepBlinkLocked(r);
+ buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
}
if (notification.getSmallIcon() != null) {
@@ -6346,6 +6356,10 @@
}
maybeRecordInterruptionLocked(r);
+
+ // Log event to statsd
+ mNotificationRecordLogger.logNotificationReported(r, old, position,
+ buzzBeepBlinkLoggingCode);
} finally {
int N = mEnqueuedNotifications.size();
for (int i = 0; i < N; i++) {
@@ -6574,9 +6588,13 @@
@VisibleForTesting
@GuardedBy("mNotificationLock")
- void buzzBeepBlinkLocked(NotificationRecord record) {
+ /**
+ * Determine whether this notification should attempt to make noise, vibrate, or flash the LED
+ * @return buzzBeepBlink - bitfield (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)
+ */
+ int buzzBeepBlinkLocked(NotificationRecord record) {
if (mIsAutomotive && !mNotificationEffectsEnabledForAutomotive) {
- return;
+ return 0;
}
boolean buzz = false;
boolean beep = false;
@@ -6674,7 +6692,8 @@
} else if (wasShowLights) {
updateLightsLocked();
}
- if (buzz || beep || blink) {
+ final int buzzBeepBlink = (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0);
+ if (buzzBeepBlink > 0) {
// Ignore summary updates because we don't display most of the information.
if (record.sbn.isGroup() && record.sbn.getNotification().isGroupSummary()) {
if (DEBUG_INTERRUPTIVENESS) {
@@ -6696,10 +6715,11 @@
MetricsLogger.action(record.getLogMaker()
.setCategory(MetricsEvent.NOTIFICATION_ALERT)
.setType(MetricsEvent.TYPE_OPEN)
- .setSubtype((buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)));
+ .setSubtype(buzzBeepBlink));
EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0);
}
record.setAudiblyAlerted(buzz || beep);
+ return buzzBeepBlink;
}
@GuardedBy("mNotificationLock")
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 2bea218..660d574 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -675,6 +675,10 @@
}
}
+ String getAdjustmentIssuer() {
+ return mAdjustmentIssuer;
+ }
+
public void setIsAppImportanceLocked(boolean isAppImportanceLocked) {
mIsAppImportanceLocked = isAppImportanceLocked;
calculateUserSentiment();
@@ -783,10 +787,22 @@
return mImportance;
}
+ int getInitialImportance() {
+ return stats.naturalImportance;
+ }
+
public float getRankingScore() {
return mRankingScore;
}
+ int getImportanceExplanationCode() {
+ return mImportanceExplanationCode;
+ }
+
+ int getInitialImportanceExplanationCode() {
+ return mInitialImportanceExplanationCode;
+ }
+
public CharSequence getImportanceExplanation() {
switch (mImportanceExplanationCode) {
case MetricsEvent.IMPORTANCE_EXPLANATION_UNKNOWN:
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
new file mode 100644
index 0000000..03929e8
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2020 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.notification;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Person;
+import android.os.Bundle;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Interface for writing NotificationReported atoms to statsd log.
+ * @hide
+ */
+public interface NotificationRecordLogger {
+
+ /**
+ * Logs a NotificationReported atom reflecting the posting or update of a notification.
+ * @param r The new NotificationRecord. If null, no action is taken.
+ * @param old The previous NotificationRecord. Null if there was no previous record.
+ * @param position The position at which this notification is ranked.
+ * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user.
+ */
+ void logNotificationReported(@Nullable NotificationRecord r, @Nullable NotificationRecord old,
+ int position, int buzzBeepBlink);
+
+ /**
+ * The UiEvent enums that this class can log.
+ */
+ enum NotificationReportedEvents implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "New notification enqueued to post")
+ NOTIFICATION_POSTED(162),
+ @UiEvent(doc = "Notification substantially updated")
+ NOTIFICATION_UPDATED(163);
+
+ private final int mId;
+ NotificationReportedEvents(int id) {
+ mId = id;
+ }
+ @Override public int getId() {
+ return mId;
+ }
+ }
+
+ /**
+ * A helper for extracting logging information from one or two NotificationRecords.
+ */
+ class NotificationRecordPair {
+ public final NotificationRecord r, old;
+ /**
+ * Construct from one or two NotificationRecords.
+ * @param r The new NotificationRecord. If null, only shouldLog() method is usable.
+ * @param old The previous NotificationRecord. Null if there was no previous record.
+ */
+ NotificationRecordPair(@Nullable NotificationRecord r, @Nullable NotificationRecord old) {
+ this.r = r;
+ this.old = old;
+ }
+
+ /**
+ * @return True if old is null, alerted, or important logged fields have changed.
+ */
+ boolean shouldLog(int buzzBeepBlink) {
+ if (r == null) {
+ return false;
+ }
+ if ((old == null) || (buzzBeepBlink > 0)) {
+ return true;
+ }
+
+ return !(Objects.equals(r.sbn.getChannelIdLogTag(), old.sbn.getChannelIdLogTag())
+ && Objects.equals(r.sbn.getGroupLogTag(), old.sbn.getGroupLogTag())
+ && (r.sbn.getNotification().isGroupSummary()
+ == old.sbn.getNotification().isGroupSummary())
+ && Objects.equals(r.sbn.getNotification().category,
+ old.sbn.getNotification().category)
+ && (r.getImportance() == old.getImportance()));
+ }
+
+ NotificationReportedEvents getUiEvent() {
+ return (old != null) ? NotificationReportedEvents.NOTIFICATION_UPDATED :
+ NotificationReportedEvents.NOTIFICATION_POSTED;
+ }
+
+ /**
+ * @return hash code for the notification style class, or 0 if none exists.
+ */
+ public int getStyle() {
+ return getStyle(r.sbn.getNotification().extras);
+ }
+
+ private int getStyle(@Nullable Bundle extras) {
+ if (extras != null) {
+ String template = extras.getString(Notification.EXTRA_TEMPLATE);
+ if (template != null && !template.isEmpty()) {
+ return template.hashCode();
+ }
+ }
+ return 0;
+ }
+
+ int getNumPeople() {
+ return getNumPeople(r.sbn.getNotification().extras);
+ }
+
+ private int getNumPeople(@Nullable Bundle extras) {
+ if (extras != null) {
+ ArrayList<Person> people = extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && !people.isEmpty()) {
+ return people.size();
+ }
+ }
+ return 0;
+ }
+
+ int getAssistantHash() {
+ String assistant = r.getAdjustmentIssuer();
+ return (assistant == null) ? 0 : assistant.hashCode();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
new file mode 100644
index 0000000..d637ad5
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.notification;
+
+import android.util.StatsLog;
+
+/**
+ * Standard implementation of NotificationRecordLogger interface.
+ * @hide
+ */
+public class NotificationRecordLoggerImpl implements NotificationRecordLogger {
+
+ @Override
+ public void logNotificationReported(NotificationRecord r, NotificationRecord old,
+ int position, int buzzBeepBlink) {
+ NotificationRecordPair p = new NotificationRecordPair(r, old);
+ if (!p.shouldLog(buzzBeepBlink)) {
+ return;
+ }
+ StatsLog.write(StatsLog.NOTIFICATION_REPORTED,
+ /* int32 event_id = 1 */ p.getUiEvent().getId(),
+ /* int32 uid = 2 */ r.getUid(),
+ /* string package_name = 3 */ r.sbn.getPackageName(),
+ /* int32 instance_id = 4 */ 0, // TODO generate and fill instance ids
+ /* int32 notification_id = 5 */ r.sbn.getId(),
+ /* string notification_tag = 6 */ r.sbn.getTag(),
+ /* string channel_id = 7 */ r.sbn.getChannelIdLogTag(),
+ /* string group_id = 8 */ r.sbn.getGroupLogTag(),
+ /* int32 group_instance_id = 9 */ 0, // TODO generate and fill instance ids
+ /* bool is_group_summary = 10 */ r.sbn.getNotification().isGroupSummary(),
+ /* string category = 11 */ r.sbn.getNotification().category,
+ /* int32 style = 12 */ p.getStyle(),
+ /* int32 num_people = 13 */ p.getNumPeople(),
+ /* int32 position = 14 */ position,
+ /* android.stats.sysui.NotificationImportance importance = 15 */ r.getImportance(),
+ /* int32 alerting = 16 */ buzzBeepBlink,
+ /* NotificationImportanceExplanation importance_source = 17 */
+ r.getImportanceExplanationCode(),
+ /* android.stats.sysui.NotificationImportance importance_initial = 18 */
+ r.getInitialImportance(),
+ /* NotificationImportanceExplanation importance_initial_source = 19 */
+ r.getInitialImportanceExplanationCode(),
+ /* android.stats.sysui.NotificationImportance importance_asst = 20 */
+ r.getAssistantImportance(),
+ /* int32 assistant_hash = 21 */ p.getAssistantHash(),
+ /* float assistant_ranking_score = 22 */ 0 // TODO connect up ranking score
+ );
+ }
+
+
+
+
+
+
+}
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
index ac3bf9a..0a9f923 100644
--- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -43,6 +43,9 @@
*/
public class OverlayActorEnforcer {
+ // By default, the reason is not logged to prevent leaks of why it failed
+ private static final boolean DEBUG_REASON = false;
+
private final VerifyCallback mVerifyCallback;
/**
@@ -92,7 +95,7 @@
throw new SecurityException("UID" + callingUid + " is not allowed to call "
+ methodName + " for "
+ (TextUtils.isEmpty(targetOverlayableName) ? "" : (targetOverlayableName + " in "))
- + overlayInfo.targetPackageName + " because " + actorState
+ + overlayInfo.targetPackageName + (DEBUG_REASON ? (" because " + actorState) : "")
);
}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index a0e6be4..39d1a51 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -20,7 +20,6 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.os.Debug.getIonHeapsSizeKb;
-import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.os.Process.getUidForPid;
import static android.os.storage.VolumeInfo.TYPE_PRIVATE;
import static android.os.storage.VolumeInfo.TYPE_PUBLIC;
@@ -32,11 +31,8 @@
import static com.android.server.stats.pull.ProcfsMemoryUtil.readCmdlineFromProcfs;
import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
-import android.app.AlarmManager;
-import android.app.AlarmManager.OnAlarmListener;
import android.app.AppOpsManager;
import android.app.AppOpsManager.HistoricalOps;
import android.app.AppOpsManager.HistoricalOpsRequest;
@@ -49,10 +45,7 @@
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.UidTraffic;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -74,23 +67,14 @@
import android.os.Bundle;
import android.os.CoolingDevice;
import android.os.Environment;
-import android.os.FileUtils;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.IPullAtomCallback;
-import android.os.IStatsCompanionService;
-import android.os.IStatsd;
import android.os.IStoraged;
import android.os.IThermalEventListener;
import android.os.IThermalService;
-import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.StatFs;
-import android.os.StatsLogEventWrapper;
import android.os.SynchronousResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
@@ -134,7 +118,6 @@
import com.android.internal.os.PowerProfile;
import com.android.internal.os.ProcessCpuTracker;
import com.android.internal.os.StoragedUidIoStatsReader;
-import com.android.internal.util.DumpUtils;
import com.android.server.BinderCallsStatsService;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -156,20 +139,15 @@
import org.json.JSONObject;
import java.io.File;
-import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.MissingResourceException;
-import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -229,6 +207,25 @@
mTelephony = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mStorageManager = (StorageManager) mContext.getSystemService(StorageManager.class);
+ final ConnectivityManager connectivityManager =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ // Default NetworkRequest should cover all transport types.
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ connectivityManager.registerNetworkCallback(request, new ConnectivityStatsCallback());
+
+ // Enable push notifications of throttling from vendor thermal
+ // management subsystem via thermalservice.
+ IThermalService thermalService = getIThermalService();
+ if (thermalService != null) {
+ try {
+ thermalService.registerThermalEventListener(
+ new ThermalEventListener());
+ Slog.i(TAG, "register thermal listener successfully");
+ } catch (RemoteException e) {
+ Slog.i(TAG, "failed to register thermal listener");
+ }
+ }
+
// Initialize state for CPU_TIME_PER_FREQ atom
PowerProfile powerProfile = new PowerProfile(mContext);
final int numClusters = powerProfile.getNumCpuClusters();
@@ -2896,4 +2893,29 @@
BackgroundThread.getExecutor()
);
}
+
+
+ // Thermal event received from vendor thermal management subsystem
+ private static final class ThermalEventListener extends IThermalEventListener.Stub {
+ @Override
+ public void notifyThrottling(Temperature temp) {
+ StatsLog.write(StatsLog.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED, temp.getType(),
+ temp.getName(), (int) (temp.getValue() * 10), temp.getStatus());
+ }
+ }
+
+ private static final class ConnectivityStatsCallback extends
+ ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(Network network) {
+ StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId,
+ StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__CONNECTED);
+ }
+
+ @Override
+ public void onLost(Network network) {
+ StatsLog.write(StatsLog.CONNECTIVITY_STATE_CHANGED, network.netId,
+ StatsLog.CONNECTIVITY_STATE_CHANGED__STATE__DISCONNECTED);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 917b437..55ba3b6 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2724,6 +2724,7 @@
boolean[] foundTop = { false };
final PooledConsumer c = PooledLambda.obtainConsumer(Task::getMaxVisibleBounds,
PooledLambda.__(ActivityRecord.class), out, foundTop);
+ forAllActivities(c);
c.recycle();
if (foundTop[0]) {
return;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index 43ee97d..9b85a7b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -75,4 +75,11 @@
public void setPersonalAppsSuspended(ComponentName admin, boolean suspended) {
}
+
+ public void setManagedProfileMaximumTimeOff(ComponentName admin, long timeoutMs) {
+ }
+
+ public long getManagedProfileMaximumTimeOff(ComponentName admin) {
+ return 0;
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index b6953f6..82f5d50 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -67,6 +67,9 @@
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+import static android.app.admin.DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED;
+import static android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_EXPLICITLY;
+import static android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT;
import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF;
import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
@@ -315,7 +318,6 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
-import java.util.stream.Collectors;
/**
* Implementation of the device policy APIs.
@@ -378,16 +380,21 @@
private static final String TAG_SECONDARY_LOCK_SCREEN = "secondary-lock-screen";
- private static final String TAG_PERSONAL_APPS_SUSPENDED = "personal-apps-suspended";
+ private static final String TAG_APPS_SUSPENDED = "apps-suspended";
private static final int REQUEST_EXPIRE_PASSWORD = 5571;
+ private static final int REQUEST_PROFILE_OFF_DEADLINE = 5572;
+
private static final long MS_PER_DAY = TimeUnit.DAYS.toMillis(1);
private static final long EXPIRATION_GRACE_PERIOD_MS = 5 * MS_PER_DAY; // 5 days, in ms
- private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION
- = "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION";
+ private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION =
+ "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION";
+
+ private static final String ACTION_PROFILE_OFF_DEADLINE =
+ "com.android.server.ACTION_PROFILE_OFF_DEADLINE";
private static final String ATTR_PERMISSION_PROVIDER = "permission-provider";
private static final String ATTR_SETUP_COMPLETE = "setup-complete";
@@ -799,9 +806,9 @@
long mPasswordTokenHandle = 0;
- // Flag reflecting the current state of the personal apps suspension. This flag should
- // only be written AFTER all the needed apps were suspended or unsuspended.
- boolean mPersonalAppsSuspended = false;
+ // Whether user's apps are suspended. This flag should only be written AFTER all the needed
+ // apps were suspended or unsuspended.
+ boolean mAppsSuspended = false;
public DevicePolicyData(int userHandle) {
mUserHandle = userHandle;
@@ -848,7 +855,7 @@
RemoteBugreportUtils.NOTIFICATION_ID,
RemoteBugreportUtils.buildNotification(mContext,
DevicePolicyManager.NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED),
- UserHandle.ALL);
+ UserHandle.ALL);
}
if (Intent.ACTION_BOOT_COMPLETED.equals(action)
|| ACTION_EXPIRED_PASSWORD_NOTIFICATION.equals(action)) {
@@ -895,12 +902,20 @@
handlePackagesChanged(null /* check all admins */, userHandle);
} else if (Intent.ACTION_USER_STOPPED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STOPPED, userHandle);
+ if (isManagedProfile(userHandle)) {
+ Slog.d(LOG_TAG, "Managed profile was stopped");
+ updatePersonalAppSuspension(userHandle, false /* profileIsOn */);
+ }
} else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle);
} else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
synchronized (getLockObject()) {
maybeSendAdminEnabledBroadcastLocked(userHandle);
}
+ if (isManagedProfile(userHandle)) {
+ Slog.d(LOG_TAG, "Managed profile became unlocked");
+ updatePersonalAppSuspension(userHandle, true /* profileIsOn */);
+ }
} else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
handlePackagesChanged(null /* check all admins */, userHandle);
} else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
@@ -918,8 +933,15 @@
// (ACTION_DATE_CHANGED), or when manual clock adjustment is made
// (ACTION_TIME_CHANGED)
updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true);
+ } else if (ACTION_PROFILE_OFF_DEADLINE.equals(action)) {
+ Slog.i(LOG_TAG, "Profile off deadline alarm was triggered");
+ final int userId = getManagedUserId(UserHandle.USER_SYSTEM);
+ if (userId >= 0) {
+ updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked(userId));
+ } else {
+ Slog.wtf(LOG_TAG, "Got deadline alarm for nonexistent profile");
+ }
}
-
}
private void sendDeviceOwnerUserCommand(String action, int userHandle) {
@@ -1033,6 +1055,8 @@
private static final String TAG_FACTORY_RESET_PROTECTION_POLICY =
"factory_reset_protection_policy";
private static final String TAG_SUSPEND_PERSONAL_APPS = "suspend-personal-apps";
+ private static final String TAG_PROFILE_MAXIMUM_TIME_OFF = "profile-max-time-off";
+ private static final String TAG_PROFILE_OFF_DEADLINE = "profile-off-deadline";
DeviceAdminInfo info;
@@ -1157,6 +1181,11 @@
// Whether the admin explicitly requires personal apps to be suspended
boolean mSuspendPersonalApps = false;
+ // Maximum time the profile owned by this admin can be off.
+ long mProfileMaximumTimeOff = 0;
+ // Time by which the profile should be turned on according to System.currentTimeMillis().
+ long mProfileOffDeadline = 0;
+
ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
info = _info;
@@ -1391,6 +1420,12 @@
if (mSuspendPersonalApps) {
writeAttributeValueToXml(out, TAG_SUSPEND_PERSONAL_APPS, mSuspendPersonalApps);
}
+ if (mProfileMaximumTimeOff != 0) {
+ writeAttributeValueToXml(out, TAG_PROFILE_MAXIMUM_TIME_OFF, mProfileMaximumTimeOff);
+ }
+ if (mProfileMaximumTimeOff != 0) {
+ writeAttributeValueToXml(out, TAG_PROFILE_OFF_DEADLINE, mProfileOffDeadline);
+ }
}
void writeTextToXml(XmlSerializer out, String tag, String text) throws IOException {
@@ -1630,6 +1665,12 @@
} else if (TAG_SUSPEND_PERSONAL_APPS.equals(tag)) {
mSuspendPersonalApps = Boolean.parseBoolean(
parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_PROFILE_MAXIMUM_TIME_OFF.equals(tag)) {
+ mProfileMaximumTimeOff =
+ Long.parseLong(parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_PROFILE_OFF_DEADLINE.equals(tag)) {
+ mProfileOffDeadline =
+ Long.parseLong(parser.getAttributeValue(null, ATTR_VALUE));
} else {
Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
XmlUtils.skipCurrentTag(parser);
@@ -1855,7 +1896,13 @@
pw.println(mCrossProfileCalendarPackages);
}
pw.print("mCrossProfilePackages=");
- pw.println(mCrossProfilePackages);
+ pw.println(mCrossProfilePackages);
+ pw.print("mSuspendPersonalApps=");
+ pw.println(mSuspendPersonalApps);
+ pw.print("mProfileMaximumTimeOff=");
+ pw.println(mProfileMaximumTimeOff);
+ pw.print("mProfileOffDeadline=");
+ pw.println(mProfileOffDeadline);
}
}
@@ -2373,12 +2420,14 @@
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BOOT_COMPLETED);
filter.addAction(ACTION_EXPIRED_PASSWORD_NOTIFICATION);
+ filter.addAction(ACTION_PROFILE_OFF_DEADLINE);
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_STARTED);
filter.addAction(Intent.ACTION_USER_STOPPED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
filter = new IntentFilter();
@@ -3436,11 +3485,10 @@
out.endTag(null, TAG_PROTECTED_PACKAGES);
}
- if (policy.mPersonalAppsSuspended) {
- out.startTag(null, TAG_PERSONAL_APPS_SUSPENDED);
- out.attribute(null, ATTR_VALUE,
- Boolean.toString(policy.mPersonalAppsSuspended));
- out.endTag(null, TAG_PERSONAL_APPS_SUSPENDED);
+ if (policy.mAppsSuspended) {
+ out.startTag(null, TAG_APPS_SUSPENDED);
+ out.attribute(null, ATTR_VALUE, Boolean.toString(policy.mAppsSuspended));
+ out.endTag(null, TAG_APPS_SUSPENDED);
}
out.endTag(null, "policies");
@@ -3659,8 +3707,8 @@
policy.mOwnerInstalledCaCerts.add(parser.getAttributeValue(null, ATTR_ALIAS));
} else if (TAG_PROTECTED_PACKAGES.equals(tag)) {
policy.mProtectedPackages.add(parser.getAttributeValue(null, ATTR_NAME));
- } else if (TAG_PERSONAL_APPS_SUSPENDED.equals(tag)) {
- policy.mPersonalAppsSuspended =
+ } else if (TAG_APPS_SUSPENDED.equals(tag)) {
+ policy.mAppsSuspended =
Boolean.parseBoolean(parser.getAttributeValue(null, ATTR_VALUE));
} else {
Slog.w(LOG_TAG, "Unknown tag: " + tag);
@@ -3802,7 +3850,10 @@
synchronized (getLockObject()) {
maybeMigrateToProfileOnOrganizationOwnedDeviceLocked();
}
- checkPackageSuspensionOnBoot();
+ final int userId = getManagedUserId(UserHandle.USER_SYSTEM);
+ if (userId >= 0) {
+ updatePersonalAppSuspension(userId, false /* running */);
+ }
break;
case SystemService.PHASE_BOOT_COMPLETED:
ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this.
@@ -3810,34 +3861,6 @@
}
}
- private void checkPackageSuspensionOnBoot() {
- int profileUserId = UserHandle.USER_NULL;
- final boolean shouldSuspend;
- synchronized (getLockObject()) {
- for (final int userId : mOwners.getProfileOwnerKeys()) {
- if (mOwners.isProfileOwnerOfOrganizationOwnedDevice(userId)) {
- profileUserId = userId;
- break;
- }
- }
-
- if (profileUserId == UserHandle.USER_NULL) {
- shouldSuspend = false;
- } else {
- shouldSuspend = getProfileOwnerAdminLocked(profileUserId).mSuspendPersonalApps;
- }
- }
-
- final boolean suspended = getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended;
- if (suspended != shouldSuspend) {
- suspendPersonalAppsInternal(shouldSuspend, UserHandle.USER_SYSTEM);
- }
-
- if (shouldSuspend) {
- sendPersonalAppsSuspendedNotification(profileUserId);
- }
- }
-
private void onLockSettingsReady() {
getUserData(UserHandle.USER_SYSTEM);
loadOwners();
@@ -3950,13 +3973,11 @@
@Override
void handleUnlockUser(int userId) {
startOwnerService(userId, "unlock-user");
- maybeUpdatePersonalAppsSuspendedNotification(userId);
}
@Override
void handleStopUser(int userId) {
stopOwnerService(userId, "stop-user");
- maybeUpdatePersonalAppsSuspendedNotification(userId);
}
private void startOwnerService(int userId, String actionForLog) {
@@ -9427,10 +9448,8 @@
pw.println();
pw.increaseIndent();
pw.print("mPasswordOwner="); pw.println(policy.mPasswordOwner);
- pw.decreaseIndent();
- pw.println();
- pw.increaseIndent();
pw.print("mProtectedPackages="); pw.println(policy.mProtectedPackages);
+ pw.print("mAppsSuspended="); pw.println(policy.mAppsSuspended);
pw.decreaseIndent();
}
}
@@ -15272,10 +15291,20 @@
false /* parent */);
// DO shouldn't be able to use this method.
enforceProfileOwnerOfOrganizationOwnedDevice(admin);
- if (admin.mSuspendPersonalApps) {
- return DevicePolicyManager.PERSONAL_APPS_SUSPENDED_EXPLICITLY;
+ final DevicePolicyData userData =
+ getUserData(getProfileParentId(mInjector.userHandleGetCallingUserId()));
+ if (!userData.mAppsSuspended) {
+ return PERSONAL_APPS_NOT_SUSPENDED;
} else {
- return DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED;
+ int reasons = PERSONAL_APPS_NOT_SUSPENDED;
+ if (admin.mSuspendPersonalApps) {
+ reasons |= PERSONAL_APPS_SUSPENDED_EXPLICITLY;
+ }
+ final long deadline = admin.mProfileOffDeadline;
+ if (deadline != 0 && System.currentTimeMillis() > deadline) {
+ reasons |= PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT;
+ }
+ return reasons;
}
}
}
@@ -15289,26 +15318,112 @@
false /* parent */);
// DO shouldn't be able to use this method.
enforceProfileOwnerOfOrganizationOwnedDevice(admin);
+ enforceHandlesCheckPolicyComplianceIntent(callingUserId, admin.info.getPackageName());
+ boolean shouldSaveSettings = false;
if (admin.mSuspendPersonalApps != suspended) {
admin.mSuspendPersonalApps = suspended;
+ shouldSaveSettings = true;
+ }
+ if (admin.mProfileOffDeadline != 0) {
+ admin.mProfileOffDeadline = 0;
+ shouldSaveSettings = true;
+ }
+ if (shouldSaveSettings) {
saveSettingsLocked(callingUserId);
}
}
- if (getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended == suspended) {
- // Admin request matches current state, nothing to do.
- return;
+ mInjector.binderWithCleanCallingIdentity(
+ () -> applyPersonalAppsSuspension(callingUserId, suspended));
+ }
+
+ /**
+ * Checks whether there is a policy that requires personal apps to be suspended and if so,
+ * applies it.
+ * @param running whether the profile is currently considered running.
+ */
+ private void updatePersonalAppSuspension(int profileUserId, boolean running) {
+ final boolean shouldSuspend;
+ synchronized (getLockObject()) {
+ final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(profileUserId);
+ if (profileOwner != null) {
+ final boolean deadlineReached =
+ updateProfileOffDeadlineLocked(profileUserId, profileOwner, running);
+ shouldSuspend = deadlineReached || profileOwner.mSuspendPersonalApps;
+ Slog.d(LOG_TAG, String.format(
+ "Should personal use be suspended: %b; explicit: %b; timeout: %b",
+ shouldSuspend, profileOwner.mSuspendPersonalApps, deadlineReached));
+ } else {
+ shouldSuspend = false;
+ }
}
- suspendPersonalAppsInternal(suspended, UserHandle.USER_SYSTEM);
+ applyPersonalAppsSuspension(profileUserId, shouldSuspend);
+ }
- mInjector.binderWithCleanCallingIdentity(() -> {
- if (suspended) {
- sendPersonalAppsSuspendedNotification(callingUserId);
- } else {
- clearPersonalAppsSuspendedNotification(callingUserId);
- }
- });
+ /**
+ * Checks work profile time off policy, scheduling personal apps suspension via alarm if
+ * necessary.
+ * @return whether the apps should be suspended based on maximum time off policy.
+ */
+ private boolean updateProfileOffDeadlineLocked(
+ int profileUserId, ActiveAdmin profileOwner, boolean unlocked) {
+ final long now = System.currentTimeMillis();
+ if (profileOwner.mProfileOffDeadline != 0 && now > profileOwner.mProfileOffDeadline) {
+ // Profile off deadline is already reached.
+ Slog.i(LOG_TAG, "Profile off deadline has been reached.");
+ return true;
+ }
+ boolean shouldSaveSettings = false;
+ if (profileOwner.mProfileOffDeadline != 0
+ && (profileOwner.mProfileMaximumTimeOff == 0 || unlocked)) {
+ // There is a deadline but either there is no policy or the profile is unlocked -> clear
+ // the deadline.
+ Slog.i(LOG_TAG, "Profile off deadline is reset to zero");
+ profileOwner.mProfileOffDeadline = 0;
+ shouldSaveSettings = true;
+ } else if (profileOwner.mProfileOffDeadline == 0
+ && (profileOwner.mProfileMaximumTimeOff != 0 && !unlocked)) {
+ // There profile is locked and there is a policy, but the deadline is not set -> set the
+ // deadline.
+ Slog.i(LOG_TAG, "Profile off deadline is set.");
+ profileOwner.mProfileOffDeadline = now + profileOwner.mProfileMaximumTimeOff;
+ shouldSaveSettings = true;
+ }
+
+ updateProfileOffAlarm(profileOwner.mProfileOffDeadline);
+
+ if (shouldSaveSettings) {
+ saveSettingsLocked(profileUserId);
+ }
+ return false;
+ }
+
+ private void updateProfileOffAlarm(long profileOffDeadline) {
+ final AlarmManager am = mInjector.getAlarmManager();
+ final PendingIntent pi = PendingIntent.getBroadcast(mContext, REQUEST_PROFILE_OFF_DEADLINE,
+ new Intent(ACTION_PROFILE_OFF_DEADLINE),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pi);
+ if (profileOffDeadline != 0) {
+ Slog.i(LOG_TAG, "Profile off deadline alarm is set.");
+ am.set(AlarmManager.RTC, profileOffDeadline, pi);
+ } else {
+ Slog.i(LOG_TAG, "Profile off deadline alarm is removed.");
+ }
+ }
+
+ private void applyPersonalAppsSuspension(int profileUserId, boolean shouldSuspend) {
+ final boolean suspended = getUserData(UserHandle.USER_SYSTEM).mAppsSuspended;
+ if (suspended != shouldSuspend) {
+ suspendPersonalAppsInternal(shouldSuspend, UserHandle.USER_SYSTEM);
+ }
+
+ if (shouldSuspend) {
+ sendPersonalAppsSuspendedNotification(profileUserId);
+ } else {
+ clearPersonalAppsSuspendedNotification();
+ }
}
private void suspendPersonalAppsInternal(boolean suspended, int userId) {
@@ -15332,21 +15447,12 @@
});
synchronized (getLockObject()) {
- getUserData(userId).mPersonalAppsSuspended = suspended;
+ getUserData(userId).mAppsSuspended = suspended;
saveSettingsLocked(userId);
}
}
- private void maybeUpdatePersonalAppsSuspendedNotification(int profileUserId) {
- // TODO(b/147414651): Unless updated, the notification stops working after turning the
- // profile off and back on, so it has to be updated more often than necessary.
- if (getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended
- && getProfileParentId(profileUserId) == UserHandle.USER_SYSTEM) {
- sendPersonalAppsSuspendedNotification(profileUserId);
- }
- }
-
- private void clearPersonalAppsSuspendedNotification(int userId) {
+ private void clearPersonalAppsSuspendedNotification() {
mInjector.binderWithCleanCallingIdentity(() ->
mInjector.getNotificationManager().cancel(
SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
@@ -15380,4 +15486,48 @@
mInjector.getNotificationManager().notify(
SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED, notification);
}
+
+ @Override
+ public void setManagedProfileMaximumTimeOff(ComponentName who, long timeoutMs) {
+ final int userId = mInjector.userHandleGetCallingUserId();
+ synchronized (getLockObject()) {
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+ DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER,
+ false /* parent */);
+ // DO shouldn't be able to use this method.
+ enforceProfileOwnerOfOrganizationOwnedDevice(admin);
+ enforceHandlesCheckPolicyComplianceIntent(userId, admin.info.getPackageName());
+ if (admin.mProfileMaximumTimeOff == timeoutMs) {
+ return;
+ }
+ admin.mProfileMaximumTimeOff = timeoutMs;
+ saveSettingsLocked(userId);
+ }
+
+ mInjector.binderWithCleanCallingIdentity(
+ () -> updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked()));
+ }
+
+ void enforceHandlesCheckPolicyComplianceIntent(@UserIdInt int userId, String packageName) {
+ mInjector.binderWithCleanCallingIdentity(() -> {
+ final Intent intent = new Intent(DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE);
+ intent.setPackage(packageName);
+ final List<ResolveInfo> handlers = mInjector.getPackageManager()
+ .queryIntentActivitiesAsUser(intent, /* flags= */ 0, userId);
+ Preconditions.checkState(!handlers.isEmpty(),
+ "Admin doesn't handle " + DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE);
+ });
+ }
+
+ @Override
+ public long getManagedProfileMaximumTimeOff(ComponentName who) {
+ synchronized (getLockObject()) {
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+ DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER,
+ false /* parent */);
+ // DO shouldn't be able to use this method.
+ enforceProfileOwnerOfOrganizationOwnedDevice(admin);
+ return admin.mProfileMaximumTimeOff;
+ }
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 587cfbf..651ad40 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -98,6 +98,7 @@
NotificationUsageStats mUsageStats;
@Mock
IAccessibilityManager mAccessibilityService;
+ NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
private NotificationManagerService mService;
private String mPkg = "com.android.server.notification";
@@ -148,7 +149,7 @@
verify(mAccessibilityService).addClient(any(IAccessibilityManagerClient.class), anyInt());
assertTrue(accessibilityManager.isEnabled());
- mService = spy(new NotificationManagerService(getContext()));
+ mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger));
mService.setAudioManager(mAudioManager);
mService.setVibrator(mVibrator);
mService.setSystemReady(true);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 93e09df..1b92abe 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -256,6 +256,8 @@
UserManager mUm;
@Mock
NotificationHistoryManager mHistoryManager;
+ NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
+
// Use a Testable subclass so we can simulate calls from the system without failing.
private static class TestableNotificationManagerService extends NotificationManagerService {
@@ -265,8 +267,8 @@
@Nullable
NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
- TestableNotificationManagerService(Context context) {
- super(context);
+ TestableNotificationManagerService(Context context, NotificationRecordLogger logger) {
+ super(context, logger);
}
@Override
@@ -353,7 +355,7 @@
doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
- mService = new TestableNotificationManagerService(mContext);
+ mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger);
// Use this testable looper.
mTestableLooper = TestableLooper.get(this);
@@ -1118,6 +1120,61 @@
}
@Test
+ public void testEnqueueNotificationWithTag_WritesExpectedLog() throws Exception {
+ final String tag = "testEnqueueNotificationWithTag_WritesExpectedLog";
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
+ generateNotificationRecord(null).getNotification(), 0);
+ waitForIdle();
+ assertEquals(1, mNotificationRecordLogger.getCalls().size());
+ NotificationRecordLoggerFake.CallRecord call = mNotificationRecordLogger.get(0);
+ assertTrue(call.shouldLog());
+ assertNotNull(call.r);
+ assertNull(call.old);
+ assertEquals(0, call.position);
+ assertEquals(0, call.buzzBeepBlink);
+ assertEquals(PKG, call.r.sbn.getPackageName());
+ assertEquals(0, call.r.sbn.getId());
+ assertEquals(tag, call.r.sbn.getTag());
+ }
+
+ @Test
+ public void testEnqueueNotificationWithTag_LogsOnMajorUpdates() throws Exception {
+ final String tag = "testEnqueueNotificationWithTag_LogsOnMajorUpdates";
+ Notification original = new Notification.Builder(mContext,
+ mTestNotificationChannel.getId())
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, original, 0);
+ Notification update = new Notification.Builder(mContext,
+ mTestNotificationChannel.getId())
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setCategory(Notification.CATEGORY_ALARM).build();
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, update, 0);
+ waitForIdle();
+ assertEquals(2, mNotificationRecordLogger.getCalls().size());
+ assertTrue(mNotificationRecordLogger.get(0).shouldLog());
+ assertEquals(
+ NotificationRecordLogger.NotificationReportedEvents.NOTIFICATION_POSTED,
+ mNotificationRecordLogger.get(0).getUiEvent());
+ assertEquals(
+ NotificationRecordLogger.NotificationReportedEvents.NOTIFICATION_UPDATED,
+ mNotificationRecordLogger.get(1).getUiEvent());
+ assertTrue(mNotificationRecordLogger.get(1).shouldLog());
+ }
+
+ @Test
+ public void testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdates() throws Exception {
+ final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdates";
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
+ generateNotificationRecord(null).getNotification(), 0);
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0,
+ generateNotificationRecord(null).getNotification(), 0);
+ waitForIdle();
+ assertEquals(2, mNotificationRecordLogger.getCalls().size());
+ assertTrue(mNotificationRecordLogger.get(0).shouldLog());
+ assertFalse(mNotificationRecordLogger.get(1).shouldLog());
+ }
+
+ @Test
public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception {
mBinderService.enqueueNotificationWithTag(PKG, PKG,
"testCancelNotificationImmediatelyAfterEnqueue", 0,
@@ -2252,7 +2309,7 @@
@Test
public void testHasCompanionDevice_noService() {
- mService = new TestableNotificationManagerService(mContext);
+ mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger);
assertFalse(mService.hasCompanionDevice(mListener));
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java
new file mode 100644
index 0000000..11972fd6
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 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.notification;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fake implementation of NotificationRecordLogger, for testing.
+ */
+class NotificationRecordLoggerFake implements NotificationRecordLogger {
+ class CallRecord extends NotificationRecordPair {
+ public int position, buzzBeepBlink;
+ CallRecord(NotificationRecord r, NotificationRecord old, int position,
+ int buzzBeepBlink) {
+ super(r, old);
+ this.position = position;
+ this.buzzBeepBlink = buzzBeepBlink;
+ }
+ boolean shouldLog() {
+ return shouldLog(buzzBeepBlink);
+ }
+ }
+ List<CallRecord> mCalls = new ArrayList<CallRecord>();
+
+ List<CallRecord> getCalls() {
+ return mCalls;
+ }
+
+ CallRecord get(int index) {
+ return mCalls.get(index);
+ }
+
+ @Override
+ public void logNotificationReported(NotificationRecord r, NotificationRecord old,
+ int position, int buzzBeepBlink) {
+ mCalls.add(new CallRecord(r, old, position, buzzBeepBlink));
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
index c828f02..e18c891 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
@@ -18,7 +18,6 @@
import static android.app.role.RoleManager.ROLE_DIALER;
import static android.app.role.RoleManager.ROLE_EMERGENCY;
-import static android.app.role.RoleManager.ROLE_SMS;
import static android.content.pm.PackageManager.MATCH_ALL;
import static junit.framework.Assert.assertFalse;
@@ -92,24 +91,20 @@
private Executor mExecutor;
@Mock
private RoleManager mRoleManager;
+ NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
private List<UserInfo> mUsers;
private static class TestableNotificationManagerService extends NotificationManagerService {
-
- TestableNotificationManagerService(Context context) {
- super(context);
+ TestableNotificationManagerService(Context context, NotificationRecordLogger logger) {
+ super(context, logger);
}
@Override
- protected void handleSavePolicyFile() {
- return;
- }
+ protected void handleSavePolicyFile() { }
@Override
- protected void loadPolicyFile() {
- return;
- }
+ protected void loadPolicyFile() { }
}
@Before
@@ -125,7 +120,7 @@
mUsers.add(new UserInfo(10, "second", 0));
when(mUm.getUsers()).thenReturn(mUsers);
- mService = new TestableNotificationManagerService(mContext);
+ mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger);
mRoleObserver = mService.new RoleObserver(mRoleManager, mPm, mExecutor);
try {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 1d89665..86913a6 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -7348,6 +7348,30 @@
}
}
+
+ /**
+ * Resets the {@link android.telephony.ims.ImsService} associated with the specified sim slot.
+ * Used by diagnostic apps to force the IMS stack to be disabled and re-enabled in an effort to
+ * recover from scenarios where the {@link android.telephony.ims.ImsService} gets in to a bad
+ * state.
+ *
+ * @param slotIndex the sim slot to reset the IMS stack on.
+ * @hide */
+ @SystemApi
+ @WorkerThread
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void resetIms(int slotIndex) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ telephony.resetIms(slotIndex);
+ }
+ } catch (RemoteException e) {
+ Rlog.e(TAG, "toggleImsOnOff, RemoteException: "
+ + e.getMessage());
+ }
+ }
+
/**
* Enables IMS for the framework. This will trigger IMS registration and ImsFeature capability
* status updates, if not already enabled.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 6aa5a52..3f573c9 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -831,6 +831,11 @@
void disableIms(int slotId);
/**
+ * Toggle framework IMS disables and enables.
+ */
+ void resetIms(int slotIndex);
+
+ /**
* Get IImsMmTelFeature binder from ImsResolver that corresponds to the subId and MMTel feature
* as well as registering the MmTelFeature for callbacks using the IImsServiceFeatureCallback
* interface.
diff --git a/tests/net/common/java/android/net/NetworkCapabilitiesTest.java b/tests/net/common/java/android/net/NetworkCapabilitiesTest.java
index 797fd83..3e4f3d8 100644
--- a/tests/net/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/net/common/java/android/net/NetworkCapabilitiesTest.java
@@ -269,6 +269,7 @@
.setUids(uids)
.addCapability(NET_CAPABILITY_EIMS)
.addCapability(NET_CAPABILITY_NOT_METERED);
+ netCap.setOwnerUid(123);
assertParcelingIsLossless(netCap);
netCap.setSSID(TEST_SSID);
assertParcelSane(netCap, 13);
diff --git a/tests/net/java/android/net/ConnectivityDiagnosticsManagerTest.java b/tests/net/java/android/net/ConnectivityDiagnosticsManagerTest.java
index 065add4..7ab4b56 100644
--- a/tests/net/java/android/net/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/net/java/android/net/ConnectivityDiagnosticsManagerTest.java
@@ -16,6 +16,8 @@
package android.net;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsBinder;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
@@ -25,12 +27,19 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import android.os.PersistableBundle;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.concurrent.Executor;
@RunWith(JUnit4.class)
public class ConnectivityDiagnosticsManagerTest {
@@ -41,6 +50,19 @@
private static final String BUNDLE_KEY = "key";
private static final String BUNDLE_VALUE = "value";
+ private static final Executor INLINE_EXECUTOR = x -> x.run();
+
+ @Mock private ConnectivityDiagnosticsCallback mCb;
+
+ private ConnectivityDiagnosticsBinder mBinder;
+
+ @Before
+ public void setUp() {
+ mCb = mock(ConnectivityDiagnosticsCallback.class);
+
+ mBinder = new ConnectivityDiagnosticsBinder(mCb, INLINE_EXECUTOR);
+ }
+
private ConnectivityReport createSampleConnectivityReport() {
final LinkProperties linkProperties = new LinkProperties();
linkProperties.setInterfaceName(INTERFACE_NAME);
@@ -193,4 +215,34 @@
public void testDataStallReportParcelUnparcel() {
assertParcelSane(createSampleDataStallReport(), 4);
}
+
+ @Test
+ public void testConnectivityDiagnosticsCallbackOnConnectivityReport() {
+ mBinder.onConnectivityReport(createSampleConnectivityReport());
+
+ // The callback will be invoked synchronously by inline executor. Immediately check the
+ // latch without waiting.
+ verify(mCb).onConnectivityReport(eq(createSampleConnectivityReport()));
+ }
+
+ @Test
+ public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() {
+ mBinder.onDataStallSuspected(createSampleDataStallReport());
+
+ // The callback will be invoked synchronously by inline executor. Immediately check the
+ // latch without waiting.
+ verify(mCb).onDataStallSuspected(eq(createSampleDataStallReport()));
+ }
+
+ @Test
+ public void testConnectivityDiagnosticsCallbackOnNetworkConnectivityReported() {
+ final Network n = new Network(NET_ID);
+ final boolean connectivity = true;
+
+ mBinder.onNetworkConnectivityReported(n, connectivity);
+
+ // The callback will be invoked synchronously by inline executor. Immediately check the
+ // latch without waiting.
+ verify(mCb).onNetworkConnectivityReported(eq(n), eq(connectivity));
+ }
}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 09cc69e..a0a1352 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -6313,12 +6313,24 @@
assertEquals(wifiLp, mService.getActiveLinkProperties());
}
+ @Test
+ public void testNetworkCapabilitiesRestrictedForCallerPermissions() {
+ int callerUid = Process.myUid();
+ final NetworkCapabilities originalNc = new NetworkCapabilities();
+ originalNc.setOwnerUid(callerUid);
- private TestNetworkAgentWrapper establishVpn(LinkProperties lp, int establishingUid,
- Set<UidRange> vpnRange) throws Exception {
+ final NetworkCapabilities newNc =
+ mService.networkCapabilitiesRestrictedForCallerPermissions(
+ originalNc, Process.myPid(), callerUid);
+
+ assertEquals(Process.INVALID_UID, newNc.getOwnerUid());
+ }
+
+ private TestNetworkAgentWrapper establishVpn(
+ LinkProperties lp, int ownerUid, Set<UidRange> vpnRange) throws Exception {
final TestNetworkAgentWrapper
vpnNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp);
- vpnNetworkAgent.getNetworkCapabilities().setEstablishingVpnAppUid(establishingUid);
+ vpnNetworkAgent.getNetworkCapabilities().setOwnerUid(ownerUid);
mMockVpn.setNetworkAgent(vpnNetworkAgent);
mMockVpn.connect();
mMockVpn.setUids(vpnRange);
diff --git a/wifi/java/android/net/wifi/WifiConfiguration.java b/wifi/java/android/net/wifi/WifiConfiguration.java
index 114e0fa..0ef224a 100644
--- a/wifi/java/android/net/wifi/WifiConfiguration.java
+++ b/wifi/java/android/net/wifi/WifiConfiguration.java
@@ -481,12 +481,14 @@
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
break;
case SECURITY_TYPE_SAE:
+ allowedProtocols.set(WifiConfiguration.Protocol.RSN);
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SAE);
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
requirePMF = true;
break;
case SECURITY_TYPE_EAP_SUITE_B:
+ allowedProtocols.set(WifiConfiguration.Protocol.RSN);
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SUITE_B_192);
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.GCMP_256);
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GCMP_256);
@@ -496,6 +498,7 @@
requirePMF = true;
break;
case SECURITY_TYPE_OWE:
+ allowedProtocols.set(WifiConfiguration.Protocol.RSN);
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OWE);
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);