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