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