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.clear(); 225 if (channels != null) { 226 mChannels.addAll(channels); 227 } 228 } 229 230 /** 231 * @hide 232 */ 233 @TestApi lockFields(int field)234 public void lockFields(int field) { 235 mUserLockedFields |= field; 236 } 237 238 /** 239 * @hide 240 */ unlockFields(int field)241 public void unlockFields(int field) { 242 mUserLockedFields &= ~field; 243 } 244 245 /** 246 * @hide 247 */ 248 @TestApi getUserLockedFields()249 public int getUserLockedFields() { 250 return mUserLockedFields; 251 } 252 253 /** 254 * @hide 255 */ populateFromXml(TypedXmlPullParser parser)256 public void populateFromXml(TypedXmlPullParser parser) { 257 // Name, id, and importance are set in the constructor. 258 setDescription(parser.getAttributeValue(null, ATT_DESC)); 259 setBlocked(parser.getAttributeBoolean(null, ATT_BLOCKED, false)); 260 } 261 262 /** 263 * @hide 264 */ writeXml(TypedXmlSerializer out)265 public void writeXml(TypedXmlSerializer out) throws IOException { 266 out.startTag(null, TAG_GROUP); 267 268 out.attribute(null, ATT_ID, getId()); 269 if (getName() != null) { 270 out.attribute(null, ATT_NAME, getName().toString()); 271 } 272 if (getDescription() != null) { 273 out.attribute(null, ATT_DESC, getDescription().toString()); 274 } 275 out.attributeBoolean(null, ATT_BLOCKED, isBlocked()); 276 out.attributeInt(null, ATT_USER_LOCKED, mUserLockedFields); 277 278 out.endTag(null, TAG_GROUP); 279 } 280 281 /** 282 * @hide 283 */ 284 @SystemApi toJson()285 public JSONObject toJson() throws JSONException { 286 JSONObject record = new JSONObject(); 287 record.put(ATT_ID, getId()); 288 record.put(ATT_NAME, getName()); 289 record.put(ATT_DESC, getDescription()); 290 record.put(ATT_BLOCKED, isBlocked()); 291 record.put(ATT_USER_LOCKED, mUserLockedFields); 292 return record; 293 } 294 295 public static final @android.annotation.NonNull Creator<NotificationChannelGroup> CREATOR = 296 new Creator<NotificationChannelGroup>() { 297 @Override 298 public NotificationChannelGroup createFromParcel(Parcel in) { 299 return new NotificationChannelGroup(in); 300 } 301 302 @Override 303 public NotificationChannelGroup[] newArray(int size) { 304 return new NotificationChannelGroup[size]; 305 } 306 }; 307 308 @Override describeContents()309 public int describeContents() { 310 return 0; 311 } 312 313 @Override equals(@ullable Object o)314 public boolean equals(@Nullable Object o) { 315 if (this == o) return true; 316 if (o == null || getClass() != o.getClass()) return false; 317 NotificationChannelGroup that = (NotificationChannelGroup) o; 318 return isBlocked() == that.isBlocked() && 319 mUserLockedFields == that.mUserLockedFields && 320 Objects.equals(getId(), that.getId()) && 321 Objects.equals(getName(), that.getName()) && 322 Objects.equals(getDescription(), that.getDescription()) && 323 Objects.equals(getChannels(), that.getChannels()); 324 } 325 326 @Override hashCode()327 public int hashCode() { 328 return Objects.hash(getId(), getName(), getDescription(), isBlocked(), getChannels(), 329 mUserLockedFields); 330 } 331 332 @Override clone()333 public NotificationChannelGroup clone() { 334 NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName()); 335 cloned.setDescription(getDescription()); 336 cloned.setBlocked(isBlocked()); 337 for (NotificationChannel c : mChannels) { 338 cloned.addChannel(c.copy()); 339 } 340 cloned.lockFields(mUserLockedFields); 341 return cloned; 342 } 343 344 @Override toString()345 public String toString() { 346 return "NotificationChannelGroup{" 347 + "mId='" + mId + '\'' 348 + ", mName=" + mName 349 + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "") 350 + ", mBlocked=" + mBlocked 351 + ", mChannels=" + mChannels 352 + ", mUserLockedFields=" + mUserLockedFields 353 + '}'; 354 } 355 356 /** @hide */ dumpDebug(ProtoOutputStream proto, long fieldId)357 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 358 final long token = proto.start(fieldId); 359 360 proto.write(NotificationChannelGroupProto.ID, mId); 361 proto.write(NotificationChannelGroupProto.NAME, mName.toString()); 362 proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription); 363 proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked); 364 for (NotificationChannel channel : mChannels) { 365 channel.dumpDebug(proto, NotificationChannelGroupProto.CHANNELS); 366 } 367 proto.end(token); 368 } 369 } 370