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