diff --git a/api/current.txt b/api/current.txt index 6b140b3ecc12f..6ad9a1fea1f8e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5363,17 +5363,19 @@ package android.app { ctor protected NotificationChannel(android.os.Parcel); method public boolean canBypassDnd(); method public int describeContents(); + method public void enableVibration(boolean); method public java.lang.String getId(); method public int getImportance(); method public int getLockscreenVisibility(); method public java.lang.CharSequence getName(); method public android.net.Uri getSound(); + method public long[] getVibrationPattern(); method public void setBypassDnd(boolean); method public void setImportance(int); method public void setLights(boolean); method public void setLockscreenVisibility(int); method public void setSound(android.net.Uri); - method public void setVibration(boolean); + method public void setVibrationPattern(long[]); method public boolean shouldShowLights(); method public boolean shouldVibrate(); method public void writeToParcel(android.os.Parcel, int); diff --git a/api/system-current.txt b/api/system-current.txt index 9d8cfb836d9d1..92f456faa2331 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5519,12 +5519,14 @@ package android.app { ctor protected NotificationChannel(android.os.Parcel); method public boolean canBypassDnd(); method public int describeContents(); + method public void enableVibration(boolean); method public java.lang.String getId(); method public int getImportance(); method public int getLockscreenVisibility(); method public java.lang.CharSequence getName(); method public android.net.Uri getSound(); method public int getUserLockedFields(); + method public long[] getVibrationPattern(); method public void lockFields(int); method public void populateFromXml(org.xmlpull.v1.XmlPullParser); method public void setBypassDnd(boolean); @@ -5532,7 +5534,7 @@ package android.app { method public void setLights(boolean); method public void setLockscreenVisibility(int); method public void setSound(android.net.Uri); - method public void setVibration(boolean); + method public void setVibrationPattern(long[]); method public boolean shouldShowLights(); method public boolean shouldVibrate(); method public org.json.JSONObject toJson() throws org.json.JSONException; diff --git a/api/test-current.txt b/api/test-current.txt index 73ea0acb1b618..0f8a82ea05fa5 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5373,17 +5373,19 @@ package android.app { ctor protected NotificationChannel(android.os.Parcel); method public boolean canBypassDnd(); method public int describeContents(); + method public void enableVibration(boolean); method public java.lang.String getId(); method public int getImportance(); method public int getLockscreenVisibility(); method public java.lang.CharSequence getName(); method public android.net.Uri getSound(); + method public long[] getVibrationPattern(); method public void setBypassDnd(boolean); method public void setImportance(int); method public void setLights(boolean); method public void setLockscreenVisibility(int); method public void setSound(android.net.Uri); - method public void setVibration(boolean); + method public void setVibrationPattern(long[]); method public boolean shouldShowLights(); method public boolean shouldVibrate(); method public void writeToParcel(android.os.Parcel, int); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index bfabc0d80aa25..79a01c29cb4af 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -28,6 +28,7 @@ import android.service.notification.NotificationListenerService; import android.text.TextUtils; import java.io.IOException; +import java.util.Arrays; /** * A representation of settings that apply to a collection of similarly themed notifications. @@ -48,10 +49,12 @@ public final class NotificationChannel implements Parcelable { private static final String ATT_IMPORTANCE = "importance"; private static final String ATT_LIGHTS = "lights"; private static final String ATT_VIBRATION = "vibration"; + private static final String ATT_VIBRATION_ENABLED = "vibration_enabled"; private static final String ATT_SOUND = "sound"; //TODO: add audio attributes support private static final String ATT_AUDIO_ATTRIBUTES = "audio_attributes"; private static final String ATT_USER_LOCKED = "locked"; + private static final String DELIMITER = ","; /** * @hide @@ -96,8 +99,9 @@ public final class NotificationChannel implements Parcelable { private int mLockscreenVisibility = DEFAULT_VISIBILITY; private Uri mSound; private boolean mLights; - private boolean mVibration; + private long[] mVibration; private int mUserLockedFields; + private boolean mVibrationEnabled; /** * Creates a notification channel. @@ -130,8 +134,9 @@ public final class NotificationChannel implements Parcelable { mSound = null; } mLights = in.readByte() != 0; - mVibration = in.readByte() != 0; + mVibration = in.createLongArray(); mUserLockedFields = in.readInt(); + mVibrationEnabled = in.readByte() != 0; } @Override @@ -153,8 +158,9 @@ public final class NotificationChannel implements Parcelable { dest.writeByte((byte) 0); } dest.writeByte(mLights ? (byte) 1 : (byte) 0); - dest.writeByte(mVibration ? (byte) 1 : (byte) 0); + dest.writeLongArray(mVibration); dest.writeInt(mUserLockedFields); + dest.writeByte(mVibrationEnabled ? (byte) 1 : (byte) 0); } /** @@ -221,12 +227,20 @@ public final class NotificationChannel implements Parcelable { } /** - * Sets whether notification posted to this channel should vibrate, even if individual - * notifications are marked as having vibration only modifiable before the channel is submitted - * to the NotificationManager. + * Sets whether notification posted to this channel should vibrate. The vibration pattern can + * be set with {@link #setVibrationPattern(long[])}. Only modifiable before the channel is + * submitted to the NotificationManager. */ - public void setVibration(boolean vibration) { - this.mVibration = vibration; + public void enableVibration(boolean vibration) { + this.mVibrationEnabled = vibration; + } + + /** + * Sets whether notification posted to this channel should vibrate. Only modifiable before the + * channel is submitted to the NotificationManager. + */ + public void setVibrationPattern(long[] vibrationPattern) { + this.mVibration = vibrationPattern; } /** @@ -277,6 +291,14 @@ public final class NotificationChannel implements Parcelable { * Returns whether notifications posted to this channel always vibrate. */ public boolean shouldVibrate() { + return mVibrationEnabled; + } + + /** + * Returns the vibration pattern for notifications posted to this channel. Will be ignored if + * vibration is not enabled ({@link #shouldVibrate()}. + */ + public long[] getVibrationPattern() { return mVibration; } @@ -307,7 +329,8 @@ public final class NotificationChannel implements Parcelable { setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY)); setSound(safeUri(parser, ATT_SOUND)); setLights(safeBool(parser, ATT_LIGHTS, false)); - setVibration(safeBool(parser, ATT_VIBRATION, false)); + enableVibration(safeBool(parser, ATT_VIBRATION_ENABLED, false)); + setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null)); lockFields(safeInt(parser, ATT_USER_LOCKED, 0)); } @@ -338,7 +361,10 @@ public final class NotificationChannel implements Parcelable { out.attribute(null, ATT_LIGHTS, Boolean.toString(shouldShowLights())); } if (shouldVibrate()) { - out.attribute(null, ATT_VIBRATION, Boolean.toString(shouldVibrate())); + out.attribute(null, ATT_VIBRATION_ENABLED, Boolean.toString(shouldVibrate())); + } + if (getVibrationPattern() != null) { + out.attribute(null, ATT_VIBRATION, longArrayToString(getVibrationPattern())); } if (getUserLockedFields() != 0) { out.attribute(null, ATT_USER_LOCKED, Integer.toString(getUserLockedFields())); @@ -369,8 +395,9 @@ public final class NotificationChannel implements Parcelable { record.put(ATT_SOUND, getSound().toString()); } record.put(ATT_LIGHTS, Boolean.toString(shouldShowLights())); - record.put(ATT_VIBRATION, Boolean.toString(shouldVibrate())); + record.put(ATT_VIBRATION_ENABLED, Boolean.toString(shouldVibrate())); record.put(ATT_USER_LOCKED, Integer.toString(getUserLockedFields())); + record.put(ATT_VIBRATION, longArrayToString(getVibrationPattern())); return record; } @@ -400,6 +427,30 @@ public final class NotificationChannel implements Parcelable { return Boolean.parseBoolean(value); } + private static long[] safeLongArray(XmlPullParser parser, String att, long[] defValue) { + final String attributeValue = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(attributeValue)) return defValue; + String[] values = attributeValue.split(DELIMITER); + long[] longValues = new long[values.length]; + for (int i = 0; i < values.length; i++) { + try { + longValues[i] = Long.parseLong(values[i]); + } catch (NumberFormatException e) { + longValues[i] = 0; + } + } + return longValues; + } + + private static String longArrayToString(long[] values) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < values.length - 1; i++) { + sb.append(values[i]).append(DELIMITER); + } + sb.append(values[values.length - 1]); + return sb.toString(); + } + public static final Creator CREATOR = new Creator() { @Override public NotificationChannel createFromParcel(Parcel in) { @@ -424,34 +475,35 @@ public final class NotificationChannel implements Parcelable { NotificationChannel that = (NotificationChannel) o; - if (getImportance() != that.getImportance()) return false; + if (mImportance != that.mImportance) return false; if (mBypassDnd != that.mBypassDnd) return false; - if (getLockscreenVisibility() != that.getLockscreenVisibility()) return false; + if (mLockscreenVisibility != that.mLockscreenVisibility) return false; if (mLights != that.mLights) return false; - if (mVibration != that.mVibration) return false; - if (getUserLockedFields() != that.getUserLockedFields()) return false; - if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; - if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) - return false; - return getSound() != null ? getSound().equals( - that.getSound()) : that.getSound() == null; + if (mUserLockedFields != that.mUserLockedFields) return false; + if (mVibrationEnabled != that.mVibrationEnabled) return false; + if (mId != null ? !mId.equals(that.mId) : that.mId != null) return false; + if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false; + if (mSound != null ? !mSound.equals(that.mSound) : that.mSound != null) return false; + return Arrays.equals(mVibration, that.mVibration); } @Override public int hashCode() { - int result = getId() != null ? getId().hashCode() : 0; - result = 31 * result + (getName() != null ? getName().hashCode() : 0); - result = 31 * result + getImportance(); + int result = mId != null ? mId.hashCode() : 0; + result = 31 * result + (mName != null ? mName.hashCode() : 0); + result = 31 * result + mImportance; result = 31 * result + (mBypassDnd ? 1 : 0); - result = 31 * result + getLockscreenVisibility(); - result = 31 * result + (getSound() != null ? getSound().hashCode() : 0); + result = 31 * result + mLockscreenVisibility; + result = 31 * result + (mSound != null ? mSound.hashCode() : 0); result = 31 * result + (mLights ? 1 : 0); - result = 31 * result + (mVibration ? 1 : 0); - result = 31 * result + getUserLockedFields(); + result = 31 * result + Arrays.hashCode(mVibration); + result = 31 * result + mUserLockedFields; + result = 31 * result + (mVibrationEnabled ? 1 : 0); return result; } + @Override public String toString() { return "NotificationChannel{" + @@ -462,8 +514,9 @@ public final class NotificationChannel implements Parcelable { ", mLockscreenVisibility=" + mLockscreenVisibility + ", mSound=" + mSound + ", mLights=" + mLights + - ", mVibration=" + mVibration + + ", mVibration=" + Arrays.toString(mVibration) + ", mUserLockedFields=" + mUserLockedFields + + ", mVibrationEnabled=" + mVibrationEnabled + '}'; } } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 984fc38be25cc..0213258e44952 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -169,7 +169,8 @@ public final class NotificationRecord { NotificationManagerService.VIBRATE_PATTERN_MAXLEN, NotificationManagerService.DEFAULT_VIBRATE_PATTERN); if (getChannel().shouldVibrate()) { - vibration = defaultVibration; + vibration = getChannel().getVibrationPattern() == null + ? defaultVibration : getChannel().getVibrationPattern(); } else { vibration = null; } diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 90b3715c98c9c..f4acef7a1c301 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -501,7 +501,8 @@ public class RankingHelper implements RankingConfig { channel.setSound(updatedChannel.getSound()); } if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) { - channel.setVibration(updatedChannel.shouldVibrate()); + channel.enableVibration(updatedChannel.shouldVibrate()); + channel.setVibrationPattern(updatedChannel.getVibrationPattern()); } if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) { if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { diff --git a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java index 39c5238d137a0..ad436724a37e8 100644 --- a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -211,8 +211,9 @@ public class BuzzBeepBlinkTest { defaults |= Notification.DEFAULT_VIBRATE; } else { builder.setVibrate(CUSTOM_VIBRATION); + channel.setVibrationPattern(CUSTOM_VIBRATION); } - channel.setVibration(true); + channel.enableVibration(true); } if (lights) { if (defaultLights) { diff --git a/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java index b8f38320f2be5..a7d2c04ecb613 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java @@ -81,6 +81,7 @@ public class NotificationRecordTest { 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 }; + private static final long[] CUSTOM_CHANNEL_VIBRATION = new long[] {300, 400, 300, 400 }; private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI; private static final AudioAttributes CUSTOM_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) @@ -124,6 +125,7 @@ public class NotificationRecordTest { defaults |= Notification.DEFAULT_VIBRATE; } else { builder.setVibrate(CUSTOM_VIBRATION); + channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION); } } builder.setDefaults(defaults); @@ -193,7 +195,7 @@ public class NotificationRecordTest { @Test public void testVibration_default_preUpgradeUsesNotification() throws Exception { - defaultChannel.setVibration(false); + defaultChannel.enableVibration(false); // pre upgrade, default vibration. StatusBarNotification sbn = getNotification(true /*preO */, false /* noisy */, false /* defaultSound */, true /* buzzy */, true /* defaultBuzz */); @@ -204,7 +206,7 @@ public class NotificationRecordTest { @Test public void testVibration_custom_preUpgradeUsesNotification() throws Exception { - defaultChannel.setVibration(false); + defaultChannel.enableVibration(false); // pre upgrade, custom vibration. StatusBarNotification sbn = getNotification(true /*preO */, false /* noisy */, false /* defaultSound */, true /* buzzy */, false /* defaultBuzz */); @@ -215,7 +217,7 @@ public class NotificationRecordTest { @Test public void testVibration_custom_userLocked_preUpgrade() throws Exception { - defaultChannel.setVibration(true); + defaultChannel.enableVibration(true); defaultChannel.lockFields(NotificationChannel.USER_LOCKED_VIBRATION); // pre upgrade, custom vibration. StatusBarNotification sbn = getNotification(true /*preO */, false /* noisy */, @@ -227,13 +229,13 @@ public class NotificationRecordTest { @Test public void testVibration_custom_upgradeUsesChannel() throws Exception { - channel.setVibration(true); + channel.enableVibration(true); // post upgrade, custom vibration. StatusBarNotification sbn = getNotification(false /*preO */, false /* noisy */, false /* defaultSound */, true /* buzzy */, false /* defaultBuzz */); NotificationRecord record = new NotificationRecord(mMockContext, sbn); - assertTrue(!Objects.equals(CUSTOM_VIBRATION, record.getVibration())); + assertEquals(CUSTOM_CHANNEL_VIBRATION, record.getVibration()); } @Test diff --git a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java index 3df0d66baebf4..cb7e6fbf97578 100644 --- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java @@ -46,6 +46,7 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -168,6 +169,18 @@ public class RankingHelperTest { return baos; } + private void compareChannels(NotificationChannel expected, NotificationChannel actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.shouldVibrate(), actual.shouldVibrate()); + assertEquals(expected.shouldShowLights(), actual.shouldShowLights()); + assertEquals(expected.getImportance(), actual.getImportance()); + assertEquals(expected.getLockscreenVisibility(), actual.getLockscreenVisibility()); + assertEquals(expected.getSound(), actual.getSound()); + assertEquals(expected.canBypassDnd(), actual.canBypassDnd()); + assertTrue(Arrays.equals(expected.getVibrationPattern(), actual.getVibrationPattern())); + } + @Test public void testFindAfterRankingWithASplitGroup() throws Exception { ArrayList notificationList = new ArrayList(3); @@ -228,6 +241,8 @@ public class RankingHelperTest { channel2.setLights(true); channel2.setBypassDnd(true); channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); + channel2.enableVibration(true); + channel2.setVibrationPattern(new long[] {100, 67, 145, 156}); mHelper.createNotificationChannel(pkg, uid, channel1); mHelper.createNotificationChannel(pkg, uid, channel2); @@ -246,7 +261,7 @@ public class RankingHelperTest { mHelper.readXml(parser, false); assertEquals(channel1, mHelper.getNotificationChannel(pkg, uid, channel1.getId())); - assertEquals(channel2, mHelper.getNotificationChannel(pkg, uid, channel2.getId())); + compareChannels(channel2, mHelper.getNotificationChannel(pkg, uid, channel2.getId())); assertNotNull( mHelper.getNotificationChannel(pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID)); } @@ -385,7 +400,8 @@ public class RankingHelperTest { // same id, try to update final NotificationChannel channel2 = new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH); - channel2.setVibration(true); + channel2.enableVibration(true); + channel2.setVibrationPattern(new long[] {100}); mHelper.updateNotificationChannelFromRanker(pkg, uid, channel2);