Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8887c6da authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Controls API - Publisher change - Phase 1"

parents be1e77ed d0553b04
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -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";
  }
+150 −105
Original line number Diff line number Diff line
@@ -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
     */
    @Deprecated
    public void loadAvailableControls(@NonNull Consumer<List<Control>> consumer) {
        // pending removal
        consumer.accept(Collections.emptyList());
    }

    /**
     * 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.
     */
    public abstract void loadAvailableControls(@NonNull Consumer<List<Control>> consumer);
    @Nullable
    public Publisher<Control> publisherForAllAvailable() {
        // will be abstract and @nonnull when consumers are removed
        return null;
    }

    /**
     * (Optional) The service may be asked to provide a small number of recommended controls, in
     * (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}, 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.
     * 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.
     */
    public void loadSuggestedControls(int maxNumber, @NonNull Consumer<List<Control>> consumer) {
        // Override to change the default behavior
        consumer.accept(Collections.emptyList());
    @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 Subscription
     * is valid.
     * 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,75 +171,58 @@ 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));
                case MSG_LOAD: {
                    final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);

                    Publisher<Control> publisher =
                            ControlsProviderService.this.publisherForAllAvailable();
                    if (publisher == null) {
                        ControlsProviderService.this.loadAvailableControls(consumerFor(proxy));
                    } else {
                        publisher.subscribe(proxy);
                    }
                    break;
                }

                case MSG_LOAD_SUGGESTED:
                    final LoadMessage lMsg = (LoadMessage) msg.obj;
                    ControlsProviderService.this.loadSuggestedControls(lMsg.mMaxNumber,
                            consumerFor(lMsg.mMaxNumber, lMsg.mCb));
                    break;
                case MSG_LOAD_SUGGESTED: {
                    final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);

                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();
                    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;
                }
                        };
                    ControlsProviderService.this.publisherFor(sMsg.mControlIds).subscribe(s);

                case MSG_SUBSCRIBE: {
                    final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
                            sMsg.mSubscriber);

                    ControlsProviderService.this.publisherFor(sMsg.mControlIds).subscribe(proxy);
                    break;
                }

                case MSG_ACTION:
                case MSG_ACTION: {
                    final ActionMessage aMsg = (ActionMessage) msg.obj;
                    ControlsProviderService.this.performControlAction(aMsg.mControlId,
                            aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
                    break;
                }
            }
        }

        private Consumer<Integer> consumerFor(final String controlId,
                final IControlsActionCallback cb) {
@@ -234,40 +240,89 @@ 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<>();
                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)) {
                        list.add(control);
                                    c = control;
                                } else {
                                    Log.w(TAG, "onLoad: control is not stateless.");
                        list.add(new Control.StatelessBuilder(control).build());
                                    c = new Control.StatelessBuilder(control).build();
                                }

                                subscriber.onNext(c);
                            }
                try {
                    cb.accept(mToken, list);
                } catch (RemoteException ex) {
                    ex.rethrowAsRuntimeException();
                            subscriber.onComplete();
                        }

                        public void cancel() {}
                    });
            };
        }
    }

        private boolean isStatelessControl(Control control) {
    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();
            }
        }
    }

    private static class SubscriptionAdapter extends IControlsSubscription.Stub {
@@ -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;
        }
    }
}
+0 −26
Original line number Diff line number Diff line
/*
 * 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);
}
 No newline at end of file
+2 −3
Original line number Diff line number Diff line
@@ -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);
+65 −46
Original line number Diff line number Diff line
@@ -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);

        verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
        Control c = controlCaptor.getValue();
        assertTrue(equals(c, list.get(0)));

        for (int i = 0; i < maxSuggested; ++i) {
            assertTrue(equals(list.get(i), l.get(i)));
        }
        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);
                    s.onSubscribe(createSubscription(s, mControls));
                }
            };
        }
                            public void cancel() {}
                        });

        @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() {}
            };
        }
    }
}

Loading