KeyedAppState.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.enterprise.feedback;

import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_DATA;
import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_KEY;
import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_MESSAGE;
import static androidx.enterprise.feedback.KeyedAppStatesReporter.APP_STATE_SEVERITY;

import android.annotation.SuppressLint;
import android.os.Bundle;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.auto.value.AutoValue;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * A keyed app state to be sent to an EMM (enterprise mobility management), with the intention that
 * it is displayed to the management organization.
 */
@AutoValue
public abstract class KeyedAppState {

    // Create a no-args constructor so it doesn't appear in current.txt
    KeyedAppState() {}

    @IntDef({SEVERITY_INFO, SEVERITY_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    @interface Severity {
    }

    public static final int SEVERITY_INFO = 1;
    public static final int SEVERITY_ERROR = 2;

    /** The maximum length of the key. */
    @SuppressLint("MinMaxConstant")
    public static final int MAX_KEY_LENGTH = 100;
    /** The maximum length of the message field. */
    @SuppressLint("MinMaxConstant")
    public static final int MAX_MESSAGE_LENGTH = 1000;
    /** The maximum length of the data field. */
    @SuppressLint("MinMaxConstant")
    public static final int MAX_DATA_LENGTH = 1000;

    /** Create a {@link KeyedAppStateBuilder}. */
    @NonNull
    public static KeyedAppStateBuilder builder() {
        return new AutoValue_KeyedAppState.Builder().setSeverity(SEVERITY_INFO);
    }

    /**
     * The key for the app state. Acts as a point of reference for what the app is providing state
     * for. For example, when providing managed configuration feedback, this key could be the
     * managed configuration key to allow EMMs to take advantage of the connection in their UI.
     */
    @NonNull
    public abstract String getKey();

    /**
     * The severity of the app state. This allows EMMs to choose to notify admins of errors. This
     * should only be set to {@link #SEVERITY_ERROR} for genuine error conditions that a management
     * organization needs to take action to fix.
     *
     * <p>When sending an app state containing errors, it is critical that follow-up app states are
     * sent when the errors have been resolved, using the same key and this value set to
     * {@link #SEVERITY_INFO}.
     */
    @Severity
    public abstract int getSeverity();

    /**
     * Optionally, a free-form message string to explain the app state. If the state was
     * triggered by a particular value (e.g. a managed configuration value), it should be
     * included in the message.
     */
    @Nullable
    public abstract String getMessage();

    /**
     * Optionally, a machine-readable value to be read by the EMM. For example, setting values that
     * the admin can choose to query against in the EMM console (e.g. “notify me if the
     * battery_warning data < 10”).
     */
    @Nullable
    public abstract String getData();

    Bundle toStateBundle() {
        Bundle bundle = new Bundle();
        bundle.putString(APP_STATE_KEY, getKey());
        bundle.putInt(APP_STATE_SEVERITY, getSeverity());
        if (getMessage() != null) {
            bundle.putString(APP_STATE_MESSAGE, getMessage());
        }
        if (getData() != null) {
            bundle.putString(APP_STATE_DATA, getData());
        }
        return bundle;
    }

    /** Assumes {@link #isValid(Bundle)}. */
    static KeyedAppState fromBundle(Bundle bundle) {
        if (!isValid(bundle)) {
            throw new IllegalArgumentException("Bundle is not valid");
        }

        return KeyedAppState.builder()
                .setKey(bundle.getString(APP_STATE_KEY))
                .setSeverity(bundle.getInt(APP_STATE_SEVERITY))
                .setMessage(bundle.getString(APP_STATE_MESSAGE))
                .setData(bundle.getString(APP_STATE_DATA))
                .build();
    }

    static boolean isValid(Bundle bundle) {
        String key = bundle.getString(APP_STATE_KEY);
        if (key == null || key.length() > MAX_KEY_LENGTH) {
            return false;
        }

        int severity = bundle.getInt(APP_STATE_SEVERITY);
        if (severity != SEVERITY_INFO && severity != SEVERITY_ERROR) {
            return false;
        }

        String message = bundle.getString(APP_STATE_MESSAGE);
        if (message != null && message.length() > MAX_MESSAGE_LENGTH) {
            return false;
        }

        String data = bundle.getString(APP_STATE_DATA);
        if (data != null && data.length() > MAX_DATA_LENGTH) {
            return false;
        }

        return true;
    }

    /** The builder for {@link KeyedAppState}. */
    @AutoValue.Builder
    public abstract static class KeyedAppStateBuilder {

        // Create a no-args constructor so it doesn't appear in current.txt
        KeyedAppStateBuilder() {}

        /** Set {@link KeyedAppState#getKey()}. */
        @NonNull
        public abstract KeyedAppStateBuilder setKey(@NonNull String key);

        /** Set {@link KeyedAppState#getSeverity()}. */
        @NonNull
        public abstract KeyedAppStateBuilder setSeverity(@Severity int severity);

        /** Set {@link KeyedAppState#getMessage()}. */
        @NonNull
        public abstract KeyedAppStateBuilder setMessage(@Nullable String message);

        /** Set {@link KeyedAppState#getData()}. */
        @NonNull
        public abstract KeyedAppStateBuilder setData(@Nullable String data);

        abstract KeyedAppState autoBuild();

        /**
         * Instantiate the {@link KeyedAppState}.
         *
         * <p>Severity will default to {@link #SEVERITY_INFO} if not set.
         *
         * <p>Assumes the key is set, key length is at most 100 characters, message length is as
         * most 1000 characters, data length is at most 1000 characters, and severity is set to
         * either {@link #SEVERITY_INFO} or {@link #SEVERITY_ERROR}.
         */
        @NonNull
        public KeyedAppState build() {
            KeyedAppState keyedAppState = autoBuild();
            if (keyedAppState.getKey().length() > MAX_KEY_LENGTH) {
                throw new IllegalStateException(
                        String.format("Key length can be at most %s", MAX_KEY_LENGTH));
            }

            if (keyedAppState.getMessage() != null
                    && keyedAppState.getMessage().length() > MAX_MESSAGE_LENGTH) {
                throw new IllegalStateException(
                        String.format("Message length can be at most %s", MAX_MESSAGE_LENGTH));
            }

            if (keyedAppState.getData() != null
                    && keyedAppState.getData().length() > MAX_DATA_LENGTH) {
                throw new IllegalStateException(
                        String.format("Data length can be at most %s", MAX_DATA_LENGTH));
            }

            if (keyedAppState.getSeverity() != SEVERITY_ERROR
                    && keyedAppState.getSeverity() != SEVERITY_INFO) {
                throw new IllegalStateException("Severity must be SEVERITY_ERROR or SEVERITY_INFO");
            }

            return keyedAppState;
        }
    }
}