diff --git a/api/current.txt b/api/current.txt
index 1581960852d83..1c05b6041ef4d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -43850,6 +43850,7 @@ package android.service.controls.templates {
field public static final int TYPE_RANGE = 2; // 0x2
field public static final int TYPE_STATELESS = 8; // 0x8
field public static final int TYPE_TEMPERATURE = 7; // 0x7
+ field public static final int TYPE_THUMBNAIL = 3; // 0x3
field public static final int TYPE_TOGGLE = 1; // 0x1
field public static final int TYPE_TOGGLE_RANGE = 6; // 0x6
}
@@ -43889,6 +43890,14 @@ package android.service.controls.templates {
field public static final int MODE_UNKNOWN = 0; // 0x0
}
+ public final class ThumbnailTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public ThumbnailTemplate(@NonNull String, boolean, @NonNull android.graphics.drawable.Icon, @NonNull CharSequence);
+ method @NonNull public CharSequence getContentDescription();
+ method public int getTemplateType();
+ method @NonNull public android.graphics.drawable.Icon getThumbnail();
+ method public boolean isActive();
+ }
+
public final class ToggleRangeTemplate extends android.service.controls.templates.ControlTemplate {
ctor public ToggleRangeTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlButton, @NonNull android.service.controls.templates.RangeTemplate);
ctor public ToggleRangeTemplate(@NonNull String, boolean, @NonNull CharSequence, @NonNull android.service.controls.templates.RangeTemplate);
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 6bd376a19fc5e..c5277ee0ed5e4 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -206,8 +206,8 @@ public abstract class ControlsProviderService extends Service {
case MSG_SUBSCRIBE: {
final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
- final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
- sMsg.mSubscriber);
+ final SubscriberProxy proxy = new SubscriberProxy(
+ ControlsProviderService.this, false, mToken, sMsg.mSubscriber);
ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
.subscribe(proxy);
@@ -251,6 +251,7 @@ public abstract class ControlsProviderService extends Service {
private IBinder mToken;
private IControlsSubscriber mCs;
private boolean mEnforceStateless;
+ private Context mContext;
SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) {
mEnforceStateless = enforceStateless;
@@ -258,6 +259,12 @@ public abstract class ControlsProviderService extends Service {
mCs = cs;
}
+ SubscriberProxy(Context context, boolean enforceStateless, IBinder token,
+ IControlsSubscriber cs) {
+ this(enforceStateless, token, cs);
+ mContext = context;
+ }
+
public void onSubscribe(Subscription subscription) {
try {
mCs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
@@ -273,6 +280,9 @@ public abstract class ControlsProviderService extends Service {
+ "Control.StatelessBuilder() to build the control.");
control = new Control.StatelessBuilder(control).build();
}
+ if (mContext != null) {
+ control.getControlTemplate().prepareTemplateForBinder(mContext);
+ }
mCs.onNext(mToken, control);
} catch (RemoteException ex) {
ex.rethrowAsRuntimeException();
diff --git a/core/java/android/service/controls/templates/ControlTemplate.java b/core/java/android/service/controls/templates/ControlTemplate.java
index e592fad394b84..3902d6af69e7a 100644
--- a/core/java/android/service/controls/templates/ControlTemplate.java
+++ b/core/java/android/service/controls/templates/ControlTemplate.java
@@ -20,6 +20,7 @@ import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.Context;
import android.os.Bundle;
import android.service.controls.Control;
import android.service.controls.actions.ControlAction;
@@ -78,6 +79,7 @@ public abstract class ControlTemplate {
TYPE_NO_TEMPLATE,
TYPE_TOGGLE,
TYPE_RANGE,
+ TYPE_THUMBNAIL,
TYPE_TOGGLE_RANGE,
TYPE_TEMPERATURE,
TYPE_STATELESS
@@ -104,6 +106,11 @@ public abstract class ControlTemplate {
*/
public static final @TemplateType int TYPE_RANGE = 2;
+ /**
+ * Type identifier of {@link ThumbnailTemplate}.
+ */
+ public static final @TemplateType int TYPE_THUMBNAIL = 3;
+
/**
* Type identifier of {@link ToggleRangeTemplate}.
*/
@@ -168,6 +175,13 @@ public abstract class ControlTemplate {
mTemplateId = templateId;
}
+ /**
+ * Call to prepare values for Binder transport.
+ *
+ * @hide
+ */
+ public void prepareTemplateForBinder(@NonNull Context context) {}
+
/**
*
* @param bundle
@@ -187,6 +201,8 @@ public abstract class ControlTemplate {
return new ToggleTemplate(bundle);
case TYPE_RANGE:
return new RangeTemplate(bundle);
+ case TYPE_THUMBNAIL:
+ return new ThumbnailTemplate(bundle);
case TYPE_TOGGLE_RANGE:
return new ToggleRangeTemplate(bundle);
case TYPE_TEMPERATURE:
diff --git a/core/java/android/service/controls/templates/ThumbnailTemplate.java b/core/java/android/service/controls/templates/ThumbnailTemplate.java
new file mode 100644
index 0000000000000..a7c481e38e556
--- /dev/null
+++ b/core/java/android/service/controls/templates/ThumbnailTemplate.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 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 android.service.controls.templates;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.service.controls.Control;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+/**
+ * A template for a {@link Control} that displays an image.
+ */
+public final class ThumbnailTemplate extends ControlTemplate {
+
+ private static final @TemplateType int TYPE = TYPE_THUMBNAIL;
+ private static final String KEY_ICON = "key_icon";
+ private static final String KEY_ACTIVE = "key_active";
+ private static final String KEY_CONTENT_DESCRIPTION = "key_content_description";
+
+ private final boolean mActive;
+ private final @NonNull Icon mThumbnail;
+ private final @NonNull CharSequence mContentDescription;
+
+ /**
+ * @param templateId the identifier for this template object
+ * @param active whether the image corresponds to an active (live) stream.
+ * @param thumbnail an image to display on the {@link Control}
+ * @param contentDescription a description of the image for accessibility.
+ */
+ public ThumbnailTemplate(
+ @NonNull String templateId,
+ boolean active,
+ @NonNull Icon thumbnail,
+ @NonNull CharSequence contentDescription) {
+ super(templateId);
+ Preconditions.checkNotNull(thumbnail);
+ Preconditions.checkNotNull(contentDescription);
+ mActive = active;
+ mThumbnail = thumbnail;
+ mContentDescription = contentDescription;
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ ThumbnailTemplate(Bundle b) {
+ super(b);
+ mActive = b.getBoolean(KEY_ACTIVE);
+ mThumbnail = b.getParcelable(KEY_ICON);
+ mContentDescription = b.getCharSequence(KEY_CONTENT_DESCRIPTION, "");
+ }
+
+ /*
+ * @return {@code true} if the thumbnail corresponds to an active (live) stream.
+ */
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * The {@link Icon} (image) displayed by this template.
+ */
+ @NonNull
+ public Icon getThumbnail() {
+ return mThumbnail;
+ }
+
+ /**
+ * The description of the image returned by {@link ThumbnailTemplate#getThumbnail()}
+ */
+ @NonNull
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * @return {@link ControlTemplate#TYPE_THUMBNAIL}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE;
+ }
+
+ /**
+ * Rescales the image down if necessary (in the case of a Bitmap).
+ *
+ * @hide
+ */
+ @Override
+ public void prepareTemplateForBinder(@NonNull Context context) {
+ int width = context.getResources()
+ .getDimensionPixelSize(R.dimen.controls_thumbnail_image_max_width);
+ int height = context.getResources()
+ .getDimensionPixelSize(R.dimen.controls_thumbnail_image_max_height);
+ rescaleThumbnail(width, height);
+ }
+
+ private void rescaleThumbnail(int width, int height) {
+ mThumbnail.scaleDownIfNecessary(width, height);
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putBoolean(KEY_ACTIVE, mActive);
+ b.putObject(KEY_ICON, mThumbnail);
+ b.putObject(KEY_CONTENT_DESCRIPTION, mContentDescription);
+ return b;
+ }
+}
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e6ebd4b543c54..7eff3d7bcb06a 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -857,4 +857,9 @@
0px
0px
0px
+
+
+ 140dp
+
+ 280dp
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 94397d661af07..6058313c3b67a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4079,4 +4079,7 @@
+
+
+
diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
index f4ebe2f9a755f..63e464240630c 100644
--- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
+++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
@@ -32,6 +32,8 @@ import android.content.Context;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -39,11 +41,14 @@ import android.os.RemoteException;
import android.service.controls.actions.CommandAction;
import android.service.controls.actions.ControlAction;
import android.service.controls.actions.ControlActionWrapper;
+import android.service.controls.templates.ThumbnailTemplate;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.R;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -107,7 +112,8 @@ public class ControlProviderServiceTest {
mPendingIntent = new PendingIntent(mIIntentSender);
- mControlsProviderService = new FakeControlsProviderService();
+ mControlsProviderService = new FakeControlsProviderService(
+ InstrumentationRegistry.getInstrumentation().getContext());
mControlsProvider = IControlsProvider.Stub.asInterface(
mControlsProviderService.onBind(intent));
}
@@ -134,7 +140,8 @@ public class ControlProviderServiceTest {
verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
subscriptionCaptor.getValue().request(1000);
- verify(mSubscriber, times(2)).onNext(eq(mToken), controlCaptor.capture());
+ verify(mSubscriber, times(2))
+ .onNext(eq(mToken), controlCaptor.capture());
List values = controlCaptor.getAllValues();
assertTrue(equals(values.get(0), list.get(0)));
assertTrue(equals(values.get(1), list.get(1)));
@@ -210,26 +217,69 @@ public class ControlProviderServiceTest {
.setStatus(Control.STATUS_OK)
.build();
- @SuppressWarnings("unchecked")
- ArgumentCaptor controlCaptor =
- ArgumentCaptor.forClass(Control.class);
- ArgumentCaptor subscriptionCaptor =
- ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
+ Control c = sendControlGetControl(control);
+ assertTrue(equals(c, control));
+ }
- ArrayList list = new ArrayList<>();
- list.add(control);
+ @Test
+ public void testThumbnailRescaled_bigger() throws RemoteException {
+ Context context = mControlsProviderService.getBaseContext();
+ int maxWidth = context.getResources().getDimensionPixelSize(
+ R.dimen.controls_thumbnail_image_max_width);
+ int maxHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.controls_thumbnail_image_max_height);
- mControlsProviderService.setControls(list);
+ int min = Math.min(maxWidth, maxHeight);
+ int max = Math.max(maxWidth, maxHeight);
- mControlsProvider.subscribe(new ArrayList(), mSubscriber);
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ Bitmap bitmap = Bitmap.createBitmap(max * 2, max * 2, Bitmap.Config.ALPHA_8);
+ Icon icon = Icon.createWithBitmap(bitmap);
+ ThumbnailTemplate template = new ThumbnailTemplate("ID", false, icon, "");
- verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
- subscriptionCaptor.getValue().request(1);
+ Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
+ .setTitle("TEST_TITLE")
+ .setStatus(Control.STATUS_OK)
+ .setControlTemplate(template)
+ .build();
- verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
- Control c = controlCaptor.getValue();
- assertTrue(equals(c, list.get(0)));
+ Control c = sendControlGetControl(control);
+
+ ThumbnailTemplate sentTemplate = (ThumbnailTemplate) c.getControlTemplate();
+ Bitmap sentBitmap = sentTemplate.getThumbnail().getBitmap();
+
+ // Aspect ratio is kept
+ assertEquals(sentBitmap.getWidth(), sentBitmap.getHeight());
+
+ assertEquals(min, sentBitmap.getWidth());
+ }
+
+ @Test
+ public void testThumbnailRescaled_smaller() throws RemoteException {
+ Context context = mControlsProviderService.getBaseContext();
+ int maxWidth = context.getResources().getDimensionPixelSize(
+ R.dimen.controls_thumbnail_image_max_width);
+ int maxHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.controls_thumbnail_image_max_height);
+
+ int min = Math.min(maxWidth, maxHeight);
+
+ Bitmap bitmap = Bitmap.createBitmap(min / 2, min / 2, Bitmap.Config.ALPHA_8);
+ Icon icon = Icon.createWithBitmap(bitmap);
+ ThumbnailTemplate template = new ThumbnailTemplate("ID", false, icon, "");
+
+ Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
+ .setTitle("TEST_TITLE")
+ .setStatus(Control.STATUS_OK)
+ .setControlTemplate(template)
+ .build();
+
+ Control c = sendControlGetControl(control);
+
+ ThumbnailTemplate sentTemplate = (ThumbnailTemplate) c.getControlTemplate();
+ Bitmap sentBitmap = sentTemplate.getThumbnail().getBitmap();
+
+ assertEquals(bitmap.getHeight(), sentBitmap.getHeight());
+ assertEquals(bitmap.getWidth(), sentBitmap.getWidth());
}
@Test
@@ -257,6 +307,32 @@ public class ControlProviderServiceTest {
intent.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL)));
}
+ /**
+ * Sends the control through the publisher in {@code mControlsProviderService}, returning
+ * the control obtained by the subscriber
+ */
+ private Control sendControlGetControl(Control control) throws RemoteException {
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor controlCaptor =
+ ArgumentCaptor.forClass(Control.class);
+ ArgumentCaptor subscriptionCaptor =
+ ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
+
+ ArrayList list = new ArrayList<>();
+ list.add(control);
+
+ mControlsProviderService.setControls(list);
+
+ mControlsProvider.subscribe(new ArrayList(), mSubscriber);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
+ subscriptionCaptor.getValue().request(1);
+
+ verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
+ return controlCaptor.getValue();
+ }
+
private static boolean equals(Control c1, Control c2) {
if (c1 == c2) return true;
if (c1 == null || c2 == null) return false;
@@ -276,6 +352,11 @@ public class ControlProviderServiceTest {
static class FakeControlsProviderService extends ControlsProviderService {
+ FakeControlsProviderService(Context context) {
+ super();
+ attachBaseContext(context);
+ }
+
private List mControls;
public void setControls(List controls) {
diff --git a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
index 87dc1b7c83d54..91a3ba7d0e748 100644
--- a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
+++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
@@ -102,6 +102,17 @@ public class ControlTemplateTest {
new RangeTemplate(TEST_ID, 0, 2, 1, -1, "%f");
}
+ @Test
+ public void testUnparcelingCorrectClass_thumbnail() {
+ ControlTemplate toParcel = new ThumbnailTemplate(TEST_ID, false, mIcon,
+ TEST_ACTION_DESCRIPTION);
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_THUMBNAIL, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof ThumbnailTemplate);
+ }
+
@Test
public void testUnparcelingCorrectClass_toggleRange() {
ControlTemplate toParcel = new ToggleRangeTemplate(TEST_ID, mControlButton,
diff --git a/non-updatable-api/current.txt b/non-updatable-api/current.txt
index a98ea695eaf18..677d3c00062fa 100644
--- a/non-updatable-api/current.txt
+++ b/non-updatable-api/current.txt
@@ -41976,6 +41976,7 @@ package android.service.controls.templates {
field public static final int TYPE_RANGE = 2; // 0x2
field public static final int TYPE_STATELESS = 8; // 0x8
field public static final int TYPE_TEMPERATURE = 7; // 0x7
+ field public static final int TYPE_THUMBNAIL = 3; // 0x3
field public static final int TYPE_TOGGLE = 1; // 0x1
field public static final int TYPE_TOGGLE_RANGE = 6; // 0x6
}
@@ -42015,6 +42016,14 @@ package android.service.controls.templates {
field public static final int MODE_UNKNOWN = 0; // 0x0
}
+ public final class ThumbnailTemplate extends android.service.controls.templates.ControlTemplate {
+ ctor public ThumbnailTemplate(@NonNull String, boolean, @NonNull android.graphics.drawable.Icon, @NonNull CharSequence);
+ method @NonNull public CharSequence getContentDescription();
+ method public int getTemplateType();
+ method @NonNull public android.graphics.drawable.Icon getThumbnail();
+ method public boolean isActive();
+ }
+
public final class ToggleRangeTemplate extends android.service.controls.templates.ControlTemplate {
ctor public ToggleRangeTemplate(@NonNull String, @NonNull android.service.controls.templates.ControlButton, @NonNull android.service.controls.templates.RangeTemplate);
ctor public ToggleRangeTemplate(@NonNull String, boolean, @NonNull CharSequence, @NonNull android.service.controls.templates.RangeTemplate);