1 /*
2  * Copyright 2019 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 androidx.enterprise.feedback;
18 
19 import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_DATA;
20 import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_KEY;
21 import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_MESSAGE;
22 import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_SEVERITY;
23 
24 import android.annotation.SuppressLint;
25 import android.os.Bundle;
26 
27 import androidx.annotation.IntDef;
28 
29 import com.google.auto.value.AutoValue;
30 
31 import org.jspecify.annotations.NonNull;
32 import org.jspecify.annotations.Nullable;
33 
34 import java.lang.annotation.Retention;
35 import java.lang.annotation.RetentionPolicy;
36 
37 /**
38  * A keyed app state to be sent to an EMM (enterprise mobility management), with the intention that
39  * it is displayed to the management organization.
40  */
41 @AutoValue
42 public abstract class KeyedAppState {
43 
44     // Create a no-args constructor so it doesn't appear in current.txt
KeyedAppState()45     KeyedAppState() {}
46 
47     @IntDef({SEVERITY_INFO, SEVERITY_ERROR})
48     @Retention(RetentionPolicy.SOURCE)
49     @interface Severity {
50     }
51 
52     public static final int SEVERITY_INFO = 1;
53     public static final int SEVERITY_ERROR = 2;
54 
55     /** @deprecated Use {@link #getMaxKeyLength()} */
56     @SuppressLint("MinMaxConstant")
57     @Deprecated
58     public static final int MAX_KEY_LENGTH = 100;
59     /** @deprecated Use {@link #getMaxMessageLength()} */
60     @SuppressLint("MinMaxConstant")
61     @Deprecated
62     public static final int MAX_MESSAGE_LENGTH = 1000;
63     /** @deprecated Use {@link #getMaxDataLength()} */
64     @SuppressLint("MinMaxConstant")
65     @Deprecated
66     public static final int MAX_DATA_LENGTH = 1000;
67 
68     /**
69      * Get the maximum length of {@link #getKey()}.
70      */
getMaxKeyLength()71     public static final int getMaxKeyLength() {
72         return MAX_KEY_LENGTH;
73     }
74 
75     /**
76      * Get the maximum length of {@link #getMessage()}.
77      */
getMaxMessageLength()78     public static final int getMaxMessageLength() {
79         return MAX_MESSAGE_LENGTH;
80     }
81 
82     /**
83      * Get the maximum length of {@link #getData()}.
84      */
getMaxDataLength()85     public static final int getMaxDataLength() {
86         return MAX_DATA_LENGTH;
87     }
88 
89     /** Create a {@link KeyedAppStateBuilder}. */
builder()90     public static @NonNull KeyedAppStateBuilder builder() {
91         return new AutoValue_KeyedAppState.Builder().setSeverity(SEVERITY_INFO);
92     }
93 
94     /**
95      * The key for the app state. Acts as a point of reference for what the app is providing state
96      * for. For example, when providing managed configuration feedback, this key could be the
97      * managed configuration key to allow EMMs to take advantage of the connection in their UI.
98      */
getKey()99     public abstract @NonNull String getKey();
100 
101     /**
102      * The severity of the app state. This allows EMMs to choose to notify admins of errors. This
103      * should only be set to {@link #SEVERITY_ERROR} for genuine error conditions that a management
104      * organization needs to take action to fix.
105      *
106      * <p>When sending an app state containing errors, it is critical that follow-up app states are
107      * sent when the errors have been resolved, using the same key and this value set to
108      * {@link #SEVERITY_INFO}.
109      */
110     @Severity
getSeverity()111     public abstract int getSeverity();
112 
113     /**
114      * Optionally, a free-form message string to explain the app state. If the state was
115      * triggered by a particular value (e.g. a managed configuration value), it should be
116      * included in the message.
117      */
getMessage()118     public abstract @Nullable String getMessage();
119 
120     /**
121      * Optionally, a machine-readable value to be read by the EMM. For example, setting values that
122      * the admin can choose to query against in the EMM console (e.g. “notify me if the
123      * battery_warning data < 10”).
124      */
getData()125     public abstract @Nullable String getData();
126 
toStateBundle()127     Bundle toStateBundle() {
128         Bundle bundle = new Bundle();
129         bundle.putString(APP_STATE_KEY, getKey());
130         bundle.putInt(APP_STATE_SEVERITY, getSeverity());
131         if (getMessage() != null) {
132             bundle.putString(APP_STATE_MESSAGE, getMessage());
133         }
134         if (getData() != null) {
135             bundle.putString(APP_STATE_DATA, getData());
136         }
137         return bundle;
138     }
139 
140     /** Assumes {@link #isValid(Bundle)}. */
fromBundle(Bundle bundle)141     static KeyedAppState fromBundle(Bundle bundle) {
142         if (!isValid(bundle)) {
143             throw new IllegalArgumentException("Bundle is not valid");
144         }
145 
146         return KeyedAppState.builder()
147                 .setKey(bundle.getString(APP_STATE_KEY))
148                 .setSeverity(bundle.getInt(APP_STATE_SEVERITY))
149                 .setMessage(bundle.getString(APP_STATE_MESSAGE))
150                 .setData(bundle.getString(APP_STATE_DATA))
151                 .build();
152     }
153 
isValid(Bundle bundle)154     static boolean isValid(Bundle bundle) {
155         String key = bundle.getString(APP_STATE_KEY);
156         if (key == null || key.length() > MAX_KEY_LENGTH) {
157             return false;
158         }
159 
160         int severity = bundle.getInt(APP_STATE_SEVERITY);
161         if (severity != SEVERITY_INFO && severity != SEVERITY_ERROR) {
162             return false;
163         }
164 
165         String message = bundle.getString(APP_STATE_MESSAGE);
166         if (message != null && message.length() > MAX_MESSAGE_LENGTH) {
167             return false;
168         }
169 
170         String data = bundle.getString(APP_STATE_DATA);
171         if (data != null && data.length() > MAX_DATA_LENGTH) {
172             return false;
173         }
174 
175         return true;
176     }
177 
178     /** The builder for {@link KeyedAppState}. */
179     @AutoValue.Builder
180     public abstract static class KeyedAppStateBuilder {
181 
182         // Create a no-args constructor so it doesn't appear in current.txt
KeyedAppStateBuilder()183         KeyedAppStateBuilder() {}
184 
185         /** Set {@link KeyedAppState#getKey()}. */
setKey(@onNull String key)186         public abstract @NonNull KeyedAppStateBuilder setKey(@NonNull String key);
187 
188         /** Set {@link KeyedAppState#getSeverity()}. */
setSeverity(@everity int severity)189         public abstract @NonNull KeyedAppStateBuilder setSeverity(@Severity int severity);
190 
191         /** Set {@link KeyedAppState#getMessage()}. */
setMessage(@ullable String message)192         public abstract @NonNull KeyedAppStateBuilder setMessage(@Nullable String message);
193 
194         /** Set {@link KeyedAppState#getData()}. */
setData(@ullable String data)195         public abstract @NonNull KeyedAppStateBuilder setData(@Nullable String data);
196 
autoBuild()197         abstract KeyedAppState autoBuild();
198 
199         /**
200          * Instantiate the {@link KeyedAppState}.
201          *
202          * <p>Severity will default to {@link #SEVERITY_INFO} if not set.
203          *
204          * <p>Assumes the key is set, key length is at most 100 characters, message length is as
205          * most 1000 characters, data length is at most 1000 characters, and severity is set to
206          * either {@link #SEVERITY_INFO} or {@link #SEVERITY_ERROR}.
207          */
build()208         public @NonNull KeyedAppState build() {
209             KeyedAppState keyedAppState = autoBuild();
210             if (keyedAppState.getKey().length() > MAX_KEY_LENGTH) {
211                 throw new IllegalStateException(
212                         String.format("Key length can be at most %s", MAX_KEY_LENGTH));
213             }
214 
215             if (keyedAppState.getMessage() != null
216                     && keyedAppState.getMessage().length() > MAX_MESSAGE_LENGTH) {
217                 throw new IllegalStateException(
218                         String.format("Message length can be at most %s", MAX_MESSAGE_LENGTH));
219             }
220 
221             if (keyedAppState.getData() != null
222                     && keyedAppState.getData().length() > MAX_DATA_LENGTH) {
223                 throw new IllegalStateException(
224                         String.format("Data length can be at most %s", MAX_DATA_LENGTH));
225             }
226 
227             if (keyedAppState.getSeverity() != SEVERITY_ERROR
228                     && keyedAppState.getSeverity() != SEVERITY_INFO) {
229                 throw new IllegalStateException("Severity must be SEVERITY_ERROR or SEVERITY_INFO");
230             }
231 
232             return keyedAppState;
233         }
234     }
235 }
236