blob: bc65818870481f1270c71540032e538a8a6dd086 [file] [log] [blame]
/*
* Copyright (C) 2019 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.annotation.NonNull;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.service.controls.actions.ControlAction;
import android.service.controls.actions.ControlActionWrapper;
import android.service.controls.templates.ControlTemplate;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;
/**
* Service implementation allowing applications to contribute controls to the
* System UI.
*/
public abstract class ControlsProviderService extends Service {
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_CONTROLS =
"android.service.controls.ControlsProviderService";
/**
* @hide
*/
public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
/**
* @hide
*/
public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
public static final @NonNull String TAG = "ControlsProviderService";
private IBinder mToken;
private RequestHandler mHandler;
/**
* 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.
*/
public abstract void loadAvailableControls(@NonNull Consumer<List<Control>> consumer);
/**
* 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.
*/
@NonNull
public abstract Publisher<Control> publisherFor(@NonNull List<String> controlIds);
/**
* The user has interacted with a Control. The action is dictated by the type of
* {@link ControlAction} that was sent. A response can be sent via
* {@link Consumer#accept}, with the Integer argument being one of the provided
* {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
* was received successfully, or if additional prompts should be presented to
* the user. Any visual control updates should be sent via the Publisher.
*/
public abstract void performControlAction(@NonNull String controlId,
@NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
@Override
@NonNull
public final IBinder onBind(@NonNull Intent intent) {
mHandler = new RequestHandler(Looper.getMainLooper());
Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
mToken = bundle.getBinder(CALLBACK_TOKEN);
return new IControlsProvider.Stub() {
public void load(IControlsLoadCallback cb) {
mHandler.obtainMessage(RequestHandler.MSG_LOAD, cb).sendToTarget();
}
public void subscribe(List<String> controlIds,
IControlsSubscriber subscriber) {
SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
}
public void action(String controlId, ControlActionWrapper action,
IControlsActionCallback cb) {
ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb);
mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget();
}
};
}
@Override
public boolean onUnbind(@NonNull Intent intent) {
mHandler = null;
return true;
}
private class RequestHandler extends Handler {
private static final int MSG_LOAD = 1;
private static final int MSG_SUBSCRIBE = 2;
private static final int MSG_ACTION = 3;
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(cb));
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;
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) {
return (@NonNull Integer response) -> {
Preconditions.checkNotNull(response);
if (!ControlAction.isValidResponse(response)) {
Log.e(TAG, "Not valid response result: " + response);
response = ControlAction.RESPONSE_UNKNOWN;
}
try {
cb.accept(mToken, controlId, response);
} catch (RemoteException ex) {
ex.rethrowAsRuntimeException();
}
};
}
private Consumer<List<Control>> consumerFor(IControlsLoadCallback cb) {
return (@NonNull List<Control> controls) -> {
Preconditions.checkNotNull(controls);
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();
}
};
}
private boolean isStatelessControl(Control control) {
return (control.getStatus() == Control.STATUS_UNKNOWN
&& control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
&& TextUtils.isEmpty(control.getStatusText()));
}
}
private static class SubscriptionAdapter extends IControlsSubscription.Stub {
final Subscription mSubscription;
SubscriptionAdapter(Subscription s) {
this.mSubscription = s;
}
public void request(long n) {
mSubscription.request(n);
}
public void cancel() {
mSubscription.cancel();
}
}
private static class ActionMessage {
final String mControlId;
final ControlAction mAction;
final IControlsActionCallback mCb;
ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) {
this.mControlId = controlId;
this.mAction = action;
this.mCb = cb;
}
}
private static class SubscribeMessage {
final List<String> mControlIds;
final IControlsSubscriber mSubscriber;
SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) {
this.mControlIds = controlIds;
this.mSubscriber = subscriber;
}
}
}