Merge "Add an experimental extension layout element" into androidx-main
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index b20db67..0864423 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -275,6 +275,9 @@
public static interface DynamicBuilders.DynamicType {
}
+ @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ExperimentalProtoLayoutExtensionApi {
+ }
+
@androidx.wear.protolayout.expression.ProtoLayoutExperimental public class PlatformHealthSources {
method @RequiresApi(android.os.Build.VERSION_CODES.Q) @RequiresPermission(android.Manifest.permission.ACTIVITY_RECOGNITION) @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 dailySteps();
method @RequiresPermission(android.Manifest.permission.BODY_SENSORS) @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 heartRateBpm();
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ExperimentalProtoLayoutExtensionApi.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ExperimentalProtoLayoutExtensionApi.java
new file mode 100644
index 0000000..e2b12f6
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ExperimentalProtoLayoutExtensionApi.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 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.protolayout.expression;
+
+import androidx.annotation.RequiresOptIn;
+import androidx.annotation.RequiresOptIn.Level;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that this API surface is not usable by default and requires existence of an extension
+ * provider (on the rendering side).
+ */
+@RequiresOptIn(level = Level.ERROR)
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+public @interface ExperimentalProtoLayoutExtensionApi {}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java
new file mode 100644
index 0000000..ca4f923
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java
@@ -0,0 +1,41 @@
+package androidx.wear.protolayout.renderer;
+
+/*
+ * Copyright 2023 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.
+ */
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * View provider for a View ExtensionLayoutElement. This should check that the given renderer
+ * extension ID matches the expected renderer extension ID, then return a View based on the given
+ * payload. The returned View will be measured using the width/height from the {@link
+ * androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement} message.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ProtoLayoutExtensionViewProvider {
+ /**
+ * Return an Android View from the given renderer extension. In case of an error, this method
+ * should return null, and not throw any exceptions.
+ *
+ * <p>Note: The renderer extension must not set the default tag of the returned View object.
+ */
+ @Nullable
+ View provideView(@NonNull byte[] extensionPayload, @NonNull String vendorId);
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 916c3ee..19fafca 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -40,6 +40,7 @@
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto;
import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.ProtoLayoutExtensionViewProvider;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
@@ -96,6 +97,8 @@
@NonNull private final ListeningExecutorService mBgExecutorService;
@NonNull private final String mClickableIdExtra;
+ @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
+
private final boolean mAnimationEnabled;
private final boolean mAdaptiveUpdateRatesEnabled;
@@ -299,6 +302,7 @@
@NonNull private final LoadActionListener mLoadActionListener;
@NonNull private final ListeningExecutorService mUiExecutorService;
@NonNull private final ListeningExecutorService mBgExecutorService;
+ @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@NonNull private final String mClickableIdExtra;
private final boolean mAnimationEnabled;
private final int mRunningAnimationsLimit;
@@ -317,6 +321,7 @@
@NonNull LoadActionListener loadActionListener,
@NonNull ListeningExecutorService uiExecutorService,
@NonNull ListeningExecutorService bgExecutorService,
+ @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
boolean animationEnabled,
int runningAnimationsLimit,
@@ -332,6 +337,7 @@
this.mLoadActionListener = loadActionListener;
this.mUiExecutorService = uiExecutorService;
this.mBgExecutorService = bgExecutorService;
+ this.mExtensionViewProvider = extensionViewProvider;
this.mClickableIdExtra = clickableIdExtra;
this.mAnimationEnabled = animationEnabled;
this.mRunningAnimationsLimit = runningAnimationsLimit;
@@ -396,6 +402,13 @@
return mBgExecutorService;
}
+ /** Returns provider for renderer extension. */
+ @RestrictTo(Scope.LIBRARY)
+ @Nullable
+ public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
+ return mExtensionViewProvider;
+ }
+
/** Returns extra used for storing clickable id. */
@NonNull
public String getClickableIdExtra() {
@@ -444,6 +457,7 @@
@Nullable private LoadActionListener mLoadActionListener;
@NonNull private final ListeningExecutorService mUiExecutorService;
@NonNull private final ListeningExecutorService mBgExecutorService;
+ @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@NonNull private final String mClickableIdExtra;
private boolean mAnimationEnabled = true;
private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS;
@@ -520,6 +534,15 @@
return this;
}
+ /** Sets provider for the renderer extension. */
+ @RestrictTo(Scope.LIBRARY)
+ @NonNull
+ public Builder setExtensionViewProvider(
+ @NonNull ProtoLayoutExtensionViewProvider extensionViewProvider) {
+ this.mExtensionViewProvider = extensionViewProvider;
+ return this;
+ }
+
/**
* Sets whether animation are enabled. If disabled, none of the animation will be
* played.
@@ -596,6 +619,7 @@
loadActionListener,
mUiExecutorService,
mBgExecutorService,
+ mExtensionViewProvider,
mClickableIdExtra,
mAnimationEnabled,
mRunningAnimationsLimit,
@@ -614,6 +638,7 @@
this.mLoadActionListener = config.getLoadActionListener();
this.mUiExecutorService = config.getUiExecutorService();
this.mBgExecutorService = config.getBgExecutorService();
+ this.mExtensionViewProvider = config.getExtensionViewProvider();
this.mAnimationEnabled = config.getAnimationEnabled();
this.mClickableIdExtra = config.getClickableIdExtra();
this.mAdaptiveUpdateRatesEnabled = config.getAdaptiveUpdateRatesEnabled();
@@ -667,6 +692,10 @@
inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline);
}
+ if (mExtensionViewProvider != null) {
+ inflaterConfigBuilder.setExtensionViewProvider(mExtensionViewProvider);
+ }
+
ProtoLayoutInflater inflater = new ProtoLayoutInflater(inflaterConfigBuilder.build());
// mark the view and skip doing diff update (to avoid doubling the work each time).
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index de2ee97..a50e95d 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -116,6 +116,7 @@
import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
import androidx.wear.protolayout.proto.DimensionProto.WrappedDimensionProp;
import androidx.wear.protolayout.proto.FingerprintProto.NodeFingerprint;
+import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLine;
@@ -161,6 +162,7 @@
import androidx.wear.protolayout.proto.TriggerProto.OnLoadTrigger;
import androidx.wear.protolayout.proto.TriggerProto.Trigger;
import androidx.wear.protolayout.proto.TypesProto.StringProp;
+import androidx.wear.protolayout.renderer.ProtoLayoutExtensionViewProvider;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme.FontSet;
import androidx.wear.protolayout.renderer.R;
@@ -272,6 +274,8 @@
private final Optional<ProtoLayoutDynamicDataPipeline> mDataPipeline;
+ @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
+
private final boolean mAllowLayoutChangingBindsWithoutDefault;
final String mClickableIdExtra;
@@ -490,6 +494,7 @@
@NonNull private final ProtoLayoutTheme mProtoLayoutTheme;
@Nullable private final ProtoLayoutDynamicDataPipeline mDataPipeline;
@NonNull private final String mClickableIdExtra;
+ @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
private final boolean mAnimationEnabled;
private final boolean mAllowLayoutChangingBindsWithoutDefault;
@@ -502,6 +507,7 @@
@NonNull Resources rendererResources,
@NonNull ProtoLayoutTheme protoLayoutTheme,
@Nullable ProtoLayoutDynamicDataPipeline dataPipeline,
+ @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
boolean animationEnabled,
boolean allowLayoutChangingBindsWithoutDefault) {
@@ -516,6 +522,7 @@
this.mAnimationEnabled = animationEnabled;
this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault;
this.mClickableIdExtra = clickableIdExtra;
+ this.mExtensionViewProvider = extensionViewProvider;
}
/** A {@link Context} suitable for interacting with UI. */
@@ -581,6 +588,12 @@
return mClickableIdExtra;
}
+ /** View provider for the renderer extension. */
+ @Nullable
+ public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
+ return mExtensionViewProvider;
+ }
+
/** Whether animation is enabled, which decides whether to load contentUpdateAnimations. */
public boolean getAnimationEnabled() {
return mAnimationEnabled;
@@ -609,6 +622,7 @@
private boolean mAllowLayoutChangingBindsWithoutDefault = false;
@Nullable private String mClickableIdExtra;
+ @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider = null;
/**
* @param uiContext A {@link Context} suitable for interacting with UI with.
* @param layout The layout to be rendered.
@@ -679,6 +693,14 @@
return this;
}
+ /** Sets the view provider for the renderer extension. */
+ @NonNull
+ public Builder setExtensionViewProvider(
+ @NonNull ProtoLayoutExtensionViewProvider extensionViewProvider) {
+ this.mExtensionViewProvider = extensionViewProvider;
+ return this;
+ }
+
/**
* Sets whether animation is enabled, which decides whether to load
* contentUpdateAnimations. Defaults to true.
@@ -738,6 +760,7 @@
mRendererResources,
checkNotNull(mProtoLayoutTheme),
mDataPipeline,
+ mExtensionViewProvider,
checkNotNull(mClickableIdExtra),
mAnimationEnabled,
mAllowLayoutChangingBindsWithoutDefault);
@@ -763,6 +786,7 @@
this.mAllowLayoutChangingBindsWithoutDefault =
config.getAllowLayoutChangingBindsWithoutDefault();
this.mClickableIdExtra = config.getClickableIdExtra();
+ this.mExtensionViewProvider = config.getExtensionViewProvider();
}
private int safeDpToPx(float dp) {
@@ -3152,7 +3176,13 @@
pipelineMaker);
break;
case EXTENSION:
- // TODO(b/276703002): Add support for vendor extension.
+ try {
+ inflatedView =
+ inflateExtension(
+ parentViewWrapper, element.getExtension());
+ } catch (IllegalStateException ex) {
+ Log.w(TAG, "Error inflating Extension.", ex);
+ }
break;
case INNER_NOT_SET:
Log.w(TAG, "Unknown child type: " + element.getInnerCase().name());
@@ -3180,6 +3210,56 @@
return inflatedView;
}
+ @Nullable
+ private InflatedView inflateExtension(
+ ParentViewWrapper parentViewWrapper, ExtensionLayoutElement element) {
+ int widthPx = safeDpToPx(element.getWidth().getLinearDimension());
+ int heightPx = safeDpToPx(element.getHeight().getLinearDimension());
+
+ if (widthPx == 0 && heightPx == 0) {
+ return null;
+ }
+
+ if (mExtensionViewProvider == null) {
+ Log.e(TAG, "Layout has extension payload, but no extension provider is available.");
+ return inflateFailedExtension(parentViewWrapper, element);
+ }
+
+ View view =
+ mExtensionViewProvider.provideView(
+ element.getPayload().toByteArray(), element.getExtensionId());
+
+ if (view == null) {
+ Log.w(TAG, "Extension view provider returned null.");
+ // A failed extension should still occupy space.
+ return inflateFailedExtension(parentViewWrapper, element);
+ }
+
+ if (view.getTag() != null) {
+ throw new IllegalStateException("Extension must not set View's default tag");
+ }
+
+ LayoutParams lp = new LayoutParams(widthPx, heightPx);
+ parentViewWrapper.maybeAddView(view, lp);
+
+ return new InflatedView(
+ view, parentViewWrapper.getParentProperties().applyPendingChildLayoutParams(lp));
+ }
+
+ private InflatedView inflateFailedExtension(
+ ParentViewWrapper parentViewWrapper, ExtensionLayoutElement element) {
+ int widthPx = safeDpToPx(element.getWidth().getLinearDimension());
+ int heightPx = safeDpToPx(element.getHeight().getLinearDimension());
+
+ Space space = new Space(mUiContext);
+
+ LayoutParams lp = new LayoutParams(widthPx, heightPx);
+ parentViewWrapper.maybeAddView(space, lp);
+
+ return new InflatedView(
+ space, parentViewWrapper.getParentProperties().applyPendingChildLayoutParams(lp));
+ }
+
/**
* Either yield the constant value stored in stringProp, or register for updates if it is
* dynamic property.
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 73fe1a8..4a49fcd 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -56,6 +56,7 @@
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils.TruncateAt;
+import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
@@ -66,6 +67,7 @@
import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.Space;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -115,6 +117,7 @@
import androidx.wear.protolayout.proto.DimensionProto.DpProp;
import androidx.wear.protolayout.proto.DimensionProto.ExpandedAngularDimensionProp;
import androidx.wear.protolayout.proto.DimensionProto.ExpandedDimensionProp;
+import androidx.wear.protolayout.proto.DimensionProto.ExtensionDimension;
import androidx.wear.protolayout.proto.DimensionProto.ImageDimension;
import androidx.wear.protolayout.proto.DimensionProto.ProportionalDimensionProp;
import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
@@ -128,6 +131,7 @@
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.ColorFilter;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
+import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
import androidx.wear.protolayout.proto.LayoutElementProto.Image;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
@@ -200,6 +204,7 @@
import org.robolectric.shadows.ShadowSystemClock;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2348,6 +2353,79 @@
}
@Test
+ public void inflate_extension_onlySpaceIfNoExtension() {
+ byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
+ int size = 5;
+
+ ExtensionDimension dim =
+ ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build();
+ LayoutElement rootElement =
+ LayoutElement.newBuilder()
+ .setExtension(
+ ExtensionLayoutElement.newBuilder()
+ .setExtensionId("foo")
+ .setPayload(ByteString.copyFrom(payload))
+ .setWidth(dim)
+ .setHeight(dim))
+ .build();
+
+ FrameLayout inflatedLayout = renderer(fingerprintedLayout(rootElement)).inflate();
+
+ assertThat(inflatedLayout.getChildCount()).isEqualTo(1);
+ assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(Space.class);
+
+ Space s = (Space) inflatedLayout.getChildAt(0);
+ assertThat(s.getMeasuredWidth()).isEqualTo(size);
+ assertThat(s.getMeasuredHeight()).isEqualTo(size);
+ }
+
+ @Test
+ public void inflate_rendererExtension_withExtension_callsExtension() {
+ List<Pair<byte[], String>> invokedExtensions = new ArrayList<>();
+
+ final byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
+ final int size = 5;
+ final String extensionId = "foo";
+
+ ExtensionDimension dim =
+ ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build();
+ LayoutElement rootElement =
+ LayoutElement.newBuilder()
+ .setExtension(
+ ExtensionLayoutElement.newBuilder()
+ .setExtensionId(extensionId)
+ .setPayload(ByteString.copyFrom(payload))
+ .setWidth(dim)
+ .setHeight(dim))
+ .build();
+
+ FrameLayout inflatedLayout =
+ renderer(
+ newRendererConfigBuilder(fingerprintedLayout(rootElement))
+ .setExtensionViewProvider(
+ (extensionPayload, id) -> {
+ invokedExtensions.add(
+ new Pair<>(extensionPayload, id));
+ TextView returnedView =
+ new TextView(getApplicationContext());
+ returnedView.setText("testing");
+
+ return returnedView;
+ }))
+ .inflate();
+
+ assertThat(inflatedLayout.getChildCount()).isEqualTo(1);
+ assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(TextView.class);
+
+ TextView tv = (TextView) inflatedLayout.getChildAt(0);
+ assertThat(tv.getText().toString()).isEqualTo("testing");
+
+ assertThat(invokedExtensions).hasSize(1);
+ assertThat(invokedExtensions.get(0).first).isEqualTo(payload);
+ assertThat(invokedExtensions.get(0).second).isEqualTo(extensionId);
+ }
+
+ @Test
public void inflateThenMutate_withChangeToText_causesUpdate() {
Layout layout1 =
layout(
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index 5d8554a..4a1e57f 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -224,7 +224,7 @@
method public androidx.wear.protolayout.DimensionBuilders.DegreesProp.Builder setValue(float);
}
- public static final class DimensionBuilders.DpProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension androidx.wear.protolayout.DimensionBuilders.SpacerDimension {
+ public static final class DimensionBuilders.DpProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ExtensionDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension androidx.wear.protolayout.DimensionBuilders.SpacerDimension {
method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getDynamicValue();
method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
}
@@ -257,6 +257,9 @@
method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp.Builder setLayoutWeight(androidx.wear.protolayout.TypeBuilders.FloatProp);
}
+ @androidx.wear.protolayout.expression.ExperimentalProtoLayoutExtensionApi public static interface DimensionBuilders.ExtensionDimension {
+ }
+
public static final class DimensionBuilders.HorizontalLayoutConstraint {
method public int getHorizontalAlignment();
method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
@@ -537,6 +540,22 @@
method public androidx.wear.protolayout.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
}
+ @androidx.wear.protolayout.expression.ExperimentalProtoLayoutExtensionApi public static final class LayoutElementBuilders.ExtensionLayoutElement implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
+ method public String getExtensionId();
+ method public androidx.wear.protolayout.DimensionBuilders.ExtensionDimension? getHeight();
+ method public byte[] getPayload();
+ method public androidx.wear.protolayout.DimensionBuilders.ExtensionDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.ExtensionLayoutElement.Builder implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.ExtensionLayoutElement.Builder();
+ method public androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement build();
+ method public androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement.Builder setExtensionId(String);
+ method public androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement.Builder setHeight(androidx.wear.protolayout.DimensionBuilders.ExtensionDimension);
+ method public androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement.Builder setPayload(byte[]);
+ method public androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ExtensionDimension);
+ }
+
public static final class LayoutElementBuilders.FontStyle {
method public androidx.wear.protolayout.ColorBuilders.ColorProp? getColor();
method public androidx.wear.protolayout.TypeBuilders.BoolProp? getItalic();
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
index fe21d0a..e84479a 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
@@ -24,11 +24,13 @@
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.TypeBuilders.FloatProp;
import androidx.wear.protolayout.expression.DynamicBuilders;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
+import androidx.wear.protolayout.expression.ExperimentalProtoLayoutExtensionApi;
import androidx.wear.protolayout.expression.Fingerprint;
import androidx.wear.protolayout.proto.DimensionProto;
@@ -103,13 +105,9 @@
return WRAP;
}
- /**
- * A type for linear dimensions, measured in dp.
- *
- * @since 1.0
- */
+ @OptIn(markerClass = ExperimentalProtoLayoutExtensionApi.class)
public static final class DpProp
- implements ContainerDimension, ImageDimension, SpacerDimension {
+ implements ContainerDimension, ImageDimension, SpacerDimension, ExtensionDimension {
private final DimensionProto.DpProp mImpl;
@Nullable private final Fingerprint mFingerprint;
@@ -191,6 +189,14 @@
}
@Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @ExperimentalProtoLayoutExtensionApi
+ public DimensionProto.ExtensionDimension toExtensionDimensionProto() {
+ return DimensionProto.ExtensionDimension.newBuilder().setLinearDimension(mImpl).build();
+ }
+
+ @Override
@NonNull
public String toString() {
return "DpProp{" + "value=" + getValue() + ", dynamicValue=" + getDynamicValue() + "}";
@@ -200,7 +206,8 @@
public static final class Builder
implements ContainerDimension.Builder,
ImageDimension.Builder,
- SpacerDimension.Builder {
+ SpacerDimension.Builder,
+ ExtensionDimension.Builder {
private final DimensionProto.DpProp.Builder mImpl = DimensionProto.DpProp.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(756413087);
@@ -270,8 +277,8 @@
/**
* Gets the value to use when laying out components which can have a dynamic value.
- * Constrains the layout so that components are not changing size or location regardless
- * of the dynamic value that is being provided.
+ * Constrains the layout so that components are not changing size or location regardless of
+ * the dynamic value that is being provided.
*
* @since 1.2
*/
@@ -305,9 +312,8 @@
* Creates a new builder for {@link DpPropLayoutConstraint}.
*
* @param value Sets the value to use when laying out components which can have a
- * dynamic value. Constrains the layout so that components are not
- * changing size or location regardless of the dynamic value that is
- * being provided.
+ * dynamic value. Constrains the layout so that components are not changing size or
+ * location regardless of the dynamic value that is being provided.
* @since 1.2
*/
protected Builder(@Dimension(unit = DP) float value) {
@@ -316,8 +322,8 @@
/**
* Sets the value to use when laying out components which can have a dynamic value.
- * Constrains the layout so that components are not changing size or location
- * regardless of the dynamic value that is being provided.
+ * Constrains the layout so that components are not changing size or location regardless
+ * of the dynamic value that is being provided.
*
* @since 1.2
*/
@@ -362,9 +368,8 @@
* Creates a new builder for {@link HorizontalLayoutConstraint}.
*
* @param value Sets the value to use when laying out components which can have a
- * dynamic value. Constrains the layout so that components are not
- * changing size or location regardless of the dynamic value that is
- * being provided.
+ * dynamic value. Constrains the layout so that components are not changing size or
+ * location regardless of the dynamic value that is being provided.
* @since 1.2
*/
public Builder(@Dimension(unit = DP) float value) {
@@ -425,9 +430,8 @@
* Creates a new builder for {@link VerticalLayoutConstraint}.
*
* @param value Sets the value to use when laying out components which can have a
- * dynamic value. Constrains the layout so that components are not
- * changing size or location regardless of the dynamic value that is
- * being provided.
+ * dynamic value. Constrains the layout so that components are not changing size or
+ * location regardless of the dynamic value that is being provided.
* @since 1.2
*/
public Builder(@Dimension(unit = DP) float value) {
@@ -1360,4 +1364,50 @@
static SpacerDimension spacerDimensionFromProto(@NonNull DimensionProto.SpacerDimension proto) {
return spacerDimensionFromProto(proto, null);
}
+
+ /**
+ * Interface defining a dimension that can be applied to a {@link
+ * androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement} element.
+ *
+ * @since 1.0
+ */
+ @ExperimentalProtoLayoutExtensionApi
+ public interface ExtensionDimension {
+ /** Get the protocol buffer representation of this object. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ DimensionProto.ExtensionDimension toExtensionDimensionProto();
+
+ /** Get the fingerprint for this object or null if unknown. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ Fingerprint getFingerprint();
+
+ /** Builder to create {@link ExtensionDimension} objects. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ interface Builder {
+
+ /** Builds an instance with values accumulated in this Builder. */
+ @NonNull
+ ExtensionDimension build();
+ }
+ }
+
+ /** Creates a new wrapper instance from the proto. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static ExtensionDimension extensionDimensionFromProto(
+ @NonNull DimensionProto.ExtensionDimension proto, @Nullable Fingerprint fingerprint) {
+ if (proto.hasLinearDimension()) {
+ return DpProp.fromProto(proto.getLinearDimension(), fingerprint);
+ }
+ throw new IllegalStateException(
+ "Proto was not a recognised instance of ExtensionDimension");
+ }
+
+ @NonNull
+ static ExtensionDimension extensionDimensionFromProto(
+ @NonNull DimensionProto.ExtensionDimension proto) {
+ return extensionDimensionFromProto(proto, null);
+ }
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index f97ed264..e3570fd 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -34,6 +34,7 @@
import androidx.wear.protolayout.DimensionBuilders.DegreesProp;
import androidx.wear.protolayout.DimensionBuilders.DpProp;
import androidx.wear.protolayout.DimensionBuilders.EmProp;
+import androidx.wear.protolayout.DimensionBuilders.ExtensionDimension;
import androidx.wear.protolayout.DimensionBuilders.HorizontalLayoutConstraint;
import androidx.wear.protolayout.DimensionBuilders.ImageDimension;
import androidx.wear.protolayout.DimensionBuilders.SpProp;
@@ -46,6 +47,7 @@
import androidx.wear.protolayout.TypeBuilders.Int32Prop;
import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint;
import androidx.wear.protolayout.TypeBuilders.StringProp;
+import androidx.wear.protolayout.expression.ExperimentalProtoLayoutExtensionApi;
import androidx.wear.protolayout.expression.Fingerprint;
import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
import androidx.wear.protolayout.proto.AlignmentProto;
@@ -54,11 +56,13 @@
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.TypesProto;
+import androidx.wear.protolayout.protobuf.ByteString;
import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -4238,8 +4242,204 @@
}
/**
+ * A layout element which can be defined by a renderer extension. The payload in this message
+ * will be passed verbatim to any registered renderer extension in the renderer. It is then
+ * expected that the extension can parse this message, and emit the relevant element.
+ *
+ * <p>If a renderer extension is not installed, this resource will not render any element,
+ * although the specified space will still be occupied. If the payload cannot be parsed by the
+ * renderer extension, then still nothing should be rendered, although this behaviour is defined
+ * by the renderer extension.
+ *
+ * @since 1.2
+ */
+ @ExperimentalProtoLayoutExtensionApi
+ public static final class ExtensionLayoutElement implements LayoutElement {
+ private final LayoutElementProto.ExtensionLayoutElement mImpl;
+ @Nullable private final Fingerprint mFingerprint;
+
+ ExtensionLayoutElement(
+ LayoutElementProto.ExtensionLayoutElement impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the content of the renderer extension element. This can be any data; it is expected
+ * that the renderer extension knows how to parse this field.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public byte[] getPayload() {
+ return mImpl.getPayload().toByteArray();
+ }
+
+ /**
+ * Gets the ID of the renderer extension that should be used for rendering this layout
+ * element.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public String getExtensionId() {
+ return mImpl.getExtensionId();
+ }
+
+ /**
+ * Gets the width of this element.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public ExtensionDimension getWidth() {
+ if (mImpl.hasWidth()) {
+ return DimensionBuilders.extensionDimensionFromProto(mImpl.getWidth());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the height of this element.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public ExtensionDimension getHeight() {
+ if (mImpl.hasHeight()) {
+ return DimensionBuilders.extensionDimensionFromProto(mImpl.getHeight());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ /** Creates a new wrapper instance from the proto. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static ExtensionLayoutElement fromProto(
+ @NonNull LayoutElementProto.ExtensionLayoutElement proto,
+ @Nullable Fingerprint fingerprint) {
+ return new ExtensionLayoutElement(proto, fingerprint);
+ }
+
+ @NonNull
+ static ExtensionLayoutElement fromProto(
+ @NonNull LayoutElementProto.ExtensionLayoutElement proto) {
+ return fromProto(proto, null);
+ }
+
+ /** Returns the internal proto instance. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ LayoutElementProto.ExtensionLayoutElement toProto() {
+ return mImpl;
+ }
+
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public LayoutElementProto.LayoutElement toLayoutElementProto() {
+ return LayoutElementProto.LayoutElement.newBuilder()
+ .setExtension(mImpl)
+ .build();
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "ExtensionLayoutElement{"
+ + "payload="
+ + Arrays.toString(getPayload())
+ + ", extensionId="
+ + getExtensionId()
+ + ", width="
+ + getWidth()
+ + ", height="
+ + getHeight()
+ + "}";
+ }
+
+ /** Builder for {@link ExtensionLayoutElement}. */
+ public static final class Builder implements LayoutElement.Builder {
+ private final LayoutElementProto.ExtensionLayoutElement.Builder mImpl =
+ LayoutElementProto.ExtensionLayoutElement.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(661980356);
+
+ public Builder() {}
+
+ /**
+ * Sets the content of the renderer extension element. This can be any data; it is
+ * expected that the renderer extension knows how to parse this field.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setPayload(@NonNull byte[] payload) {
+ mImpl.setPayload(ByteString.copyFrom(payload));
+ mFingerprint.recordPropertyUpdate(1, Arrays.hashCode(payload));
+ return this;
+ }
+
+ /**
+ * Sets the ID of the renderer extension that should be used for rendering this layout
+ * element.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setExtensionId(@NonNull String extensionId) {
+ mImpl.setExtensionId(extensionId);
+ mFingerprint.recordPropertyUpdate(2, extensionId.hashCode());
+ return this;
+ }
+
+ /**
+ * Sets the width of this element.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setWidth(@NonNull ExtensionDimension width) {
+ mImpl.setWidth(width.toExtensionDimensionProto());
+ mFingerprint.recordPropertyUpdate(
+ 3, checkNotNull(width.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the height of this element.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setHeight(@NonNull ExtensionDimension height) {
+ mImpl.setHeight(height.toExtensionDimensionProto());
+ mFingerprint.recordPropertyUpdate(
+ 4, checkNotNull(height.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public ExtensionLayoutElement build() {
+ return new ExtensionLayoutElement(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* Interface defining the root of all layout elements. This exists to act as a holder for all of
* the actual layout elements above.
+ *
+ * @since 1.0
*/
public interface LayoutElement {
/** Get the protocol buffer representation of this object. */
@@ -4265,6 +4465,7 @@
/** Creates a new wrapper instance from the proto. */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
+ @OptIn(markerClass = ExperimentalProtoLayoutExtensionApi.class)
public static LayoutElement layoutElementFromProto(
@NonNull LayoutElementProto.LayoutElement proto) {
if (proto.hasColumn()) {
@@ -4291,6 +4492,9 @@
if (proto.hasSpannable()) {
return Spannable.fromProto(proto.getSpannable());
}
+ if (proto.hasExtension()) {
+ return ExtensionLayoutElement.fromProto(proto.getExtension());
+ }
throw new IllegalStateException("Proto was not a recognised instance of LayoutElement");
}
@@ -4582,7 +4786,6 @@
* {@link androidx.wear.protolayout.LayoutElementBuilders.Column}.
*
* @since 1.2
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef({