ActionBuilders.java

/*
 * Copyright 2021 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.wear.tiles;

import static java.util.stream.Collectors.toMap;

import android.annotation.SuppressLint;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.StateBuilders.State;
import androidx.wear.tiles.proto.ActionProto;

import java.util.Collections;
import java.util.Map;

/** Builders for actions that can be performed when a user interacts with layout elements. */
public final class ActionBuilders {
    private ActionBuilders() {}

    /** Shortcut for building an {@link AndroidStringExtra}. */
    @NonNull
    public static AndroidStringExtra stringExtra(@NonNull String value) {
        return new AndroidStringExtra.Builder().setValue(value).build();
    }

    /** Shortcut for building an {@link AndroidIntExtra}. */
    @NonNull
    public static AndroidIntExtra intExtra(int value) {
        return new AndroidIntExtra.Builder().setValue(value).build();
    }

    /** Shortcut for building an {@link AndroidLongExtra}. */
    @NonNull
    public static AndroidLongExtra longExtra(long value) {
        return new AndroidLongExtra.Builder().setValue(value).build();
    }

    /** Shortcut for building an {@link AndroidDoubleExtra}. */
    @NonNull
    public static AndroidDoubleExtra doubleExtra(double value) {
        return new AndroidDoubleExtra.Builder().setValue(value).build();
    }

    /** Shortcut for building an {@link AndroidBooleanExtra}. */
    @NonNull
    public static AndroidBooleanExtra booleanExtra(boolean value) {
        return new AndroidBooleanExtra.Builder().setValue(value).build();
    }

    /** A string value that can be added to an Android intent's extras. */
    public static final class AndroidStringExtra implements AndroidExtra {
        private final ActionProto.AndroidStringExtra mImpl;

        private AndroidStringExtra(ActionProto.AndroidStringExtra impl) {
            this.mImpl = impl;
        }

