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);