1 /*
2  * Copyright (c) 2014, 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 
17 package android.service.notification;
18 
19 import static com.android.internal.util.Preconditions.checkArgument;
20 
21 import android.annotation.FlaggedApi;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.Flags;
26 import android.content.Context;
27 import android.net.Uri;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.util.proto.ProtoOutputStream;
31 
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.util.Objects;
35 
36 /**
37  * The current condition of an {@link android.app.AutomaticZenRule}, provided by the
38  * app that owns the rule. Used to tell the system to enter Do Not
39  * Disturb mode and request that the system exit Do Not Disturb mode.
40  */
41 public final class Condition implements Parcelable {
42 
43     public static final String SCHEME = "condition";
44 
45     /** @hide */
46     @IntDef(prefix = { "STATE_" }, value = {
47             STATE_FALSE,
48             STATE_TRUE,
49             STATE_UNKNOWN,
50             STATE_ERROR
51     })
52     @Retention(RetentionPolicy.SOURCE)
53     public @interface State {}
54 
55     /**
56      * Indicates that Do Not Disturb should be turned off. Note that all Conditions from all
57      * {@link android.app.AutomaticZenRule} providers must be off for Do Not Disturb to be turned
58      * off on the device.
59      */
60     public static final int STATE_FALSE = 0;
61     /**
62      * Indicates that Do Not Disturb should be turned on.
63      */
64     public static final int STATE_TRUE = 1;
65     public static final int STATE_UNKNOWN = 2;
66     public static final int STATE_ERROR = 3;
67 
68     public static final int FLAG_RELEVANT_NOW = 1 << 0;
69     public static final int FLAG_RELEVANT_ALWAYS = 1 << 1;
70 
71     /**
72      * The URI representing the rule being updated.
73      * See {@link android.app.AutomaticZenRule#getConditionId()}.
74      */
75     public final Uri id;
76 
77     /**
78      * A summary of what the rule encoded in {@link #id} means when it is enabled. User visible
79      * if the state of the condition is {@link #STATE_TRUE}.
80      */
81     public final String summary;
82 
83     public final String line1;
84     public final String line2;
85 
86     /**
87      * The state of this condition. {@link #STATE_TRUE} will enable Do Not Disturb mode.
88      * {@link #STATE_FALSE} will turn Do Not Disturb off for this rule. Note that Do Not Disturb
89      * might still be enabled globally if other conditions are in a {@link #STATE_TRUE} state.
90      */
91     @State
92     public final int state;
93 
94     public final int flags;
95     public final int icon;
96 
97     /** @hide */
98     @IntDef(prefix = { "SOURCE_" }, value = {
99             SOURCE_UNKNOWN,
100             SOURCE_USER_ACTION,
101             SOURCE_SCHEDULE,
102             SOURCE_CONTEXT
103     })
104     @Retention(RetentionPolicy.SOURCE)
105     public @interface Source {}
106 
107     /** The state is changing due to an unknown reason. */
108     @FlaggedApi(Flags.FLAG_MODES_API)
109     public static final int SOURCE_UNKNOWN = 0;
110     /** The state is changing due to an explicit user action. */
111     @FlaggedApi(Flags.FLAG_MODES_API)
112     public static final int SOURCE_USER_ACTION = 1;
113     /** The state is changing due to an automatic schedule (alarm, set time, etc). */
114     @FlaggedApi(Flags.FLAG_MODES_API)
115     public static final int SOURCE_SCHEDULE = 2;
116     /** The state is changing due to a change in context (such as detected driving or sleeping). */
117     @FlaggedApi(Flags.FLAG_MODES_API)
118     public static final int SOURCE_CONTEXT = 3;
119 
120     /** The source of, or reason for, the state change represented by this Condition. **/
121     @FlaggedApi(Flags.FLAG_MODES_API)
122     public final @Source int source; // default = SOURCE_UNKNOWN
123 
124     /**
125      * The maximum string length for any string contained in this condition.
126      * @hide
127      */
128     public static final int MAX_STRING_LENGTH = 1000;
129 
130     /**
131      * An object representing the current state of a {@link android.app.AutomaticZenRule}.
132      * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
133      * @param summary a user visible description of the rule state
134      * @param state whether the mode should be activated or deactivated
135      */
136     // TODO: b/310208502 - Deprecate this in favor of constructor which specifies source.
Condition(Uri id, String summary, int state)137     public Condition(Uri id, String summary, int state) {
138         this(id, summary, "", "", -1, state, SOURCE_UNKNOWN, FLAG_RELEVANT_ALWAYS);
139     }
140 
141     /**
142      * An object representing the current state of a {@link android.app.AutomaticZenRule}.
143      * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
144      * @param summary a user visible description of the rule state
145      * @param state whether the mode should be activated or deactivated
146      * @param source the source of, or reason for, the state change represented by this Condition
147      */
148     @FlaggedApi(Flags.FLAG_MODES_API)
Condition(@ullable Uri id, @Nullable String summary, @State int state, @Source int source)149     public Condition(@Nullable Uri id, @Nullable String summary, @State int state,
150                      @Source int source) {
151         this(id, summary, "", "", -1, state, source, FLAG_RELEVANT_ALWAYS);
152     }
153 
154     // TODO: b/310208502 - Deprecate this in favor of constructor which specifies source.
Condition(Uri id, String summary, String line1, String line2, int icon, int state, int flags)155     public Condition(Uri id, String summary, String line1, String line2, int icon,
156             int state, int flags) {
157         this(id, summary, line1, line2, icon, state, SOURCE_UNKNOWN, flags);
158     }
159 
160     /**
161      * An object representing the current state of a {@link android.app.AutomaticZenRule}.
162      * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
163      * @param summary a user visible description of the rule state
164      * @param line1 a user-visible description of when the rule will end
165      * @param line2 a continuation of the user-visible description of when the rule will end
166      * @param icon an icon representing this condition
167      * @param state whether the mode should be activated or deactivated
168      * @param source the source of, or reason for, the state change represented by this Condition
169      * @param flags flags on this condition
170      */
171     @FlaggedApi(Flags.FLAG_MODES_API)
Condition(@ullable Uri id, @Nullable String summary, @Nullable String line1, @Nullable String line2, int icon, @State int state, @Source int source, int flags)172     public Condition(@Nullable Uri id, @Nullable String summary, @Nullable String line1,
173                      @Nullable String line2, int icon, @State int state, @Source int source,
174                      int flags) {
175         if (id == null) throw new IllegalArgumentException("id is required");
176         if (summary == null) throw new IllegalArgumentException("summary is required");
177         if (!isValidState(state)) throw new IllegalArgumentException("state is invalid: " + state);
178         this.id = getTrimmedUri(id);
179         this.summary = getTrimmedString(summary);
180         this.line1 = getTrimmedString(line1);
181         this.line2 = getTrimmedString(line2);
182         this.icon = icon;
183         this.state = state;
184         this.source = checkValidSource(source);
185         this.flags = flags;
186     }
187 
Condition(Parcel source)188     public Condition(Parcel source) {
189         // This constructor passes all fields directly into the constructor that takes all the
190         // fields as arguments; that constructor will trim each of the input strings to
191         // max length if necessary.
192         this((Uri)source.readParcelable(Condition.class.getClassLoader(), android.net.Uri.class),
193                 source.readString(),
194                 source.readString(),
195                 source.readString(),
196                 source.readInt(),
197                 source.readInt(),
198                 Flags.modesApi() ? source.readInt() : SOURCE_UNKNOWN,
199                 source.readInt());
200     }
201 
202     /** @hide */
validate()203     public void validate() {
204         if (Flags.modesApi()) {
205             checkValidSource(source);
206         }
207     }
208 
isValidState(int state)209     private static boolean isValidState(int state) {
210         return state >= STATE_FALSE && state <= STATE_ERROR;
211     }
212 
checkValidSource(@ource int source)213     private static int checkValidSource(@Source int source) {
214         if (Flags.modesApi()) {
215             checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT,
216                     "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, "
217                             + "SOURCE_SCHEDULE, or SOURCE_CONTEXT");
218         }
219         return source;
220     }
221 
222     @Override
writeToParcel(Parcel dest, int flags)223     public void writeToParcel(Parcel dest, int flags) {
224         dest.writeParcelable(id, 0);
225         dest.writeString(summary);
226         dest.writeString(line1);
227         dest.writeString(line2);
228         dest.writeInt(icon);
229         dest.writeInt(state);
230         if (Flags.modesApi()) {
231             dest.writeInt(this.source);
232         }
233         dest.writeInt(this.flags);
234     }
235 
236     @Override
toString()237     public String toString() {
238         StringBuilder sb = new StringBuilder(Condition.class.getSimpleName()).append('[')
239                 .append("state=").append(stateToString(state))
240                 .append(",id=").append(id)
241                 .append(",summary=").append(summary)
242                 .append(",line1=").append(line1)
243                 .append(",line2=").append(line2)
244                 .append(",icon=").append(icon);
245         if (Flags.modesApi()) {
246             sb.append(",source=").append(sourceToString(source));
247         }
248         return sb.append(",flags=").append(flags)
249                 .append(']').toString();
250 
251     }
252 
253     /** @hide */
dumpDebug(ProtoOutputStream proto, long fieldId)254     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
255         final long token = proto.start(fieldId);
256 
257         // id is guaranteed not to be null.
258         proto.write(ConditionProto.ID, id.toString());
259         proto.write(ConditionProto.SUMMARY, summary);
260         proto.write(ConditionProto.LINE_1, line1);
261         proto.write(ConditionProto.LINE_2, line2);
262         proto.write(ConditionProto.ICON, icon);
263         proto.write(ConditionProto.STATE, state);
264         // TODO: b/310644464 - Add source to dump.
265         proto.write(ConditionProto.FLAGS, flags);
266 
267         proto.end(token);
268     }
269 
stateToString(int state)270     public static String stateToString(int state) {
271         if (state == STATE_FALSE) return "STATE_FALSE";
272         if (state == STATE_TRUE) return "STATE_TRUE";
273         if (state == STATE_UNKNOWN) return "STATE_UNKNOWN";
274         if (state == STATE_ERROR) return "STATE_ERROR";
275         throw new IllegalArgumentException("state is invalid: " + state);
276     }
277 
278     /**
279      * Provides a human-readable string version of the Source enum.
280      * @hide
281      */
282     @FlaggedApi(Flags.FLAG_MODES_API)
sourceToString(@ource int source)283     public static @NonNull String sourceToString(@Source int source) {
284         if (source == SOURCE_UNKNOWN) return "SOURCE_UNKNOWN";
285         if (source == SOURCE_USER_ACTION) return "SOURCE_USER_ACTION";
286         if (source == SOURCE_SCHEDULE) return "SOURCE_SCHEDULE";
287         if (source == SOURCE_CONTEXT) return "SOURCE_CONTEXT";
288         throw new IllegalArgumentException("source is invalid: " + source);
289     }
290 
relevanceToString(int flags)291     public static String relevanceToString(int flags) {
292         final boolean now = (flags & FLAG_RELEVANT_NOW) != 0;
293         final boolean always = (flags & FLAG_RELEVANT_ALWAYS) != 0;
294         if (!now && !always) return "NONE";
295         if (now && always) return "NOW, ALWAYS";
296         return now ? "NOW" : "ALWAYS";
297     }
298 
299     @Override
equals(@ullable Object o)300     public boolean equals(@Nullable Object o) {
301         if (!(o instanceof Condition)) return false;
302         if (o == this) return true;
303         final Condition other = (Condition) o;
304         boolean finalEquals = Objects.equals(other.id, id)
305                 && Objects.equals(other.summary, summary)
306                 && Objects.equals(other.line1, line1)
307                 && Objects.equals(other.line2, line2)
308                 && other.icon == icon
309                 && other.state == state
310                 && other.flags == flags;
311         if (Flags.modesApi()) {
312             return finalEquals && other.source == source;
313         }
314         return finalEquals;
315     }
316 
317     @Override
hashCode()318     public int hashCode() {
319         if (Flags.modesApi()) {
320             return Objects.hash(id, summary, line1, line2, icon, state, source, flags);
321         }
322         return Objects.hash(id, summary, line1, line2, icon, state, flags);
323     }
324 
325     @Override
describeContents()326     public int describeContents() {
327         return 0;
328     }
329 
copy()330     public Condition copy() {
331         final Parcel parcel = Parcel.obtain();
332         try {
333             writeToParcel(parcel, 0);
334             parcel.setDataPosition(0);
335             return new Condition(parcel);
336         } finally {
337             parcel.recycle();
338         }
339     }
340 
newId(Context context)341     public static Uri.Builder newId(Context context) {
342         return new Uri.Builder()
343                 .scheme(Condition.SCHEME)
344                 .authority(context.getPackageName());
345     }
346 
isValidId(Uri id, String pkg)347     public static boolean isValidId(Uri id, String pkg) {
348         return id != null && SCHEME.equals(id.getScheme()) && pkg.equals(id.getAuthority());
349     }
350 
351     public static final @android.annotation.NonNull Parcelable.Creator<Condition> CREATOR
352             = new Parcelable.Creator<Condition>() {
353         @Override
354         public Condition createFromParcel(Parcel source) {
355             return new Condition(source);
356         }
357 
358         @Override
359         public Condition[] newArray(int size) {
360             return new Condition[size];
361         }
362     };
363 
364     /**
365      * Returns a truncated copy of the string if the string is longer than MAX_STRING_LENGTH.
366      */
getTrimmedString(String input)367     private static String getTrimmedString(String input) {
368         if (input != null && input.length() > MAX_STRING_LENGTH) {
369             return input.substring(0, MAX_STRING_LENGTH);
370         }
371         return input;
372     }
373 
374     /**
375      * Returns a truncated copy of the Uri by trimming the string representation to the maximum
376      * string length.
377      */
getTrimmedUri(Uri input)378     private static Uri getTrimmedUri(Uri input) {
379         if (input != null && input.toString().length() > MAX_STRING_LENGTH) {
380             return Uri.parse(getTrimmedString(input.toString()));
381         }
382         return input;
383     }
384 }
385