Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1181fd1b authored by Yuri Lin's avatar Yuri Lin
Browse files

Limit the size of vibration effects stored on a NotificationChannel

This change adds a cropToLengthOrNull() @hide method to the VibrationEffect interface, implemented only by compositions, to provide a best-effort crop of the number of segments involved in a vibration effect.

For notification channels, changes the max vibration length to 500 from 1000. We probably don't need that much space, and serializing vibration effects means that the data ends up taking up a lot more space than just the array for the vibration pattern.

Adds android.app.notif_channel_crop_vibration_effects bugfix flag that limits when we attempt to crop the vibration effects.

Bug: 345881518
Test: manual with flag on/off; NotificationChannelTest; VibrationEffectTest
Flag: android.app.notif_channel_crop_vibration_effects
Change-Id: I885f733112af89fe9f255db626fcdc297b1a18c8
parent 00c0e676
Loading
Loading
Loading
Loading
+53 −6
Original line number Diff line number Diff line
@@ -168,7 +168,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";
@@ -368,6 +372,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;
@@ -582,6 +589,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
     */
@@ -685,6 +709,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 =
@@ -731,9 +760,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();
        }
    }

    /**
@@ -1172,7 +1213,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);
            }
        }
@@ -1365,8 +1408,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());
        }
+10 −0
Original line number Diff line number Diff line
@@ -127,6 +127,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"
+42 −0
Original line number Diff line number Diff line
@@ -540,6 +540,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.
     *
@@ -866,6 +877,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) {
@@ -1150,6 +1185,13 @@ public abstract class VibrationEffect implements Parcelable {
                    "Vendor effect bundle must be non-empty");
        }

        /** @hide */
        @Override
        @Nullable
        public VibrationEffect cropToLengthOrNull(int length) {
            return null;
        }

        @Override
        public long getDuration() {
            return -1; // UNKNOWN
+27 −0
Original line number Diff line number Diff line
@@ -232,6 +232,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");
+80 −0
Original line number Diff line number Diff line
@@ -429,6 +429,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]);