        /** Gets the value. Intended for testing purposes only. */
        @NonNull
        public String getValue() {
            return mImpl.getValue();
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidStringExtra fromProto(@NonNull ActionProto.AndroidStringExtra proto) {
            return new AndroidStringExtra(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidStringExtra toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidExtra toAndroidExtraProto() {
            return ActionProto.AndroidExtra.newBuilder().setStringVal(mImpl).build();
        }

        /** Builder for {@link AndroidStringExtra}. */
        public static final class Builder implements AndroidExtra.Builder {
            private final ActionProto.AndroidStringExtra.Builder mImpl =
                    ActionProto.AndroidStringExtra.newBuilder();

            public Builder() {}

            /** Sets the value. */
            @NonNull
            public Builder setValue(@NonNull String value) {
                mImpl.setValue(value);
                return this;
            }

            @Override
            @NonNull
            public AndroidStringExtra build() {
                return AndroidStringExtra.fromProto(mImpl.build());
            }
        }
    }

    /** An integer value that can be added to an Android intent's extras. */
    public static final class AndroidIntExtra implements AndroidExtra {
        private final ActionProto.AndroidIntExtra mImpl;

        private AndroidIntExtra(ActionProto.AndroidIntExtra impl) {
            this.mImpl = impl;
        }

        /** Gets the value. Intended for testing purposes only. */
        public int getValue() {
            return mImpl.getValue();
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidIntExtra fromProto(@NonNull ActionProto.AndroidIntExtra proto) {
            return new AndroidIntExtra(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidIntExtra toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidExtra toAndroidExtraProto() {
            return ActionProto.AndroidExtra.newBuilder().setIntVal(mImpl).build();
        }

        /** Builder for {@link AndroidIntExtra}. */
        public static final class Builder implements AndroidExtra.Builder {
            private final ActionProto.AndroidIntExtra.Builder mImpl =
                    ActionProto.AndroidIntExtra.newBuilder();

            public Builder() {}

            /** Sets the value. */
            @NonNull
            public Builder setValue(int value) {
                mImpl.setValue(value);
                return this;
            }

            @Override
            @NonNull
            public AndroidIntExtra build() {
                return AndroidIntExtra.fromProto(mImpl.build());
            }
        }
    }

    /** A long value that can be added to an Android intent's extras. */
    public static final class AndroidLongExtra implements AndroidExtra {
        private final ActionProto.AndroidLongExtra mImpl;

        private AndroidLongExtra(ActionProto.AndroidLongExtra impl) {
            this.mImpl = impl;
        }

        /** Gets the value. Intended for testing purposes only. */
        public long getValue() {
            return mImpl.getValue();
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidLongExtra fromProto(@NonNull ActionProto.AndroidLongExtra proto) {
            return new AndroidLongExtra(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidLongExtra toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidExtra toAndroidExtraProto() {
            return ActionProto.AndroidExtra.newBuilder().setLongVal(mImpl).build();
        }

        /** Builder for {@link AndroidLongExtra}. */
        public static final class Builder implements AndroidExtra.Builder {
            private final ActionProto.AndroidLongExtra.Builder mImpl =
                    ActionProto.AndroidLongExtra.newBuilder();

            public Builder() {}

            /** Sets the value. */
            @NonNull
            public Builder setValue(long value) {
                mImpl.setValue(value);
                return this;
            }

            @Override
            @NonNull
            public AndroidLongExtra build() {
                return AndroidLongExtra.fromProto(mImpl.build());
            }
        }
    }

    /** A double value that can be added to an Android intent's extras. */
    public static final class AndroidDoubleExtra implements AndroidExtra {
        private final ActionProto.AndroidDoubleExtra mImpl;

        private AndroidDoubleExtra(ActionProto.AndroidDoubleExtra impl) {
            this.mImpl = impl;
        }

        /** Gets the value. Intended for testing purposes only. */
        public double getValue() {
            return mImpl.getValue();
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidDoubleExtra fromProto(@NonNull ActionProto.AndroidDoubleExtra proto) {
            return new AndroidDoubleExtra(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidDoubleExtra toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidExtra toAndroidExtraProto() {
            return ActionProto.AndroidExtra.newBuilder().setDoubleVal(mImpl).build();
        }

        /** Builder for {@link AndroidDoubleExtra}. */
        public static final class Builder implements AndroidExtra.Builder {
            private final ActionProto.AndroidDoubleExtra.Builder mImpl =
                    ActionProto.AndroidDoubleExtra.newBuilder();

            public Builder() {}

            /** Sets the value. */
            @NonNull
            public Builder setValue(double value) {
                mImpl.setValue(value);
                return this;
            }

            @Override
            @NonNull
            public AndroidDoubleExtra build() {
                return AndroidDoubleExtra.fromProto(mImpl.build());
            }
        }
    }

    /** A boolean value that can be added to an Android intent's extras. */
    public static final class AndroidBooleanExtra implements AndroidExtra {
        private final ActionProto.AndroidBooleanExtra mImpl;

        private AndroidBooleanExtra(ActionProto.AndroidBooleanExtra impl) {
            this.mImpl = impl;
        }

        /** Gets the value. Intended for testing purposes only. */
        public boolean getValue() {
            return mImpl.getValue();
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidBooleanExtra fromProto(
                @NonNull ActionProto.AndroidBooleanExtra proto) {
            return new AndroidBooleanExtra(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidBooleanExtra toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidExtra toAndroidExtraProto() {
            return ActionProto.AndroidExtra.newBuilder().setBooleanVal(mImpl).build();
        }

        /** Builder for {@link AndroidBooleanExtra}. */
        public static final class Builder implements AndroidExtra.Builder {
            private final ActionProto.AndroidBooleanExtra.Builder mImpl =
                    ActionProto.AndroidBooleanExtra.newBuilder();

            public Builder() {}

            /** Sets the value. */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder setValue(boolean value) {
                mImpl.setValue(value);
                return this;
            }

            @Override
            @NonNull
            public AndroidBooleanExtra build() {
                return AndroidBooleanExtra.fromProto(mImpl.build());
            }
        }
    }

    /**
     * Interface defining an item that can be included in the extras of an intent that will be sent
     * to an Android activity. Supports types in android.os.PersistableBundle, excluding arrays.
     */
    public interface AndroidExtra {
        /**
         * Get the protocol buffer representation of this object.
         *
         * @hide
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.AndroidExtra toAndroidExtraProto();

        /**
         * Return an instance of one of this object's subtypes, from the protocol buffer
         * representation.
         *
         * @hide
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        static AndroidExtra fromAndroidExtraProto(@NonNull ActionProto.AndroidExtra proto) {
            if (proto.hasStringVal()) {
                return AndroidStringExtra.fromProto(proto.getStringVal());
            }
            if (proto.hasIntVal()) {
                return AndroidIntExtra.fromProto(proto.getIntVal());
            }
            if (proto.hasLongVal()) {
                return AndroidLongExtra.fromProto(proto.getLongVal());
            }
            if (proto.hasDoubleVal()) {
                return AndroidDoubleExtra.fromProto(proto.getDoubleVal());
            }
            if (proto.hasBooleanVal()) {
                return AndroidBooleanExtra.fromProto(proto.getBooleanVal());
            }
            throw new IllegalStateException("Proto was not a recognised instance of AndroidExtra");
        }

        /** Builder to create {@link AndroidExtra} objects. */
        @SuppressLint("StaticFinalBuilder")
        interface Builder {

            /** Builds an instance with values accumulated in this Builder. */
            @NonNull
            AndroidExtra build();
        }
    }

    /** A launch action to send an intent to an Android activity. */
    public static final class AndroidActivity {
        private final ActionProto.AndroidActivity mImpl;

        private AndroidActivity(ActionProto.AndroidActivity impl) {
            this.mImpl = impl;
        }

        /**
         * Gets the package name to send the intent to, for example, "com.google.weather". Intended
         * for testing purposes only.
         */
        @NonNull
        public String getPackageName() {
            return mImpl.getPackageName();
        }

        /**
         * Gets the fully qualified class name (including the package) to send the intent to, for
         * example, "com.google.weather.WeatherOverviewActivity". Intended for testing purposes
         * only.
         */
        @NonNull
        public String getClassName() {
            return mImpl.getClassName();
        }

        /** Gets the extras to be included in the intent. Intended for testing purposes only. */
        @NonNull
        public Map<String, AndroidExtra> getKeyToExtraMapping() {
            return Collections.unmodifiableMap(
                    mImpl.getKeyToExtraMap().entrySet().stream()
                            .collect(
                                    toMap(
                                            Map.Entry::getKey,
                                            f ->
                                                    AndroidExtra.fromAndroidExtraProto(
                                                            f.getValue()))));
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AndroidActivity fromProto(@NonNull ActionProto.AndroidActivity proto) {
            return new AndroidActivity(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.AndroidActivity toProto() {
            return mImpl;
        }

        /** Builder for {@link AndroidActivity} */
        public static final class Builder {
            private final ActionProto.AndroidActivity.Builder mImpl =
                    ActionProto.AndroidActivity.newBuilder();

            public Builder() {}

            /** Sets the package name to send the intent to, for example, "com.google.weather". */
            @NonNull
            public Builder setPackageName(@NonNull String packageName) {
                mImpl.setPackageName(packageName);
                return this;
            }

            /**
             * Sets the fully qualified class name (including the package) to send the intent to,
             * for example, "com.google.weather.WeatherOverviewActivity".
             */
            @NonNull
            public Builder setClassName(@NonNull String className) {
                mImpl.setClassName(className);
                return this;
            }

            /** Adds an entry into the extras to be included in the intent. */
            @SuppressLint("MissingGetterMatchingBuilder")
            @NonNull
            public Builder addKeyToExtraMapping(@NonNull String key, @NonNull AndroidExtra extra) {
                mImpl.putKeyToExtra(key, extra.toAndroidExtraProto());
                return this;
            }

            /** Builds an instance from accumulated values. */
            @NonNull
            public AndroidActivity build() {
                return AndroidActivity.fromProto(mImpl.build());
            }
        }
    }

    /**
     * An action used to launch another activity on the system. This can hold multiple different
     * underlying action types, which will be picked based on what the underlying runtime believes
     * to be suitable.
     */
    public static final class LaunchAction implements Action {
        private final ActionProto.LaunchAction mImpl;

        private LaunchAction(ActionProto.LaunchAction impl) {
            this.mImpl = impl;
        }

        /** Gets an action to launch an Android activity. Intended for testing purposes only. */
        @Nullable
        public AndroidActivity getAndroidActivity() {
            if (mImpl.hasAndroidActivity()) {
                return AndroidActivity.fromProto(mImpl.getAndroidActivity());
            } else {
                return null;
            }
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static LaunchAction fromProto(@NonNull ActionProto.LaunchAction proto) {
            return new LaunchAction(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.LaunchAction toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.Action toActionProto() {
            return ActionProto.Action.newBuilder().setLaunchAction(mImpl).build();
        }

        /** Builder for {@link LaunchAction}. */
        public static final class Builder implements Action.Builder {
            private final ActionProto.LaunchAction.Builder mImpl =
                    ActionProto.LaunchAction.newBuilder();

            public Builder() {}

            /** Sets an action to launch an Android activity. */
            @NonNull
            public Builder setAndroidActivity(@NonNull AndroidActivity androidActivity) {
                mImpl.setAndroidActivity(androidActivity.toProto());
                return this;
            }

            @Override
            @NonNull
            public LaunchAction build() {
                return LaunchAction.fromProto(mImpl.build());
            }
        }
    }

    /** An action used to load (or reload) the tile contents. */
    public static final class LoadAction implements Action {
        private final ActionProto.LoadAction mImpl;

        private LoadAction(ActionProto.LoadAction impl) {
            this.mImpl = impl;
        }

        /**
         * Gets the state to load the next tile with. This will be included in the {@link
         * androidx.wear.tiles.RequestBuilders.TileRequest} sent after this action is invoked by a
         * {@link androidx.wear.tiles.ModifiersBuilders.Clickable}. Intended for testing purposes
         * only.
         */
        @Nullable
        public State getRequestState() {
            if (mImpl.hasRequestState()) {
                return State.fromProto(mImpl.getRequestState());
            } else {
                return null;
            }
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static LoadAction fromProto(@NonNull ActionProto.LoadAction proto) {
            return new LoadAction(proto);
        }

        /** @hide */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.LoadAction toProto() {
            return mImpl;
        }

        /** @hide */
        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public ActionProto.Action toActionProto() {
            return ActionProto.Action.newBuilder().setLoadAction(mImpl).build();
        }

        /** Builder for {@link LoadAction}. */
        public static final class Builder implements Action.Builder {
            private final ActionProto.LoadAction.Builder mImpl =
                    ActionProto.LoadAction.newBuilder();

            public Builder() {}

            /**
             * Sets the state to load the next tile with. This will be included in the {@link
             * androidx.wear.tiles.RequestBuilders.TileRequest} sent after this action is invoked by
             * a {@link androidx.wear.tiles.ModifiersBuilders.Clickable}.
             */
            @NonNull
            public Builder setRequestState(@NonNull State requestState) {
                mImpl.setRequestState(requestState.toProto());
                return this;
            }

            @Override
            @NonNull
            public LoadAction build() {
                return LoadAction.fromProto(mImpl.build());
            }
        }
    }

    /** Interface defining an action that can be used by a layout element. */
    public interface Action {
        /**
         * Get the protocol buffer representation of this object.
         *
         * @hide
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        ActionProto.Action toActionProto();

        /**
         * Return an instance of one of this object's subtypes, from the protocol buffer
         * representation.
         *
         * @hide
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        static Action fromActionProto(@NonNull ActionProto.Action proto) {
            if (proto.hasLaunchAction()) {
                return LaunchAction.fromProto(proto.getLaunchAction());
            }
            if (proto.hasLoadAction()) {
                return LoadAction.fromProto(proto.getLoadAction());
            }
            throw new IllegalStateException("Proto was not a recognised instance of Action");
        }

        /** Builder to create {@link Action} objects. */
        @SuppressLint("StaticFinalBuilder")
        interface Builder {

            /** Builds an instance with values accumulated in this Builder. */
            @NonNull
            Action build();
        }
    }
}