Loading core/java/android/app/NotificationChannel.java +53 −6 Original line number Diff line number Diff line Loading @@ -139,7 +139,11 @@ public final class NotificationChannel implements Parcelable { /** * @hide */ public static final int MAX_VIBRATION_LENGTH = 1000; public static final int MAX_VIBRATION_LENGTH = 500; /** * @hide */ public static final int MAX_SERIALIZED_VIBRATION_LENGTH = 32_768; private static final String TAG_CHANNEL = "channel"; private static final String ATT_NAME = "name"; Loading Loading @@ -339,6 +343,9 @@ public final class NotificationChannel implements Parcelable { if (Flags.notificationChannelVibrationEffectApi()) { mVibrationEffect = in.readInt() != 0 ? VibrationEffect.CREATOR.createFromParcel(in) : null; if (Flags.notifChannelCropVibrationEffects() && mVibrationEffect != null) { mVibrationEffect = getTrimmedVibrationEffect(mVibrationEffect); } } mUserLockedFields = in.readInt(); mUserVisibleTaskShown = in.readByte() != 0; Loading Loading @@ -519,6 +526,23 @@ public final class NotificationChannel implements Parcelable { return input; } // Returns trimmed vibration effect or null if not trimmable. private VibrationEffect getTrimmedVibrationEffect(VibrationEffect effect) { if (effect == null) { return null; } // trim if possible; check serialized length; reject if it is still too long VibrationEffect result = effect; VibrationEffect trimmed = effect.cropToLengthOrNull(MAX_VIBRATION_LENGTH); if (trimmed != null) { result = trimmed; } if (vibrationToString(result).length() > MAX_SERIALIZED_VIBRATION_LENGTH) { return null; } return result; } /** * @hide */ Loading Loading @@ -616,6 +640,11 @@ public final class NotificationChannel implements Parcelable { public void setVibrationPattern(long[] vibrationPattern) { this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0; this.mVibrationPattern = vibrationPattern; if (Flags.notifChannelCropVibrationEffects()) { if (vibrationPattern != null && vibrationPattern.length > MAX_VIBRATION_LENGTH) { this.mVibrationPattern = Arrays.copyOf(vibrationPattern, MAX_VIBRATION_LENGTH); } } if (Flags.notificationChannelVibrationEffectApi()) { try { this.mVibrationEffect = Loading Loading @@ -662,9 +691,21 @@ public final class NotificationChannel implements Parcelable { public void setVibrationEffect(@Nullable VibrationEffect effect) { this.mVibrationEnabled = effect != null; this.mVibrationEffect = effect; if (Flags.notifChannelCropVibrationEffects() && effect != null) { // Try converting to a vibration pattern and trimming that array. If not convertible // to a pattern directly, try trimming the vibration effect if possible and storing // that version instead. long[] pattern = effect.computeCreateWaveformOffOnTimingsOrNull(); if (pattern != null) { setVibrationPattern(pattern); } else { this.mVibrationEffect = getTrimmedVibrationEffect(mVibrationEffect); } } else { this.mVibrationPattern = effect == null ? null : effect.computeCreateWaveformOffOnTimingsOrNull(); mVibrationEffect == null ? null : mVibrationEffect.computeCreateWaveformOffOnTimingsOrNull(); } } /** Loading Loading @@ -1097,7 +1138,9 @@ public final class NotificationChannel implements Parcelable { if (vibrationEffect != null) { // Restore the effect only if it is not null. This allows to avoid undoing a // `setVibrationPattern` call above, if that was done with a non-null pattern // (e.g. back up from a version that did not support `setVibrationEffect`). // (e.g. back up from a version that did not support `setVibrationEffect`), or // when notif_channel_crop_vibration_effects is true, if there is an equivalent // vibration pattern available. setVibrationEffect(vibrationEffect); } } Loading Loading @@ -1290,8 +1333,12 @@ public final class NotificationChannel implements Parcelable { out.attribute(null, ATT_VIBRATION, longArrayToString(getVibrationPattern())); } if (getVibrationEffect() != null) { if (!Flags.notifChannelCropVibrationEffects() || getVibrationPattern() == null) { // When notif_channel_crop_vibration_effects is on, only serialize the vibration // effect if we do not already have an equivalent vibration pattern. out.attribute(null, ATT_VIBRATION_EFFECT, vibrationToString(getVibrationEffect())); } } if (getUserLockedFields() != 0) { out.attributeInt(null, ATT_USER_LOCKED, getUserLockedFields()); } Loading core/java/android/app/notification.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -58,6 +58,16 @@ flag { bug: "241732519" } flag { name: "notif_channel_crop_vibration_effects" namespace: "systemui" description: "Limits the size of vibration effects that can be stored in a NotificationChannel" bug: "345881518" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "evenly_divided_call_style_action_layout" namespace: "systemui" Loading core/java/android/os/VibrationEffect.java +35 −0 Original line number Diff line number Diff line Loading @@ -504,6 +504,17 @@ public abstract class VibrationEffect implements Parcelable { /** @hide */ public abstract void validate(); /** * If supported, truncate the length of this vibration effect to the provided length and return * the result. Will always return null for repeating effects. * * @return The desired effect, or {@code null} if truncation is not applicable. * @hide */ @Nullable public abstract VibrationEffect cropToLengthOrNull(int length); /** * Gets the estimated duration of the vibration in milliseconds. * Loading Loading @@ -805,6 +816,30 @@ public abstract class VibrationEffect implements Parcelable { } } /** @hide */ @Override @Nullable public VibrationEffect cropToLengthOrNull(int length) { // drop repeating effects if (mRepeatIndex >= 0) { return null; } int segmentCount = mSegments.size(); if (segmentCount <= length) { return this; } ArrayList truncated = new ArrayList(mSegments.subList(0, length)); Composed updated = new Composed(truncated, mRepeatIndex); try { updated.validate(); } catch (IllegalArgumentException e) { return null; } return updated; } @Override public long getDuration() { if (mRepeatIndex >= 0) { Loading core/tests/coretests/src/android/app/NotificationChannelTest.java +27 −0 Original line number Diff line number Diff line Loading @@ -230,6 +230,33 @@ public class NotificationChannelTest { fromParcel.getSound().toString().length()); } @Test @EnableFlags({Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API, Flags.FLAG_NOTIF_CHANNEL_CROP_VIBRATION_EFFECTS}) public void testLongVibrationFields_canWriteToXml() throws Exception { NotificationChannel channel = new NotificationChannel("id", "name", 3); // populate pattern with contents long[] pattern = new long[65550 / 2]; for (int i = 0; i < pattern.length; i++) { pattern[i] = 100; } channel.setVibrationPattern(pattern); // with flag on, also sets effect // Send it through parceling & unparceling to simulate being passed through a binder call NotificationChannel fromParcel = writeToAndReadFromParcel(channel); assertThat(fromParcel.getVibrationPattern().length).isEqualTo( NotificationChannel.MAX_VIBRATION_LENGTH); // Confirm that this also survives writing to & restoring from XML NotificationChannel result = backUpAndRestore(fromParcel); assertThat(result.getVibrationPattern().length).isEqualTo( NotificationChannel.MAX_VIBRATION_LENGTH); assertThat(result.getVibrationEffect()).isNotNull(); assertThat(result.getVibrationEffect() .computeCreateWaveformOffOnTimingsOrNull()) .isEqualTo(result.getVibrationPattern()); } @Test public void testRestoreSoundUri_customLookup() throws Exception { Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1"); Loading core/tests/vibrator/src/android/os/VibrationEffectTest.java +80 −0 Original line number Diff line number Diff line Loading @@ -419,6 +419,86 @@ public class VibrationEffectTest { assertNull(effect.computeCreateWaveformOffOnTimingsOrNull()); } @Test public void cropToLength_waveform_underLength() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(5); assertThat(result).isEqualTo(effect); // unchanged } @Test public void cropToLength_waveform_overLength() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(4); assertThat(result).isEqualTo(VibrationEffect.createWaveform( new long[]{0, 1, 2, 3}, -1)); } @Test public void cropToLength_waveform_repeating() { // repeating waveforms cannot be truncated VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* repeatIndex= */ 2); VibrationEffect result = effect.cropToLengthOrNull(3); assertThat(result).isNull(); } @Test public void cropToLength_waveform_withAmplitudes() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* amplitudes= */ new int[]{10, 20, 40, 10, 20, 40, 10}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(3); assertThat(result).isEqualTo(VibrationEffect.createWaveform( new long[]{0, 1, 2}, new int[]{10, 20, 40}, -1)); } @Test public void cropToLength_composed() { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) .compose(); VibrationEffect result = effect.cropToLengthOrNull(1); assertThat(result).isNotNull(); assertThat(result).isEqualTo(VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .compose()); } @Test public void cropToLength_composed_repeating() { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .repeatEffectIndefinitely(TEST_ONE_SHOT) .compose(); assertThat(effect.cropToLengthOrNull(1)).isNull(); } @Test @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void cropToLength_vendorEffect() { PersistableBundle vendorData = new PersistableBundle(); vendorData.putInt("key", 1); VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); assertThat(effect.cropToLengthOrNull(2)).isNull(); } @Test public void getRingtones_noPrebakedRingtones() { Resources r = mockRingtoneResources(new String[0]); Loading Loading
core/java/android/app/NotificationChannel.java +53 −6 Original line number Diff line number Diff line Loading @@ -139,7 +139,11 @@ public final class NotificationChannel implements Parcelable { /** * @hide */ public static final int MAX_VIBRATION_LENGTH = 1000; public static final int MAX_VIBRATION_LENGTH = 500; /** * @hide */ public static final int MAX_SERIALIZED_VIBRATION_LENGTH = 32_768; private static final String TAG_CHANNEL = "channel"; private static final String ATT_NAME = "name"; Loading Loading @@ -339,6 +343,9 @@ public final class NotificationChannel implements Parcelable { if (Flags.notificationChannelVibrationEffectApi()) { mVibrationEffect = in.readInt() != 0 ? VibrationEffect.CREATOR.createFromParcel(in) : null; if (Flags.notifChannelCropVibrationEffects() && mVibrationEffect != null) { mVibrationEffect = getTrimmedVibrationEffect(mVibrationEffect); } } mUserLockedFields = in.readInt(); mUserVisibleTaskShown = in.readByte() != 0; Loading Loading @@ -519,6 +526,23 @@ public final class NotificationChannel implements Parcelable { return input; } // Returns trimmed vibration effect or null if not trimmable. private VibrationEffect getTrimmedVibrationEffect(VibrationEffect effect) { if (effect == null) { return null; } // trim if possible; check serialized length; reject if it is still too long VibrationEffect result = effect; VibrationEffect trimmed = effect.cropToLengthOrNull(MAX_VIBRATION_LENGTH); if (trimmed != null) { result = trimmed; } if (vibrationToString(result).length() > MAX_SERIALIZED_VIBRATION_LENGTH) { return null; } return result; } /** * @hide */ Loading Loading @@ -616,6 +640,11 @@ public final class NotificationChannel implements Parcelable { public void setVibrationPattern(long[] vibrationPattern) { this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0; this.mVibrationPattern = vibrationPattern; if (Flags.notifChannelCropVibrationEffects()) { if (vibrationPattern != null && vibrationPattern.length > MAX_VIBRATION_LENGTH) { this.mVibrationPattern = Arrays.copyOf(vibrationPattern, MAX_VIBRATION_LENGTH); } } if (Flags.notificationChannelVibrationEffectApi()) { try { this.mVibrationEffect = Loading Loading @@ -662,9 +691,21 @@ public final class NotificationChannel implements Parcelable { public void setVibrationEffect(@Nullable VibrationEffect effect) { this.mVibrationEnabled = effect != null; this.mVibrationEffect = effect; if (Flags.notifChannelCropVibrationEffects() && effect != null) { // Try converting to a vibration pattern and trimming that array. If not convertible // to a pattern directly, try trimming the vibration effect if possible and storing // that version instead. long[] pattern = effect.computeCreateWaveformOffOnTimingsOrNull(); if (pattern != null) { setVibrationPattern(pattern); } else { this.mVibrationEffect = getTrimmedVibrationEffect(mVibrationEffect); } } else { this.mVibrationPattern = effect == null ? null : effect.computeCreateWaveformOffOnTimingsOrNull(); mVibrationEffect == null ? null : mVibrationEffect.computeCreateWaveformOffOnTimingsOrNull(); } } /** Loading Loading @@ -1097,7 +1138,9 @@ public final class NotificationChannel implements Parcelable { if (vibrationEffect != null) { // Restore the effect only if it is not null. This allows to avoid undoing a // `setVibrationPattern` call above, if that was done with a non-null pattern // (e.g. back up from a version that did not support `setVibrationEffect`). // (e.g. back up from a version that did not support `setVibrationEffect`), or // when notif_channel_crop_vibration_effects is true, if there is an equivalent // vibration pattern available. setVibrationEffect(vibrationEffect); } } Loading Loading @@ -1290,8 +1333,12 @@ public final class NotificationChannel implements Parcelable { out.attribute(null, ATT_VIBRATION, longArrayToString(getVibrationPattern())); } if (getVibrationEffect() != null) { if (!Flags.notifChannelCropVibrationEffects() || getVibrationPattern() == null) { // When notif_channel_crop_vibration_effects is on, only serialize the vibration // effect if we do not already have an equivalent vibration pattern. out.attribute(null, ATT_VIBRATION_EFFECT, vibrationToString(getVibrationEffect())); } } if (getUserLockedFields() != 0) { out.attributeInt(null, ATT_USER_LOCKED, getUserLockedFields()); } Loading
core/java/android/app/notification.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -58,6 +58,16 @@ flag { bug: "241732519" } flag { name: "notif_channel_crop_vibration_effects" namespace: "systemui" description: "Limits the size of vibration effects that can be stored in a NotificationChannel" bug: "345881518" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "evenly_divided_call_style_action_layout" namespace: "systemui" Loading
core/java/android/os/VibrationEffect.java +35 −0 Original line number Diff line number Diff line Loading @@ -504,6 +504,17 @@ public abstract class VibrationEffect implements Parcelable { /** @hide */ public abstract void validate(); /** * If supported, truncate the length of this vibration effect to the provided length and return * the result. Will always return null for repeating effects. * * @return The desired effect, or {@code null} if truncation is not applicable. * @hide */ @Nullable public abstract VibrationEffect cropToLengthOrNull(int length); /** * Gets the estimated duration of the vibration in milliseconds. * Loading Loading @@ -805,6 +816,30 @@ public abstract class VibrationEffect implements Parcelable { } } /** @hide */ @Override @Nullable public VibrationEffect cropToLengthOrNull(int length) { // drop repeating effects if (mRepeatIndex >= 0) { return null; } int segmentCount = mSegments.size(); if (segmentCount <= length) { return this; } ArrayList truncated = new ArrayList(mSegments.subList(0, length)); Composed updated = new Composed(truncated, mRepeatIndex); try { updated.validate(); } catch (IllegalArgumentException e) { return null; } return updated; } @Override public long getDuration() { if (mRepeatIndex >= 0) { Loading
core/tests/coretests/src/android/app/NotificationChannelTest.java +27 −0 Original line number Diff line number Diff line Loading @@ -230,6 +230,33 @@ public class NotificationChannelTest { fromParcel.getSound().toString().length()); } @Test @EnableFlags({Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API, Flags.FLAG_NOTIF_CHANNEL_CROP_VIBRATION_EFFECTS}) public void testLongVibrationFields_canWriteToXml() throws Exception { NotificationChannel channel = new NotificationChannel("id", "name", 3); // populate pattern with contents long[] pattern = new long[65550 / 2]; for (int i = 0; i < pattern.length; i++) { pattern[i] = 100; } channel.setVibrationPattern(pattern); // with flag on, also sets effect // Send it through parceling & unparceling to simulate being passed through a binder call NotificationChannel fromParcel = writeToAndReadFromParcel(channel); assertThat(fromParcel.getVibrationPattern().length).isEqualTo( NotificationChannel.MAX_VIBRATION_LENGTH); // Confirm that this also survives writing to & restoring from XML NotificationChannel result = backUpAndRestore(fromParcel); assertThat(result.getVibrationPattern().length).isEqualTo( NotificationChannel.MAX_VIBRATION_LENGTH); assertThat(result.getVibrationEffect()).isNotNull(); assertThat(result.getVibrationEffect() .computeCreateWaveformOffOnTimingsOrNull()) .isEqualTo(result.getVibrationPattern()); } @Test public void testRestoreSoundUri_customLookup() throws Exception { Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1"); Loading
core/tests/vibrator/src/android/os/VibrationEffectTest.java +80 −0 Original line number Diff line number Diff line Loading @@ -419,6 +419,86 @@ public class VibrationEffectTest { assertNull(effect.computeCreateWaveformOffOnTimingsOrNull()); } @Test public void cropToLength_waveform_underLength() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(5); assertThat(result).isEqualTo(effect); // unchanged } @Test public void cropToLength_waveform_overLength() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(4); assertThat(result).isEqualTo(VibrationEffect.createWaveform( new long[]{0, 1, 2, 3}, -1)); } @Test public void cropToLength_waveform_repeating() { // repeating waveforms cannot be truncated VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* repeatIndex= */ 2); VibrationEffect result = effect.cropToLengthOrNull(3); assertThat(result).isNull(); } @Test public void cropToLength_waveform_withAmplitudes() { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 1, 2, 3, 4, 5, 6}, /* amplitudes= */ new int[]{10, 20, 40, 10, 20, 40, 10}, /* repeatIndex= */ -1); VibrationEffect result = effect.cropToLengthOrNull(3); assertThat(result).isEqualTo(VibrationEffect.createWaveform( new long[]{0, 1, 2}, new int[]{10, 20, 40}, -1)); } @Test public void cropToLength_composed() { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK) .compose(); VibrationEffect result = effect.cropToLengthOrNull(1); assertThat(result).isNotNull(); assertThat(result).isEqualTo(VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .compose()); } @Test public void cropToLength_composed_repeating() { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .repeatEffectIndefinitely(TEST_ONE_SHOT) .compose(); assertThat(effect.cropToLengthOrNull(1)).isNull(); } @Test @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void cropToLength_vendorEffect() { PersistableBundle vendorData = new PersistableBundle(); vendorData.putInt("key", 1); VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); assertThat(effect.cropToLengthOrNull(2)).isNull(); } @Test public void getRingtones_noPrebakedRingtones() { Resources r = mockRingtoneResources(new String[0]); Loading