Merge "Controls API - Publisher change - Phase 1"
This commit is contained in:
committed by
Android (Google) Code Review
commit
8887c6da17
@@ -43404,11 +43404,12 @@ package android.service.controls {
|
||||
|
||||
public abstract class ControlsProviderService extends android.app.Service {
|
||||
ctor public ControlsProviderService();
|
||||
method public abstract void loadAvailableControls(@NonNull java.util.function.Consumer<java.util.List<android.service.controls.Control>>);
|
||||
method public void loadSuggestedControls(int, @NonNull java.util.function.Consumer<java.util.List<android.service.controls.Control>>);
|
||||
method @Deprecated public void loadAvailableControls(@NonNull java.util.function.Consumer<java.util.List<android.service.controls.Control>>);
|
||||
method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent);
|
||||
method public abstract void performControlAction(@NonNull String, @NonNull android.service.controls.actions.ControlAction, @NonNull java.util.function.Consumer<java.lang.Integer>);
|
||||
method @NonNull public abstract java.util.concurrent.Flow.Publisher<android.service.controls.Control> publisherFor(@NonNull java.util.List<java.lang.String>);
|
||||
method @Nullable public java.util.concurrent.Flow.Publisher<android.service.controls.Control> publisherForAllAvailable();
|
||||
method @Nullable public java.util.concurrent.Flow.Publisher<android.service.controls.Control> publisherForSuggested();
|
||||
field public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService";
|
||||
field @NonNull public static final String TAG = "ControlsProviderService";
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package android.service.controls;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SdkConstant;
|
||||
import android.annotation.SdkConstant.SdkConstantType;
|
||||
import android.app.Service;
|
||||
@@ -34,7 +35,6 @@ import android.util.Log;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Flow.Publisher;
|
||||
@@ -70,25 +70,48 @@ public abstract class ControlsProviderService extends Service {
|
||||
* Retrieve all available controls, using the stateless builder
|
||||
* {@link Control.StatelessBuilder} to build each Control, then use the
|
||||
* provided consumer to callback to the call originator.
|
||||
*
|
||||
* @deprecated Removing consumer-based load apis. Use publisherForAllAvailable() instead
|
||||
*/
|
||||
public abstract void loadAvailableControls(@NonNull Consumer<List<Control>> consumer);
|
||||
|
||||
/**
|
||||
* (Optional) The service may be asked to provide a small number of recommended controls, in
|
||||
* order to suggest some controls to the user for favoriting. The controls shall be built using
|
||||
* the stateless builder {@link Control.StatelessBuilder}, followed by an invocation to the
|
||||
* provided consumer to callback to the call originator. If the number of controls
|
||||
* is greater than maxNumber, the list will be truncated.
|
||||
*/
|
||||
public void loadSuggestedControls(int maxNumber, @NonNull Consumer<List<Control>> consumer) {
|
||||
// Override to change the default behavior
|
||||
@Deprecated
|
||||
public void loadAvailableControls(@NonNull Consumer<List<Control>> consumer) {
|
||||
// pending removal
|
||||
consumer.accept(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid Publisher for the given controlIds. This publisher will be asked
|
||||
* to provide updates for the given list of controlIds as long as the Subscription
|
||||
* is valid.
|
||||
* Publisher for all available controls
|
||||
*
|
||||
* Retrieve all available controls. Use the stateless builder {@link Control.StatelessBuilder}
|
||||
* to build each Control. Call {@link Subscriber#onComplete} when done loading all unique
|
||||
* controls, or {@link Subscriber#onError} for error scenarios. Duplicate Controls will
|
||||
* replace the original.
|
||||
*/
|
||||
@Nullable
|
||||
public Publisher<Control> publisherForAllAvailable() {
|
||||
// will be abstract and @nonnull when consumers are removed
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optional) Publisher for suggested controls
|
||||
*
|
||||
* The service may be asked to provide a small number of recommended controls, in
|
||||
* order to suggest some controls to the user for favoriting. The controls shall be built using
|
||||
* the stateless builder {@link Control.StatelessBuilder}. The number of controls requested
|
||||
* through {@link Subscription#request} will be limited. Call {@link Subscriber#onComplete}
|
||||
* when done, or {@link Subscriber#onError} for error scenarios.
|
||||
*/
|
||||
@Nullable
|
||||
public Publisher<Control> publisherForSuggested() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a valid Publisher for the given controlIds. This publisher will be asked to provide
|
||||
* updates for the given list of controlIds as long as the {@link Subscription} is valid.
|
||||
* Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from
|
||||
* {@link Subscription#cancel} to indicate that updates are no longer required.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Publisher<Control> publisherFor(@NonNull List<String> controlIds);
|
||||
@@ -113,13 +136,13 @@ public abstract class ControlsProviderService extends Service {
|
||||
mToken = bundle.getBinder(CALLBACK_TOKEN);
|
||||
|
||||
return new IControlsProvider.Stub() {
|
||||
public void load(IControlsLoadCallback cb) {
|
||||
mHandler.obtainMessage(RequestHandler.MSG_LOAD, cb).sendToTarget();
|
||||
public void load(IControlsSubscriber subscriber) {
|
||||
mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
|
||||
}
|
||||
|
||||
public void loadSuggested(int maxNumber, IControlsLoadCallback cb) {
|
||||
LoadMessage msg = new LoadMessage(maxNumber, cb);
|
||||
mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, msg).sendToTarget();
|
||||
public void loadSuggested(IControlsSubscriber subscriber) {
|
||||
mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
|
||||
.sendToTarget();
|
||||
}
|
||||
|
||||
public void subscribe(List<String> controlIds,
|
||||
@@ -148,73 +171,56 @@ public abstract class ControlsProviderService extends Service {
|
||||
private static final int MSG_ACTION = 3;
|
||||
private static final int MSG_LOAD_SUGGESTED = 4;
|
||||
|
||||
/**
|
||||
* This the maximum number of controls that can be loaded via
|
||||
* {@link ControlsProviderService#loadAvailablecontrols}. Anything over this number
|
||||
* will be truncated.
|
||||
*/
|
||||
private static final int MAX_NUMBER_OF_CONTROLS_ALLOWED = 1000;
|
||||
|
||||
RequestHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
public void handleMessage(Message msg) {
|
||||
switch(msg.what) {
|
||||
case MSG_LOAD:
|
||||
final IControlsLoadCallback cb = (IControlsLoadCallback) msg.obj;
|
||||
ControlsProviderService.this.loadAvailableControls(consumerFor(
|
||||
MAX_NUMBER_OF_CONTROLS_ALLOWED, cb));
|
||||
break;
|
||||
case MSG_LOAD: {
|
||||
final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
|
||||
final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
|
||||
|
||||
case MSG_LOAD_SUGGESTED:
|
||||
final LoadMessage lMsg = (LoadMessage) msg.obj;
|
||||
ControlsProviderService.this.loadSuggestedControls(lMsg.mMaxNumber,
|
||||
consumerFor(lMsg.mMaxNumber, lMsg.mCb));
|
||||
Publisher<Control> publisher =
|
||||
ControlsProviderService.this.publisherForAllAvailable();
|
||||
if (publisher == null) {
|
||||
ControlsProviderService.this.loadAvailableControls(consumerFor(proxy));
|
||||
} else {
|
||||
publisher.subscribe(proxy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_SUBSCRIBE:
|
||||
case MSG_LOAD_SUGGESTED: {
|
||||
final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
|
||||
final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
|
||||
|
||||
Publisher<Control> publisher =
|
||||
ControlsProviderService.this.publisherForSuggested();
|
||||
if (publisher == null) {
|
||||
Log.i(TAG, "No publisher provided for suggested controls");
|
||||
proxy.onComplete();
|
||||
} else {
|
||||
publisher.subscribe(proxy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_SUBSCRIBE: {
|
||||
final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
|
||||
final IControlsSubscriber cs = sMsg.mSubscriber;
|
||||
Subscriber<Control> s = new Subscriber<Control>() {
|
||||
public void onSubscribe(Subscription subscription) {
|
||||
try {
|
||||
cs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onNext(@NonNull Control statefulControl) {
|
||||
Preconditions.checkNotNull(statefulControl);
|
||||
try {
|
||||
cs.onNext(mToken, statefulControl);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onError(Throwable t) {
|
||||
try {
|
||||
cs.onError(mToken, t.toString());
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onComplete() {
|
||||
try {
|
||||
cs.onComplete(mToken);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
};
|
||||
ControlsProviderService.this.publisherFor(sMsg.mControlIds).subscribe(s);
|
||||
break;
|
||||
final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
|
||||
sMsg.mSubscriber);
|
||||
|
||||
case MSG_ACTION:
|
||||
ControlsProviderService.this.publisherFor(sMsg.mControlIds).subscribe(proxy);
|
||||
break;
|
||||
}
|
||||
|
||||
case MSG_ACTION: {
|
||||
final ActionMessage aMsg = (ActionMessage) msg.obj;
|
||||
ControlsProviderService.this.performControlAction(aMsg.mControlId,
|
||||
aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,39 +240,88 @@ public abstract class ControlsProviderService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<List<Control>> consumerFor(int maxNumber, IControlsLoadCallback cb) {
|
||||
return (@NonNull List<Control> controls) -> {
|
||||
/**
|
||||
* Method will be removed during migration to publisher
|
||||
*/
|
||||
private Consumer<List<Control>> consumerFor(final Subscriber<Control> subscriber) {
|
||||
return (@NonNull final List<Control> controls) -> {
|
||||
Preconditions.checkNotNull(controls);
|
||||
if (controls.size() > maxNumber) {
|
||||
Log.w(TAG, "Too many controls. Provided: " + controls.size() + ", Max allowed: "
|
||||
+ maxNumber + ". Truncating the list.");
|
||||
controls = controls.subList(0, maxNumber);
|
||||
}
|
||||
|
||||
List<Control> list = new ArrayList<>();
|
||||
for (Control control: controls) {
|
||||
if (control == null) {
|
||||
Log.e(TAG, "onLoad: null control.");
|
||||
}
|
||||
if (isStatelessControl(control)) {
|
||||
list.add(control);
|
||||
} else {
|
||||
Log.w(TAG, "onLoad: control is not stateless.");
|
||||
list.add(new Control.StatelessBuilder(control).build());
|
||||
}
|
||||
}
|
||||
try {
|
||||
cb.accept(mToken, list);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
subscriber.onSubscribe(new Subscription() {
|
||||
public void request(long n) {
|
||||
for (Control control: controls) {
|
||||
Control c;
|
||||
if (control == null) {
|
||||
Log.e(TAG, "onLoad: null control.");
|
||||
}
|
||||
if (isStatelessControl(control)) {
|
||||
c = control;
|
||||
} else {
|
||||
Log.w(TAG, "onLoad: control is not stateless.");
|
||||
c = new Control.StatelessBuilder(control).build();
|
||||
}
|
||||
|
||||
subscriber.onNext(c);
|
||||
}
|
||||
subscriber.onComplete();
|
||||
}
|
||||
|
||||
public void cancel() {}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStatelessControl(Control control) {
|
||||
return (control.getStatus() == Control.STATUS_UNKNOWN
|
||||
&& control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
|
||||
&& TextUtils.isEmpty(control.getStatusText()));
|
||||
private static boolean isStatelessControl(Control control) {
|
||||
return (control.getStatus() == Control.STATUS_UNKNOWN
|
||||
&& control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
|
||||
&& TextUtils.isEmpty(control.getStatusText()));
|
||||
}
|
||||
|
||||
private static class SubscriberProxy implements Subscriber<Control> {
|
||||
private IBinder mToken;
|
||||
private IControlsSubscriber mCs;
|
||||
private boolean mEnforceStateless;
|
||||
|
||||
SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) {
|
||||
mEnforceStateless = enforceStateless;
|
||||
mToken = token;
|
||||
mCs = cs;
|
||||
}
|
||||
|
||||
public void onSubscribe(Subscription subscription) {
|
||||
try {
|
||||
mCs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onNext(@NonNull Control control) {
|
||||
Preconditions.checkNotNull(control);
|
||||
try {
|
||||
if (mEnforceStateless && !isStatelessControl(control)) {
|
||||
Log.w(TAG, "onNext(): control is not stateless. Use the "
|
||||
+ "Control.StatelessBuilder() to build the control.");
|
||||
control = new Control.StatelessBuilder(control).build();
|
||||
}
|
||||
mCs.onNext(mToken, control);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onError(Throwable t) {
|
||||
try {
|
||||
mCs.onError(mToken, t.toString());
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
public void onComplete() {
|
||||
try {
|
||||
mCs.onComplete(mToken);
|
||||
} catch (RemoteException ex) {
|
||||
ex.rethrowAsRuntimeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +362,4 @@ public abstract class ControlsProviderService extends Service {
|
||||
this.mSubscriber = subscriber;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LoadMessage {
|
||||
final int mMaxNumber;
|
||||
final IControlsLoadCallback mCb;
|
||||
|
||||
LoadMessage(int maxNumber, IControlsLoadCallback cb) {
|
||||
this.mMaxNumber = maxNumber;
|
||||
this.mCb = cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.service.controls.Control;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IControlsLoadCallback {
|
||||
void accept(in IBinder token, in List<Control> controls);
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package android.service.controls;
|
||||
|
||||
import android.service.controls.IControlsActionCallback;
|
||||
import android.service.controls.IControlsLoadCallback;
|
||||
import android.service.controls.IControlsSubscriber;
|
||||
import android.service.controls.actions.ControlActionWrapper;
|
||||
|
||||
@@ -25,9 +24,9 @@ import android.service.controls.actions.ControlActionWrapper;
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IControlsProvider {
|
||||
void load(IControlsLoadCallback cb);
|
||||
void load(IControlsSubscriber subscriber);
|
||||
|
||||
void loadSuggested(int maxNumber, IControlsLoadCallback cb);
|
||||
void loadSuggested(IControlsSubscriber subscriber);
|
||||
|
||||
void subscribe(in List<String> controlIds,
|
||||
IControlsSubscriber subscriber);
|
||||
|
||||
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -62,8 +63,6 @@ public class ControlProviderServiceTest {
|
||||
@Mock
|
||||
private IControlsActionCallback.Stub mActionCallback;
|
||||
@Mock
|
||||
private IControlsLoadCallback.Stub mLoadCallback;
|
||||
@Mock
|
||||
private IControlsSubscriber.Stub mSubscriber;
|
||||
@Mock
|
||||
private IIntentSender mIIntentSender;
|
||||
@@ -79,8 +78,6 @@ public class ControlProviderServiceTest {
|
||||
|
||||
when(mActionCallback.asBinder()).thenCallRealMethod();
|
||||
when(mActionCallback.queryLocalInterface(any())).thenReturn(mActionCallback);
|
||||
when(mLoadCallback.asBinder()).thenCallRealMethod();
|
||||
when(mLoadCallback.queryLocalInterface(any())).thenReturn(mLoadCallback);
|
||||
when(mSubscriber.asBinder()).thenCallRealMethod();
|
||||
when(mSubscriber.queryLocalInterface(any())).thenReturn(mSubscriber);
|
||||
|
||||
@@ -102,22 +99,28 @@ public class ControlProviderServiceTest {
|
||||
Control control2 = new Control.StatelessBuilder("TEST_ID_2", mPendingIntent)
|
||||
.setDeviceType(DeviceTypes.TYPE_AIR_FRESHENER).build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<Control>> captor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
|
||||
ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
|
||||
ArgumentCaptor<Control> controlCaptor =
|
||||
ArgumentCaptor.forClass(Control.class);
|
||||
|
||||
ArrayList<Control> list = new ArrayList<>();
|
||||
list.add(control1);
|
||||
list.add(control2);
|
||||
|
||||
mControlsProviderService.setControls(list);
|
||||
mControlsProvider.load(mLoadCallback);
|
||||
mControlsProvider.load(mSubscriber);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
verify(mLoadCallback).accept(eq(mToken), captor.capture());
|
||||
List<Control> l = captor.getValue();
|
||||
assertEquals(2, l.size());
|
||||
assertTrue(equals(control1, l.get(0)));
|
||||
assertTrue(equals(control2, l.get(1)));
|
||||
verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
|
||||
subscriptionCaptor.getValue().request(1000);
|
||||
|
||||
verify(mSubscriber, times(2)).onNext(eq(mToken), controlCaptor.capture());
|
||||
List<Control> values = controlCaptor.getAllValues();
|
||||
assertTrue(equals(values.get(0), list.get(0)));
|
||||
assertTrue(equals(values.get(1), list.get(1)));
|
||||
|
||||
verify(mSubscriber).onComplete(eq(mToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,50 +131,57 @@ public class ControlProviderServiceTest {
|
||||
.build();
|
||||
Control statelessControl = new Control.StatelessBuilder(control).build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<Control>> captor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
|
||||
ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
|
||||
ArgumentCaptor<Control> controlCaptor =
|
||||
ArgumentCaptor.forClass(Control.class);
|
||||
|
||||
ArrayList<Control> list = new ArrayList<>();
|
||||
list.add(control);
|
||||
|
||||
mControlsProviderService.setControls(list);
|
||||
mControlsProvider.load(mLoadCallback);
|
||||
mControlsProvider.load(mSubscriber);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
verify(mLoadCallback).accept(eq(mToken), captor.capture());
|
||||
List<Control> l = captor.getValue();
|
||||
assertEquals(1, l.size());
|
||||
assertFalse(equals(control, l.get(0)));
|
||||
assertTrue(equals(statelessControl, l.get(0)));
|
||||
assertEquals(Control.STATUS_UNKNOWN, l.get(0).getStatus());
|
||||
verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
|
||||
subscriptionCaptor.getValue().request(1000);
|
||||
|
||||
verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
|
||||
Control c = controlCaptor.getValue();
|
||||
assertFalse(equals(control, c));
|
||||
assertTrue(equals(statelessControl, c));
|
||||
assertEquals(Control.STATUS_UNKNOWN, c.getStatus());
|
||||
|
||||
verify(mSubscriber).onComplete(eq(mToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadSuggested_withMaxNumber() throws RemoteException {
|
||||
public void testOnLoadSuggested_allStateless() throws RemoteException {
|
||||
Control control1 = new Control.StatelessBuilder("TEST_ID", mPendingIntent).build();
|
||||
Control control2 = new Control.StatelessBuilder("TEST_ID_2", mPendingIntent)
|
||||
.setDeviceType(DeviceTypes.TYPE_AIR_FRESHENER).build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<Control>> captor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
|
||||
ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
|
||||
ArgumentCaptor<Control> controlCaptor =
|
||||
ArgumentCaptor.forClass(Control.class);
|
||||
|
||||
ArrayList<Control> list = new ArrayList<>();
|
||||
list.add(control1);
|
||||
list.add(control2);
|
||||
|
||||
final int maxSuggested = 1;
|
||||
|
||||
mControlsProviderService.setControls(list);
|
||||
mControlsProvider.loadSuggested(maxSuggested, mLoadCallback);
|
||||
mControlsProvider.loadSuggested(mSubscriber);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
verify(mLoadCallback).accept(eq(mToken), captor.capture());
|
||||
List<Control> l = captor.getValue();
|
||||
assertEquals(maxSuggested, l.size());
|
||||
verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
|
||||
subscriptionCaptor.getValue().request(1);
|
||||
|
||||
for (int i = 0; i < maxSuggested; ++i) {
|
||||
assertTrue(equals(list.get(i), l.get(i)));
|
||||
}
|
||||
verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
|
||||
Control c = controlCaptor.getValue();
|
||||
assertTrue(equals(c, list.get(0)));
|
||||
|
||||
verify(mSubscriber).onComplete(eq(mToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,23 +253,20 @@ public class ControlProviderServiceTest {
|
||||
cb.accept(mControls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSuggestedControls(int maxNumber, Consumer<List<Control>> cb) {
|
||||
cb.accept(mControls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Control> publisherFor(List<String> ids) {
|
||||
return new Publisher<Control>() {
|
||||
public void subscribe(final Subscriber s) {
|
||||
s.onSubscribe(new Subscription() {
|
||||
public void request(long n) {
|
||||
for (Control c : mControls) {
|
||||
s.onNext(c);
|
||||
}
|
||||
}
|
||||
public void cancel() {}
|
||||
});
|
||||
s.onSubscribe(createSubscription(s, mControls));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<Control> publisherForSuggested() {
|
||||
return new Publisher<Control>() {
|
||||
public void subscribe(final Subscriber s) {
|
||||
s.onSubscribe(createSubscription(s, mControls));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -269,7 +276,19 @@ public class ControlProviderServiceTest {
|
||||
Consumer<Integer> cb) {
|
||||
cb.accept(ControlAction.RESPONSE_OK);
|
||||
}
|
||||
|
||||
private Subscription createSubscription(Subscriber s, List<Control> controls) {
|
||||
return new Subscription() {
|
||||
public void request(long n) {
|
||||
int i = 0;
|
||||
for (Control c : mControls) {
|
||||
if (i++ < n) s.onNext(c);
|
||||
else break;
|
||||
}
|
||||
s.onComplete();
|
||||
}
|
||||
public void cancel() {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import android.os.IBinder
|
||||
import android.os.UserHandle
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.IControlsActionCallback
|
||||
import android.service.controls.IControlsLoadCallback
|
||||
import android.service.controls.IControlsSubscriber
|
||||
import android.service.controls.IControlsSubscription
|
||||
import android.service.controls.actions.ControlAction
|
||||
@@ -63,12 +62,6 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
private val componentMap: MutableMap<Key, ControlsProviderLifecycleManager> =
|
||||
ArrayMap<Key, ControlsProviderLifecycleManager>()
|
||||
|
||||
private val loadCallbackService = object : IControlsLoadCallback.Stub() {
|
||||
override fun accept(token: IBinder, controls: MutableList<Control>) {
|
||||
backgroundExecutor.execute(OnLoadRunnable(token, controls))
|
||||
}
|
||||
}
|
||||
|
||||
private val actionCallbackService = object : IControlsActionCallback.Stub() {
|
||||
override fun accept(
|
||||
token: IBinder,
|
||||
@@ -106,7 +99,6 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
return ControlsProviderLifecycleManager(
|
||||
context,
|
||||
backgroundExecutor,
|
||||
loadCallbackService,
|
||||
actionCallbackService,
|
||||
subscriberService,
|
||||
currentUser,
|
||||
@@ -130,7 +122,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
callback: ControlsBindingController.LoadCallback
|
||||
) {
|
||||
val provider = retrieveLifecycleManager(component)
|
||||
provider.maybeBindAndLoad(callback)
|
||||
provider.maybeBindAndLoad(LoadSubscriber(callback))
|
||||
}
|
||||
|
||||
override fun subscribe(controls: List<ControlInfo>) {
|
||||
@@ -216,7 +208,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
|
||||
private inner class OnLoadRunnable(
|
||||
token: IBinder,
|
||||
val list: List<Control>
|
||||
val list: List<Control>,
|
||||
val callback: ControlsBindingController.LoadCallback
|
||||
) : CallbackRunnable(token) {
|
||||
override fun run() {
|
||||
if (provider == null) {
|
||||
@@ -233,9 +226,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
return
|
||||
}
|
||||
}
|
||||
provider.lastLoadCallback?.accept(list) ?: run {
|
||||
Log.w(TAG, "Null callback")
|
||||
}
|
||||
callback.accept(list)
|
||||
provider.unbindService()
|
||||
}
|
||||
}
|
||||
@@ -277,7 +268,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
) : CallbackRunnable(token) {
|
||||
override fun run() {
|
||||
provider?.let {
|
||||
Log.i(TAG, "onComplete receive from '${provider.componentName}'")
|
||||
Log.i(TAG, "onComplete receive from '${it.componentName}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +279,7 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
) : CallbackRunnable(token) {
|
||||
override fun run() {
|
||||
provider?.let {
|
||||
Log.e(TAG, "onError receive from '${provider.componentName}': $error")
|
||||
Log.e(TAG, "onError receive from '${it.componentName}': $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +299,44 @@ open class ControlsBindingControllerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnLoadErrorRunnable(
|
||||
token: IBinder,
|
||||
val error: String,
|
||||
val callback: ControlsBindingController.LoadCallback
|
||||
) : CallbackRunnable(token) {
|
||||
override fun run() {
|
||||
callback.error(error)
|
||||
provider?.let {
|
||||
Log.e(TAG, "onError receive from '${it.componentName}': $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LoadSubscriber(
|
||||
val callback: ControlsBindingController.LoadCallback
|
||||
) : IControlsSubscriber.Stub() {
|
||||
val loadedControls = ArrayList<Control>()
|
||||
var hasError = false
|
||||
|
||||
override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
|
||||
backgroundExecutor.execute(OnSubscribeRunnable(token, subs))
|
||||
}
|
||||
|
||||
override fun onNext(token: IBinder, c: Control) {
|
||||
backgroundExecutor.execute { loadedControls.add(c) }
|
||||
}
|
||||
override fun onError(token: IBinder, s: String) {
|
||||
hasError = true
|
||||
backgroundExecutor.execute(OnLoadErrorRunnable(token, s, callback))
|
||||
}
|
||||
|
||||
override fun onComplete(token: IBinder) {
|
||||
if (!hasError) {
|
||||
backgroundExecutor.execute(OnLoadRunnable(token, loadedControls, callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Key(val component: ComponentName, val user: UserHandle)
|
||||
private data class Key(val component: ComponentName, val user: UserHandle)
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.service.controls.ControlsProviderService
|
||||
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
|
||||
import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
|
||||
import android.service.controls.IControlsActionCallback
|
||||
import android.service.controls.IControlsLoadCallback
|
||||
import android.service.controls.IControlsProvider
|
||||
import android.service.controls.IControlsSubscriber
|
||||
import android.service.controls.IControlsSubscription
|
||||
@@ -48,8 +47,6 @@ import java.util.concurrent.TimeUnit
|
||||
*
|
||||
* @property context A SystemUI context for binding to the services
|
||||
* @property executor A delayable executor for posting timeouts
|
||||
* @property loadCallbackService a callback interface to hand the remote service for loading
|
||||
* controls
|
||||
* @property actionCallbackService a callback interface to hand the remote service for sending
|
||||
* action responses
|
||||
* @property subscriberService an "subscriber" interface for requesting and accepting updates for
|
||||
@@ -60,15 +57,12 @@ import java.util.concurrent.TimeUnit
|
||||
class ControlsProviderLifecycleManager(
|
||||
private val context: Context,
|
||||
private val executor: DelayableExecutor,
|
||||
private val loadCallbackService: IControlsLoadCallback.Stub,
|
||||
private val actionCallbackService: IControlsActionCallback.Stub,
|
||||
private val subscriberService: IControlsSubscriber.Stub,
|
||||
val user: UserHandle,
|
||||
val componentName: ComponentName
|
||||
) : IBinder.DeathRecipient {
|
||||
|
||||
var lastLoadCallback: ControlsBindingController.LoadCallback? = null
|
||||
private set
|
||||
val token: IBinder = Binder()
|
||||
@GuardedBy("subscriptions")
|
||||
private val subscriptions = mutableListOf<IControlsSubscription>()
|
||||
@@ -156,9 +150,12 @@ class ControlsProviderLifecycleManager(
|
||||
bindService(false)
|
||||
return
|
||||
}
|
||||
if (Message.Load in queue) {
|
||||
load()
|
||||
|
||||
queue.filter { it is Message.Load }.forEach {
|
||||
val msg = it as Message.Load
|
||||
load(msg.subscriber)
|
||||
}
|
||||
|
||||
queue.filter { it is Message.Subscribe }.flatMap { (it as Message.Subscribe).list }.run {
|
||||
if (this.isNotEmpty()) {
|
||||
subscribe(this)
|
||||
@@ -193,12 +190,12 @@ class ControlsProviderLifecycleManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
private fun load(subscriber: IControlsSubscriber.Stub) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "load $componentName")
|
||||
}
|
||||
if (!(wrapper?.load(loadCallbackService) ?: false)) {
|
||||
queueMessage(Message.Load)
|
||||
if (!(wrapper?.load(subscriber) ?: false)) {
|
||||
queueMessage(Message.Load(subscriber))
|
||||
binderDied()
|
||||
}
|
||||
}
|
||||
@@ -213,27 +210,23 @@ class ControlsProviderLifecycleManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a call to [ControlsProviderService.loadAvailableControls].
|
||||
* Request a call to [IControlsProvider.load].
|
||||
*
|
||||
* If the service is not bound, the call will be queued and the service will be bound first.
|
||||
* The service will be bound after the controls are returned or the call times out.
|
||||
* The service will be unbound after the controls are returned or the call times out.
|
||||
*
|
||||
* @param callback a callback in which to return the result back. If the call times out
|
||||
* [ControlsBindingController.LoadCallback.error] will be called instead.
|
||||
* @param subscriber the subscriber that manages coordination for loading controls
|
||||
*/
|
||||
fun maybeBindAndLoad(callback: ControlsBindingController.LoadCallback) {
|
||||
fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) {
|
||||
unqueueMessage(Message.Unbind)
|
||||
lastLoadCallback = callback
|
||||
onLoadCanceller = executor.executeDelayed({
|
||||
// Didn't receive a response in time, log and send back error
|
||||
Log.d(TAG, "Timeout waiting onLoad for $componentName")
|
||||
callback.error("Timeout waiting onLoad")
|
||||
// Don't accept load callbacks after this
|
||||
lastLoadCallback = null
|
||||
subscriber.onError(token, "Timeout waiting onLoad")
|
||||
unbindService()
|
||||
}, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
invokeOrQueue(::load, Message.Load)
|
||||
invokeOrQueue({ load(subscriber) }, Message.Load(subscriber))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,7 +317,6 @@ class ControlsProviderLifecycleManager(
|
||||
* Request unbind from the service.
|
||||
*/
|
||||
fun unbindService() {
|
||||
lastLoadCallback = null
|
||||
onLoadCanceller?.run()
|
||||
onLoadCanceller = null
|
||||
|
||||
@@ -344,7 +336,7 @@ class ControlsProviderLifecycleManager(
|
||||
*/
|
||||
sealed class Message {
|
||||
abstract val type: Int
|
||||
object Load : Message() {
|
||||
class Load(val subscriber: IControlsSubscriber.Stub) : Message() {
|
||||
override val type = MSG_LOAD
|
||||
}
|
||||
object Unbind : Message() {
|
||||
|
||||
@@ -18,7 +18,6 @@ package com.android.systemui.controls.controller
|
||||
|
||||
import android.service.controls.actions.ControlAction
|
||||
import android.service.controls.IControlsActionCallback
|
||||
import android.service.controls.IControlsLoadCallback
|
||||
import android.service.controls.IControlsProvider
|
||||
import android.service.controls.IControlsSubscriber
|
||||
import android.service.controls.IControlsSubscription
|
||||
@@ -45,9 +44,9 @@ class ServiceWrapper(val service: IControlsProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
fun load(cb: IControlsLoadCallback): Boolean {
|
||||
fun load(subscriber: IControlsSubscriber): Boolean {
|
||||
return callThroughService {
|
||||
service.load(cb)
|
||||
service.load(subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class ControlsBindingControllerTest : SysuiTestCase() {
|
||||
class ControlsBindingControllerImplTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
fun <T> any(): T = Mockito.any<T>()
|
||||
@@ -95,7 +95,7 @@ class ControlsBindingControllerTest : SysuiTestCase() {
|
||||
|
||||
assertEquals(1, providers.size)
|
||||
val provider = providers.first()
|
||||
verify(provider).maybeBindAndLoad(callback)
|
||||
verify(provider).maybeBindAndLoad(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.controls.controller
|
||||
import android.content.ComponentName
|
||||
import android.os.UserHandle
|
||||
import android.service.controls.IControlsActionCallback
|
||||
import android.service.controls.IControlsLoadCallback
|
||||
import android.service.controls.IControlsProvider
|
||||
import android.service.controls.IControlsSubscriber
|
||||
import android.service.controls.actions.ControlAction
|
||||
@@ -32,13 +31,13 @@ import com.android.systemui.util.time.FakeSystemClock
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Captor
|
||||
@@ -54,8 +53,6 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var actionCallbackService: IControlsActionCallback.Stub
|
||||
@Mock
|
||||
private lateinit var loadCallbackService: IControlsLoadCallback.Stub
|
||||
@Mock
|
||||
private lateinit var subscriberService: IControlsSubscriber.Stub
|
||||
@Mock
|
||||
private lateinit var service: IControlsProvider.Stub
|
||||
@@ -85,7 +82,6 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
|
||||
manager = ControlsProviderLifecycleManager(
|
||||
context,
|
||||
executor,
|
||||
loadCallbackService,
|
||||
actionCallbackService,
|
||||
subscriberService,
|
||||
UserHandle.of(0),
|
||||
@@ -113,31 +109,29 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun testMaybeBindAndLoad() {
|
||||
manager.maybeBindAndLoad(loadCallback)
|
||||
manager.maybeBindAndLoad(subscriberService)
|
||||
|
||||
verify(service).load(loadCallbackService)
|
||||
verify(service).load(subscriberService)
|
||||
|
||||
assertTrue(mContext.isBound(componentName))
|
||||
assertEquals(loadCallback, manager.lastLoadCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaybeUnbind_bindingAndCallback() {
|
||||
manager.maybeBindAndLoad(loadCallback)
|
||||
manager.maybeBindAndLoad(subscriberService)
|
||||
|
||||
manager.unbindService()
|
||||
assertFalse(mContext.isBound(componentName))
|
||||
assertNull(manager.lastLoadCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMaybeBindAndLoad_timeout() {
|
||||
manager.maybeBindAndLoad(loadCallback)
|
||||
manager.maybeBindAndLoad(subscriberService)
|
||||
|
||||
executor.advanceClockToLast()
|
||||
executor.runAllReady()
|
||||
|
||||
verify(loadCallback).error(anyString())
|
||||
verify(subscriberService).onError(any(), anyString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,4 +154,4 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
|
||||
eq(actionCallbackService))
|
||||
assertEquals(action, wrapperCaptor.getValue().getWrappedAction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package com.android.systemui.controls.controller
|
||||
|
||||
import android.os.RemoteException
|
||||
import android.service.controls.IControlsActionCallback
|
||||
import android.service.controls.IControlsLoadCallback
|
||||
import android.service.controls.IControlsProvider
|
||||
import android.service.controls.IControlsSubscriber
|
||||
import android.service.controls.IControlsSubscription
|
||||
@@ -55,9 +54,6 @@ class ServiceWrapperTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var subscriber: IControlsSubscriber
|
||||
|
||||
@Mock
|
||||
private lateinit var loadCallback: IControlsLoadCallback
|
||||
|
||||
@Mock
|
||||
private lateinit var actionCallback: IControlsActionCallback
|
||||
|
||||
@@ -81,16 +77,16 @@ class ServiceWrapperTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun testLoad_happyPath() {
|
||||
val result = wrapper.load(loadCallback)
|
||||
val result = wrapper.load(subscriber)
|
||||
|
||||
assertTrue(result)
|
||||
verify(service).load(loadCallback)
|
||||
verify(service).load(subscriber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoad_error() {
|
||||
`when`(service.load(any())).thenThrow(exception)
|
||||
val result = wrapper.load(loadCallback)
|
||||
val result = wrapper.load(subscriber)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user