1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.app; 17 18 import android.annotation.Nullable; 19 import android.annotation.SystemApi; 20 import android.annotation.TestApi; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.Intent; 23 import android.content.pm.ParceledListSlice; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.text.TextUtils; 27 import android.util.proto.ProtoOutputStream; 28 29 import com.android.modules.utils.TypedXmlPullParser; 30 import com.android.modules.utils.TypedXmlSerializer; 31 32 import org.json.JSONException; 33 import org.json.JSONObject; 34 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.Objects; 39 40 /** 41 * A grouping of related notification channels. e.g., channels that all belong to a single account. 42 */ 43 public final class NotificationChannelGroup implements Parcelable { 44 45 /** 46 * The maximum length for text fields in a NotificationChannelGroup. Fields will be truncated at 47 * this limit. 48 * @hide 49 */ 50 public static final int MAX_TEXT_LENGTH = 1000; 51 52 private static final String TAG_GROUP = "channelGroup"; 53 private static final String ATT_NAME = "name"; 54 private static final String ATT_DESC = "desc"; 55 private static final String ATT_ID = "id"; 56 private static final String ATT_BLOCKED = "blocked"; 57 private static final String ATT_USER_LOCKED = "locked"; 58 59 /** 60 * @hide 61 */ 62 public static final int USER_LOCKED_BLOCKED_STATE = 0x00000001; 63 64 /** 65 * @see #getId() 66 */ 67 @UnsupportedAppUsage 68 private final String mId; 69 private CharSequence mName; 70 private String mDescription; 71 private boolean mBlocked; 72 private List<NotificationChannel> mChannels = new ArrayList(); 73 // Bitwise representation of fields that have been changed by the user 74 private int mUserLockedFields; 75 76 /** 77 * Creates a notification channel group. 78 * 79 * @param id The id of the group. Must be unique per package. the value may be truncated if 80 * it is too long. 81 * @param name The user visible name of the group. You can rename this group when the system 82 * locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED} 83 * broadcast. <p>The recommended maximum length is 40 characters; the value may be 84 * truncated if it is too long. 85 */ NotificationChannelGroup(String id, CharSequence name)86 public NotificationChannelGroup(String id, CharSequence name) { 87 this.mId = getTrimmedString(id); 88 this.mName = name != null ? getTrimmedString(name.toString()) : null; 89 } 90 91 /** 92 * @hide 93 */ NotificationChannelGroup(Parcel in)94 protected NotificationChannelGroup(Parcel in) { 95 if (in.readByte() != 0) { 96 mId = getTrimmedString(in.readString()); 97 } else { 98 mId = null; 99 } 100 if (in.readByte() != 0) { 101 mName = getTrimmedString(in.readString()); 102 } else { 103 mName = ""; 104 } 105 if (in.readByte() != 0) { 106 mDescription = getTrimmedString(in.readString()); 107 } else { 108 mDescription = null; 109 } 110 if (in.readByte() != 0) { 111 mChannels = in.readParcelable(NotificationChannelGroup.class.getClassLoader(), 112 ParceledListSlice.class).getList(); 113 } else { 114 mChannels = new ArrayList<>(); 115 } 116 mBlocked = in.readBoolean(); 117 mUserLockedFields = in.readInt(); 118 } 119 getTrimmedString(String input)120 private String getTrimmedString(String input) { 121 if (input != null && input.length() > MAX_TEXT_LENGTH) { 122 return input.substring(0, MAX_TEXT_LENGTH); 123 } 124 return input; 125 } 126 127 @Override writeToParcel(Parcel dest, int flags)128 public void writeToParcel(Parcel dest, int flags) { 129 if (mId != null) { 130 dest.writeByte((byte) 1); 131 dest.writeString(mId); 132 } else { 133 dest.writeByte((byte) 0); 134 } 135 if (mName != null) { 136 dest.writeByte((byte) 1); 137 dest.writeString(mName.toString()); 138 } else { 139 dest.writeByte((byte) 0); 140 } 141 if (mDescription != null) { 142 dest.writeByte((byte) 1); 143 dest.writeString(mDescription); 144 } else { 145 dest.writeByte((byte) 0); 146 } 147 if (mChannels != null) { 148 dest.writeByte((byte) 1); 149 dest.writeParcelable(new ParceledListSlice<>(mChannels), flags); 150 } else { 151 dest.writeByte((byte) 0); 152 } 153 dest.writeBoolean(mBlocked); 154 dest.writeInt(mUserLockedFields); 155 } 156 157 /** 158 * Returns the id of this group. 159 */ getId()160 public String getId() { 161 return mId; 162 } 163 164 /** 165 * Returns the user visible name of this group. 166 */ getName()167 public CharSequence getName() { 168 return mName; 169 } 170 171 /** 172 * Returns the user visible description of this group. 173 */ getDescription()174 public String getDescription() { 175 return mDescription; 176 } 177 178 /** 179 * Returns the list of channels that belong to this group 180 */ getChannels()181 public List<NotificationChannel> getChannels() { 182 return mChannels; 183 } 184 185 /** 186 * Returns whether or not notifications posted to {@link NotificationChannel channels} belonging 187 * to this group are blocked. This value is independent of 188 * {@link NotificationManager#areNotificationsEnabled()} and 189 * {@link NotificationChannel#getImportance()}. 190 */ isBlocked()191 public boolean isBlocked() { 192 return mBlocked; 193 } 194 195 /** 196 * Sets the user visible description of this group. 197 * 198 * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too 199 * long. 200 */ setDescription(String description)201 public void setDescription(String description) { 202 mDescription = getTrimmedString(description); 203 } 204 205 /** 206 * @hide 207 */ 208 @TestApi setBlocked(boolean blocked)209 public void setBlocked(boolean blocked) { 210 mBlocked = blocked; 211 } 212 213 /** 214 * @hide 215 */ addChannel(NotificationChannel channel)216 public void addChannel(NotificationChannel channel) { 217 mChannels.add(channel); 218 } 219 220 /** 221 * @hide 222 */ setChannels(List<NotificationChannel> channels)223 public void setChannels(List<NotificationChannel> channels) { 224 mChannels = channels; 225 } 226 227 /** 228 * @hide 229 */ 230 @TestApi lockFields(int field)231 public void lockFields(int field) { 232 mUserLockedFields |= field; 233 } 234 235 /** 236 * @hide 237 */ unlockFields(int field)238 public void unlockFields(int field) { 239 mUserLockedFields &= ~field; 240 } 241 242 /** 243 * @hide 244 */ 245 @TestApi getUserLockedFields()246 public int getUserLockedFields() { 247 return mUserLockedFields; 248 } 249 250 /** 251 * @hide 252 */ populateFromXml(TypedXmlPullParser parser)253 public void populateFromXml(TypedXmlPullParser parser) { 254 // Name, id, and importance are set in the constructor. 255 setDescription(parser.getAttributeValue(null, ATT_DESC)); 256 setBlocked(parser.getAttributeBoolean(null, ATT_BLOCKED, false)); 257 } 258 259 /** 260 * @hide 261 */ writeXml(TypedXmlSerializer out)262 public void writeXml(TypedXmlSerializer out) throws IOException { 263 out.startTag(null, TAG_GROUP); 264 265 out.attribute(null, ATT_ID, getId()); 266 if (getName() != null) { 267 out.attribute(null, ATT_NAME, getName().toString()); 268 } 269 if (getDescription() != null) { 270 out.attribute(null, ATT_DESC, getDescription().toString()); 271 } 272 out.attributeBoolean(null, ATT_BLOCKED, isBlocked()); 273 out.attributeInt(null, ATT_USER_LOCKED, mUserLockedFields); 274 275 out.endTag(null, TAG_GROUP); 276 } 277 278 /** 279 * @hide 280 */ 281 @SystemApi toJson()282 public JSONObject toJson() throws JSONException { 283 JSONObject record = new JSONObject(); 284 record.put(ATT_ID, getId()); 285 record.put(ATT_NAME, getName()); 286 record.put(ATT_DESC, getDescription()); 287 record.put(ATT_BLOCKED, isBlocked()); 288 record.put(ATT_USER_LOCKED, mUserLockedFields); 289 return record; 290 } 291 292 public static final @android.annotation.NonNull Creator<NotificationChannelGroup> CREATOR = 293 new Creator<NotificationChannelGroup>() { 294 @Override 295 public NotificationChannelGroup createFromParcel(Parcel in) { 296 return new NotificationChannelGroup(in); 297 } 298 299 @Override 300 public NotificationChannelGroup[] newArray(int size) { 301 return new NotificationChannelGroup[size]; 302 } 303 }; 304 305 @Override describeContents()306 public int describeContents() { 307 return 0; 308 } 309 310 @Override equals(@ullable Object o)311 public boolean equals(@Nullable Object o) { 312 if (this == o) return true; 313 if (o == null || getClass() != o.getClass()) return false; 314 NotificationChannelGroup that = (NotificationChannelGroup) o; 315 return isBlocked() == that.isBlocked() && 316 mUserLockedFields == that.mUserLockedFields && 317 Objects.equals(getId(), that.getId()) && 318 Objects.equals(getName(), that.getName()) && 319 Objects.equals(getDescription(), that.getDescription()) && 320 Objects.equals(getChannels(), that.getChannels()); 321 } 322 323 @Override hashCode()324 public int hashCode() { 325 return Objects.hash(getId(), getName(), getDescription(), isBlocked(), getChannels(), 326 mUserLockedFields); 327 } 328 329 @Override clone()330 public NotificationChannelGroup clone() { 331 NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName()); 332 cloned.setDescription(getDescription()); 333 cloned.setBlocked(isBlocked()); 334 cloned.setChannels(getChannels()); 335 cloned.lockFields(mUserLockedFields); 336 return cloned; 337 } 338 339 @Override toString()340 public String toString() { 341 return "NotificationChannelGroup{" 342 + "mId='" + mId + '\'' 343 + ", mName=" + mName 344 + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "") 345 + ", mBlocked=" + mBlocked 346 + ", mChannels=" + mChannels 347 + ", mUserLockedFields=" + mUserLockedFields 348 + '}'; 349 } 350 351 /** @hide */ dumpDebug(ProtoOutputStream proto, long fieldId)352 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 353 final long token = proto.start(fieldId); 354 355 proto.write(NotificationChannelGroupProto.ID, mId); 356 proto.write(NotificationChannelGroupProto.NAME, mName.toString()); 357 proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription); 358 proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked); 359 for (NotificationChannel channel : mChannels) { 360 channel.dumpDebug(proto, NotificationChannelGroupProto.CHANNELS); 361 } 362 proto.end(token); 363 } 364 } 365