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