KeyedAppStatesReporter.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 android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.Message;

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

import java.util.Collection;
import java.util.concurrent.Executor;

/**
 * A reporter of keyed app states to enable communication between an app and an EMM (enterprise
 * mobility management).
 *
 * For production, create an instance using {@link #create(Context)}.
 * For testing see the {@code FakeKeyedAppStatesReporter} class in
 * the {@code enterprise-feedback-testing} artifact.
 */
public abstract class KeyedAppStatesReporter {

    // Package-private constructor to restrict subclasses to the same package
    KeyedAppStatesReporter() {}

    /**
     * Create a reporter that binds to device owners, profile owners, and the Play store.
     *
     * <p>Each instance maintains bindings, so it's recommended that you maintain a single
     * instance for your whole app, rather than creating instances as needed.
     */
    public static @NonNull KeyedAppStatesReporter create(@NonNull Context context) {
        return new DefaultKeyedAppStatesReporter(context);
    }

    /**
     * Create a reporter using the specified executor.
     *
     * <p>Each instance maintains bindings, so it's recommended that you maintain a single
     * instance for your whole app, rather than creating instances as needed.
     *
     * <p>The executor must run all {@link Runnable} instances on the same thread, serially.
     */
    public static @NonNull KeyedAppStatesReporter create(
            @NonNull Context context, @NonNull Executor executor) {
        return new DefaultKeyedAppStatesReporter(context, executor);
    }

    static final String PHONESKY_PACKAGE_NAME = "com.android.vending";

    /** The value of {@link Message#what} to indicate a state update. */
    static final int WHAT_STATE = 1;

    /**
     * The value of {@link Message#what} to indicate a state update with request for immediate
     * upload.
     */
    static final int WHAT_IMMEDIATE_STATE = 2;

    /** The name for the bundle (stored as a parcelable) containing the keyed app states. */
    static final String APP_STATES = "androidx.enterprise.feedback.APP_STATES";

    /**
     * The name for the keyed app state key for a given bundle in {@link #APP_STATES}.
     *
     * @see KeyedAppState#getKey()
     */
    static final String APP_STATE_KEY = "androidx.enterprise.feedback.APP_STATE_KEY";

    /**
     * The name for the severity of the app state.
     *
     * @see KeyedAppState#getSeverity()
     */
    static final String APP_STATE_SEVERITY = "androidx.enterprise.feedback.APP_STATE_SEVERITY";

    /**
     * The name for the optional app state message for a given bundle in {@link #APP_STATES}.
     *
     * @see KeyedAppState#getMessage()
     */
    static final String APP_STATE_MESSAGE = "androidx.enterprise.feedback.APP_STATE_MESSAGE";

    /**
     * The name for the optional app state data for a given bundle in {@link #APP_STATES}.
     *
     * @see KeyedAppState#getData()
     */
    static final String APP_STATE_DATA = "androidx.enterprise.feedback.APP_STATE_DATA";

    /** The intent action for reporting app states. */
    static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";

    static boolean canPackageReceiveAppStates(Context context, String packageName) {
        DevicePolicyManager devicePolicyManager =
                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);

        return packageName.equals(PHONESKY_PACKAGE_NAME)
            || devicePolicyManager.isDeviceOwnerApp(packageName)
            || devicePolicyManager.isProfileOwnerApp(packageName);
    }

    /**
     * @deprecated use {@link #setStates(Collection, KeyedAppStatesCallback)} which reports
     * errors.
     */
    @Deprecated
    public abstract void setStates(@NonNull Collection<KeyedAppState> states);

    /**
     * Set app states to be sent to an EMM (enterprise mobility management). The EMM can then
     * display this information to the management organization.
     *
     * <p>Do not send personally-identifiable information with this method.
     *
     * <p>Each provided keyed app state will replace any previously set keyed app states with the
     * same key for this package name.
     *
     * <p>If multiple keyed app states are set with the same key, only one will be received by the
     * EMM. Which will be received is not defined.
     *
     * <p>This information is sent immediately to all device owner and profile owner apps on the
     * device. It is also sent immediately to the app with package name com.android.vending if it
     * exists, which is the Play Store on GMS devices.
     *
     * <p>EMMs can access these states either directly in a custom DPC (device policy manager), via
     * Android Management APIs, or via Play EMM APIs.
     *
     * <p>{@link KeyedAppStatesCallback#onResult(int, Throwable)} will be called when an
     * error occurs.
     *
     * @see #setStatesImmediate(Collection, KeyedAppStatesCallback)
     */
    public void setStates(@NonNull Collection<KeyedAppState> states,
            @Nullable KeyedAppStatesCallback callback) {
        throw new UnsupportedOperationException();
    }

    /**
     * @deprecated use {@link #setStatesImmediate(Collection, KeyedAppStatesCallback)} which
     * reports errors.
     */
    @Deprecated
    public abstract void setStatesImmediate(@NonNull Collection<KeyedAppState> states);

    /**
     * Performs the same function as {@link #setStates(Collection, KeyedAppStatesCallback)},
     * except it also requests that the states are immediately uploaded to be accessible
     * via server APIs.
     *
     * <p>The receiver is not obligated to meet this immediate upload request.
     * For example, Play and Android Management APIs have daily quotas.
     *
     * <p>{@link KeyedAppStatesCallback#onResult(int, Throwable)} will be called
     * when an error occurs.
     *
     * @see #setStates(Collection, KeyedAppStatesCallback)
     */
    public void setStatesImmediate(@NonNull Collection<KeyedAppState> states,
            @Nullable KeyedAppStatesCallback callback) {
        throw new UnsupportedOperationException();
    }
}