diff options
74 files changed, 3078 insertions, 748 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index 08b1c2b9f548..abf78c67fa6f 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -56,6 +56,12 @@ import java.util.List; * instantiate this class directly; instead, retrieve it through * {@link android.content.Context#getSystemService * Context.getSystemService(Context.JOB_SCHEDULER_SERVICE)}. + * + * <p class="caution"><strong>Note:</strong> Beginning with API 30 + * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications. + * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency is indicative + * of an app bug and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin + * to throttle apps that show buggy behavior, regardless of target SDK version. */ @SystemService(Context.JOB_SCHEDULER_SERVICE) public abstract class JobScheduler { diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java index d2d942a4a7e5..dc72d6d9c4b3 100644 --- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -85,6 +85,7 @@ public interface AppStandbyInternal { /** * Checks if an app has been idle for a while and filters out apps that are excluded. * It returns false if the current system state allows all apps to be considered active. + * This happens if the device is plugged in or otherwise temporarily allowed to make exceptions. * Called by interface impls. */ boolean isAppIdleFiltered(String packageName, int appId, int userId, diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index b516279b58a5..e4c6b52f94bb 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -37,8 +37,6 @@ import android.app.job.JobSnapshot; import android.app.job.JobWorkItem; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledAfter; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -57,7 +55,6 @@ import android.net.Uri; import android.os.BatteryStats; import android.os.BatteryStatsInternal; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -90,7 +87,6 @@ import com.android.server.AppStateTracker; import com.android.server.DeviceIdleInternal; import com.android.server.FgThread; import com.android.server.LocalServices; -import com.android.server.compat.PlatformCompat; import com.android.server.job.JobSchedulerServiceDumpProto.ActiveJob; import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob; import com.android.server.job.controllers.BackgroundJobsController; @@ -155,16 +151,6 @@ public class JobSchedulerService extends com.android.server.SystemService /** The maximum number of jobs that we allow an unprivileged app to schedule */ private static final int MAX_JOBS_PER_APP = 100; - /** - * {@link #schedule(JobInfo)}, {@link #scheduleAsPackage(JobInfo, String, int, String)}, and - * {@link #enqueue(JobInfo, JobWorkItem)} will throw a {@link IllegalStateException} if the app - * calls the APIs too frequently. - */ - @ChangeId - // This means the change will be enabled for target SDK larger than 29 (Q), meaning R and up. - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) - protected static final long CRASH_ON_EXCEEDED_LIMIT = 144363383L; - @VisibleForTesting public static Clock sSystemClock = Clock.systemUTC(); @@ -264,7 +250,6 @@ public class JobSchedulerService extends com.android.server.SystemService private final CountQuotaTracker mQuotaTracker; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; - private final PlatformCompat mPlatformCompat; /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list @@ -986,9 +971,7 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times"); mAppStandbyInternal.restrictApp( pkg, userId, UsageStatsManager.REASON_SUB_RESTRICT_BUGGY); - if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION - && mPlatformCompat.isChangeEnabledByPackageName( - CRASH_ON_EXCEEDED_LIMIT, pkg, userId)) { + if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) { final boolean isDebuggable; synchronized (mLock) { if (!mDebuggableApps.containsKey(packageName)) { @@ -1370,8 +1353,6 @@ public class JobSchedulerService extends com.android.server.SystemService // Set up the app standby bucketing tracker mStandbyTracker = new StandbyTracker(); mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); - mPlatformCompat = - (PlatformCompat) ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE); mQuotaTracker = new CountQuotaTracker(context, Categorizer.SINGLE_CATEGORIZER); mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, mConstants.API_QUOTA_SCHEDULE_COUNT, diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index f1bfa0411978..e343478ec61f 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -48,6 +48,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; +import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import android.annotation.NonNull; @@ -71,9 +72,8 @@ import android.content.pm.ParceledListSlice; import android.database.ContentObserver; import android.hardware.display.DisplayManager; import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkRequest; import android.net.NetworkScoreManager; +import android.os.BatteryManager; import android.os.BatteryStats; import android.os.Build; import android.os.Environment; @@ -285,6 +285,7 @@ public class AppStandbyController implements AppStandbyInternal { long mInitialForegroundServiceStartTimeoutMillis; private volatile boolean mAppIdleEnabled; + private boolean mIsCharging; private boolean mSystemServicesReady = false; // There was a system update, defaults need to be initialized after services are ready private boolean mPendingInitializeDefaults; @@ -360,6 +361,11 @@ public class AppStandbyController implements AppStandbyInternal { mHandler = new AppStandbyHandler(mInjector.getLooper()); mPackageManager = mContext.getPackageManager(); + DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver(); + IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING); + deviceStates.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(deviceStateReceiver, deviceStates); + synchronized (mAppIdleLock) { mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(), mInjector.elapsedRealtime()); @@ -417,6 +423,8 @@ public class AppStandbyController implements AppStandbyInternal { if (mPendingOneTimeCheckIdleStates) { postOneTimeCheckIdleStates(); } + } else if (phase == PHASE_BOOT_COMPLETED) { + setChargingState(mInjector.isCharging()); } } @@ -515,6 +523,16 @@ public class AppStandbyController implements AppStandbyInternal { appUsage.bucketingReason, false); } + @VisibleForTesting + void setChargingState(boolean isCharging) { + synchronized (mAppIdleLock) { + if (mIsCharging != isCharging) { + if (DEBUG) Slog.d(TAG, "Setting mIsCharging to " + isCharging); + mIsCharging = isCharging; + } + } + } + @Override public void postCheckIdleStates(int userId) { mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0)); @@ -977,6 +995,11 @@ public class AppStandbyController implements AppStandbyInternal { if (isAppSpecial(packageName, appId, userId)) { return false; } else { + synchronized (mAppIdleLock) { + if (!mAppIdleEnabled || mIsCharging) { + return false; + } + } return isAppIdleUnfiltered(packageName, userId, elapsedRealtime); } } @@ -1543,6 +1566,8 @@ public class AppStandbyController implements AppStandbyInternal { pw.println(); pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled); + pw.print(" mIsCharging="); + pw.print(mIsCharging); pw.println(); pw.print("mScreenThresholds="); pw.println(Arrays.toString(mAppStandbyScreenThresholds)); pw.print("mElapsedThresholds="); pw.println(Arrays.toString(mAppStandbyElapsedThresholds)); @@ -1560,6 +1585,7 @@ public class AppStandbyController implements AppStandbyInternal { private final Looper mLooper; private IDeviceIdleController mDeviceIdleController; private IBatteryStats mBatteryStats; + private BatteryManager mBatteryManager; private PackageManagerInternal mPackageManagerInternal; private DisplayManager mDisplayManager; private PowerManager mPowerManager; @@ -1593,6 +1619,7 @@ public class AppStandbyController implements AppStandbyInternal { mDisplayManager = (DisplayManager) mContext.getSystemService( Context.DISPLAY_SERVICE); mPowerManager = mContext.getSystemService(PowerManager.class); + mBatteryManager = mContext.getSystemService(BatteryManager.class); final ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); @@ -1630,6 +1657,10 @@ public class AppStandbyController implements AppStandbyInternal { return buildFlag && runtimeFlag; } + boolean isCharging() { + return mBatteryManager.isCharging(); + } + boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException { return mDeviceIdleController.isPowerSaveWhitelistExceptIdleApp(packageName); } @@ -1766,15 +1797,19 @@ public class AppStandbyController implements AppStandbyInternal { } }; - private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder().build(); - - private final ConnectivityManager.NetworkCallback mNetworkCallback - = new ConnectivityManager.NetworkCallback() { + private class DeviceStateReceiver extends BroadcastReceiver { @Override - public void onAvailable(Network network) { - mConnectivityManager.unregisterNetworkCallback(this); + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BatteryManager.ACTION_CHARGING: + setChargingState(true); + break; + case BatteryManager.ACTION_DISCHARGING: + setChargingState(false); + break; + } } - }; + } private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java index 3eed26b108f4..7d18578aff66 100644 --- a/apex/media/framework/java/android/media/MediaParser.java +++ b/apex/media/framework/java/android/media/MediaParser.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.video.ColorInfo; import java.io.EOFException; import java.io.IOException; @@ -810,19 +811,17 @@ public final class MediaParser { // Private static methods. private static MediaFormat toMediaFormat(Format format) { - MediaFormat result = new MediaFormat(); - if (format.bitrate != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); - } - if (format.channelCount != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); - } + setOptionalMediaFormatInt(result, MediaFormat.KEY_BIT_RATE, format.bitrate); + setOptionalMediaFormatInt(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); + + ColorInfo colorInfo = format.colorInfo; + if (colorInfo != null) { + setOptionalMediaFormatInt( + result, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); - if (format.colorInfo != null) { - result.setInteger(MediaFormat.KEY_COLOR_TRANSFER, format.colorInfo.colorTransfer); - result.setInteger(MediaFormat.KEY_COLOR_RANGE, format.colorInfo.colorRange); - result.setInteger(MediaFormat.KEY_COLOR_STANDARD, format.colorInfo.colorSpace); if (format.colorInfo.hdrStaticInfo != null) { result.setByteBuffer( MediaFormat.KEY_HDR_STATIC_INFO, @@ -830,63 +829,50 @@ public final class MediaParser { } } - if (format.sampleMimeType != null) { - result.setString(MediaFormat.KEY_MIME, format.sampleMimeType); - } - if (format.codecs != null) { - result.setString(MediaFormat.KEY_CODECS_STRING, format.codecs); - } + setOptionalMediaFormatString(result, MediaFormat.KEY_MIME, format.sampleMimeType); + setOptionalMediaFormatString(result, MediaFormat.KEY_CODECS_STRING, format.codecs); if (format.frameRate != Format.NO_VALUE) { result.setFloat(MediaFormat.KEY_FRAME_RATE, format.frameRate); } - if (format.width != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_WIDTH, format.width); - } - if (format.height != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_HEIGHT, format.height); - } + setOptionalMediaFormatInt(result, MediaFormat.KEY_WIDTH, format.width); + setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height); + List<byte[]> initData = format.initializationData; if (initData != null) { for (int i = 0; i < initData.size(); i++) { result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); } } - if (format.language != null) { - result.setString(MediaFormat.KEY_LANGUAGE, format.language); - } - if (format.maxInputSize != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - } - if (format.pcmEncoding != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_PCM_ENCODING, format.pcmEncoding); - } - if (format.rotationDegrees != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_ROTATION, format.rotationDegrees); - } - if (format.sampleRate != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); - } + setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language); + setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + setOptionalMediaFormatInt(result, MediaFormat.KEY_PCM_ENCODING, format.pcmEncoding); + setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees); + setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); + int selectionFlags = format.selectionFlags; - if ((selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) { - result.setInteger(MediaFormat.KEY_IS_AUTOSELECT, 1); - } - if ((selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) { - result.setInteger(MediaFormat.KEY_IS_DEFAULT, 1); - } - if ((selectionFlags & C.SELECTION_FLAG_FORCED) != 0) { - result.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 1); - } - if (format.encoderDelay != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay); - } - if (format.encoderPadding != Format.NO_VALUE) { - result.setInteger(MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding); + result.setInteger( + MediaFormat.KEY_IS_AUTOSELECT, selectionFlags & C.SELECTION_FLAG_AUTOSELECT); + result.setInteger(MediaFormat.KEY_IS_DEFAULT, selectionFlags & C.SELECTION_FLAG_DEFAULT); + result.setInteger( + MediaFormat.KEY_IS_FORCED_SUBTITLE, selectionFlags & C.SELECTION_FLAG_FORCED); + + setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay); + setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding); + + if (format.pixelWidthHeightRatio != Format.NO_VALUE && format.pixelWidthHeightRatio != 0) { + int parWidth = 1; + int parHeight = 1; + if (format.pixelWidthHeightRatio < 1.0f) { + parHeight = 1 << 30; + parWidth = (int) (format.pixelWidthHeightRatio * parHeight); + } else if (format.pixelWidthHeightRatio > 1.0f) { + parWidth = 1 << 30; + parHeight = (int) (parWidth / format.pixelWidthHeightRatio); + } + result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, parWidth); + result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, parHeight); + result.setFloat("pixel-width-height-ratio-float", format.pixelWidthHeightRatio); } - // TODO: Implement float to fraction conversion. - // if (format.pixelWidthHeightRatio != Format.NO_VALUE) { - // result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, ); - // result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, ); - // } // LACK OF SUPPORT FOR: // format.accessibilityChannel; @@ -899,6 +885,19 @@ public final class MediaParser { return result; } + private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) { + if (value != Format.NO_VALUE) { + mediaFormat.setInteger(key, value); + } + } + + private static void setOptionalMediaFormatString( + MediaFormat mediaFormat, String key, @Nullable String value) { + if (value != null) { + mediaFormat.setString(key, value); + } + } + private static DrmInitData toFrameworkDrmInitData( com.google.android.exoplayer2.drm.DrmInitData drmInitData) { // TODO: Implement. diff --git a/apex/statsd/Android.bp b/apex/statsd/Android.bp index 1f9f18cd051a..c0f84a0ba070 100644 --- a/apex/statsd/Android.bp +++ b/apex/statsd/Android.bp @@ -20,6 +20,7 @@ apex { apex_defaults { native_shared_libs: [ + "libstatspull", "libstats_jni", ], // binaries: ["vold"], @@ -28,6 +29,7 @@ apex_defaults { "service-statsd", ], // prebuilts: ["my_prebuilt"], + compile_multilib: "both", name: "com.android.os.statsd-defaults", key: "com.android.os.statsd.key", certificate: ":com.android.os.statsd.certificate", diff --git a/apex/statsd/aidl/Android.bp b/apex/statsd/aidl/Android.bp index 7c93bc73e45d..4ccdd7e734db 100644 --- a/apex/statsd/aidl/Android.bp +++ b/apex/statsd/aidl/Android.bp @@ -38,6 +38,10 @@ aidl_interface { }, ndk: { enabled: true, + apex_available: [ + "com.android.os.statsd", + ], } - } + + }, } diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 03f97d80824d..23a4437910f7 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -381,7 +381,7 @@ message Atom { UserspaceRebootReported userspace_reboot_reported = 243 [(module) = "framework"]; NotificationReported notification_reported = 244 [(module) = "framework"]; NotificationPanelReported notification_panel_reported = 245; - NotificationChannelModified notification_panel_modified = 246; + NotificationChannelModified notification_channel_modified = 246; IntegrityCheckResultReported integrity_check_result_reported = 247 [(module) = "framework"]; IntegrityRulesPushed integrity_rules_pushed = 248 [(module) = "framework"]; CellBroadcastMessageReported cb_message_reported = diff --git a/core/java/android/app/role/RoleManager.java b/core/java/android/app/role/RoleManager.java index ea66fd475153..db4f1de1f743 100644 --- a/core/java/android/app/role/RoleManager.java +++ b/core/java/android/app/role/RoleManager.java @@ -90,6 +90,7 @@ public final class RoleManager { * The name of the dialer role. * * @see Intent#ACTION_DIAL + * @see android.telecom.InCallService */ public static final String ROLE_DIALER = "android.app.role.DIALER"; diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 5668944dfd4e..2c701b48455c 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -599,7 +599,8 @@ public final class UsageStatsManager { /** * Returns whether the specified app is currently considered inactive. This will be true if the * app hasn't been used directly or indirectly for a period of time defined by the system. This - * could be of the order of several hours or days. + * could be of the order of several hours or days. Apps are not considered inactive when the + * device is charging. * @param packageName The package name of the app to query * @return whether the app is currently considered inactive */ diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index ef28e6c6db2a..ac36188ee61a 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -75,6 +75,18 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * @hide */ + public static final String KEY_DEVICE_CREDENTIAL_TITLE = "device_credential_title"; + /** + * @hide + */ + public static final String KEY_DEVICE_CREDENTIAL_SUBTITLE = "device_credential_subtitle"; + /** + * @hide + */ + public static final String KEY_DEVICE_CREDENTIAL_DESCRIPTION = "device_credential_description"; + /** + * @hide + */ public static final String KEY_NEGATIVE_TEXT = "negative_text"; /** * @hide @@ -221,6 +233,30 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Sets an optional title, subtitle, and/or description that will override other text when + * the user is authenticating with PIN/pattern/password. Currently for internal use only. + * @return This builder. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + @NonNull + public Builder setTextForDeviceCredential( + @Nullable CharSequence title, + @Nullable CharSequence subtitle, + @Nullable CharSequence description) { + if (title != null) { + mBundle.putCharSequence(KEY_DEVICE_CREDENTIAL_TITLE, title); + } + if (subtitle != null) { + mBundle.putCharSequence(KEY_DEVICE_CREDENTIAL_SUBTITLE, subtitle); + } + if (description != null) { + mBundle.putCharSequence(KEY_DEVICE_CREDENTIAL_DESCRIPTION, description); + } + return this; + } + + /** * Required: Sets the text, executor, and click listener for the negative button on the * prompt. This is typically a cancel button, but may be also used to show an alternative * method for authentication, such as a screen that asks for a backup password. diff --git a/core/java/android/net/IpSecManager.java b/core/java/android/net/IpSecManager.java index 09ec6c35fcb9..d83715c692f7 100644 --- a/core/java/android/net/IpSecManager.java +++ b/core/java/android/net/IpSecManager.java @@ -51,7 +51,7 @@ import java.net.Socket; * * <p>Note that not all aspects of IPsec are permitted by this API. Applications may create * transport mode security associations and apply them to individual sockets. Applications looking - * to create a VPN should use {@link VpnService}. + * to create an IPsec VPN should use {@link VpnManager} and {@link Ikev2VpnProfile}. * * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the * Internet Protocol</a> diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index 06cb51927ba8..0c1edac6608c 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -19,7 +19,6 @@ package android.view; import android.animation.Animator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.compat.annotation.UnsupportedAppUsage; import android.graphics.CanvasProperty; import android.graphics.Paint; import android.graphics.RecordingCanvas; @@ -109,12 +108,10 @@ public class RenderNodeAnimator extends Animator { private long mStartDelay = 0; private long mStartTime; - @UnsupportedAppUsage public static int mapViewPropertyToRenderProperty(int viewProperty) { return sViewPropertyAnimatorMap.get(viewProperty); } - @UnsupportedAppUsage public RenderNodeAnimator(int property, float finalValue) { mRenderProperty = property; mFinalValue = finalValue; @@ -122,7 +119,6 @@ public class RenderNodeAnimator extends Animator { init(nCreateAnimator(property, finalValue)); } - @UnsupportedAppUsage public RenderNodeAnimator(CanvasProperty<Float> property, float finalValue) { init(nCreateCanvasPropertyFloatAnimator( property.getNativeContainer(), finalValue)); @@ -137,7 +133,6 @@ public class RenderNodeAnimator extends Animator { * {@link #PAINT_STROKE_WIDTH} * @param finalValue The target value for the property */ - @UnsupportedAppUsage public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) { init(nCreateCanvasPropertyPaintAnimator( property.getNativeContainer(), paintField, finalValue)); @@ -289,7 +284,6 @@ public class RenderNodeAnimator extends Animator { } /** @hide */ - @UnsupportedAppUsage public void setTarget(View view) { mViewTarget = view; setTarget(mViewTarget.mRenderNode); @@ -301,7 +295,6 @@ public class RenderNodeAnimator extends Animator { } /** @hide */ - @UnsupportedAppUsage public void setTarget(DisplayListCanvas canvas) { setTarget((RecordingCanvas) canvas); } @@ -316,7 +309,6 @@ public class RenderNodeAnimator extends Animator { mTarget.addAnimator(this); } - @UnsupportedAppUsage public void setStartValue(float startValue) { checkMutable(); nSetStartValue(mNativePtr.get(), startValue); @@ -337,7 +329,6 @@ public class RenderNodeAnimator extends Animator { return mUnscaledStartDelay; } - @UnsupportedAppUsage @Override public RenderNodeAnimator setDuration(long duration) { checkMutable(); @@ -502,7 +493,6 @@ public class RenderNodeAnimator extends Animator { } // Called by native - @UnsupportedAppUsage private static void callOnFinished(RenderNodeAnimator animator) { if (animator.mHandler != null) { animator.mHandler.post(animator::onFinished); diff --git a/core/java/android/webkit/UserPackage.java b/core/java/android/webkit/UserPackage.java index 8a1a0b5ef9b0..556b24c94b36 100644 --- a/core/java/android/webkit/UserPackage.java +++ b/core/java/android/webkit/UserPackage.java @@ -34,7 +34,7 @@ public class UserPackage { private final UserInfo mUserInfo; private final PackageInfo mPackageInfo; - public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.Q; + public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.R; public UserPackage(UserInfo user, PackageInfo packageInfo) { this.mUserInfo = user; diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 941af6ef1d7a..8790bbdcd8f7 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -47,7 +47,7 @@ public final class WebViewFactory { // visible for WebViewZygoteInit to look up the class by reflection and call preloadInZygote. /** @hide */ private static final String CHROMIUM_WEBVIEW_FACTORY = - "com.android.webview.chromium.WebViewChromiumFactoryProviderForQ"; + "com.android.webview.chromium.WebViewChromiumFactoryProviderForR"; private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"; diff --git a/core/proto/android/stats/mediametrics/mediametrics.proto b/core/proto/android/stats/mediametrics/mediametrics.proto index 34ed90a8c90c..e1af9622adb3 100644 --- a/core/proto/android/stats/mediametrics/mediametrics.proto +++ b/core/proto/android/stats/mediametrics/mediametrics.proto @@ -154,6 +154,8 @@ message CodecData { optional int64 latency_avg = 18; optional int64 latency_count = 19; optional int64 latency_unknown = 20; + optional int32 queue_input_buffer_error = 21; + optional int32 queue_secure_input_buffer_error = 22; } /** diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 08c3f988e85e..44f9b7c6eb39 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -27,16 +27,34 @@ #pragma GCC diagnostic ignored "-Wunused-function" using ::android::hardware::Void; +using ::android::hardware::hidl_bitfield; using ::android::hardware::hidl_vec; using ::android::hardware::tv::tuner::V1_0::DataFormat; +using ::android::hardware::tv::tuner::V1_0::DemuxAlpFilterSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxAlpFilterType; +using ::android::hardware::tv::tuner::V1_0::DemuxFilterAvSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxFilterDownloadSettings; using ::android::hardware::tv::tuner::V1_0::DemuxFilterMainType; using ::android::hardware::tv::tuner::V1_0::DemuxFilterPesDataSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxFilterRecordSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxFilterSectionBits; +using ::android::hardware::tv::tuner::V1_0::DemuxFilterSectionSettings; using ::android::hardware::tv::tuner::V1_0::DemuxFilterSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxIpFilterSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxIpFilterType; +using ::android::hardware::tv::tuner::V1_0::DemuxMmtpFilterSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxMmtpFilterType; using ::android::hardware::tv::tuner::V1_0::DemuxMmtpPid; using ::android::hardware::tv::tuner::V1_0::DemuxQueueNotifyBits; +using ::android::hardware::tv::tuner::V1_0::DemuxRecordScIndexType; +using ::android::hardware::tv::tuner::V1_0::DemuxScHevcIndex; +using ::android::hardware::tv::tuner::V1_0::DemuxScIndex; +using ::android::hardware::tv::tuner::V1_0::DemuxTlvFilterSettings; +using ::android::hardware::tv::tuner::V1_0::DemuxTlvFilterType; using ::android::hardware::tv::tuner::V1_0::DemuxTpid; using ::android::hardware::tv::tuner::V1_0::DemuxTsFilterSettings; using ::android::hardware::tv::tuner::V1_0::DemuxTsFilterType; +using ::android::hardware::tv::tuner::V1_0::DemuxTsIndex; using ::android::hardware::tv::tuner::V1_0::DvrSettings; using ::android::hardware::tv::tuner::V1_0::FrontendAnalogSettings; using ::android::hardware::tv::tuner::V1_0::FrontendAnalogSifStandard; @@ -1367,10 +1385,133 @@ static jobject android_media_tv_Tuner_open_time_filter(JNIEnv, jobject) { return NULL; } -static DemuxFilterSettings getFilterSettings( +static DemuxFilterSectionBits getFilterSectionBits(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/SectionSettingsWithSectionBits"); + jbyteArray jfilterBytes = static_cast<jbyteArray>( + env->GetObjectField(settings, env->GetFieldID(clazz, "mFilter", "[B"))); + jsize size = env->GetArrayLength(jfilterBytes); + std::vector<uint8_t> filterBytes(size); + env->GetByteArrayRegion( + jfilterBytes, 0, size, reinterpret_cast<jbyte*>(&filterBytes[0])); + + jbyteArray jmask = static_cast<jbyteArray>( + env->GetObjectField(settings, env->GetFieldID(clazz, "mMask", "[B"))); + size = env->GetArrayLength(jmask); + std::vector<uint8_t> mask(size); + env->GetByteArrayRegion(jmask, 0, size, reinterpret_cast<jbyte*>(&mask[0])); + + jbyteArray jmode = static_cast<jbyteArray>( + env->GetObjectField(settings, env->GetFieldID(clazz, "mMode", "[B"))); + size = env->GetArrayLength(jmode); + std::vector<uint8_t> mode(size); + env->GetByteArrayRegion(jmode, 0, size, reinterpret_cast<jbyte*>(&mode[0])); + + DemuxFilterSectionBits filterSectionBits { + .filter = filterBytes, + .mask = mask, + .mode = mode, + }; + return filterSectionBits; +} + +static DemuxFilterSectionSettings::Condition::TableInfo getFilterTableInfo( + JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/SectionSettingsWithTableInfo"); + uint16_t tableId = static_cast<uint16_t>( + env->GetIntField(settings, env->GetFieldID(clazz, "mTableId", "I"))); + uint16_t version = static_cast<uint16_t>( + env->GetIntField(settings, env->GetFieldID(clazz, "mVersion", "I"))); + DemuxFilterSectionSettings::Condition::TableInfo tableInfo { + .tableId = tableId, + .version = version, + }; + return tableInfo; +} + +static DemuxFilterSectionSettings getFilterSectionSettings(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/SectionSettings"); + bool isCheckCrc = static_cast<bool>( + env->GetBooleanField(settings, env->GetFieldID(clazz, "mCrcEnabled", "Z"))); + bool isRepeat = static_cast<bool>( + env->GetBooleanField(settings, env->GetFieldID(clazz, "mIsRepeat", "Z"))); + bool isRaw = static_cast<bool>( + env->GetBooleanField(settings, env->GetFieldID(clazz, "mIsRaw", "Z"))); + + DemuxFilterSectionSettings filterSectionSettings { + .isCheckCrc = isCheckCrc, + .isRepeat = isRepeat, + .isRaw = isRaw, + }; + if (env->IsInstanceOf( + settings, + env->FindClass("android/media/tv/tuner/filter/SectionSettingsWithSectionBits"))) { + filterSectionSettings.condition.sectionBits(getFilterSectionBits(env, settings)); + } else if (env->IsInstanceOf( + settings, + env->FindClass("android/media/tv/tuner/filter/SectionSettingsWithTableInfo"))) { + filterSectionSettings.condition.tableInfo(getFilterTableInfo(env, settings)); + } + return filterSectionSettings; +} + +static DemuxFilterAvSettings getFilterAvSettings(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/AvSettings"); + bool isPassthrough = static_cast<bool>( + env->GetBooleanField(settings, env->GetFieldID(clazz, "mIsPassthrough", "Z"))); + DemuxFilterAvSettings filterAvSettings { + .isPassthrough = isPassthrough, + }; + return filterAvSettings; +} + +static DemuxFilterPesDataSettings getFilterPesDataSettings(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/PesSettings"); + uint16_t streamId = static_cast<uint16_t>( + env->GetIntField(settings, env->GetFieldID(clazz, "mStreamId", "I"))); + bool isRaw = static_cast<bool>( + env->GetBooleanField(settings, env->GetFieldID(clazz, "mIsRaw", "Z"))); + DemuxFilterPesDataSettings filterPesDataSettings { + .streamId = streamId, + .isRaw = isRaw, + }; + return filterPesDataSettings; +} + +static DemuxFilterRecordSettings getFilterRecordSettings(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/RecordSettings"); + hidl_bitfield<DemuxTsIndex> tsIndexMask = static_cast<hidl_bitfield<DemuxTsIndex>>( + env->GetIntField(settings, env->GetFieldID(clazz, "mTsIndexMask", "I"))); + DemuxRecordScIndexType scIndexType = static_cast<DemuxRecordScIndexType>( + env->GetIntField(settings, env->GetFieldID(clazz, "mScIndexType", "I"))); + jint scIndexMask = env->GetIntField(settings, env->GetFieldID(clazz, "mScIndexMask", "I")); + + DemuxFilterRecordSettings filterRecordSettings { + .tsIndexMask = tsIndexMask, + .scIndexType = scIndexType, + }; + if (scIndexType == DemuxRecordScIndexType::SC) { + filterRecordSettings.scIndexMask.sc(static_cast<hidl_bitfield<DemuxScIndex>>(scIndexMask)); + } else if (scIndexType == DemuxRecordScIndexType::SC_HEVC) { + filterRecordSettings.scIndexMask.scHevc( + static_cast<hidl_bitfield<DemuxScHevcIndex>>(scIndexMask)); + } + return filterRecordSettings; +} + +static DemuxFilterDownloadSettings getFilterDownloadSettings(JNIEnv *env, const jobject& settings) { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/DownloadSettings"); + uint32_t downloadId = static_cast<uint32_t>( + env->GetIntField(settings, env->GetFieldID(clazz, "mDownloadId", "I"))); + + DemuxFilterDownloadSettings filterDownloadSettings { + .downloadId = downloadId, + }; + return filterDownloadSettings; +} + +static DemuxFilterSettings getFilterConfiguration( JNIEnv *env, int type, int subtype, jobject filterSettingsObj) { DemuxFilterSettings filterSettings; - // TODO: more setting types jobject settingsObj = env->GetObjectField( filterSettingsObj, @@ -1378,27 +1519,121 @@ static DemuxFilterSettings getFilterSettings( env->FindClass("android/media/tv/tuner/filter/FilterConfiguration"), "mSettings", "Landroid/media/tv/tuner/filter/Settings;")); - if (type == (int)DemuxFilterMainType::TS) { - // DemuxTsFilterSettings - jclass clazz = env->FindClass("android/media/tv/tuner/filter/TsFilterConfiguration"); - int tpid = env->GetIntField(filterSettingsObj, env->GetFieldID(clazz, "mTpid", "I")); - if (subtype == (int)DemuxTsFilterType::PES) { - // DemuxFilterPesDataSettings - jclass settingClazz = - env->FindClass("android/media/tv/tuner/filter/PesSettings"); - int streamId = env->GetIntField( - settingsObj, env->GetFieldID(settingClazz, "mStreamId", "I")); - bool isRaw = (bool)env->GetBooleanField( - settingsObj, env->GetFieldID(settingClazz, "mIsRaw", "Z")); - DemuxFilterPesDataSettings filterPesDataSettings { - .streamId = static_cast<uint16_t>(streamId), - .isRaw = isRaw, - }; + DemuxFilterMainType mainType = static_cast<DemuxFilterMainType>(type); + switch (mainType) { + case DemuxFilterMainType::TS: { + jclass clazz = env->FindClass("android/media/tv/tuner/filter/TsFilterConfiguration"); + int tpid = env->GetIntField(filterSettingsObj, env->GetFieldID(clazz, "mTpid", "I")); DemuxTsFilterSettings tsFilterSettings { - .tpid = static_cast<uint16_t>(tpid), + .tpid = static_cast<uint16_t>(tpid), }; - tsFilterSettings.filterSettings.pesData(filterPesDataSettings); + + DemuxTsFilterType tsType = static_cast<DemuxTsFilterType>(subtype); + switch (tsType) { + case DemuxTsFilterType::SECTION: + tsFilterSettings.filterSettings.section( + getFilterSectionSettings(env, settingsObj)); + break; + case DemuxTsFilterType::AUDIO: + case DemuxTsFilterType::VIDEO: + tsFilterSettings.filterSettings.av(getFilterAvSettings(env, settingsObj)); + break; + case DemuxTsFilterType::PES: + tsFilterSettings.filterSettings.pesData( + getFilterPesDataSettings(env, settingsObj)); + break; + case DemuxTsFilterType::RECORD: + tsFilterSettings.filterSettings.record( + getFilterRecordSettings(env, settingsObj)); + break; + default: + break; + } filterSettings.ts(tsFilterSettings); + break; + } + case DemuxFilterMainType::MMTP: { + DemuxMmtpFilterSettings mmtpFilterSettings; + DemuxMmtpFilterType mmtpType = static_cast<DemuxMmtpFilterType>(subtype); + switch (mmtpType) { + case DemuxMmtpFilterType::SECTION: + mmtpFilterSettings.filterSettings.section( + getFilterSectionSettings(env, settingsObj)); + break; + case DemuxMmtpFilterType::AUDIO: + case DemuxMmtpFilterType::VIDEO: + mmtpFilterSettings.filterSettings.av(getFilterAvSettings(env, settingsObj)); + break; + case DemuxMmtpFilterType::PES: + mmtpFilterSettings.filterSettings.pesData( + getFilterPesDataSettings(env, settingsObj)); + break; + case DemuxMmtpFilterType::RECORD: + mmtpFilterSettings.filterSettings.record( + getFilterRecordSettings(env, settingsObj)); + break; + case DemuxMmtpFilterType::DOWNLOAD: + mmtpFilterSettings.filterSettings.download( + getFilterDownloadSettings(env, settingsObj)); + break; + default: + break; + } + filterSettings.mmtp(mmtpFilterSettings); + break; + } + case DemuxFilterMainType::IP: { + DemuxIpFilterSettings ipFilterSettings; + DemuxIpFilterType ipType = static_cast<DemuxIpFilterType>(subtype); + switch (ipType) { + case DemuxIpFilterType::SECTION: + ipFilterSettings.filterSettings.section( + getFilterSectionSettings(env, settingsObj)); + break; + case DemuxIpFilterType::IP: + // TODO: handle passthrough + ipFilterSettings.filterSettings.bPassthrough(false); + break; + default: + break; + } + filterSettings.ip(ipFilterSettings); + break; + } + case DemuxFilterMainType::TLV: { + DemuxTlvFilterSettings tlvFilterSettings; + DemuxTlvFilterType tlvType = static_cast<DemuxTlvFilterType>(subtype); + switch (tlvType) { + case DemuxTlvFilterType::SECTION: + tlvFilterSettings.filterSettings.section( + getFilterSectionSettings(env, settingsObj)); + break; + case DemuxTlvFilterType::TLV: + // TODO: handle passthrough + tlvFilterSettings.filterSettings.bPassthrough(false); + break; + default: + break; + } + filterSettings.tlv(tlvFilterSettings); + break; + } + case DemuxFilterMainType::ALP: { + DemuxAlpFilterSettings alpFilterSettings; + DemuxAlpFilterType alpType = static_cast<DemuxAlpFilterType>(subtype); + switch (alpType) { + case DemuxAlpFilterType::SECTION: + alpFilterSettings.filterSettings.section( + getFilterSectionSettings(env, settingsObj)); + break; + default: + break; + } + filterSettings.alp(alpFilterSettings); + break; + } + default: { + break; } } return filterSettings; @@ -1439,7 +1674,7 @@ static int android_media_tv_Tuner_configure_filter( ALOGD("Failed to configure filter: filter not found"); return (int)Result::INVALID_STATE; } - DemuxFilterSettings filterSettings = getFilterSettings(env, type, subtype, settings); + DemuxFilterSettings filterSettings = getFilterConfiguration(env, type, subtype, settings); Result res = iFilterSp->configure(filterSettings); MQDescriptorSync<uint8_t> filterMQDesc; if (res == Result::SUCCESS && filterSp->mFilterMQ == NULL) { diff --git a/packages/SystemUI/res/layout/bubble_overflow_activity.xml b/packages/SystemUI/res/layout/bubble_overflow_activity.xml index 95f205a1be34..481c4dbe3bf1 100644 --- a/packages/SystemUI/res/layout/bubble_overflow_activity.xml +++ b/packages/SystemUI/res/layout/bubble_overflow_activity.xml @@ -14,8 +14,45 @@ ~ limitations under the License --> -<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/bubble_overflow_recycler" - android:layout_gravity="center_horizontal" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bubble_overflow_container" android:layout_width="match_parent" - android:layout_height="wrap_content"/> + android:layout_height="match_parent" + android:orientation="vertical" + android:layout_gravity="center_horizontal"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/bubble_overflow_recycler" + android:layout_gravity="center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <LinearLayout + android:id="@+id/bubble_overflow_empty_state" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center"> + + <TextView + android:id="@+id/bubble_overflow_empty_title" + android:text="@string/bubble_overflow_empty_title" + android:fontFamily="@*android:string/config_bodyFontFamilyMedium" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" + android:textColor="?android:attr/textColorSecondary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"/> + + <TextView + android:id="@+id/bubble_overflow_empty_subtitle" + android:fontFamily="@*android:string/config_bodyFontFamily" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/bubble_overflow_empty_subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"/> + </LinearLayout> +</LinearLayout> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6dd89d88d252..ef9e705d0a81 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1780,6 +1780,12 @@ <!-- [CHAR LIMIT=150] Notification Importance title: bubble level summary --> <string name="notification_channel_summary_bubble">Keeps your attention with a floating shortcut to this content.</string> + <!-- [CHAR LIMIT=NONE] Empty overflow title --> + <string name="bubble_overflow_empty_title">No recent bubbles</string> + + <!-- [CHAR LIMIT=NONE] Empty overflow subtitle --> + <string name="bubble_overflow_empty_subtitle">Recently dismissed bubbles will appear here for easy retrieval.</string> + <!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. --> <string name="notification_unblockable_desc">These notifications can\'t be modified.</string> diff --git a/packages/SystemUI/src/com/android/systemui/SysUIToast.java b/packages/SystemUI/src/com/android/systemui/SysUIToast.java index 0f7f1bebae57..023b74b9c07e 100644 --- a/packages/SystemUI/src/com/android/systemui/SysUIToast.java +++ b/packages/SystemUI/src/com/android/systemui/SysUIToast.java @@ -19,7 +19,6 @@ import static android.widget.Toast.Duration; import android.annotation.StringRes; import android.content.Context; -import android.view.WindowManager; import android.widget.Toast; public class SysUIToast { @@ -29,10 +28,7 @@ public class SysUIToast { } public static Toast makeText(Context context, CharSequence text, @Duration int duration) { - Toast toast = Toast.makeText(context, text, duration); - toast.getWindowParams().privateFlags |= - WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - return toast; + return Toast.makeText(context, text, duration); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 68b05e358786..9de10406a822 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import android.annotation.NonNull; import android.content.Context; import android.graphics.drawable.Drawable; import android.hardware.biometrics.BiometricPrompt; @@ -33,6 +34,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -126,18 +129,18 @@ public abstract class AuthCredentialView extends LinearLayout { mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); } - private void setTextOrHide(TextView view, String string) { - if (TextUtils.isEmpty(string)) { + private void setTextOrHide(TextView view, CharSequence text) { + if (TextUtils.isEmpty(text)) { view.setVisibility(View.GONE); } else { - view.setText(string); + view.setText(text); } Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } - private void setText(TextView view, String string) { - view.setText(string); + private void setText(TextView view, CharSequence text) { + view.setText(text); } void setEffectiveUserId(int effectiveUserId) { @@ -173,11 +176,9 @@ public abstract class AuthCredentialView extends LinearLayout { protected void onAttachedToWindow() { super.onAttachedToWindow(); - setText(mTitleView, mBiometricPromptBundle.getString(BiometricPrompt.KEY_TITLE)); - setTextOrHide(mSubtitleView, - mBiometricPromptBundle.getString(BiometricPrompt.KEY_SUBTITLE)); - setTextOrHide(mDescriptionView, - mBiometricPromptBundle.getString(BiometricPrompt.KEY_DESCRIPTION)); + setText(mTitleView, getTitle(mBiometricPromptBundle)); + setTextOrHide(mSubtitleView, getSubtitle(mBiometricPromptBundle)); + setTextOrHide(mDescriptionView, getDescription(mBiometricPromptBundle)); final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); final Drawable image; @@ -279,4 +280,28 @@ public abstract class AuthCredentialView extends LinearLayout { } } } + + @Nullable + private static CharSequence getTitle(@NonNull Bundle bundle) { + final CharSequence credentialTitle = + bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_TITLE); + return credentialTitle != null ? credentialTitle + : bundle.getCharSequence(BiometricPrompt.KEY_TITLE); + } + + @Nullable + private static CharSequence getSubtitle(@NonNull Bundle bundle) { + final CharSequence credentialSubtitle = + bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_SUBTITLE); + return credentialSubtitle != null ? credentialSubtitle + : bundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE); + } + + @Nullable + private static CharSequence getDescription(@NonNull Bundle bundle) { + final CharSequence credentialDescription = + bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_DESCRIPTION); + return credentialDescription != null ? credentialDescription + : bundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION); + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 45705b76f09c..1e39954ed414 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -226,6 +226,10 @@ class Bubble { mIconView.update(this); } + void setInflated(boolean inflated) { + mInflated = inflated; + } + /** * Set visibility of bubble in the expanded state. * diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 05838abe184a..762e5f21cd63 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -749,7 +749,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } void promoteBubbleFromOverflow(Bubble bubble) { - mBubbleData.promoteBubbleFromOverflow(bubble); + bubble.setInflateSynchronously(mInflateSynchronously); + mBubbleData.promoteBubbleFromOverflow(bubble, mStackView, mBubbleIconFactory); } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index 673121f92716..8a5aad875979 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -199,16 +199,21 @@ public class BubbleData { dispatchPendingChanges(); } - public void promoteBubbleFromOverflow(Bubble bubble) { + public void promoteBubbleFromOverflow(Bubble bubble, BubbleStackView stack, + BubbleIconFactory factory) { 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(); + setSelectedBubbleInternal(bubble); + mOverflowBubbles.remove(bubble); + + bubble.inflate( + b -> notificationEntryUpdated(bubble, /* suppressFlyout */ + false, /* showInShade */ true), + mContext, stack, factory); dispatchPendingChanges(); } @@ -445,6 +450,10 @@ public class BubbleData { mOverflowBubbles.add(0, bubbleToRemove); if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { // Remove oldest bubble. + if (DEBUG_BUBBLE_DATA) { + Log.d(TAG, "Overflow full. Remove bubble: " + mOverflowBubbles.get( + mOverflowBubbles.size() - 1)); + } mOverflowBubbles.remove(mOverflowBubbles.size() - 1); } } @@ -511,7 +520,7 @@ public class BubbleData { if (Objects.equals(bubble, mSelectedBubble)) { return; } - if (bubble != null && !mBubbles.contains(bubble)) { + if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) { Log.e(TAG, "Cannot select bubble which doesn't exist!" + " (" + bubble + ") bubbles=" + mBubbles); return; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 0d5261dcb7f3..fe191f40d31f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -294,7 +294,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList ta.recycle(); mPointerDrawable.setTint(bgColor); - if (ScreenDecorationsUtils.supportsRoundedCornersOnWindows(mContext.getResources())) { + if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + mContext.getResources())) { mActivityView.setCornerRadius(cornerRadius); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java index 2d55a1ddf654..f3cfa834dfc1 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java @@ -26,7 +26,10 @@ import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -46,6 +49,7 @@ import javax.inject.Inject; public class BubbleOverflowActivity extends Activity { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES; + private LinearLayout mEmptyState; private BubbleController mBubbleController; private BubbleOverflowAdapter mAdapter; private RecyclerView mRecyclerView; @@ -64,6 +68,7 @@ public class BubbleOverflowActivity extends Activity { setBackgroundColor(); mMaxBubbles = getResources().getInteger(R.integer.bubbles_max_rendered); + mEmptyState = findViewById(R.id.bubble_overflow_empty_state); mRecyclerView = findViewById(R.id.bubble_overflow_recycler); mRecyclerView.setLayoutManager( new GridLayoutManager(getApplicationContext(), @@ -73,9 +78,9 @@ public class BubbleOverflowActivity extends Activity { mBubbleController::promoteBubbleFromOverflow); mRecyclerView.setAdapter(mAdapter); - updateData(mBubbleController.getOverflowBubbles()); + onDataChanged(mBubbleController.getOverflowBubbles()); mBubbleController.setOverflowCallback(() -> { - updateData(mBubbleController.getOverflowBubbles()); + onDataChanged(mBubbleController.getOverflowBubbles()); }); } @@ -87,7 +92,7 @@ public class BubbleOverflowActivity extends Activity { findViewById(android.R.id.content).setBackgroundColor(bgColor); } - void updateData(List<Bubble> bubbles) { + void onDataChanged(List<Bubble> bubbles) { mOverflowBubbles.clear(); if (bubbles.size() > mMaxBubbles) { mOverflowBubbles.addAll(bubbles.subList(mMaxBubbles, bubbles.size())); @@ -96,6 +101,12 @@ public class BubbleOverflowActivity extends Activity { } mAdapter.notifyDataSetChanged(); + if (mOverflowBubbles.isEmpty()) { + mEmptyState.setVisibility(View.VISIBLE); + } else { + mEmptyState.setVisibility(View.GONE); + } + if (DEBUG_OVERFLOW) { Log.d(TAG, "Updated overflow bubbles:\n" + BubbleDebugConfig.formatBubblesString( mOverflowBubbles, /*selected*/ null)); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index bce172b89187..cff78cfeaae0 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -528,6 +528,12 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.addView(mOverflowBtn, 0, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + setOverflowBtnTheme(); + mOverflowBtn.setVisibility(GONE); + } + + // TODO(b/149146374) Propagate theme change to bubbles in overflow. + private void setOverflowBtnTheme() { TypedArray ta = mContext.obtainStyledAttributes( new int[]{android.R.attr.colorBackgroundFloating}); int bgColor = ta.getColor(0, Color.WHITE /* default */); @@ -537,8 +543,6 @@ public class BubbleStackView extends FrameLayout { ColorDrawable bg = new ColorDrawable(bgColor); AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(bg, fg); mOverflowBtn.setImageDrawable(adaptiveIcon); - - mOverflowBtn.setVisibility(GONE); } void showExpandedViewContents(int displayId) { @@ -568,6 +572,9 @@ public class BubbleStackView extends FrameLayout { */ public void onThemeChanged() { setUpFlyout(); + if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) { + setOverflowBtnTheme(); + } } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ @@ -795,6 +802,7 @@ public class BubbleStackView extends FrameLayout { if (removedIndex >= 0) { mBubbleContainer.removeViewAt(removedIndex); bubble.cleanupExpandedState(); + bubble.setInflated(false); logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); } else { Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 6d4b13ccf494..91d2de760696 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -638,9 +638,9 @@ public class NotifCollection implements Dumpable { private static boolean shouldDismissOnClearAll( NotificationEntry entry, @UserIdInt int userId) { - // TODO: (b/149396544) add FLAG_BUBBLE check here + in NoManService return userIdMatches(entry, userId) && entry.isClearable() + && !hasFlag(entry, Notification.FLAG_BUBBLE) && entry.getDismissState() != DISMISSED; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt index 4f27c0f04c3f..5b4a927bb8f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt @@ -26,26 +26,23 @@ import android.content.Context import android.content.DialogInterface import android.graphics.Color import android.graphics.PixelFormat -import android.graphics.drawable.Drawable import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.Window -import android.view.WindowInsets.Type import android.view.WindowInsets.Type.statusBars import android.view.WindowManager import android.widget.TextView import com.android.internal.annotations.VisibleForTesting - import com.android.systemui.R - import javax.inject.Inject import javax.inject.Singleton -const val TAG = "ChannelDialogController" +private const val TAG = "ChannelDialogController" /** * ChannelEditorDialogController is the controller for the dialog half-shelf @@ -149,9 +146,9 @@ class ChannelEditorDialogController @Inject constructor( val channels = groupList .flatMap { group -> group.channels.asSequence().filterNot { channel -> - channel.isImportanceLockedByOEM - || channel.importance == IMPORTANCE_NONE - || channel.isImportanceLockedByCriticalDeviceFunction + channel.isImportanceLockedByOEM || + channel.importance == IMPORTANCE_NONE || + channel.isImportanceLockedByCriticalDeviceFunction } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java index e2513dac44d8..d744fc398d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java @@ -74,11 +74,15 @@ import javax.inject.Singleton; @Singleton public final class NotifBindPipeline { private final Map<NotificationEntry, BindEntry> mBindEntries = new ArrayMap<>(); + private final NotifBindPipelineLogger mLogger; private BindStage mStage; @Inject - NotifBindPipeline(CommonNotifCollection collection) { + NotifBindPipeline( + CommonNotifCollection collection, + NotifBindPipelineLogger logger) { collection.addCollectionListener(mCollectionListener); + mLogger = logger; } /** @@ -86,6 +90,8 @@ public final class NotifBindPipeline { */ public void setStage( BindStage stage) { + mLogger.logStageSet(stage.getClass().getName()); + mStage = stage; mStage.setBindRequestListener(this::onBindRequested); } @@ -96,6 +102,8 @@ public final class NotifBindPipeline { public void manageRow( @NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row) { + mLogger.logManagedRow(entry.getKey()); + final BindEntry bindEntry = getBindEntry(entry); bindEntry.row = row; if (bindEntry.invalidated) { @@ -130,6 +138,8 @@ public final class NotifBindPipeline { * callbacks when the run finishes. If a run is already in progress, it is restarted. */ private void startPipeline(NotificationEntry entry) { + mLogger.logStartPipeline(entry.getKey()); + if (mStage == null) { throw new IllegalStateException("No stage was ever set on the pipeline"); } @@ -147,10 +157,11 @@ public final class NotifBindPipeline { private void onPipelineComplete(NotificationEntry entry) { final BindEntry bindEntry = getBindEntry(entry); + final Set<BindCallback> callbacks = bindEntry.callbacks; - bindEntry.invalidated = false; + mLogger.logFinishedPipeline(entry.getKey(), callbacks.size()); - final Set<BindCallback> callbacks = bindEntry.callbacks; + bindEntry.invalidated = false; for (BindCallback cb : callbacks) { cb.onBindFinished(entry); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt new file mode 100644 index 000000000000..2717d7ad143b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt @@ -0,0 +1,61 @@ +/* + * 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.statusbar.notification.row + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.log.dagger.NotificationLog +import javax.inject.Inject + +class NotifBindPipelineLogger @Inject constructor( + @NotificationLog private val buffer: LogBuffer +) { + fun logStageSet(stageName: String) { + buffer.log(TAG, INFO, { + str1 = stageName + }, { + "Stage set: $str1" + }) + } + + fun logManagedRow(notifKey: String) { + buffer.log(TAG, INFO, { + str1 = notifKey + }, { + "Row set for notif: $str1" + }) + } + + fun logStartPipeline(notifKey: String) { + buffer.log(TAG, INFO, { + str1 = notifKey + }, { + "Start pipeline for notif: $str1" + }) + } + + fun logFinishedPipeline(notifKey: String, numCallbacks: Int) { + buffer.log(TAG, INFO, { + str1 = notifKey + int1 = numCallbacks + }, { + "Finished pipeline for notif $str1 with $int1 callbacks" + }) + } +} + +private const val TAG = "NotifBindPipeline"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index 5170d0b85b17..88ed0bb37d0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -157,6 +157,15 @@ public final class RowContentBindParams { return mViewsNeedReinflation; } + @Override + public String toString() { + return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x " + + "mUseLowPriority=%b mUseChildInGroup=%b mUseIncreasedHeight=%b " + + "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]", + mContentViews, mDirtyContentViews, mUseLowPriority, mUseChildInGroup, + mUseIncreasedHeight, mUseIncreasedHeadsUpHeight, mViewsNeedReinflation); + } + /** * Content views that should be inflated by default for all notifications. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index f78324596fb4..c632f3eb22a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -38,13 +38,16 @@ import javax.inject.Singleton; public class RowContentBindStage extends BindStage<RowContentBindParams> { private final NotificationRowContentBinder mBinder; private final NotifInflationErrorManager mNotifInflationErrorManager; + private final RowContentBindStageLogger mLogger; @Inject RowContentBindStage( NotificationRowContentBinder binder, - NotifInflationErrorManager errorManager) { + NotifInflationErrorManager errorManager, + RowContentBindStageLogger logger) { mBinder = binder; mNotifInflationErrorManager = errorManager; + mLogger = logger; } @Override @@ -54,6 +57,8 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @NonNull StageCallback callback) { RowContentBindParams params = getStageParams(entry); + mLogger.logStageParams(entry.getKey(), params.toString()); + // Resolve content to bind/unbind. @InflationFlag int inflationFlags = params.getContentViews(); @InflationFlag int invalidatedFlags = params.getDirtyContentViews(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt new file mode 100644 index 000000000000..29cce3375c8a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt @@ -0,0 +1,37 @@ +/* + * 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.statusbar.notification.row + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.log.dagger.NotificationLog +import javax.inject.Inject + +class RowContentBindStageLogger @Inject constructor( + @NotificationLog private val buffer: LogBuffer +) { + fun logStageParams(notifKey: String, stageParams: String) { + buffer.log(TAG, INFO, { + str1 = notifKey + str2 = stageParams + }, { + "Invalidated notif $str1 with params: \n$str2" + }) + } +} + +private const val TAG = "RowContentBindStage"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 70b43bfc0367..e1a20b6ac5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -365,8 +365,12 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } catch (RemoteException ex) { // system process is dead if we're here. } + if (!isBubble) { if (parentToCancelFinal != null) { + // TODO: (b/145659174) remove - this cancels the parent if the notification clicked + // on will auto-cancel and is the only child in the group. This won't be + // necessary in the new pipeline due to group pruning in ShadeListBuilder. removeNotification(parentToCancelFinal); } if (shouldAutoCancel(sbn) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java index 408bba48d422..6408f7a38133 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java @@ -59,7 +59,7 @@ public class NotifBindPipelineTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); CommonNotifCollection collection = mock(CommonNotifCollection.class); - mBindPipeline = new NotifBindPipeline(collection); + mBindPipeline = new NotifBindPipeline(collection, mock(NotifBindPipelineLogger.class)); mBindPipeline.setStage(mStage); ArgumentCaptor<NotifCollectionListener> collectionListenerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index fd5512d62968..7a1bd052a336 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -111,11 +111,13 @@ public class NotificationTestHelper { mock(NotifRemoteViewCache.class), mock(NotificationRemoteInputManager.class)); contentBinder.setInflateSynchronously(true); - mBindStage = new RowContentBindStage(contentBinder, mock(NotifInflationErrorManager.class)); + mBindStage = new RowContentBindStage(contentBinder, + mock(NotifInflationErrorManager.class), + mock(RowContentBindStageLogger.class)); CommonNotifCollection collection = mock(CommonNotifCollection.class); - mBindPipeline = new NotifBindPipeline(collection); + mBindPipeline = new NotifBindPipeline(collection, mock(NotifBindPipelineLogger.class)); mBindPipeline.setStage(mBindStage); ArgumentCaptor<NotifCollectionListener> collectionListenerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java index d9fe6551ba1c..0f2482ce9c4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java @@ -60,8 +60,10 @@ public class RowContentBindStageTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - mRowContentBindStage = new RowContentBindStage(mBinder, - mock(NotifInflationErrorManager.class)); + mRowContentBindStage = new RowContentBindStage( + mBinder, + mock(NotifInflationErrorManager.class), + mock(RowContentBindStageLogger.class)); mRowContentBindStage.createStageParams(mEntry); } diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 31ea5faa05f1..9a33fc9548a0 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -998,6 +998,11 @@ public final class ContentCaptureManagerService extends sendErrorSignal(mClientAdapterReference, serviceAdapterReference, ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN); + } finally { + synchronized (parentService.mLock) { + parentService.mPackagesWithShareRequests + .remove(mDataShareRequest.getPackageName()); + } } }); diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java index c98762074b53..9540f4336464 100644 --- a/services/core/java/com/android/server/IpSecService.java +++ b/services/core/java/com/android/server/IpSecService.java @@ -1556,16 +1556,16 @@ public class IpSecService extends IIpSecService.Stub { } Objects.requireNonNull(callingPackage, "Null calling package cannot create IpSec tunnels"); - switch (getAppOpsManager().noteOp(TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { - case AppOpsManager.MODE_DEFAULT: - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService"); - break; - case AppOpsManager.MODE_ALLOWED: - return; - default: - throw new SecurityException("Request to ignore AppOps for non-legacy API"); + + // OP_MANAGE_IPSEC_TUNNELS will return MODE_ERRORED by default, including for the system + // server. If the appop is not granted, require that the caller has the MANAGE_IPSEC_TUNNELS + // permission or is the System Server. + if (AppOpsManager.MODE_ALLOWED == getAppOpsManager().noteOpNoThrow( + TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { + return; } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService"); } private void createOrUpdateTransform( diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 97a8b8764bcf..67d7530fa11e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -128,6 +128,7 @@ import android.util.Log; import android.util.MathUtils; import android.util.PrintWriterPrinter; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseIntArray; import android.view.KeyEvent; import android.view.accessibility.AccessibilityManager; @@ -247,6 +248,7 @@ public class AudioService extends IAudioService.Stub // AudioHandler messages private static final int MSG_SET_DEVICE_VOLUME = 0; private static final int MSG_PERSIST_VOLUME = 1; + private static final int MSG_PERSIST_VOLUME_GROUP = 2; private static final int MSG_PERSIST_RINGER_MODE = 3; private static final int MSG_AUDIO_SERVER_DIED = 4; private static final int MSG_PLAY_SOUND_EFFECT = 5; @@ -780,6 +782,10 @@ public class AudioService extends IAudioService.Stub mSettingsObserver = new SettingsObserver(); createStreamStates(); + // must be called after createStreamStates() as it uses MUSIC volume as default if no + // persistent data + initVolumeGroupStates(); + // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it // relies on audio policy having correct ranges for volume indexes. mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex(); @@ -1018,6 +1024,9 @@ public class AudioService extends IAudioService.Stub streamState.applyAllVolumes(); } + // Restore audio volume groups + restoreVolumeGroups(); + // Restore mono mode updateMasterMono(mContentResolver); @@ -2288,20 +2297,20 @@ public class AudioService extends IAudioService.Stub String callingPackage) { enforceModifyAudioRoutingPermission(); Objects.requireNonNull(attr, "attr must not be null"); - // @todo not hold the caller context, post message - int stream = AudioProductStrategy.getLegacyStreamTypeForStrategyWithAudioAttributes(attr); - final int device = getDeviceForStream(stream); - - int oldIndex = AudioSystem.getVolumeIndexForAttributes(attr, device); - - AudioSystem.setVolumeIndexForAttributes(attr, index, device); - final int volumeGroup = getVolumeGroupIdForAttributes(attr); - final AudioVolumeGroup avg = getAudioVolumeGroupById(volumeGroup); - if (avg == null) { + if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) { + Log.e(TAG, ": no volume group found for attributes " + attr.toString()); return; } - for (final int groupedStream : avg.getLegacyStreamTypes()) { + final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup); + + sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(), + index/*val1*/, flags/*val2*/, callingPackage)); + + vgs.setVolumeIndex(index, flags); + + // For legacy reason, propagate to all streams associated to this volume group + for (final int groupedStream : vgs.getLegacyStreamTypes()) { setStreamVolume(groupedStream, index, flags, callingPackage, callingPackage, Binder.getCallingUid()); } @@ -2323,10 +2332,12 @@ public class AudioService extends IAudioService.Stub public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) { enforceModifyAudioRoutingPermission(); Objects.requireNonNull(attr, "attr must not be null"); - int stream = AudioProductStrategy.getLegacyStreamTypeForStrategyWithAudioAttributes(attr); - final int device = getDeviceForStream(stream); - - return AudioSystem.getVolumeIndexForAttributes(attr, device); + final int volumeGroup = getVolumeGroupIdForAttributes(attr); + if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) { + throw new IllegalArgumentException("No volume group for attributes " + attr); + } + final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup); + return vgs.getVolumeIndex(); } /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */ @@ -3754,6 +3765,8 @@ public class AudioService extends IAudioService.Stub enforceSafeMediaVolume(TAG); } } + + readVolumeGroupsSettings(); } /** @see AudioManager#setSpeakerphoneOn(boolean) */ @@ -4654,6 +4667,310 @@ public class AudioService extends IAudioService.Stub /////////////////////////////////////////////////////////////////////////// // Inner classes /////////////////////////////////////////////////////////////////////////// + /** + * Key is the AudioManager VolumeGroupId + * Value is the VolumeGroupState + */ + private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>(); + + private void initVolumeGroupStates() { + for (final AudioVolumeGroup avg : getAudioVolumeGroups()) { + try { + // if no valid attributes, this volume group is not controllable, throw exception + ensureValidAttributes(avg); + } catch (IllegalArgumentException e) { + // Volume Groups without attributes are not controllable through set/get volume + // using attributes. Do not append them. + Log.d(TAG, "volume group " + avg.name() + " for internal policy needs"); + continue; + } + sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg)); + } + for (int i = 0; i < sVolumeGroupStates.size(); i++) { + final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); + vgs.applyAllVolumes(); + } + } + + private void ensureValidAttributes(AudioVolumeGroup avg) { + boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream() + .anyMatch(aa -> !aa.equals(AudioProductStrategy.sDefaultAttributes)); + if (!hasAtLeastOneValidAudioAttributes) { + throw new IllegalArgumentException("Volume Group " + avg.name() + + " has no valid audio attributes"); + } + } + + private void readVolumeGroupsSettings() { + Log.v(TAG, "readVolumeGroupsSettings"); + for (int i = 0; i < sVolumeGroupStates.size(); i++) { + final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); + vgs.readSettings(); + vgs.applyAllVolumes(); + } + } + + // Called upon crash of AudioServer + private void restoreVolumeGroups() { + Log.v(TAG, "restoreVolumeGroups"); + for (int i = 0; i < sVolumeGroupStates.size(); i++) { + final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); + vgs.applyAllVolumes(); + } + } + + private void dumpVolumeGroups(PrintWriter pw) { + pw.println("\nVolume Groups (device: index)"); + for (int i = 0; i < sVolumeGroupStates.size(); i++) { + final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); + vgs.dump(pw); + pw.println(""); + } + } + + // NOTE: Locking order for synchronized objects related to volume management: + // 1 mSettingsLock + // 2 VolumeGroupState.class + private class VolumeGroupState { + private final AudioVolumeGroup mAudioVolumeGroup; + private final SparseIntArray mIndexMap = new SparseIntArray(8); + private int mIndexMin; + private int mIndexMax; + private int mLegacyStreamType = AudioSystem.STREAM_DEFAULT; + private int mPublicStreamType = AudioSystem.STREAM_MUSIC; + private AudioAttributes mAudioAttributes = AudioProductStrategy.sDefaultAttributes; + + // No API in AudioSystem to get a device from strategy or from attributes. + // Need a valid public stream type to use current API getDeviceForStream + private int getDeviceForVolume() { + return getDeviceForStream(mPublicStreamType); + } + + private VolumeGroupState(AudioVolumeGroup avg) { + mAudioVolumeGroup = avg; + Log.v(TAG, "VolumeGroupState for " + avg.toString()); + for (final AudioAttributes aa : avg.getAudioAttributes()) { + if (!aa.equals(AudioProductStrategy.sDefaultAttributes)) { + mAudioAttributes = aa; + break; + } + } + final int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes(); + if (streamTypes.length != 0) { + // Uses already initialized MIN / MAX if a stream type is attached to group + mLegacyStreamType = streamTypes[0]; + for (final int streamType : streamTypes) { + if (streamType != AudioSystem.STREAM_DEFAULT + && streamType < AudioSystem.getNumStreamTypes()) { + mPublicStreamType = streamType; + break; + } + } + mIndexMin = MIN_STREAM_VOLUME[mPublicStreamType]; + mIndexMax = MAX_STREAM_VOLUME[mPublicStreamType]; + } else if (!avg.getAudioAttributes().isEmpty()) { + mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes); + mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes); + } else { + Log.e(TAG, "volume group: " + mAudioVolumeGroup.name() + + " has neither valid attributes nor valid stream types assigned"); + return; + } + // Load volume indexes from data base + readSettings(); + } + + public @NonNull int[] getLegacyStreamTypes() { + return mAudioVolumeGroup.getLegacyStreamTypes(); + } + + public String name() { + return mAudioVolumeGroup.name(); + } + + public int getVolumeIndex() { + return getIndex(getDeviceForVolume()); + } + + public void setVolumeIndex(int index, int flags) { + if (mUseFixedVolume) { + return; + } + setVolumeIndex(index, getDeviceForVolume(), flags); + } + + private void setVolumeIndex(int index, int device, int flags) { + // Set the volume index + setVolumeIndexInt(index, device, flags); + + // Update local cache + mIndexMap.put(device, index); + + // update data base - post a persist volume group msg + sendMsg(mAudioHandler, + MSG_PERSIST_VOLUME_GROUP, + SENDMSG_QUEUE, + device, + 0, + this, + PERSIST_DELAY); + } + + private void setVolumeIndexInt(int index, int device, int flags) { + // Set the volume index + AudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device); + } + + public int getIndex(int device) { + synchronized (VolumeGroupState.class) { + int index = mIndexMap.get(device, -1); + // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT + return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT); + } + } + + public boolean hasIndexForDevice(int device) { + synchronized (VolumeGroupState.class) { + return (mIndexMap.get(device, -1) != -1); + } + } + + public int getMaxIndex() { + return mIndexMax; + } + + public int getMinIndex() { + return mIndexMin; + } + + public void applyAllVolumes() { + synchronized (VolumeGroupState.class) { + if (mLegacyStreamType != AudioSystem.STREAM_DEFAULT) { + // No-op to avoid regression with stream based volume management + return; + } + // apply device specific volumes first + int index; + for (int i = 0; i < mIndexMap.size(); i++) { + final int device = mIndexMap.keyAt(i); + if (device != AudioSystem.DEVICE_OUT_DEFAULT) { + index = mIndexMap.valueAt(i); + Log.v(TAG, "applyAllVolumes: restore index " + index + " for group " + + mAudioVolumeGroup.name() + " and device " + + AudioSystem.getOutputDeviceName(device)); + setVolumeIndexInt(index, device, 0 /*flags*/); + } + } + // apply default volume last: by convention , default device volume will be used + index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT); + Log.v(TAG, "applyAllVolumes: restore default device index " + index + " for group " + + mAudioVolumeGroup.name()); + setVolumeIndexInt(index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/); + } + } + + private void persistVolumeGroup(int device) { + if (mUseFixedVolume) { + return; + } + Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group " + + mAudioVolumeGroup.name() + " and device " + + AudioSystem.getOutputDeviceName(device)); + boolean success = Settings.System.putIntForUser(mContentResolver, + getSettingNameForDevice(device), + getIndex(device), + UserHandle.USER_CURRENT); + if (!success) { + Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name()); + } + } + + public void readSettings() { + synchronized (VolumeGroupState.class) { + // First clear previously loaded (previous user?) settings + mIndexMap.clear(); + // force maximum volume on all streams if fixed volume property is set + if (mUseFixedVolume) { + mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax); + return; + } + for (int device : AudioSystem.DEVICE_OUT_ALL_SET) { + // retrieve current volume for device + // if no volume stored for current volume group and device, use default volume + // if default device, continue otherwise + int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) + ? AudioSystem.DEFAULT_STREAM_VOLUME[mPublicStreamType] : -1; + int index; + String name = getSettingNameForDevice(device); + index = Settings.System.getIntForUser( + mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT); + if (index == -1) { + Log.e(TAG, "readSettings: No index stored for group " + + mAudioVolumeGroup.name() + ", device " + name); + continue; + } + Log.v(TAG, "readSettings: found stored index " + getValidIndex(index) + + " for group " + mAudioVolumeGroup.name() + ", device: " + name); + mIndexMap.put(device, getValidIndex(index)); + } + } + } + + private int getValidIndex(int index) { + if (index < mIndexMin) { + return mIndexMin; + } else if (mUseFixedVolume || index > mIndexMax) { + return mIndexMax; + } + return index; + } + + public @NonNull String getSettingNameForDevice(int device) { + final String suffix = AudioSystem.getOutputDeviceName(device); + if (suffix.isEmpty()) { + return mAudioVolumeGroup.name(); + } + return mAudioVolumeGroup.name() + "_" + AudioSystem.getOutputDeviceName(device); + } + + private void dump(PrintWriter pw) { + pw.println("- VOLUME GROUP " + mAudioVolumeGroup.name() + ":"); + pw.print(" Min: "); + pw.println(mIndexMin); + pw.print(" Max: "); + pw.println(mIndexMax); + pw.print(" Current: "); + for (int i = 0; i < mIndexMap.size(); i++) { + if (i > 0) { + pw.print(", "); + } + final int device = mIndexMap.keyAt(i); + pw.print(Integer.toHexString(device)); + final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default" + : AudioSystem.getOutputDeviceName(device); + if (!deviceName.isEmpty()) { + pw.print(" ("); + pw.print(deviceName); + pw.print(")"); + } + pw.print(": "); + pw.print(mIndexMap.valueAt(i)); + } + pw.println(); + pw.print(" Devices: "); + int n = 0; + final int devices = getDeviceForVolume(); + for (int device : AudioSystem.DEVICE_OUT_ALL_SET) { + if ((devices & device) == device) { + if (n++ > 0) { + pw.print(", "); + } + pw.print(AudioSystem.getOutputDeviceName(device)); + } + } + } + } + // NOTE: Locking order for synchronized objects related to volume or ringer mode management: // 1 mScoclient OR mSafeMediaVolumeState @@ -5298,6 +5615,11 @@ public class AudioService extends IAudioService.Stub persistVolume((VolumeStreamState) msg.obj, msg.arg1); break; + case MSG_PERSIST_VOLUME_GROUP: + final VolumeGroupState vgs = (VolumeGroupState) msg.obj; + vgs.persistVolumeGroup(msg.arg1); + break; + case MSG_PERSIST_RINGER_MODE: // note that the value persisted is the current ringer mode, not the // value of ringer mode as of the time the request was made to persist @@ -6395,6 +6717,7 @@ public class AudioService extends IAudioService.Stub } mMediaFocusControl.dump(pw); dumpStreamStates(pw); + dumpVolumeGroups(pw); dumpRingerMode(pw); pw.println("\nAudio routes:"); pw.print(" mMainType=0x"); pw.println(Integer.toHexString( diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index fcd8701f4ccf..add620e74df0 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -16,6 +16,7 @@ package com.android.server.audio; +import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioSystem; @@ -97,12 +98,15 @@ public class AudioServiceEvents { static final int VOL_ADJUST_VOL_UID = 5; static final int VOL_VOICE_ACTIVITY_HEARING_AID = 6; static final int VOL_MODE_CHANGE_HEARING_AID = 7; + static final int VOL_SET_GROUP_VOL = 8; final int mOp; final int mStream; final int mVal1; final int mVal2; final String mCaller; + final String mGroupName; + final AudioAttributes mAudioAttributes; /** used for VOL_ADJUST_VOL_UID, * VOL_ADJUST_SUGG_VOL, @@ -114,6 +118,8 @@ public class AudioServiceEvents { mVal1 = val1; mVal2 = val2; mCaller = caller; + mGroupName = null; + mAudioAttributes = null; } /** used for VOL_SET_HEARING_AID_VOL*/ @@ -124,6 +130,8 @@ public class AudioServiceEvents { // unused mStream = -1; mCaller = null; + mGroupName = null; + mAudioAttributes = null; } /** used for VOL_SET_AVRCP_VOL */ @@ -134,6 +142,8 @@ public class AudioServiceEvents { mVal2 = 0; mStream = -1; mCaller = null; + mGroupName = null; + mAudioAttributes = null; } /** used for VOL_VOICE_ACTIVITY_HEARING_AID */ @@ -144,6 +154,8 @@ public class AudioServiceEvents { mVal2 = voiceActive ? 1 : 0; // unused mCaller = null; + mGroupName = null; + mAudioAttributes = null; } /** used for VOL_MODE_CHANGE_HEARING_AID */ @@ -154,6 +166,19 @@ public class AudioServiceEvents { mVal2 = mode; // unused mCaller = null; + mGroupName = null; + mAudioAttributes = null; + } + + /** used for VOL_SET_GROUP_VOL */ + VolumeEvent(int op, AudioAttributes aa, String group, int index, int flags, String caller) { + mOp = op; + mStream = -1; + mVal1 = index; + mVal2 = flags; + mCaller = caller; + mGroupName = group; + mAudioAttributes = aa; } @Override @@ -208,6 +233,14 @@ public class AudioServiceEvents { .append(") causes setting HEARING_AID volume to idx:").append(mVal1) .append(" stream:").append(AudioSystem.streamToString(mStream)) .toString(); + case VOL_SET_GROUP_VOL: + return new StringBuilder("setVolumeIndexForAttributes(attr:") + .append(mAudioAttributes.toString()) + .append(" group: ").append(mGroupName) + .append(" index:").append(mVal1) + .append(" flags:0x").append(Integer.toHexString(mVal2)) + .append(") from ").append(mCaller) + .toString(); default: return new StringBuilder("FIXME invalid op:").append(mOp).toString(); } } diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index c9c2c96a642a..204f072a72fc 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -153,7 +153,12 @@ public class AuthService extends SystemService { // Only allow internal clients to enable non-public options. if (bundle.getBoolean(BiometricPrompt.EXTRA_DISALLOW_BIOMETRICS_IF_POLICY_EXISTS) - || bundle.getBoolean(BiometricPrompt.KEY_USE_DEFAULT_TITLE, false)) { + || bundle.getBoolean(BiometricPrompt.KEY_USE_DEFAULT_TITLE, false) + || bundle.getCharSequence(BiometricPrompt.KEY_DEVICE_CREDENTIAL_TITLE) != null + || bundle.getCharSequence( + BiometricPrompt.KEY_DEVICE_CREDENTIAL_SUBTITLE) != null + || bundle.getCharSequence( + BiometricPrompt.KEY_DEVICE_CREDENTIAL_DESCRIPTION) != null) { checkInternalPermission(); } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index cb88c4e8a739..1a68f1bff605 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -48,8 +48,12 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.net.ConnectivityManager; import android.net.INetworkManagementEventObserver; +import android.net.Ikev2VpnProfile; import android.net.IpPrefix; import android.net.IpSecManager; +import android.net.IpSecManager.IpSecTunnelInterface; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.IpSecTransform; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.LocalSocket; @@ -65,6 +69,12 @@ import android.net.RouteInfo; import android.net.UidRange; import android.net.VpnManager; import android.net.VpnService; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionParams; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionParams; import android.os.Binder; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -113,6 +123,7 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -122,6 +133,9 @@ import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; /** @@ -176,14 +190,14 @@ public class Vpn { private final Context mContext; private final NetworkInfo mNetworkInfo; - private String mPackage; + @VisibleForTesting protected String mPackage; private int mOwnerUID; private boolean mIsPackageTargetingAtLeastQ; private String mInterface; private Connection mConnection; /** Tracks the runners for all VPN types managed by the platform (eg. LegacyVpn, PlatformVpn) */ - private VpnRunner mVpnRunner; + @VisibleForTesting protected VpnRunner mVpnRunner; private PendingIntent mStatusIntent; private volatile boolean mEnableTeardown = true; @@ -196,6 +210,7 @@ public class Vpn { @VisibleForTesting protected final NetworkCapabilities mNetworkCapabilities; private final SystemServices mSystemServices; + private final Ikev2SessionCreator mIkev2SessionCreator; /** * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This @@ -238,17 +253,20 @@ public class Vpn { public Vpn(Looper looper, Context context, INetworkManagementService netService, @UserIdInt int userHandle) { - this(looper, context, netService, userHandle, new SystemServices(context)); + this(looper, context, netService, userHandle, + new SystemServices(context), new Ikev2SessionCreator()); } @VisibleForTesting protected Vpn(Looper looper, Context context, INetworkManagementService netService, - int userHandle, SystemServices systemServices) { + int userHandle, SystemServices systemServices, + Ikev2SessionCreator ikev2SessionCreator) { mContext = context; mNetd = netService; mUserHandle = userHandle; mLooper = looper; mSystemServices = systemServices; + mIkev2SessionCreator = ikev2SessionCreator; mPackage = VpnConfig.LEGACY_VPN; mOwnerUID = getAppUid(mPackage, mUserHandle); @@ -749,8 +767,9 @@ public class Vpn { private boolean isCurrentPreparedPackage(String packageName) { // We can't just check that packageName matches mPackage, because if the app was uninstalled - // and reinstalled it will no longer be prepared. Instead check the UID. - return getAppUid(packageName, mUserHandle) == mOwnerUID; + // and reinstalled it will no longer be prepared. Similarly if there is a shared UID, the + // calling package may not be the same as the prepared package. Check both UID and package. + return getAppUid(packageName, mUserHandle) == mOwnerUID && mPackage.equals(packageName); } /** Prepare the VPN for the given package. Does not perform permission checks. */ @@ -979,7 +998,11 @@ public class Vpn { } lp.setDomains(buffer.toString().trim()); - // TODO: Stop setting the MTU in jniCreate and set it here. + if (mConfig.mtu > 0) { + lp.setMtu(mConfig.mtu); + } + + // TODO: Stop setting the MTU in jniCreate return lp; } @@ -2004,30 +2027,369 @@ public class Vpn { protected abstract void exit(); } - private class IkeV2VpnRunner extends VpnRunner { - private static final String TAG = "IkeV2VpnRunner"; + interface IkeV2VpnRunnerCallback { + void onDefaultNetworkChanged(@NonNull Network network); - private final IpSecManager mIpSecManager; - private final VpnProfile mProfile; + void onChildOpened( + @NonNull Network network, @NonNull ChildSessionConfiguration childConfig); + + void onChildTransformCreated( + @NonNull Network network, @NonNull IpSecTransform transform, int direction); + + void onSessionLost(@NonNull Network network); + } + + /** + * Internal class managing IKEv2/IPsec VPN connectivity + * + * <p>The IKEv2 VPN will listen to, and run based on the lifecycle of Android's default Network. + * As a new default is selected, old IKE sessions will be torn down, and a new one will be + * started. + * + * <p>This class uses locking minimally - the Vpn instance lock is only ever held when fields of + * the outer class are modified. As such, care must be taken to ensure that no calls are added + * that might modify the outer class' state without acquiring a lock. + * + * <p>The overall structure of the Ikev2VpnRunner is as follows: + * + * <ol> + * <li>Upon startup, a NetworkRequest is registered with ConnectivityManager. This is called + * any time a new default network is selected + * <li>When a new default is connected, an IKE session is started on that Network. If there + * were any existing IKE sessions on other Networks, they are torn down before starting + * the new IKE session + * <li>Upon establishment, the onChildTransformCreated() callback is called twice, one for + * each direction, and finally onChildOpened() is called + * <li>Upon the onChildOpened() call, the VPN is fully set up. + * <li>Subsequent Network changes result in new onDefaultNetworkChanged() callbacks. See (2). + * </ol> + */ + class IkeV2VpnRunner extends VpnRunner implements IkeV2VpnRunnerCallback { + @NonNull private static final String TAG = "IkeV2VpnRunner"; + + @NonNull private final IpSecManager mIpSecManager; + @NonNull private final Ikev2VpnProfile mProfile; + @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback; + + /** + * Executor upon which ALL callbacks must be run. + * + * <p>This executor MUST be a single threaded executor, in order to ensure the consistency + * of the mutable Ikev2VpnRunner fields. The Ikev2VpnRunner is built mostly lock-free by + * virtue of everything being serialized on this executor. + */ + @NonNull private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - IkeV2VpnRunner(VpnProfile profile) { + /** Signal to ensure shutdown is honored even if a new Network is connected. */ + private boolean mIsRunning = true; + + @Nullable private UdpEncapsulationSocket mEncapSocket; + @Nullable private IpSecTunnelInterface mTunnelIface; + @Nullable private IkeSession mSession; + @Nullable private Network mActiveNetwork; + + IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; - - // TODO: move this to startVpnRunnerPrivileged() - mConfig = new VpnConfig(); - mIpSecManager = mContext.getSystemService(IpSecManager.class); + mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this); } @Override public void run() { - // TODO: Build IKE config, start IKE session + // Explicitly use only the network that ConnectivityService thinks is the "best." In + // other words, only ever use the currently selected default network. This does mean + // that in both onLost() and onConnected(), any old sessions MUST be torn down. This + // does NOT include VPNs. + final ConnectivityManager cm = ConnectivityManager.from(mContext); + cm.requestNetwork(cm.getDefaultRequest(), mNetworkCallback); + } + + private boolean isActiveNetwork(@Nullable Network network) { + return Objects.equals(mActiveNetwork, network) && mIsRunning; + } + + /** + * Called when an IKE Child session has been opened, signalling completion of the startup. + * + * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor + * thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onChildOpened( + @NonNull Network network, @NonNull ChildSessionConfiguration childConfig) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "onOpened called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // resetIkeState(). + return; + } + + try { + final String interfaceName = mTunnelIface.getInterfaceName(); + final int maxMtu = mProfile.getMaxMtu(); + final List<LinkAddress> internalAddresses = childConfig.getInternalAddresses(); + + final Collection<RouteInfo> newRoutes = VpnIkev2Utils.getRoutesFromTrafficSelectors( + childConfig.getOutboundTrafficSelectors()); + for (final LinkAddress address : internalAddresses) { + mTunnelIface.addAddress(address.getAddress(), address.getPrefixLength()); + } + + final NetworkAgent networkAgent; + final LinkProperties lp; + + synchronized (Vpn.this) { + mInterface = interfaceName; + mConfig.mtu = maxMtu; + mConfig.interfaze = mInterface; + + mConfig.addresses.clear(); + mConfig.addresses.addAll(internalAddresses); + + mConfig.routes.clear(); + mConfig.routes.addAll(newRoutes); + + // TODO: Add DNS servers from negotiation + + networkAgent = mNetworkAgent; + + // The below must be done atomically with the mConfig update, otherwise + // isRunningLocked() will be racy. + if (networkAgent == null) { + if (isSettingsVpnLocked()) { + prepareStatusIntent(); + } + agentConnect(); + return; // Link properties are already sent. + } + + lp = makeLinkProperties(); // Accesses VPN instance fields; must be locked + } + + networkAgent.sendLinkProperties(lp); + } catch (Exception e) { + Log.d(TAG, "Error in ChildOpened for network " + network, e); + onSessionLost(network); + } + } + + /** + * Called when an IPsec transform has been created, and should be applied. + * + * <p>This method is called multiple times over the lifetime of an IkeSession (or default + * network), and is MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onChildTransformCreated( + @NonNull Network network, @NonNull IpSecTransform transform, int direction) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "ChildTransformCreated for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // resetIkeState(). + return; + } + + try { + // Transforms do not need to be persisted; the IkeSession will keep + // them alive for us + mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform); + } catch (IOException e) { + Log.d(TAG, "Transform application failed for network " + network, e); + onSessionLost(network); + } + } + + /** + * Called when a new default network is connected. + * + * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE + * state in the process, and starting a new IkeSession instance. + * + * <p>This method is called multiple times over the lifetime of the Ikev2VpnRunner, and is + * called on the ConnectivityService thread. Thus, the actual work MUST be proxied to the + * mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onDefaultNetworkChanged(@NonNull Network network) { + Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network); + + // Proxy to the Ikev2VpnRunner (single-thread) executor to ensure consistency in lieu + // of locking. + mExecutor.execute(() -> { + try { + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } + + // Without MOBIKE, we have no way to seamlessly migrate. Close on old + // (non-default) network, and start the new one. + resetIkeState(); + mActiveNetwork = network; + + // TODO(b/149356682): Update this based on new IKE API + mEncapSocket = mIpSecManager.openUdpEncapsulationSocket(); + + // TODO(b/149356682): Update this based on new IKE API + final IkeSessionParams ikeSessionParams = + VpnIkev2Utils.buildIkeSessionParams(mProfile, mEncapSocket); + final ChildSessionParams childSessionParams = + VpnIkev2Utils.buildChildSessionParams(); + + // TODO: Remove the need for adding two unused addresses with + // IPsec tunnels. + mTunnelIface = + mIpSecManager.createIpSecTunnelInterface( + ikeSessionParams.getServerAddress() /* unused */, + ikeSessionParams.getServerAddress() /* unused */, + network); + mNetd.setInterfaceUp(mTunnelIface.getInterfaceName()); + + // Socket must be bound to prevent network switches from causing + // the IKE teardown to fail/timeout. + // TODO(b/149356682): Update this based on new IKE API + network.bindSocket(mEncapSocket.getFileDescriptor()); + + mSession = mIkev2SessionCreator.createIkeSession( + mContext, + ikeSessionParams, + childSessionParams, + mExecutor, + new VpnIkev2Utils.IkeSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network), + new VpnIkev2Utils.ChildSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network)); + Log.d(TAG, "Ike Session started for network " + network); + } catch (Exception e) { + Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); + onSessionLost(network); + } + }); + } + + /** + * Handles loss of a session + * + * <p>The loss of a session might be due to an onLost() call, the IKE session getting torn + * down for any reason, or an error in updating state (transform application, VPN setup) + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onSessionLost(@NonNull Network network) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "onSessionLost() called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // onSessionLost() and resetIkeState(). + return; + } + + mActiveNetwork = null; + + // Close all obsolete state, but keep VPN alive incase a usable network comes up. + // (Mirrors VpnService behavior) + Log.d(TAG, "Resetting state for network: " + network); + + synchronized (Vpn.this) { + // Since this method handles non-fatal errors only, set mInterface to null to + // prevent the NetworkManagementEventObserver from killing this VPN based on the + // interface going down (which we expect). + mInterface = null; + mConfig.interfaze = null; + + // Set as unroutable to prevent traffic leaking while the interface is down. + if (mConfig != null && mConfig.routes != null) { + final List<RouteInfo> oldRoutes = new ArrayList<>(mConfig.routes); + + mConfig.routes.clear(); + for (final RouteInfo route : oldRoutes) { + mConfig.routes.add(new RouteInfo(route.getDestination(), RTN_UNREACHABLE)); + } + if (mNetworkAgent != null) { + mNetworkAgent.sendLinkProperties(makeLinkProperties()); + } + } + } + + resetIkeState(); + } + + /** + * Cleans up all IKE state + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + private void resetIkeState() { + if (mTunnelIface != null) { + // No need to call setInterfaceDown(); the IpSecInterface is being fully torn down. + mTunnelIface.close(); + mTunnelIface = null; + } + if (mSession != null) { + mSession.kill(); // Kill here to make sure all resources are released immediately + mSession = null; + } + + // TODO(b/149356682): Update this based on new IKE API + if (mEncapSocket != null) { + try { + mEncapSocket.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close encap socket", e); + } + mEncapSocket = null; + } + } + + /** + * Triggers cleanup of outer class' state + * + * <p>Can be called from any thread, as it does not mutate state in the Ikev2VpnRunner. + */ + private void cleanupVpnState() { + synchronized (Vpn.this) { + agentDisconnect(); + } + } + + /** + * Cleans up all Ikev2VpnRunner internal state + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + private void shutdownVpnRunner() { + mActiveNetwork = null; + mIsRunning = false; + + resetIkeState(); + + final ConnectivityManager cm = ConnectivityManager.from(mContext); + cm.unregisterNetworkCallback(mNetworkCallback); + + mExecutor.shutdown(); } @Override public void exit() { - // TODO: Teardown IKE session & any resources. - agentDisconnect(); + // Cleanup outer class' state immediately, otherwise race conditions may ensue. + cleanupVpnState(); + + mExecutor.execute(() -> { + shutdownVpnRunner(); + }); } } @@ -2488,12 +2850,46 @@ public class Vpn { throw new IllegalArgumentException("No profile found for " + packageName); } - startVpnProfilePrivileged(profile); + startVpnProfilePrivileged(profile, packageName); }); } - private void startVpnProfilePrivileged(@NonNull VpnProfile profile) { - // TODO: Start PlatformVpnRunner + private void startVpnProfilePrivileged( + @NonNull VpnProfile profile, @NonNull String packageName) { + // Ensure that no other previous instance is running. + if (mVpnRunner != null) { + mVpnRunner.exit(); + mVpnRunner = null; + } + updateState(DetailedState.CONNECTING, "startPlatformVpn"); + + try { + // Build basic config + mConfig = new VpnConfig(); + mConfig.user = packageName; + mConfig.isMetered = profile.isMetered; + mConfig.startTime = SystemClock.elapsedRealtime(); + mConfig.proxyInfo = profile.proxy; + + switch (profile.type) { + case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: + case VpnProfile.TYPE_IKEV2_IPSEC_PSK: + case VpnProfile.TYPE_IKEV2_IPSEC_RSA: + mVpnRunner = new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile)); + mVpnRunner.start(); + break; + default: + updateState(DetailedState.FAILED, "Invalid platform VPN type"); + Log.d(TAG, "Unknown VPN profile type: " + profile.type); + break; + } + } catch (IOException | GeneralSecurityException e) { + // Reset mConfig + mConfig = null; + + updateState(DetailedState.FAILED, "VPN startup failed"); + throw new IllegalArgumentException("VPN startup failed", e); + } } /** @@ -2507,13 +2903,37 @@ public class Vpn { public synchronized void stopVpnProfile(@NonNull String packageName) { checkNotNull(packageName, "No package name provided"); - // To stop the VPN profile, the caller must be the current prepared package. Otherwise, - // the app is not prepared, and we can just return. - if (!isCurrentPreparedPackage(packageName)) { - // TODO: Also check to make sure that the running VPN is a VPN profile. + // To stop the VPN profile, the caller must be the current prepared package and must be + // running an Ikev2VpnProfile. + if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) { return; } prepareInternal(VpnConfig.LEGACY_VPN); } + + /** + * Proxy to allow testing + * + * @hide + */ + @VisibleForTesting + public static class Ikev2SessionCreator { + /** Creates a IKE session */ + public IkeSession createIkeSession( + @NonNull Context context, + @NonNull IkeSessionParams ikeSessionParams, + @NonNull ChildSessionParams firstChildSessionParams, + @NonNull Executor userCbExecutor, + @NonNull IkeSessionCallback ikeSessionCallback, + @NonNull ChildSessionCallback firstChildSessionCallback) { + return new IkeSession( + context, + ikeSessionParams, + firstChildSessionParams, + userCbExecutor, + ikeSessionCallback, + firstChildSessionCallback); + } + } } diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java new file mode 100644 index 000000000000..33fc32b78df7 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -0,0 +1,390 @@ +/* + * 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.connectivity; + +import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.ipsec.ike.SaProposal.DH_GROUP_1024_BIT_MODP; +import static android.net.ipsec.ike.SaProposal.DH_GROUP_2048_BIT_MODP; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_192; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256; +import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC; +import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1; + +import android.annotation.NonNull; +import android.net.Ikev2VpnProfile; +import android.net.InetAddresses; +import android.net.IpPrefix; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.IpSecTransform; +import android.net.Network; +import android.net.RouteInfo; +import android.net.eap.EapSessionConfig; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionParams; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeIdentification; +import android.net.ipsec.ike.IkeIpv4AddrIdentification; +import android.net.ipsec.ike.IkeIpv6AddrIdentification; +import android.net.ipsec.ike.IkeKeyIdIdentification; +import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.net.util.IpRange; +import android.system.OsConstants; +import android.util.Log; + +import com.android.internal.net.VpnProfile; +import com.android.internal.util.HexDump; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +/** + * Utility class to build and convert IKEv2/IPsec parameters. + * + * @hide + */ +public class VpnIkev2Utils { + static IkeSessionParams buildIkeSessionParams( + @NonNull Ikev2VpnProfile profile, @NonNull UdpEncapsulationSocket socket) { + // TODO(b/149356682): Update this based on new IKE API. Only numeric addresses supported + // until then. All others throw IAE (caught by caller). + final InetAddress serverAddr = InetAddresses.parseNumericAddress(profile.getServerAddr()); + final IkeIdentification localId = parseIkeIdentification(profile.getUserIdentity()); + final IkeIdentification remoteId = parseIkeIdentification(profile.getServerAddr()); + + // TODO(b/149356682): Update this based on new IKE API. + final IkeSessionParams.Builder ikeOptionsBuilder = + new IkeSessionParams.Builder() + .setServerAddress(serverAddr) + .setUdpEncapsulationSocket(socket) + .setLocalIdentification(localId) + .setRemoteIdentification(remoteId); + setIkeAuth(profile, ikeOptionsBuilder); + + for (final IkeSaProposal ikeProposal : getIkeSaProposals()) { + ikeOptionsBuilder.addSaProposal(ikeProposal); + } + + return ikeOptionsBuilder.build(); + } + + static ChildSessionParams buildChildSessionParams() { + final TunnelModeChildSessionParams.Builder childOptionsBuilder = + new TunnelModeChildSessionParams.Builder(); + + for (final ChildSaProposal childProposal : getChildSaProposals()) { + childOptionsBuilder.addSaProposal(childProposal); + } + + childOptionsBuilder.addInternalAddressRequest(OsConstants.AF_INET); + childOptionsBuilder.addInternalAddressRequest(OsConstants.AF_INET6); + childOptionsBuilder.addInternalDnsServerRequest(OsConstants.AF_INET); + childOptionsBuilder.addInternalDnsServerRequest(OsConstants.AF_INET6); + + return childOptionsBuilder.build(); + } + + private static void setIkeAuth( + @NonNull Ikev2VpnProfile profile, @NonNull IkeSessionParams.Builder builder) { + switch (profile.getType()) { + case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: + final EapSessionConfig eapConfig = + new EapSessionConfig.Builder() + .setEapMsChapV2Config(profile.getUsername(), profile.getPassword()) + .build(); + builder.setAuthEap(profile.getServerRootCaCert(), eapConfig); + break; + case VpnProfile.TYPE_IKEV2_IPSEC_PSK: + builder.setAuthPsk(profile.getPresharedKey()); + break; + case VpnProfile.TYPE_IKEV2_IPSEC_RSA: + builder.setAuthDigitalSignature( + profile.getServerRootCaCert(), + profile.getUserCert(), + profile.getRsaPrivateKey()); + break; + default: + throw new IllegalArgumentException("Unknown auth method set"); + } + } + + private static List<IkeSaProposal> getIkeSaProposals() { + // TODO: filter this based on allowedAlgorithms + final List<IkeSaProposal> proposals = new ArrayList<>(); + + // Encryption Algorithms: Currently only AES_CBC is supported. + final IkeSaProposal.Builder normalModeBuilder = new IkeSaProposal.Builder(); + + // Currently only AES_CBC is supported. + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_192); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128); + + // Authentication/Integrity Algorithms + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_AES_XCBC_96); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA1_96); + + // Add AEAD options + final IkeSaProposal.Builder aeadBuilder = new IkeSaProposal.Builder(); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_128); + + // Add dh, prf for both builders + for (final IkeSaProposal.Builder builder : Arrays.asList(normalModeBuilder, aeadBuilder)) { + builder.addDhGroup(DH_GROUP_2048_BIT_MODP); + builder.addDhGroup(DH_GROUP_1024_BIT_MODP); + builder.addPseudorandomFunction(PSEUDORANDOM_FUNCTION_AES128_XCBC); + builder.addPseudorandomFunction(PSEUDORANDOM_FUNCTION_HMAC_SHA1); + } + + proposals.add(normalModeBuilder.build()); + proposals.add(aeadBuilder.build()); + return proposals; + } + + private static List<ChildSaProposal> getChildSaProposals() { + // TODO: filter this based on allowedAlgorithms + final List<ChildSaProposal> proposals = new ArrayList<>(); + + // Add non-AEAD options + final ChildSaProposal.Builder normalModeBuilder = new ChildSaProposal.Builder(); + + // Encryption Algorithms: Currently only AES_CBC is supported. + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_192); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128); + + // Authentication/Integrity Algorithms + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA1_96); + + // Add AEAD options + final ChildSaProposal.Builder aeadBuilder = new ChildSaProposal.Builder(); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_128); + + proposals.add(normalModeBuilder.build()); + proposals.add(aeadBuilder.build()); + return proposals; + } + + static class IkeSessionCallbackImpl implements IkeSessionCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + private final Network mNetwork; + + IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + mTag = tag; + mCallback = callback; + mNetwork = network; + } + + @Override + public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { + Log.d(mTag, "IkeOpened for network " + mNetwork); + // Nothing to do here. + } + + @Override + public void onClosed() { + Log.d(mTag, "IkeClosed for network " + mNetwork); + mCallback.onSessionLost(mNetwork); // Server requested session closure. Retry? + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + Log.d(mTag, "IkeClosedExceptionally for network " + mNetwork, exception); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onError(@NonNull IkeProtocolException exception) { + Log.d(mTag, "IkeError for network " + mNetwork, exception); + // Non-fatal, log and continue. + } + } + + static class ChildSessionCallbackImpl implements ChildSessionCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + private final Network mNetwork; + + ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + mTag = tag; + mCallback = callback; + mNetwork = network; + } + + @Override + public void onOpened(@NonNull ChildSessionConfiguration childConfig) { + Log.d(mTag, "ChildOpened for network " + mNetwork); + mCallback.onChildOpened(mNetwork, childConfig); + } + + @Override + public void onClosed() { + Log.d(mTag, "ChildClosed for network " + mNetwork); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + Log.d(mTag, "ChildClosedExceptionally for network " + mNetwork, exception); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) { + Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; network " + mNetwork); + mCallback.onChildTransformCreated(mNetwork, transform, direction); + } + + @Override + public void onIpSecTransformDeleted(@NonNull IpSecTransform transform, int direction) { + // Nothing to be done; no references to the IpSecTransform are held by the + // Ikev2VpnRunner (or this callback class), and this transform will be closed by the + // IKE library. + Log.d(mTag, + "ChildTransformDeleted; Direction: " + direction + "; for network " + mNetwork); + } + } + + static class Ikev2VpnNetworkCallback extends NetworkCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + + Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback) { + mTag = tag; + mCallback = callback; + } + + @Override + public void onAvailable(@NonNull Network network) { + Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network); + mCallback.onDefaultNetworkChanged(network); + } + + @Override + public void onLost(@NonNull Network network) { + Log.d(mTag, "Tearing down; lost network: " + network); + mCallback.onSessionLost(network); + } + } + + /** + * Identity parsing logic using similar logic to open source implementations of IKEv2 + * + * <p>This method does NOT support using type-prefixes (eg 'fqdn:' or 'keyid'), or ASN.1 encoded + * identities. + */ + private static IkeIdentification parseIkeIdentification(@NonNull String identityStr) { + // TODO: Add identity formatting to public API javadocs. + if (identityStr.contains("@")) { + if (identityStr.startsWith("@#")) { + // KEY_ID + final String hexStr = identityStr.substring(2); + return new IkeKeyIdIdentification(HexDump.hexStringToByteArray(hexStr)); + } else if (identityStr.startsWith("@@")) { + // RFC822 (USER_FQDN) + return new IkeRfc822AddrIdentification(identityStr.substring(2)); + } else if (identityStr.startsWith("@")) { + // FQDN + return new IkeFqdnIdentification(identityStr.substring(1)); + } else { + // RFC822 (USER_FQDN) + return new IkeRfc822AddrIdentification(identityStr); + } + } else if (InetAddresses.isNumericAddress(identityStr)) { + final InetAddress addr = InetAddresses.parseNumericAddress(identityStr); + if (addr instanceof Inet4Address) { + // IPv4 + return new IkeIpv4AddrIdentification((Inet4Address) addr); + } else if (addr instanceof Inet6Address) { + // IPv6 + return new IkeIpv6AddrIdentification((Inet6Address) addr); + } else { + throw new IllegalArgumentException("IP version not supported"); + } + } else { + if (identityStr.contains(":")) { + // KEY_ID + return new IkeKeyIdIdentification(identityStr.getBytes()); + } else { + // FQDN + return new IkeFqdnIdentification(identityStr); + } + } + } + + static Collection<RouteInfo> getRoutesFromTrafficSelectors( + List<IkeTrafficSelector> trafficSelectors) { + final HashSet<RouteInfo> routes = new HashSet<>(); + + for (final IkeTrafficSelector selector : trafficSelectors) { + for (final IpPrefix prefix : + new IpRange(selector.startingAddress, selector.endingAddress).asIpPrefixes()) { + routes.add(new RouteInfo(prefix, null)); + } + } + + return routes; + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 7c2ec78c1cbc..d9e30250ba2d 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -105,32 +105,57 @@ import java.util.stream.Collectors; */ public class HdmiControlService extends SystemService { private static final String TAG = "HdmiControlService"; - private final Locale HONG_KONG = new Locale("zh", "HK"); - private final Locale MACAU = new Locale("zh", "MO"); + private static final Locale HONG_KONG = new Locale("zh", "HK"); + private static final Locale MACAU = new Locale("zh", "MO"); - private static final Map<String, String> mTerminologyToBibliographicMap; - static { - mTerminologyToBibliographicMap = new HashMap<>(); + private static final Map<String, String> sTerminologyToBibliographicMap = + createsTerminologyToBibliographicMap(); + + private static Map<String, String> createsTerminologyToBibliographicMap() { + Map<String, String> temp = new HashMap<>(); // NOTE: (TERMINOLOGY_CODE, BIBLIOGRAPHIC_CODE) - mTerminologyToBibliographicMap.put("sqi", "alb"); // Albanian - mTerminologyToBibliographicMap.put("hye", "arm"); // Armenian - mTerminologyToBibliographicMap.put("eus", "baq"); // Basque - mTerminologyToBibliographicMap.put("mya", "bur"); // Burmese - mTerminologyToBibliographicMap.put("ces", "cze"); // Czech - mTerminologyToBibliographicMap.put("nld", "dut"); // Dutch - mTerminologyToBibliographicMap.put("kat", "geo"); // Georgian - mTerminologyToBibliographicMap.put("deu", "ger"); // German - mTerminologyToBibliographicMap.put("ell", "gre"); // Greek - mTerminologyToBibliographicMap.put("fra", "fre"); // French - mTerminologyToBibliographicMap.put("isl", "ice"); // Icelandic - mTerminologyToBibliographicMap.put("mkd", "mac"); // Macedonian - mTerminologyToBibliographicMap.put("mri", "mao"); // Maori - mTerminologyToBibliographicMap.put("msa", "may"); // Malay - mTerminologyToBibliographicMap.put("fas", "per"); // Persian - mTerminologyToBibliographicMap.put("ron", "rum"); // Romanian - mTerminologyToBibliographicMap.put("slk", "slo"); // Slovak - mTerminologyToBibliographicMap.put("bod", "tib"); // Tibetan - mTerminologyToBibliographicMap.put("cym", "wel"); // Welsh + temp.put("sqi", "alb"); // Albanian + temp.put("hye", "arm"); // Armenian + temp.put("eus", "baq"); // Basque + temp.put("mya", "bur"); // Burmese + temp.put("ces", "cze"); // Czech + temp.put("nld", "dut"); // Dutch + temp.put("kat", "geo"); // Georgian + temp.put("deu", "ger"); // German + temp.put("ell", "gre"); // Greek + temp.put("fra", "fre"); // French + temp.put("isl", "ice"); // Icelandic + temp.put("mkd", "mac"); // Macedonian + temp.put("mri", "mao"); // Maori + temp.put("msa", "may"); // Malay + temp.put("fas", "per"); // Persian + temp.put("ron", "rum"); // Romanian + temp.put("slk", "slo"); // Slovak + temp.put("bod", "tib"); // Tibetan + temp.put("cym", "wel"); // Welsh + return Collections.unmodifiableMap(temp); + } + + @VisibleForTesting static String localeToMenuLanguage(Locale locale) { + if (locale.equals(Locale.TAIWAN) || locale.equals(HONG_KONG) || locale.equals(MACAU)) { + // Android always returns "zho" for all Chinese variants. + // Use "bibliographic" code defined in CEC639-2 for traditional + // Chinese used in Taiwan/Hong Kong/Macau. + return "chi"; + } else { + String language = locale.getISO3Language(); + + // locale.getISO3Language() returns terminology code and need to + // send it as bibliographic code instead since the Bibliographic + // codes of ISO/FDIS 639-2 shall be used. + // NOTE: Chinese also has terminology/bibliographic code "zho" and "chi" + // But, as it depends on the locale, is not handled here. + if (sTerminologyToBibliographicMap.containsKey(language)) { + language = sTerminologyToBibliographicMap.get(language); + } + + return language; + } } static final String PERMISSION = "android.permission.HDMI_CEC"; @@ -208,8 +233,8 @@ public class HdmiControlService extends SystemService { } break; case Intent.ACTION_CONFIGURATION_CHANGED: - String language = getMenuLanguage(); - if (!mLanguage.equals(language)) { + String language = HdmiControlService.localeToMenuLanguage(Locale.getDefault()); + if (!mMenuLanguage.equals(language)) { onLanguageChanged(language); } break; @@ -221,28 +246,6 @@ public class HdmiControlService extends SystemService { } } - private String getMenuLanguage() { - Locale locale = Locale.getDefault(); - if (locale.equals(Locale.TAIWAN) || locale.equals(HONG_KONG) || locale.equals(MACAU)) { - // Android always returns "zho" for all Chinese variants. - // Use "bibliographic" code defined in CEC639-2 for traditional - // Chinese used in Taiwan/Hong Kong/Macau. - return "chi"; - } else { - String language = locale.getISO3Language(); - - // locale.getISO3Language() returns terminology code and need to - // send it as bibliographic code instead since the Bibliographic - // codes of ISO/FDIS 639-2 shall be used. - // NOTE: Chinese also has terminology/bibliographic code "zho" and "chi" - // But, as it depends on the locale, is not handled here. - if (mTerminologyToBibliographicMap.containsKey(language)) { - language = mTerminologyToBibliographicMap.get(language); - } - - return language; - } - } } // A thread to handle synchronous IO of CEC and MHL control service. @@ -339,7 +342,7 @@ public class HdmiControlService extends SystemService { private int mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY; @ServiceThreadOnly - private String mLanguage = Locale.getDefault().getISO3Language(); + private String mMenuLanguage = localeToMenuLanguage(Locale.getDefault()); @ServiceThreadOnly private boolean mStandbyMessageReceived = false; @@ -759,7 +762,7 @@ public class HdmiControlService extends SystemService { private void initializeCec(int initiatedBy) { mAddressAllocated = false; mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, true); - mCecController.setLanguage(mLanguage); + mCecController.setLanguage(mMenuLanguage); initializeLocalDevices(initiatedBy); } @@ -2818,7 +2821,7 @@ public class HdmiControlService extends SystemService { @ServiceThreadOnly private void onLanguageChanged(String language) { assertRunOnServiceThread(); - mLanguage = language; + mMenuLanguage = language; if (isTvDeviceEnabled()) { tv().broadcastMenuLanguage(language); @@ -2826,10 +2829,19 @@ public class HdmiControlService extends SystemService { } } + /** + * Gets the CEC menu language. + * + * <p>This is the ISO/FDIS 639-2 3 letter language code sent in the CEC message @{code <Set Menu + * Language>}. + * See HDMI 1.4b section CEC 13.6.2 + * + * @see {@link Locale#getISO3Language()} + */ @ServiceThreadOnly String getLanguage() { assertRunOnServiceThread(); - return mLanguage; + return mMenuLanguage; } private void disableDevices(PendingActionClearedCallback callback) { @@ -2838,7 +2850,6 @@ public class HdmiControlService extends SystemService { device.disableDevice(mStandbyMessageReceived, callback); } } - mMhlController.clearAllLocalDevices(); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 916b63bee0e8..d8cab82cfb06 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -932,7 +932,7 @@ public class NotificationManagerService extends SystemService { StatusBarNotification sbn = r.getSbn(); cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(), sbn.getId(), Notification.FLAG_AUTO_CANCEL, - FLAG_FOREGROUND_SERVICE, false, r.getUserId(), + FLAG_FOREGROUND_SERVICE | FLAG_BUBBLE, false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null); nv.recycle(); reportUserInteraction(r); @@ -1055,7 +1055,7 @@ public class NotificationManagerService extends SystemService { if (DBG) Slog.d(TAG, "Marking notification as visible " + nv.key); reportSeen(r); } - r.setVisibility(true, nv.rank, nv.count); + r.setVisibility(true, nv.rank, nv.count, mNotificationRecordLogger); mAssistants.notifyAssistantVisibilityChangedLocked(r.getSbn(), true); boolean isHun = (nv.location == NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP); @@ -1074,7 +1074,7 @@ public class NotificationManagerService extends SystemService { for (NotificationVisibility nv : noLongerVisibleKeys) { NotificationRecord r = mNotificationsByKey.get(nv.key); if (r == null) continue; - r.setVisibility(false, nv.rank, nv.count); + r.setVisibility(false, nv.rank, nv.count, mNotificationRecordLogger); mAssistants.notifyAssistantVisibilityChangedLocked(r.getSbn(), false); nv.recycle(); } @@ -6468,7 +6468,7 @@ public class NotificationManagerService extends SystemService { mUsageStats.registerPostedByApp(r); r.setInterruptive(isVisuallyInterruptive(null, r)); } else { - old = mNotificationList.get(index); + old = mNotificationList.get(index); // Potentially *changes* old mNotificationList.set(index, r); mUsageStats.registerUpdatedByApp(r, old); // Make sure we don't lose the foreground service state. @@ -6537,7 +6537,7 @@ public class NotificationManagerService extends SystemService { maybeRecordInterruptionLocked(r); // Log event to statsd - mNotificationRecordLogger.logNotificationReported(r, old, position, + mNotificationRecordLogger.maybeLogNotificationPosted(r, old, position, buzzBeepBlinkLoggingCode); } finally { int N = mEnqueuedNotifications.size(); @@ -7986,7 +7986,8 @@ public class NotificationManagerService extends SystemService { FlagChecker flagChecker = (int flags) -> { int flagsToCheck = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; - if (REASON_LISTENER_CANCEL_ALL == reason) { + if (REASON_LISTENER_CANCEL_ALL == reason + || REASON_CANCEL_ALL == reason) { flagsToCheck |= FLAG_BUBBLE; } if ((flags & flagsToCheck) != 0) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 4785da9a5922..0ada58e1ce16 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -909,7 +909,8 @@ public final class NotificationRecord { /** * Set the visibility of the notification. */ - public void setVisibility(boolean visible, int rank, int count) { + public void setVisibility(boolean visible, int rank, int count, + NotificationRecordLogger notificationRecordLogger) { final long now = System.currentTimeMillis(); mVisibleSinceMs = visible ? now : mVisibleSinceMs; stats.onVisibilityChanged(visible); @@ -927,6 +928,7 @@ public final class NotificationRecord { getFreshnessMs(now), 0, // exposure time rank); + notificationRecordLogger.logNotificationVisibility(this, visible); } /** diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java index 2f7854226c5c..eaca066f026f 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java +++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java @@ -41,13 +41,14 @@ import java.util.Objects; public interface NotificationRecordLogger { /** - * Logs a NotificationReported atom reflecting the posting or update of a notification. + * May log 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, + void maybeLogNotificationPosted(@Nullable NotificationRecord r, + @Nullable NotificationRecord old, int position, int buzzBeepBlink); /** @@ -62,6 +63,14 @@ public interface NotificationRecordLogger { @NotificationStats.DismissalSurface int dismissalSurface); /** + * Logs a notification visibility change event using UiEventReported (event ids from the + * NotificationEvents enum). + * @param r The NotificationRecord. If null, no action is taken. + * @param visible True if the notification became visible. + */ + void logNotificationVisibility(@Nullable NotificationRecord r, boolean visible); + + /** * The UiEvent enums that this class can log. */ enum NotificationReportedEvent implements UiEventLogger.UiEventEnum { @@ -145,6 +154,7 @@ public interface NotificationRecordLogger { @Override public int getId() { return mId; } + public static NotificationCancelledEvent fromCancelReason( @NotificationListenerService.NotificationCancelReason int reason, @NotificationStats.DismissalSurface int surface) { @@ -191,6 +201,24 @@ public interface NotificationRecordLogger { } } + enum NotificationEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Notification became visible.") + NOTIFICATION_OPEN(197), + @UiEvent(doc = "Notification stopped being visible.") + NOTIFICATION_CLOSE(198); + + private final int mId; + NotificationEvent(int id) { + mId = id; + } + @Override public int getId() { + return mId; + } + + public static NotificationEvent fromVisibility(boolean visible) { + return visible ? NOTIFICATION_OPEN : NOTIFICATION_CLOSE; + } + } /** * A helper for extracting logging information from one or two NotificationRecords. */ @@ -209,7 +237,7 @@ public interface NotificationRecordLogger { /** * @return True if old is null, alerted, or important logged fields have changed. */ - boolean shouldLog(int buzzBeepBlink) { + boolean shouldLogReported(int buzzBeepBlink) { if (r == null) { return false; } diff --git a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java index bb23d1e876dc..9fcac257d328 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java +++ b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java @@ -26,13 +26,13 @@ import com.android.internal.util.FrameworkStatsLog; */ public class NotificationRecordLoggerImpl implements NotificationRecordLogger { - UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); + private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); @Override - public void logNotificationReported(NotificationRecord r, NotificationRecord old, + public void maybeLogNotificationPosted(NotificationRecord r, NotificationRecord old, int position, int buzzBeepBlink) { NotificationRecordPair p = new NotificationRecordPair(r, old); - if (!p.shouldLog(buzzBeepBlink)) { + if (!p.shouldLogReported(buzzBeepBlink)) { return; } FrameworkStatsLog.write(FrameworkStatsLog.NOTIFICATION_REPORTED, @@ -66,8 +66,19 @@ public class NotificationRecordLoggerImpl implements NotificationRecordLogger { @Override public void logNotificationCancelled(NotificationRecord r, int reason, int dismissalSurface) { - mUiEventLogger.logWithInstanceId( - NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface), - r.getUid(), r.getSbn().getPackageName(), r.getSbn().getInstanceId()); + log(NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface), r); + } + + @Override + public void logNotificationVisibility(NotificationRecord r, boolean visible) { + log(NotificationEvent.fromVisibility(visible), r); + } + + void log(UiEventLogger.UiEventEnum event, NotificationRecord r) { + if (r == null) { + return; + } + mUiEventLogger.logWithInstanceId(event, r.getUid(), r.getSbn().getPackageName(), + r.getSbn().getInstanceId()); } } diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 67a22d3e477c..39093aec07c0 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -866,6 +866,10 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { } } + default int getMaxWindowLayer() { + return 35; + } + /** * Return how to Z-order sub-windows in relation to the window they are attached to. * Return positive to have them ordered in front, negative for behind. diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index b3edc91a4129..0d365b16e228 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -102,6 +102,11 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { } @Override + public String toString() { + return mName + "@" + System.identityHashCode(this); + } + + @Override public final void dumpDebug(ProtoOutputStream proto, long fieldId, int logLevel) { final long token = proto.start(fieldId); super.dumpDebug(proto, WINDOW_CONTAINER, logLevel); diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index 9e93e1455f2c..0ec0c7b53875 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; - import android.content.res.Resources; import android.text.TextUtils; @@ -43,7 +41,7 @@ public abstract class DisplayAreaPolicy { /** * The Tasks container. Tasks etc. are automatically added to this container. */ - protected final TaskContainers mTaskContainers; + protected final DisplayArea<? extends ActivityStack> mTaskContainers; /** * Construct a new {@link DisplayAreaPolicy} @@ -58,7 +56,8 @@ public abstract class DisplayAreaPolicy { */ protected DisplayAreaPolicy(WindowManagerService wmService, DisplayContent content, DisplayArea.Root root, - DisplayArea<? extends WindowContainer> imeContainer, TaskContainers taskContainers) { + DisplayArea<? extends WindowContainer> imeContainer, + DisplayArea<? extends ActivityStack> taskContainers) { mWmService = wmService; mContent = content; mRoot = root; @@ -83,67 +82,15 @@ public abstract class DisplayAreaPolicy { */ public abstract void addWindow(WindowToken token); - /** - * Default policy that has no special features. - */ - public static class Default extends DisplayAreaPolicy { - - public Default(WindowManagerService wmService, DisplayContent content, - DisplayArea.Root root, + /** Provider for platform-default display area policy. */ + static final class DefaultProvider implements DisplayAreaPolicy.Provider { + @Override + public DisplayAreaPolicy instantiate(WindowManagerService wmService, + DisplayContent content, DisplayArea.Root root, DisplayArea<? extends WindowContainer> imeContainer, TaskContainers taskContainers) { - super(wmService, content, root, imeContainer, taskContainers); - } - - private final DisplayArea.Tokens mBelow = new DisplayArea.Tokens(mWmService, - DisplayArea.Type.BELOW_TASKS, "BelowTasks"); - private final DisplayArea<DisplayArea> mAbove = new DisplayArea<>(mWmService, - DisplayArea.Type.ABOVE_TASKS, "AboveTasks"); - private final DisplayArea.Tokens mAboveBelowIme = new DisplayArea.Tokens(mWmService, - DisplayArea.Type.ABOVE_TASKS, "AboveTasksBelowIme"); - private final DisplayArea.Tokens mAboveAboveIme = new DisplayArea.Tokens(mWmService, - DisplayArea.Type.ABOVE_TASKS, "AboveTasksAboveIme"); - - @Override - public void attachDisplayAreas() { - mRoot.addChild(mBelow, 0); - mRoot.addChild(mTaskContainers, 1); - mRoot.addChild(mAbove, 2); - - mAbove.addChild(mAboveBelowIme, 0); - mAbove.addChild(mImeContainer, 1); - mAbove.addChild(mAboveAboveIme, 2); - } - - @Override - public void addWindow(WindowToken token) { - switch (DisplayArea.Type.typeOf(token)) { - case ABOVE_TASKS: - if (token.getWindowLayerFromType() - < mWmService.mPolicy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD)) { - mAboveBelowIme.addChild(token); - } else { - mAboveAboveIme.addChild(token); - } - break; - case BELOW_TASKS: - mBelow.addChild(token); - break; - default: - throw new IllegalArgumentException("don't know how to sort " + token); - } - } - - /** Provider for {@link DisplayAreaPolicy.Default platform-default display area policy}. */ - static class Provider implements DisplayAreaPolicy.Provider { - @Override - public DisplayAreaPolicy instantiate(WindowManagerService wmService, - DisplayContent content, DisplayArea.Root root, - DisplayArea<? extends WindowContainer> imeContainer, - TaskContainers taskContainers) { - return new DisplayAreaPolicy.Default(wmService, content, root, imeContainer, - taskContainers); - } + return new DisplayAreaPolicyBuilder() + .build(wmService, content, root, imeContainer, taskContainers); } } @@ -172,7 +119,7 @@ public abstract class DisplayAreaPolicy { String name = res.getString( com.android.internal.R.string.config_deviceSpecificDisplayAreaPolicyProvider); if (TextUtils.isEmpty(name)) { - return new DisplayAreaPolicy.Default.Provider(); + return new DisplayAreaPolicy.DefaultProvider(); } try { return (Provider) Class.forName(name).newInstance(); diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java b/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java new file mode 100644 index 000000000000..885456a8488c --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicyBuilder.java @@ -0,0 +1,371 @@ +/* + * 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.wm; + +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; +import static android.view.WindowManagerPolicyConstants.APPLICATION_LAYER; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.policy.WindowManagerPolicy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A builder for instantiating a complex {@link DisplayAreaPolicy} + * + * <p>Given a set of features (that each target a set of window types), it builds the necessary + * DisplayArea hierarchy. + * + * <p>Example: <br /> + * + * <pre> + * // Feature for targeting everything below the magnification overlay: + * new DisplayAreaPolicyBuilder(...) + * .addFeature(new Feature.Builder(..., "Magnification") + * .upTo(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY) + * .build()) + * .build(...) + * + * // Builds a policy with the following hierarchy: + * - DisplayArea.Root + * - Magnification + * - DisplayArea.Tokens (Wallpapers are attached here) + * - TaskContainers + * - DisplayArea.Tokens (windows above Tasks up to IME are attached here) + * - ImeContainers + * - DisplayArea.Tokens (windows above IME up to TYPE_ACCESSIBILITY_OVERLAY attached here) + * - DisplayArea.Tokens (TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY and up are attached here) + * + * </pre> + * + * // TODO(display-area): document more complex scenarios where we need multiple areas per feature. + */ +class DisplayAreaPolicyBuilder { + + private final ArrayList<Feature> mFeatures = new ArrayList<>(); + + /** + * A feature that requires {@link DisplayArea DisplayArea(s)}. + */ + static class Feature { + private final String mName; + private final boolean[] mWindowLayers; + + private Feature(String name, boolean[] windowLayers) { + mName = name; + mWindowLayers = windowLayers; + } + + static class Builder { + private final WindowManagerPolicy mPolicy; + private final String mName; + private final boolean[] mLayers; + + /** + * Build a new feature that applies to a set of window types as specified by the builder + * methods. + * + * <p>The set of types is updated iteratively in the order of the method invocations. + * For example, {@code all().except(TYPE_STATUS_BAR)} expresses that a feature should + * apply to all types except TYPE_STATUS_BAR. + * + * The builder starts out with the feature not applying to any types. + * + * @param name the name of the feature. + */ + Builder(WindowManagerPolicy policy, String name) { + mPolicy = policy; + mName = name; + mLayers = new boolean[mPolicy.getMaxWindowLayer()]; + } + + /** + * Set that the feature applies to all window types. + */ + Builder all() { + Arrays.fill(mLayers, true); + return this; + } + + /** + * Set that the feature applies to the given window types. + */ + Builder and(int... types) { + for (int i = 0; i < types.length; i++) { + int type = types[i]; + set(type, true); + } + return this; + } + + /** + * Set that the feature does not apply to the given window types. + */ + Builder except(int... types) { + for (int i = 0; i < types.length; i++) { + int type = types[i]; + set(type, false); + } + return this; + } + + /** + * Set that the feature applies window types that are layerd at or below the layer of + * the given window type. + */ + Builder upTo(int typeInclusive) { + final int max = layerFromType(typeInclusive, false); + for (int i = 0; i < max; i++) { + mLayers[i] = true; + } + set(typeInclusive, true); + return this; + } + + Feature build() { + return new Feature(mName, mLayers.clone()); + } + + private void set(int type, boolean value) { + mLayers[layerFromType(type, true)] = value; + if (type == TYPE_APPLICATION_OVERLAY) { + mLayers[layerFromType(type, true)] = value; + mLayers[layerFromType(TYPE_SYSTEM_ALERT, false)] = value; + mLayers[layerFromType(TYPE_SYSTEM_OVERLAY, false)] = value; + mLayers[layerFromType(TYPE_SYSTEM_ERROR, false)] = value; + } + } + + private int layerFromType(int type, boolean internalWindows) { + return mPolicy.getWindowLayerFromTypeLw(type, internalWindows); + } + } + } + + static class Result extends DisplayAreaPolicy { + private static final int LEAF_TYPE_TASK_CONTAINERS = 1; + private static final int LEAF_TYPE_IME_CONTAINERS = 2; + private static final int LEAF_TYPE_TOKENS = 0; + + private final int mMaxWindowLayer = mWmService.mPolicy.getMaxWindowLayer(); + + private final ArrayList<Feature> mFeatures; + private final Map<Feature, List<DisplayArea<? extends WindowContainer>>> mAreas; + private final DisplayArea.Tokens[] mAreaForLayer = new DisplayArea.Tokens[mMaxWindowLayer]; + + Result(WindowManagerService wmService, DisplayContent content, DisplayArea.Root root, + DisplayArea<? extends WindowContainer> imeContainer, + DisplayArea<? extends ActivityStack> taskStacks, ArrayList<Feature> features) { + super(wmService, content, root, imeContainer, taskStacks); + mFeatures = features; + mAreas = new HashMap<>(features.size()); + for (int i = 0; i < mFeatures.size(); i++) { + mAreas.put(mFeatures.get(i), new ArrayList<>()); + } + } + + @Override + public void attachDisplayAreas() { + // This method constructs the layer hierarchy with the following properties: + // (1) Every feature maps to a set of DisplayAreas + // (2) After adding a window, for every feature the window's type belongs to, + // it is a descendant of one of the corresponding DisplayAreas of the feature. + // (3) Z-order is maintained, i.e. if z-range(area) denotes the set of layers of windows + // within a DisplayArea: + // for every pair of DisplayArea siblings (a,b), where a is below b, it holds that + // max(z-range(a)) <= min(z-range(b)) + // + // The algorithm below iteratively creates such a hierarchy: + // - Initially, all windows are attached to the root. + // - For each feature we create a set of DisplayAreas, by looping over the layers + // - if the feature does apply to the current layer, we need to find a DisplayArea + // for it to satisfy (2) + // - we can re-use the previous layer's area if: + // the current feature also applies to the previous layer, (to satisfy (3)) + // and the last feature that applied to the previous layer is the same as + // the last feature that applied to the current layer (to satisfy (2)) + // - otherwise we create a new DisplayArea below the last feature that applied + // to the current layer + + + PendingArea[] areaForLayer = new PendingArea[mMaxWindowLayer]; + final PendingArea root = new PendingArea(null, 0, null); + Arrays.fill(areaForLayer, root); + + final int size = mFeatures.size(); + for (int i = 0; i < size; i++) { + PendingArea featureArea = null; + for (int layer = 0; layer < mMaxWindowLayer; layer++) { + final Feature feature = mFeatures.get(i); + if (feature.mWindowLayers[layer]) { + if (featureArea == null || featureArea.mParent != areaForLayer[layer]) { + // No suitable DisplayArea - create a new one under the previous area + // for this layer. + featureArea = new PendingArea(feature, layer, areaForLayer[layer]); + areaForLayer[layer].mChildren.add(featureArea); + } + areaForLayer[layer] = featureArea; + } else { + featureArea = null; + } + } + } + + PendingArea leafArea = null; + int leafType = LEAF_TYPE_TOKENS; + for (int layer = 0; layer < mMaxWindowLayer; layer++) { + int type = typeOfLayer(mWmService.mPolicy, layer); + if (leafArea == null || leafArea.mParent != areaForLayer[layer] + || type != leafType) { + leafArea = new PendingArea(null, layer, areaForLayer[layer]); + areaForLayer[layer].mChildren.add(leafArea); + leafType = type; + if (leafType == LEAF_TYPE_TASK_CONTAINERS) { + leafArea.mExisting = mTaskContainers; + } else if (leafType == LEAF_TYPE_IME_CONTAINERS) { + leafArea.mExisting = mImeContainer; + } + } + leafArea.mMaxLayer = layer; + } + root.computeMaxLayer(); + root.instantiateChildren(mRoot, mAreaForLayer, 0, mAreas); + } + + @Override + public void addWindow(WindowToken token) { + DisplayArea.Tokens area = findAreaForToken(token); + area.addChild(token); + } + + @VisibleForTesting + DisplayArea.Tokens findAreaForToken(WindowToken token) { + int windowLayerFromType = token.getWindowLayerFromType(); + if (windowLayerFromType == APPLICATION_LAYER) { + // TODO(display-area): Better handle AboveAppWindows in APPLICATION_LAYER + windowLayerFromType += 1; + } else if (token.mRoundedCornerOverlay) { + windowLayerFromType = mMaxWindowLayer - 1; + } + return mAreaForLayer[windowLayerFromType]; + } + + public List<DisplayArea<? extends WindowContainer>> getDisplayAreas(Feature feature) { + return mAreas.get(feature); + } + + private static int typeOfLayer(WindowManagerPolicy policy, int layer) { + if (layer == APPLICATION_LAYER) { + return LEAF_TYPE_TASK_CONTAINERS; + } else if (layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD) + || layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD_DIALOG)) { + return LEAF_TYPE_IME_CONTAINERS; + } else { + return LEAF_TYPE_TOKENS; + } + } + } + + DisplayAreaPolicyBuilder addFeature(Feature feature) { + mFeatures.add(feature); + return this; + } + + Result build(WindowManagerService wmService, + DisplayContent content, DisplayArea.Root root, + DisplayArea<? extends WindowContainer> imeContainer, + DisplayArea<? extends ActivityStack> taskContainers) { + + return new Result(wmService, content, root, imeContainer, taskContainers, new ArrayList<>( + mFeatures)); + } + + static class PendingArea { + final int mMinLayer; + final ArrayList<PendingArea> mChildren = new ArrayList<>(); + final Feature mFeature; + final PendingArea mParent; + int mMaxLayer; + DisplayArea mExisting; + + PendingArea(Feature feature, + int minLayer, + PendingArea parent) { + mMinLayer = minLayer; + mFeature = feature; + mParent = parent; + } + + int computeMaxLayer() { + for (int i = 0; i < mChildren.size(); i++) { + mMaxLayer = Math.max(mMaxLayer, mChildren.get(i).computeMaxLayer()); + } + return mMaxLayer; + } + + void instantiateChildren(DisplayArea<DisplayArea> parent, + DisplayArea.Tokens[] areaForLayer, int level, Map<Feature, List<DisplayArea<? + extends WindowContainer>>> areas) { + mChildren.sort(Comparator.comparingInt(pendingArea -> pendingArea.mMinLayer)); + for (int i = 0; i < mChildren.size(); i++) { + final PendingArea child = mChildren.get(i); + final DisplayArea area = child.createArea(parent, areaForLayer); + parent.addChild(area, WindowContainer.POSITION_TOP); + if (mFeature != null) { + areas.get(mFeature).add(area); + } + child.instantiateChildren(area, areaForLayer, level + 1, areas); + } + } + + private DisplayArea createArea(DisplayArea<DisplayArea> parent, + DisplayArea.Tokens[] areaForLayer) { + if (mExisting != null) { + return mExisting; + } + DisplayArea.Type type; + if (mMinLayer > APPLICATION_LAYER) { + type = DisplayArea.Type.ABOVE_TASKS; + } else if (mMaxLayer < APPLICATION_LAYER) { + type = DisplayArea.Type.BELOW_TASKS; + } else { + type = DisplayArea.Type.ANY; + } + if (mFeature == null) { + final DisplayArea.Tokens leaf = new DisplayArea.Tokens(parent.mWmService, type, + "Leaf:" + mMinLayer + ":" + mMaxLayer); + for (int i = mMinLayer; i <= mMaxLayer; i++) { + areaForLayer[i] = leaf; + } + return leaf; + } else { + return new DisplayArea(parent.mWmService, type, mFeature.mName + ":" + + mMinLayer + ":" + mMaxLayer); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceStaticTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceStaticTest.java new file mode 100644 index 000000000000..607cd816d7dd --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceStaticTest.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 com.android.server.hdmi; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Locale; + +/** + * Tests for static methods of {@link HdmiControlService} class. + */ +@SmallTest +@RunWith(JUnit4.class) +public class HdmiControlServiceStaticTest { + + @Test + public void localToMenuLanguage_english() { + assertThat(HdmiControlService.localeToMenuLanguage(Locale.ENGLISH)).isEqualTo("eng"); + } + + @Test + public void localToMenuLanguage_german() { + assertThat(HdmiControlService.localeToMenuLanguage(Locale.GERMAN)).isEqualTo("ger"); + } + + @Test + public void localToMenuLanguage_taiwan() { + assertThat(HdmiControlService.localeToMenuLanguage(Locale.TAIWAN)).isEqualTo("chi"); + } + + @Test + public void localToMenuLanguage_macau() { + assertThat(HdmiControlService.localeToMenuLanguage(new Locale("zh", "MO"))).isEqualTo( + "chi"); + } + + @Test + public void localToMenuLanguage_hongkong() { + assertThat(HdmiControlService.localeToMenuLanguage(new Locale("zh", "HK"))).isEqualTo( + "chi"); + } + + @Test + public void localToMenuLanguage_chinese() { + assertThat(HdmiControlService.localeToMenuLanguage(Locale.CHINESE)).isEqualTo("zho"); + } + +} diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index e768205c2cf4..9e577636c1b3 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -145,6 +145,7 @@ public class AppStandbyControllerTests { static class MyInjector extends AppStandbyController.Injector { long mElapsedRealtime; boolean mIsAppIdleEnabled = true; + boolean mIsCharging; List<String> mPowerSaveWhitelistExceptIdle = new ArrayList<>(); boolean mDisplayOn; DisplayManager.DisplayListener mDisplayListener; @@ -179,6 +180,11 @@ public class AppStandbyControllerTests { } @Override + boolean isCharging() { + return mIsCharging; + } + + @Override boolean isPowerSaveWhitelistExceptIdleApp(String packageName) throws RemoteException { return mPowerSaveWhitelistExceptIdle.contains(packageName); } @@ -281,6 +287,13 @@ public class AppStandbyControllerTests { } catch (PackageManager.NameNotFoundException nnfe) {} } + private void setChargingState(AppStandbyController controller, boolean charging) { + mInjector.mIsCharging = charging; + if (controller != null) { + controller.setChargingState(charging); + } + } + private void setAppIdleEnabled(AppStandbyController controller, boolean enabled) { mInjector.mIsAppIdleEnabled = enabled; if (controller != null) { @@ -297,6 +310,7 @@ public class AppStandbyControllerTests { controller.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); mInjector.setDisplayOn(false); mInjector.setDisplayOn(true); + setChargingState(controller, false); controller.checkIdleStates(USER_ID); assertNotEquals(STANDBY_BUCKET_EXEMPTED, controller.getAppStandbyBucket(PACKAGE_1, USER_ID, @@ -324,6 +338,46 @@ public class AppStandbyControllerTests { mInjector.mElapsedRealtime, false)); } + @Test + public void testIsAppIdle_Charging() throws Exception { + setChargingState(mController, false); + mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, + REASON_MAIN_FORCED_BY_SYSTEM); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + + setChargingState(mController, true); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + + setChargingState(mController, false); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + } + + @Test + public void testIsAppIdle_Enabled() throws Exception { + setChargingState(mController, false); + setAppIdleEnabled(mController, true); + mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, + REASON_MAIN_FORCED_BY_SYSTEM); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + + setAppIdleEnabled(mController, false); + assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + + setAppIdleEnabled(mController, true); + assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); + assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + } + private void assertTimeout(AppStandbyController controller, long elapsedTime, int bucket) { mInjector.mElapsedRealtime = elapsedTime; controller.checkIdleStates(USER_ID); 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 a0f7f5b801ec..92d47c3a57b6 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -51,8 +51,6 @@ import static android.os.Build.VERSION_CODES.P; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; -import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; -import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; @@ -1144,14 +1142,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testEnqueueNotificationWithTag_WritesExpectedLog() throws Exception { + public void testEnqueueNotificationWithTag_WritesExpectedLogs() 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); + assertTrue(call.shouldLogReported); assertEquals(NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, call.event); assertNotNull(call.r); @@ -1161,7 +1160,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertEquals(PKG, call.r.getSbn().getPackageName()); assertEquals(0, call.r.getSbn().getId()); assertEquals(tag, call.r.getSbn().getTag()); - assertNotNull(call.r.getSbn().getInstanceId()); assertEquals(0, call.getInstanceId()); // Fake instance IDs are assigned in order } @@ -1180,13 +1178,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); assertEquals(2, mNotificationRecordLogger.getCalls().size()); - assertTrue(mNotificationRecordLogger.get(0).shouldLog); + assertTrue(mNotificationRecordLogger.get(0).shouldLogReported); assertEquals( NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, mNotificationRecordLogger.get(0).event); assertEquals(0, mNotificationRecordLogger.get(0).getInstanceId()); - assertTrue(mNotificationRecordLogger.get(1).shouldLog); + assertTrue(mNotificationRecordLogger.get(1).shouldLogReported); assertEquals( NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED, mNotificationRecordLogger.get(1).event); @@ -1195,16 +1193,37 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdates() throws Exception { - final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdates"; + public void testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate() throws Exception { + final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnMinorUpdate"; 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); + assertTrue(mNotificationRecordLogger.get(0).shouldLogReported); + assertEquals( + NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, + mNotificationRecordLogger.get(0).event); + assertFalse(mNotificationRecordLogger.get(1).shouldLogReported); + assertNull(mNotificationRecordLogger.get(1).event); + } + + @Test + public void testEnqueueNotificationWithTag_DoesNotLogOnTitleUpdate() throws Exception { + final String tag = "testEnqueueNotificationWithTag_DoesNotLogOnTitleUpdate"; + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, + generateNotificationRecord(null).getNotification(), + 0); + final Notification notif = generateNotificationRecord(null).getNotification(); + notif.extras.putString(Notification.EXTRA_TITLE, "Changed title"); + mBinderService.enqueueNotificationWithTag(PKG, PKG, tag, 0, notif, 0); + waitForIdle(); + assertEquals(2, mNotificationRecordLogger.getCalls().size()); + assertEquals( + NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, + mNotificationRecordLogger.get(0).event); + assertNull(mNotificationRecordLogger.get(1).event); } @Test @@ -1224,20 +1243,18 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertEquals( NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, mNotificationRecordLogger.get(0).event); - assertTrue(mNotificationRecordLogger.get(0).shouldLog); + assertTrue(mNotificationRecordLogger.get(0).shouldLogReported); assertEquals(0, mNotificationRecordLogger.get(0).getInstanceId()); - assertEquals(REASON_APP_CANCEL, mNotificationRecordLogger.get(1).reason); assertEquals( NotificationRecordLogger.NotificationCancelledEvent.NOTIFICATION_CANCEL_APP_CANCEL, mNotificationRecordLogger.get(1).event); - assertTrue(mNotificationRecordLogger.get(1).shouldLog); assertEquals(0, mNotificationRecordLogger.get(1).getInstanceId()); assertEquals( NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED, mNotificationRecordLogger.get(2).event); - assertTrue(mNotificationRecordLogger.get(2).shouldLog); + assertTrue(mNotificationRecordLogger.get(2).shouldLogReported); // New instance ID because notification was canceled before re-post assertEquals(1, mNotificationRecordLogger.get(2).getInstanceId()); } @@ -3396,11 +3413,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // so we only get the cancel notification. assertEquals(1, mNotificationRecordLogger.getCalls().size()); - assertEquals(REASON_CANCEL, mNotificationRecordLogger.get(0).reason); assertEquals( NotificationRecordLogger.NotificationCancelledEvent.NOTIFICATION_CANCEL_USER_AOD, mNotificationRecordLogger.get(0).event); - assertTrue(mNotificationRecordLogger.get(0).shouldLog); assertEquals(0, mNotificationRecordLogger.get(0).getInstanceId()); } @@ -4326,6 +4341,34 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testOnNotificationVisibilityChanged_triggersVisibilityLog() { + final NotificationRecord r = generateNotificationRecord( + mTestNotificationChannel, 1, null, true); + r.setTextChanged(true); + mService.addNotification(r); + + mService.mNotificationDelegate.onNotificationVisibilityChanged(new NotificationVisibility[] + {NotificationVisibility.obtain(r.getKey(), 1, 1, true)}, + new NotificationVisibility[]{}); + + assertEquals(1, mNotificationRecordLogger.getCalls().size()); + assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_OPEN, + mNotificationRecordLogger.get(0).event); + assertEquals(0, mNotificationRecordLogger.get(0).getInstanceId()); + + mService.mNotificationDelegate.onNotificationVisibilityChanged( + new NotificationVisibility[]{}, + new NotificationVisibility[] + {NotificationVisibility.obtain(r.getKey(), 1, 1, true)} + ); + + assertEquals(2, mNotificationRecordLogger.getCalls().size()); + assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_CLOSE, + mNotificationRecordLogger.get(1).event); + assertEquals(0, mNotificationRecordLogger.get(1).getInstanceId()); + } + + @Test public void testSetNotificationsShownFromListener_triggersInterruptionUsageStat() throws RemoteException { final NotificationRecord r = generateNotificationRecord( @@ -5373,21 +5416,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testCancelAllNotifications_cancelsBubble() throws Exception { - final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); - nr.getSbn().getNotification().flags |= FLAG_BUBBLE; - mService.addNotification(nr); - - mBinderService.cancelAllNotifications(PKG, nr.getSbn().getUserId()); - waitForIdle(); - - StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); - assertEquals(0, notifs.length); - assertEquals(0, mService.getNotificationRecordCount()); - } - - @Test - public void testAppCancelNotifications_cancelsBubbles() throws Exception { + public void testCancelNotificationsFromApp_cancelsBubbles() throws Exception { final NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel); nrBubble.getSbn().getNotification().flags |= FLAG_BUBBLE; @@ -5413,6 +5442,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testCancelAllNotificationsFromApp_cancelsBubble() throws Exception { + final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); + nr.getSbn().getNotification().flags |= FLAG_BUBBLE; + mService.addNotification(nr); + + mBinderService.cancelAllNotifications(PKG, nr.getSbn().getUserId()); + waitForIdle(); + + StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); + assertEquals(0, notifs.length); + assertEquals(0, mService.getNotificationRecordCount()); + } + + @Test public void testCancelAllNotificationsFromListener_ignoresBubbles() throws Exception { final NotificationRecord nrNormal = generateNotificationRecord(mTestNotificationChannel); final NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel); @@ -5448,6 +5491,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testCancelAllNotificationsFromStatusBar_ignoresBubble() throws Exception { + // GIVEN a notification bubble + final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); + nr.getSbn().getNotification().flags |= FLAG_BUBBLE; + mService.addNotification(nr); + + // WHEN the status bar clears all notifications + mService.mNotificationDelegate.onClearAll(mUid, Binder.getCallingPid(), + nr.getSbn().getUserId()); + waitForIdle(); + + // THEN the bubble notification does not get removed + StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); + assertEquals(1, notifs.length); + assertEquals(1, mService.getNotificationRecordCount()); + } + + + @Test public void testGetAllowedAssistantAdjustments() throws Exception { List<String> capabilities = mBinderService.getAllowedAssistantAdjustments(null); assertNotNull(capabilities); @@ -6105,6 +6167,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testNotificationBubbles_bubbleStays_whenClicked() + throws Exception { + // GIVEN a notification that has the auto cancels flag (cancel on click) and is a bubble + setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); + nr.getSbn().getNotification().flags |= FLAG_BUBBLE | FLAG_AUTO_CANCEL; + mService.addNotification(nr); + + // WHEN we click the notification + final NotificationVisibility nv = NotificationVisibility.obtain(nr.getKey(), 1, 2, true); + mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(), + nr.getKey(), nv); + waitForIdle(); + + // THEN the bubble should still exist + StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG); + assertEquals(1, notifsAfter.length); + } + + @Test public void testLoadDefaultApprovedServices_emptyResources() { TestableResources tr = mContext.getOrCreateTestableResources(); tr.addOverride(com.android.internal.R.string.config_defaultListenerAccessPackages, ""); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java index b120dbee03c5..2a17bae57c8e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerFake.java @@ -26,24 +26,26 @@ import java.util.List; */ class NotificationRecordLoggerFake implements NotificationRecordLogger { static class CallRecord extends NotificationRecordPair { - static final int INVALID = -1; - public int position = INVALID, buzzBeepBlink = INVALID, reason = INVALID; - public boolean shouldLog; public UiEventLogger.UiEventEnum event; + + // The following fields are only relevant to maybeLogNotificationPosted() calls. + static final int INVALID = -1; + public int position = INVALID, buzzBeepBlink = INVALID; + public boolean shouldLogReported; + CallRecord(NotificationRecord r, NotificationRecord old, int position, int buzzBeepBlink) { super(r, old); - this.position = position; this.buzzBeepBlink = buzzBeepBlink; - shouldLog = shouldLog(buzzBeepBlink); - event = NotificationReportedEvent.fromRecordPair(this); + shouldLogReported = shouldLogReported(buzzBeepBlink); + event = shouldLogReported ? NotificationReportedEvent.fromRecordPair(this) : null; } - CallRecord(NotificationRecord r, int reason, int dismissalSurface) { + + CallRecord(NotificationRecord r, UiEventLogger.UiEventEnum event) { super(r, null); - this.reason = reason; - shouldLog = true; - event = NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface); + shouldLogReported = false; + this.event = event; } } private List<CallRecord> mCalls = new ArrayList<>(); @@ -57,14 +59,19 @@ class NotificationRecordLoggerFake implements NotificationRecordLogger { } @Override - public void logNotificationReported(NotificationRecord r, NotificationRecord old, + public void maybeLogNotificationPosted(NotificationRecord r, NotificationRecord old, int position, int buzzBeepBlink) { mCalls.add(new CallRecord(r, old, position, buzzBeepBlink)); } @Override public void logNotificationCancelled(NotificationRecord r, int reason, int dismissalSurface) { - mCalls.add(new CallRecord(r, reason, dismissalSurface)); + mCalls.add(new CallRecord(r, + NotificationCancelledEvent.fromCancelReason(reason, dismissalSurface))); } + @Override + public void logNotificationVisibility(NotificationRecord r, boolean visible) { + mCalls.add(new CallRecord(r, NotificationEvent.fromVisibility(visible))); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java new file mode 100644 index 000000000000..8ac1d24333be --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyBuilderTest.java @@ -0,0 +1,214 @@ +/* + * 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.wm; + +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManagerPolicyConstants.APPLICATION_LAYER; + +import static com.android.server.wm.DisplayArea.Type.ABOVE_TASKS; +import static com.android.server.wm.DisplayArea.Type.ANY; +import static com.android.server.wm.DisplayAreaPolicyBuilder.Feature; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static java.util.stream.Collectors.toList; + +import android.platform.test.annotations.Presubmit; +import android.view.SurfaceControl; + +import org.hamcrest.CustomTypeSafeMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Presubmit +public class DisplayAreaPolicyBuilderTest { + + @Rule + public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule(); + + private TestWindowManagerPolicy mPolicy = new TestWindowManagerPolicy(null, null); + + @Test + public void testBuilder() { + WindowManagerService wms = mSystemServices.getWindowManagerService(); + DisplayArea.Root root = new SurfacelessDisplayAreaRoot(wms); + DisplayArea<WindowContainer> ime = new DisplayArea<>(wms, ABOVE_TASKS, "Ime"); + DisplayArea<ActivityStack> tasks = new DisplayArea<>(wms, ANY, "Tasks"); + + final Feature foo; + final Feature bar; + + DisplayAreaPolicyBuilder.Result policy = new DisplayAreaPolicyBuilder() + .addFeature(foo = new Feature.Builder(mPolicy, "Foo") + .upTo(TYPE_STATUS_BAR) + .and(TYPE_NAVIGATION_BAR) + .build()) + .addFeature(bar = new Feature.Builder(mPolicy, "Bar") + .all() + .except(TYPE_STATUS_BAR) + .build()) + .build(wms, mock(DisplayContent.class), root, ime, tasks); + + policy.attachDisplayAreas(); + + assertThat(policy.getDisplayAreas(foo), is(not(empty()))); + assertThat(policy.getDisplayAreas(bar), is(not(empty()))); + + assertThat(policy.findAreaForToken(tokenOfType(TYPE_STATUS_BAR)), + is(decendantOfOneOf(policy.getDisplayAreas(foo)))); + assertThat(policy.findAreaForToken(tokenOfType(TYPE_STATUS_BAR)), + is(not(decendantOfOneOf(policy.getDisplayAreas(bar))))); + + assertThat(tasks, + is(decendantOfOneOf(policy.getDisplayAreas(foo)))); + assertThat(tasks, + is(decendantOfOneOf(policy.getDisplayAreas(bar)))); + + assertThat(ime, + is(decendantOfOneOf(policy.getDisplayAreas(foo)))); + assertThat(ime, + is(decendantOfOneOf(policy.getDisplayAreas(bar)))); + + List<DisplayArea<?>> actualOrder = collectLeafAreas(root); + Map<DisplayArea<?>, Set<Integer>> zSets = calculateZSets(policy, root, ime, tasks); + actualOrder = actualOrder.stream().filter(zSets::containsKey).collect(toList()); + + Map<DisplayArea<?>, Integer> expectedByMinLayer = mapValues(zSets, + v -> v.stream().min(Integer::compareTo).get()); + Map<DisplayArea<?>, Integer> expectedByMaxLayer = mapValues(zSets, + v -> v.stream().max(Integer::compareTo).get()); + + assertThat(expectedByMinLayer, is(equalTo(expectedByMaxLayer))); + assertThat(actualOrder, is(equalTo(expectedByMaxLayer))); + } + + private <K, V, R> Map<K, R> mapValues(Map<K, V> zSets, Function<V, R> f) { + return zSets.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> f.apply(e.getValue()))); + } + + private List<DisplayArea<?>> collectLeafAreas(DisplayArea<?> root) { + ArrayList<DisplayArea<?>> leafs = new ArrayList<>(); + traverseLeafAreas(root, leafs::add); + return leafs; + } + + private Map<DisplayArea<?>, Set<Integer>> calculateZSets( + DisplayAreaPolicyBuilder.Result policy, DisplayArea.Root root, + DisplayArea<WindowContainer> ime, + DisplayArea<ActivityStack> tasks) { + Map<DisplayArea<?>, Set<Integer>> zSets = new HashMap<>(); + int[] types = {TYPE_STATUS_BAR, TYPE_NAVIGATION_BAR, TYPE_PRESENTATION, + TYPE_APPLICATION_OVERLAY}; + for (int type : types) { + WindowToken token = tokenOfType(type); + recordLayer(policy.findAreaForToken(token), token.getWindowLayerFromType(), zSets); + } + recordLayer(tasks, APPLICATION_LAYER, zSets); + recordLayer(ime, mPolicy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD), zSets); + return zSets; + } + + private void recordLayer(DisplayArea<?> area, int layer, + Map<DisplayArea<?>, Set<Integer>> zSets) { + zSets.computeIfAbsent(area, k -> new HashSet<>()).add(layer); + } + + private Matcher<WindowContainer> decendantOfOneOf(List<? extends WindowContainer> expected) { + return new CustomTypeSafeMatcher<WindowContainer>("descendant of one of " + expected) { + @Override + protected boolean matchesSafely(WindowContainer actual) { + for (WindowContainer expected : expected) { + WindowContainer candidate = actual; + while (candidate != null && candidate.getParent() != candidate) { + if (candidate.getParent() == expected) { + return true; + } + candidate = candidate.getParent(); + } + } + return false; + } + + @Override + protected void describeMismatchSafely(WindowContainer item, + Description description) { + description.appendText("was ").appendValue(item); + while (item != null && item.getParent() != item) { + item = item.getParent(); + description.appendText(", child of ").appendValue(item); + } + } + }; + } + + private WindowToken tokenOfType(int type) { + WindowToken m = mock(WindowToken.class); + when(m.getWindowLayerFromType()).thenReturn(mPolicy.getWindowLayerFromTypeLw(type)); + return m; + } + + private static void traverseLeafAreas(DisplayArea<?> root, Consumer<DisplayArea<?>> consumer) { + boolean leaf = true; + for (int i = 0; i < root.getChildCount(); i++) { + WindowContainer child = root.getChildAt(i); + if (child instanceof DisplayArea<?>) { + traverseLeafAreas((DisplayArea<?>) child, consumer); + leaf = false; + } + } + if (leaf) { + consumer.accept(root); + } + } + + private static class SurfacelessDisplayAreaRoot extends DisplayArea.Root { + + SurfacelessDisplayAreaRoot(WindowManagerService wms) { + super(wms); + } + + @Override + SurfaceControl.Builder makeChildSurface(WindowContainer child) { + return new MockSurfaceControlBuilder(); + } + } + +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaProviderTest.java index c1a1d5ecd3c8..31206315618e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaProviderTest.java @@ -34,13 +34,13 @@ public class DisplayAreaProviderTest { @Test public void testFromResources_emptyProvider() { Assert.assertThat(DisplayAreaPolicy.Provider.fromResources(resourcesWithProvider("")), - Matchers.instanceOf(DisplayAreaPolicy.Default.Provider.class)); + Matchers.instanceOf(DisplayAreaPolicy.DefaultProvider.class)); } @Test public void testFromResources_nullProvider() { Assert.assertThat(DisplayAreaPolicy.Provider.fromResources(resourcesWithProvider(null)), - Matchers.instanceOf(DisplayAreaPolicy.Default.Provider.class)); + Matchers.instanceOf(DisplayAreaPolicy.DefaultProvider.class)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index f517881d835b..8ad75053060f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -56,6 +56,7 @@ import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.StrictMode; import android.os.UserHandle; +import android.util.Log; import android.view.InputChannel; import android.view.Surface; import android.view.SurfaceControl; @@ -120,11 +121,22 @@ public class SystemServicesTestRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { + Throwable throwable = null; try { runWithDexmakerShareClassLoader(SystemServicesTestRule.this::setUp); base.evaluate(); + } catch (Throwable t) { + throwable = t; } finally { - tearDown(); + try { + tearDown(); + } catch (Throwable t) { + if (throwable != null) { + Log.e("SystemServicesTestRule", "Suppressed: ", throwable); + t.addSuppressed(throwable); + } + throw t; + } } } }; diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 0d5e6885a2fd..52213d8c4fae 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -17,6 +17,7 @@ package android.telecom; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; @@ -458,8 +459,14 @@ public final class Call { /** Call supports the deflect feature. */ public static final int CAPABILITY_SUPPORT_DEFLECT = 0x01000000; + /** + * Call supports adding participants to the call via + * {@link #addConferenceParticipants(List)}. + * @hide + */ + public static final int CAPABILITY_ADD_PARTICIPANT = 0x02000000; //****************************************************************************************** - // Next CAPABILITY value: 0x02000000 + // Next CAPABILITY value: 0x04000000 //****************************************************************************************** /** @@ -689,6 +696,9 @@ public final class Call { if (can(capabilities, CAPABILITY_SUPPORT_DEFLECT)) { builder.append(" CAPABILITY_SUPPORT_DEFLECT"); } + if (can(capabilities, CAPABILITY_ADD_PARTICIPANT)) { + builder.append(" CAPABILITY_ADD_PARTICIPANT"); + } builder.append("]"); return builder.toString(); } @@ -1703,6 +1713,17 @@ public final class Call { } /** + * Pulls participants to existing call by forming a conference call. + * See {@link Details#CAPABILITY_ADD_PARTICIPANT}. + * + * @param participants participants to be pulled to existing call. + * @hide + */ + public void addConferenceParticipants(@NonNull List<Uri> participants) { + mInCallAdapter.addConferenceParticipants(mTelecomCallId, participants); + } + + /** * Initiates a request to the {@link ConnectionService} to pull an external call to the local * device. * <p> diff --git a/telecomm/java/android/telecom/Conference.java b/telecomm/java/android/telecom/Conference.java index 9c80c79bcebc..f019a9d33005 100644 --- a/telecomm/java/android/telecom/Conference.java +++ b/telecomm/java/android/telecom/Conference.java @@ -324,6 +324,13 @@ public abstract class Conference extends Conferenceable { public void onConnectionAdded(Connection connection) {} /** + * Notifies the {@link Conference} of a request to add a new participants to the conference call + * @param participants that will be added to this conference call + * @hide + */ + public void onAddConferenceParticipants(@NonNull List<Uri> participants) {} + + /** * Notifies this Conference, which is in {@code STATE_RINGING}, of * a request to accept. * For managed {@link ConnectionService}s, this will be called when the user answers a call via diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 5f860a1530bb..3b0ba2548660 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -381,8 +381,14 @@ public abstract class Connection extends Conferenceable { /** Call supports the deflect feature. */ public static final int CAPABILITY_SUPPORT_DEFLECT = 0x02000000; + /** + * When set, indicates that this {@link Connection} supports initiation of a conference call + * by directly adding participants using {@link #onAddConferenceParticipants(List)}. + * @hide + */ + public static final int CAPABILITY_ADD_PARTICIPANT = 0x04000000; //********************************************************************************************** - // Next CAPABILITY value: 0x04000000 + // Next CAPABILITY value: 0x08000000 //********************************************************************************************** /** @@ -958,7 +964,9 @@ public abstract class Connection extends Conferenceable { if ((capabilities & CAPABILITY_SUPPORT_DEFLECT) == CAPABILITY_SUPPORT_DEFLECT) { builder.append(isLong ? " CAPABILITY_SUPPORT_DEFLECT" : " sup_def"); } - + if ((capabilities & CAPABILITY_ADD_PARTICIPANT) == CAPABILITY_ADD_PARTICIPANT) { + builder.append(isLong ? " CAPABILITY_ADD_PARTICIPANT" : " add_participant"); + } builder.append("]"); return builder.toString(); } @@ -2975,6 +2983,14 @@ public abstract class Connection extends Conferenceable { public void onSeparate() {} /** + * Supports initiation of a conference call by directly adding participants to an ongoing call. + * + * @param participants with which conference call will be formed. + * @hide + */ + public void onAddConferenceParticipants(@NonNull List<Uri> participants) {} + + /** * Notifies this Connection of a request to abort. */ public void onAbort() {} diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java index f77e52964e08..2aea723cf418 100644 --- a/telecomm/java/android/telecom/ConnectionService.java +++ b/telecomm/java/android/telecom/ConnectionService.java @@ -141,6 +141,7 @@ public abstract class ConnectionService extends Service { private static final String SESSION_SPLIT_CONFERENCE = "CS.sFC"; private static final String SESSION_MERGE_CONFERENCE = "CS.mC"; private static final String SESSION_SWAP_CONFERENCE = "CS.sC"; + private static final String SESSION_ADD_PARTICIPANT = "CS.aP"; private static final String SESSION_POST_DIAL_CONT = "CS.oPDC"; private static final String SESSION_PULL_EXTERNAL_CALL = "CS.pEC"; private static final String SESSION_SEND_CALL_EVENT = "CS.sCE"; @@ -194,6 +195,7 @@ public abstract class ConnectionService extends Service { private static final int MSG_CREATE_CONFERENCE_COMPLETE = 36; private static final int MSG_CREATE_CONFERENCE_FAILED = 37; private static final int MSG_REJECT_WITH_REASON = 38; + private static final int MSG_ADD_PARTICIPANT = 39; private static Connection sNullConnection; @@ -626,6 +628,21 @@ public abstract class ConnectionService extends Service { } @Override + public void addConferenceParticipants(String callId, List<Uri> participants, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_ADD_PARTICIPANT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = participants; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ADD_PARTICIPANT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override public void onPostDialContinue(String callId, boolean proceed, Session.Info sessionInfo) { Log.startSession(sessionInfo, SESSION_POST_DIAL_CONT); try { @@ -1223,6 +1240,19 @@ public abstract class ConnectionService extends Service { } break; } + case MSG_ADD_PARTICIPANT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_ADD_PARTICIPANT); + addConferenceParticipants((String) args.arg1, (List<Uri>)args.arg2); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_POST_DIAL_CONTINUE: { SomeArgs args = (SomeArgs) msg.obj; try { @@ -2151,6 +2181,17 @@ public abstract class ConnectionService extends Service { } } + private void addConferenceParticipants(String callId, List<Uri> participants) { + Log.d(this, "addConferenceParticipants(%s)", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "addConferenceParticipants") + .onAddConferenceParticipants(participants); + } else { + findConferenceForAction(callId, "addConferenceParticipants") + .onAddConferenceParticipants(participants); + } + } + /** * Notifies a {@link Connection} of a request to pull an external call. * diff --git a/telecomm/java/android/telecom/InCallAdapter.java b/telecomm/java/android/telecom/InCallAdapter.java index 594c1eb392b3..9d29174059ad 100644 --- a/telecomm/java/android/telecom/InCallAdapter.java +++ b/telecomm/java/android/telecom/InCallAdapter.java @@ -283,6 +283,20 @@ public final class InCallAdapter { } /** + * Instructs Telecom to pull participants to existing call + * + * @param callId The unique ID of the call. + * @param participants participants to be pulled to existing call. + */ + public void addConferenceParticipants(String callId, List<Uri> participants) { + try { + mAdapter.addConferenceParticipants(callId, participants); + } catch (RemoteException ignored) { + } + } + + + /** * Instructs Telecom to split the specified call from any conference call with which it may be * connected. * diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java index ebfa3a15639a..982e5f30e28c 100644 --- a/telecomm/java/android/telecom/InCallService.java +++ b/telecomm/java/android/telecom/InCallService.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.app.Service; +import android.app.UiModeManager; import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.hardware.camera2.CameraManager; @@ -43,12 +44,32 @@ import java.util.List; * phone calls. * <h2>Becoming the Default Phone App</h2> * The default dialer/phone app is one which provides the in-call user interface while the device is - * in a call. A device is bundled with a system provided default dialer/phone app. The user may - * choose a single app to take over this role from the system app. An app which wishes to fulfill - * one this role uses the {@code android.app.role.RoleManager} to request that they fill the role. + * in a call. It also provides the user with a means to initiate calls and see a history of calls + * on their device. A device is bundled with a system provided default dialer/phone app. The user + * may choose a single app to take over this role from the system app. An app which wishes to + * fulfill one this role uses the {@link android.app.role.RoleManager} to request that they fill the + * {@link android.app.role.RoleManager#ROLE_DIALER} role. * <p> - * An app filling the role of the default phone app provides a user interface while the device is in - * a call, and the device is not in car mode. + * The default phone app provides a user interface while the device is in a call, and the device is + * not in car mode (i.e. {@link UiModeManager#getCurrentModeType()} is not + * {@link android.content.res.Configuration#UI_MODE_TYPE_CAR}). + * <p> + * In order to fill the {@link android.app.role.RoleManager#ROLE_DIALER} role, an app must meet a + * number of requirements: + * <ul> + * <li>It must handle the {@link Intent#ACTION_DIAL} intent. This means the app must provide + * a dial pad UI for the user to initiate outgoing calls.</li> + * <li>It must fully implement the {@link InCallService} API and provide both an incoming call + * UI, as well as an ongoing call UI.</li> + * </ul> + * <p> + * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} crashes during + * {@link InCallService} binding, the Telecom framework will automatically fall back to using the + * dialer app pre-loaded on the device. The system will display a notification to the user to let + * them know that the app has crashed and that their call was continued using the pre-loaded dialer + * app. + * <p> + * Further, the pre-loaded dialer will ALWAYS be used when the user places an emergency call. * <p> * Below is an example manifest registration for an {@code InCallService}. The meta-data * {@link TelecomManager#METADATA_IN_CALL_SERVICE_UI} indicates that this particular @@ -82,6 +103,11 @@ import java.util.List; * <action android:name="android.intent.action.DIAL" /> * <category android:name="android.intent.category.DEFAULT" /> * </intent-filter> + * <intent-filter> + * <action android:name="android.intent.action.DIAL" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <data android:scheme="tel" /> + * </intent-filter> * </activity> * } * </pre> @@ -111,6 +137,7 @@ import java.util.List; * } * } * } + * } * </pre> * <p id="incomingCallNotification"> * <h3>Showing the Incoming Call Notification</h3> diff --git a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl index 4249dff151c7..a397d77db2f6 100644 --- a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl +++ b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl @@ -104,6 +104,9 @@ oneway interface IConnectionService { void swapConference(String conferenceCallId, in Session.Info sessionInfo); + void addConferenceParticipants(String CallId, in List<Uri> participants, + in Session.Info sessionInfo); + void onPostDialContinue(String callId, boolean proceed, in Session.Info sessionInfo); void pullExternalCall(String callId, in Session.Info sessionInfo); diff --git a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl index eb2d714fe3f4..9beff22ce52e 100644 --- a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl +++ b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl @@ -67,6 +67,8 @@ oneway interface IInCallAdapter { void swapConference(String callId); + void addConferenceParticipants(String callId, in List<Uri> participants); + void turnOnProximitySensor(); void turnOffProximitySensor(boolean screenOnImmediately); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index ebb53c50ca98..51b4a31ea8b2 100755 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -1090,6 +1090,14 @@ public class CarrierConfigManager { "support_adhoc_conference_calls_bool"; /** + * Determines whether conference participants can be added to existing call. When {@code true}, + * adding conference participants to existing call is supported, {@code false otherwise}. + * @hide + */ + public static final String KEY_SUPPORT_ADD_CONFERENCE_PARTICIPANTS_BOOL = + "support_add_conference_participants_bool"; + + /** * Determines whether conference calls are supported by a carrier. When {@code true}, * conference calling is supported, {@code false otherwise}. */ @@ -4004,6 +4012,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_IGNORE_RTT_MODE_SETTING_BOOL, false); sDefaults.putInt(KEY_CDMA_3WAYCALL_FLASH_DELAY_INT , 0); sDefaults.putBoolean(KEY_SUPPORT_ADHOC_CONFERENCE_CALLS_BOOL, false); + sDefaults.putBoolean(KEY_SUPPORT_ADD_CONFERENCE_PARTICIPANTS_BOOL, false); sDefaults.putBoolean(KEY_SUPPORT_CONFERENCE_CALL_BOOL, true); sDefaults.putBoolean(KEY_SUPPORT_IMS_CONFERENCE_CALL_BOOL, true); sDefaults.putBoolean(KEY_SUPPORT_MANAGE_IMS_CONFERENCE_CALL_BOOL, true); diff --git a/telephony/java/android/telephony/PreciseDisconnectCause.java b/telephony/java/android/telephony/PreciseDisconnectCause.java index 54980a29c0a6..250d9e8b212e 100644 --- a/telephony/java/android/telephony/PreciseDisconnectCause.java +++ b/telephony/java/android/telephony/PreciseDisconnectCause.java @@ -256,337 +256,6 @@ public final class PreciseDisconnectCause { /** Access Blocked by CDMA network. */ public static final int CDMA_ACCESS_BLOCKED = 1009; - /** Mapped from ImsReasonInfo */ - // TODO: remove ImsReasonInfo from preciseDisconnectCause - /* The passed argument is an invalid */ - /** @hide */ - public static final int LOCAL_ILLEGAL_ARGUMENT = 1200; - // The operation is invoked in invalid call state - /** @hide */ - public static final int LOCAL_ILLEGAL_STATE = 1201; - // IMS service internal error - /** @hide */ - public static final int LOCAL_INTERNAL_ERROR = 1202; - // IMS service goes down (service connection is lost) - /** @hide */ - public static final int LOCAL_IMS_SERVICE_DOWN = 1203; - // No pending incoming call exists - /** @hide */ - public static final int LOCAL_NO_PENDING_CALL = 1204; - // Service unavailable; by power off - /** @hide */ - public static final int LOCAL_POWER_OFF = 1205; - // Service unavailable; by low battery - /** @hide */ - public static final int LOCAL_LOW_BATTERY = 1206; - // Service unavailable; by out of service (data service state) - /** @hide */ - public static final int LOCAL_NETWORK_NO_SERVICE = 1207; - /* Service unavailable; by no LTE coverage - * (VoLTE is not supported even though IMS is registered) - */ - /** @hide */ - public static final int LOCAL_NETWORK_NO_LTE_COVERAGE = 1208; - /** Service unavailable; by located in roaming area */ - /** @hide */ - public static final int LOCAL_NETWORK_ROAMING = 1209; - /** Service unavailable; by IP changed */ - /** @hide */ - public static final int LOCAL_NETWORK_IP_CHANGED = 1210; - /** Service unavailable; other */ - /** @hide */ - public static final int LOCAL_SERVICE_UNAVAILABLE = 1211; - /* Service unavailable; IMS connection is lost (IMS is not registered) */ - /** @hide */ - public static final int LOCAL_NOT_REGISTERED = 1212; - /** Max call exceeded */ - /** @hide */ - public static final int LOCAL_MAX_CALL_EXCEEDED = 1213; - /** Call decline */ - /** @hide */ - public static final int LOCAL_CALL_DECLINE = 1214; - /** SRVCC is in progress */ - /** @hide */ - public static final int LOCAL_CALL_VCC_ON_PROGRESSING = 1215; - /** Resource reservation is failed (QoS precondition) */ - /** @hide */ - public static final int LOCAL_CALL_RESOURCE_RESERVATION_FAILED = 1216; - /** Retry CS call; VoLTE service can't be provided by the network or remote end - * Resolve the extra code(EXTRA_CODE_CALL_RETRY_*) if the below code is set - * @hide - */ - public static final int LOCAL_CALL_CS_RETRY_REQUIRED = 1217; - /** Retry VoLTE call; VoLTE service can't be provided by the network temporarily */ - /** @hide */ - public static final int LOCAL_CALL_VOLTE_RETRY_REQUIRED = 1218; - /** IMS call is already terminated (in TERMINATED state) */ - /** @hide */ - public static final int LOCAL_CALL_TERMINATED = 1219; - /** Handover not feasible */ - /** @hide */ - public static final int LOCAL_HO_NOT_FEASIBLE = 1220; - - /** 1xx waiting timer is expired after sending INVITE request (MO only) */ - /** @hide */ - public static final int TIMEOUT_1XX_WAITING = 1221; - /** User no answer during call setup operation (MO/MT) - * MO : 200 OK to INVITE request is not received, - * MT : No action from user after alerting the call - * @hide - */ - public static final int TIMEOUT_NO_ANSWER = 1222; - /** User no answer during call update operation (MO/MT) - * MO : 200 OK to re-INVITE request is not received, - * MT : No action from user after alerting the call - * @hide - */ - public static final int TIMEOUT_NO_ANSWER_CALL_UPDATE = 1223; - - /** - * STATUSCODE (SIP response code) (IMS -> Telephony) - */ - /** SIP request is redirected */ - /** @hide */ - public static final int SIP_REDIRECTED = 1300; - /** 4xx responses */ - /** 400 : Bad Request */ - /** @hide */ - public static final int SIP_BAD_REQUEST = 1310; - /** 403 : Forbidden */ - /** @hide */ - public static final int SIP_FORBIDDEN = 1311; - /** 404 : Not Found */ - /** @hide */ - public static final int SIP_NOT_FOUND = 1312; - /** 415 : Unsupported Media Type - * 416 : Unsupported URI Scheme - * 420 : Bad Extension - */ - /** @hide */ - public static final int SIP_NOT_SUPPORTED = 1313; - /** 408 : Request Timeout */ - /** @hide */ - public static final int SIP_REQUEST_TIMEOUT = 1314; - /** 480 : Temporarily Unavailable */ - /** @hide */ - public static final int SIP_TEMPRARILY_UNAVAILABLE = 1315; - /** 484 : Address Incomplete */ - /** @hide */ - public static final int SIP_BAD_ADDRESS = 1316; - /** 486 : Busy Here - * 600 : Busy Everywhere - */ - /** @hide */ - public static final int SIP_BUSY = 1317; - /** 487 : Request Terminated */ - /** @hide */ - public static final int SIP_REQUEST_CANCELLED = 1318; - /** 406 : Not Acceptable - * 488 : Not Acceptable Here - * 606 : Not Acceptable - */ - /** @hide */ - public static final int SIP_NOT_ACCEPTABLE = 1319; - /** 410 : Gone - * 604 : Does Not Exist Anywhere - */ - /** @hide */ - public static final int SIP_NOT_REACHABLE = 1320; - /** Others */ - /** @hide */ - public static final int SIP_CLIENT_ERROR = 1321; - /** 481 : Transaction Does Not Exist */ - /** @hide */ - public static final int SIP_TRANSACTION_DOES_NOT_EXIST = 1322; - /** 5xx responses - * 501 : Server Internal Error - */ - /** @hide */ - public static final int SIP_SERVER_INTERNAL_ERROR = 1330; - /** 503 : Service Unavailable */ - /** @hide */ - public static final int SIP_SERVICE_UNAVAILABLE = 1331; - /** 504 : Server Time-out */ - /** @hide */ - public static final int SIP_SERVER_TIMEOUT = 1332; - /** Others */ - /** @hide */ - public static final int SIP_SERVER_ERROR = 1333; - /** 6xx responses - * 603 : Decline - */ - /** @hide */ - public static final int SIP_USER_REJECTED = 1340; - /** Others */ - /** @hide */ - public static final int SIP_GLOBAL_ERROR = 1341; - /** Emergency failure */ - /** @hide */ - public static final int EMERGENCY_TEMP_FAILURE = 1342; - /** @hide */ - public static final int EMERGENCY_PERM_FAILURE = 1343; - /** Media resource initialization failed */ - /** @hide */ - public static final int MEDIA_INIT_FAILED = 1400; - /** RTP timeout (no audio / video traffic in the session) */ - /** @hide */ - public static final int MEDIA_NO_DATA = 1401; - /** Media is not supported; so dropped the call */ - /** @hide */ - public static final int MEDIA_NOT_ACCEPTABLE = 1402; - /** Unknown media related errors */ - /** @hide */ - public static final int MEDIA_UNSPECIFIED = 1403; - /** User triggers the call end */ - /** @hide */ - public static final int USER_TERMINATED = 1500; - /** No action while an incoming call is ringing */ - /** @hide */ - public static final int USER_NOANSWER = 1501; - /** User ignores an incoming call */ - /** @hide */ - public static final int USER_IGNORE = 1502; - /** User declines an incoming call */ - /** @hide */ - public static final int USER_DECLINE = 1503; - /** Device declines/ends a call due to low battery */ - /** @hide */ - public static final int LOW_BATTERY = 1504; - /** Device declines call due to blacklisted call ID */ - /** @hide */ - public static final int BLACKLISTED_CALL_ID = 1505; - /** The call is terminated by the network or remote user */ - /** @hide */ - public static final int USER_TERMINATED_BY_REMOTE = 1510; - - /** - * UT - */ - /** @hide */ - public static final int UT_NOT_SUPPORTED = 1800; - /** @hide */ - public static final int UT_SERVICE_UNAVAILABLE = 1801; - /** @hide */ - public static final int UT_OPERATION_NOT_ALLOWED = 1802; - /** @hide */ - public static final int UT_NETWORK_ERROR = 1803; - /** @hide */ - public static final int UT_CB_PASSWORD_MISMATCH = 1804; - - /** - * ECBM - * @hide - */ - public static final int ECBM_NOT_SUPPORTED = 1900; - - /** - * Fail code used to indicate that Multi-endpoint is not supported by the Ims framework. - * @hide - */ - public static final int MULTIENDPOINT_NOT_SUPPORTED = 1901; - - /** - * CALL DROP error codes (Call could drop because of many reasons like Network not available, - * handover, failed, etc) - */ - - /** - * CALL DROP error code for the case when a device is ePDG capable and when the user is on an - * active wifi call and at the edge of coverage and there is no qualified LTE network available - * to handover the call to. We get a handover NOT_TRIGERRED message from the modem. This error - * code is received as part of the handover message. - * @hide - */ - public static final int CALL_DROP_IWLAN_TO_LTE_UNAVAILABLE = 2000; - - /** - * MT call has ended due to a release from the network - * because the call was answered elsewhere - * @hide - */ - public static final int ANSWERED_ELSEWHERE = 2100; - - /** - * For MultiEndpoint - Call Pull request has failed - * @hide - */ - public static final int CALL_PULL_OUT_OF_SYNC = 2101; - - /** - * For MultiEndpoint - Call has been pulled from primary to secondary - * @hide - */ - public static final int CALL_PULLED = 2102; - - /** - * Supplementary services (HOLD/RESUME) failure error codes. - * Values for Supplemetary services failure - Failed, Cancelled and Re-Invite collision. - * @hide - */ - public static final int SUPP_SVC_FAILED = 2300; - /** @hide */ - public static final int SUPP_SVC_CANCELLED = 2301; - /** @hide */ - public static final int SUPP_SVC_REINVITE_COLLISION = 2302; - - /** - * DPD Procedure received no response or send failed - * @hide - */ - public static final int IWLAN_DPD_FAILURE = 2400; - - /** - * Establishment of the ePDG Tunnel Failed - * @hide - */ - public static final int EPDG_TUNNEL_ESTABLISH_FAILURE = 2500; - - /** - * Re-keying of the ePDG Tunnel Failed; may not always result in teardown - * @hide - */ - public static final int EPDG_TUNNEL_REKEY_FAILURE = 2501; - - /** - * Connection to the packet gateway is lost - * @hide - */ - public static final int EPDG_TUNNEL_LOST_CONNECTION = 2502; - - /** - * The maximum number of calls allowed has been reached. Used in a multi-endpoint scenario - * where the number of calls across all connected devices has reached the maximum. - * @hide - */ - public static final int MAXIMUM_NUMBER_OF_CALLS_REACHED = 2503; - - /** - * Similar to {@link #CODE_LOCAL_CALL_DECLINE}, except indicates that a remote device has - * declined the call. Used in a multi-endpoint scenario where a remote device declined an - * incoming call. - * @hide - */ - public static final int REMOTE_CALL_DECLINE = 2504; - - /** - * Indicates the call was disconnected due to the user reaching their data limit. - * @hide - */ - public static final int DATA_LIMIT_REACHED = 2505; - - /** - * Indicates the call was disconnected due to the user disabling cellular data. - * @hide - */ - public static final int DATA_DISABLED = 2506; - - /** - * Indicates a call was disconnected due to loss of wifi signal. - * @hide - */ - public static final int WIFI_LOST = 2507; - - /* OEM specific error codes. To be used by OEMs when they don't want to reveal error code which would be replaced by ERROR_UNSPECIFIED */ public static final int OEM_CAUSE_1 = 0xf001; diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java index 20d0e9690e8e..ae20cae36839 100644 --- a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java +++ b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java @@ -430,10 +430,9 @@ public class ApkVerityTest extends BaseHostJUnit4Test { private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException { String apkPath = getApkPath(TARGET_PACKAGE); String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); + // Exclude directories since we only care about files. HashSet<String> actualFiles = new HashSet<>(Arrays.asList( - expectRemoteCommandToSucceed("ls " + appDir).split("\n"))); - assertTrue(actualFiles.remove("lib")); - assertTrue(actualFiles.remove("oat")); + expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n"))); HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames)); assertEquals(expectedFiles, actualFiles); diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 155c61f3f8c7..eb78529e8715 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -148,6 +148,7 @@ public class VpnTest { @Mock private AppOpsManager mAppOps; @Mock private NotificationManager mNotificationManager; @Mock private Vpn.SystemServices mSystemServices; + @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; @Mock private ConnectivityManager mConnectivityManager; @Mock private KeyStore mKeyStore; private final VpnProfile mVpnProfile = new VpnProfile("key"); @@ -867,7 +868,8 @@ public class VpnTest { * Mock some methods of vpn object. */ private Vpn createVpn(@UserIdInt int userId) { - return new Vpn(Looper.myLooper(), mContext, mNetService, userId, mSystemServices); + return new Vpn(Looper.myLooper(), mContext, mNetService, + userId, mSystemServices, mIkev2SessionCreator); } private static void assertBlocked(Vpn vpn, int... uids) { diff --git a/wifi/java/android/net/wifi/hotspot2/pps/Credential.java b/wifi/java/android/net/wifi/hotspot2/pps/Credential.java index 99901808ec3e..9c01d3643c19 100644 --- a/wifi/java/android/net/wifi/hotspot2/pps/Credential.java +++ b/wifi/java/android/net/wifi/hotspot2/pps/Credential.java @@ -738,7 +738,16 @@ public final class Credential implements Parcelable { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("IMSI: ").append(mImsi).append("\n"); + String imsi; + if (mImsi != null) { + if (mImsi.length() > 6 && mImsi.charAt(6) != '*') { + // Truncate the full IMSI from the log + imsi = mImsi.substring(0, 6) + "****"; + } else { + imsi = mImsi; + } + builder.append("IMSI: ").append(imsi).append("\n"); + } builder.append("EAPType: ").append(mEapType).append("\n"); return builder.toString(); } |