blob: eca8541c9d33efcfc5b9e00be3355eba4b757e71 [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.templates.ControlTemplate;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
/**
* Service implementation allowing applications to contribute controls to the
* System UI.
* @hide
*/
public abstract class ControlsProviderService extends Service {
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String CONTROLS_ACTION = "android.service.controls.ControlsProviderService";
public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
public static final String CALLBACK_BINDER = "CALLBACK_BINDER";
public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
public final String TAG = getClass().getSimpleName();
private IControlsProviderCallback mCallback;
private IBinder mToken;
private RequestHandler mHandler;
/**
* Signal to retrieve all Controls. When complete, call
* {@link IControlsProviderCallback#onLoad} to inform the caller.
*/
public abstract void load();
/**
* Informs the service that the caller is listening for updates to the given controlIds.
* {@link IControlsProviderCallback#onRefreshState} should be called any time
* there are Control updates to render.
*/
public abstract void subscribe(@NonNull List<String> controlIds);
/**
* Informs the service that the caller is done listening for updates,
* and any calls to {@link IControlsProviderCallback#onRefreshState} will be ignored.
*/
public abstract void unsubscribe();
/**
* The user has interacted with a Control. The action is dictated by the type of
* {@link ControlAction} that was sent.
*/
public abstract void onAction(@NonNull String controlId, @NonNull ControlAction action);
/**
* Sends a list of the controls available from this service.
*
* The items in the list must not have state information (as created by
* {@link Control.StatelessBuilder}).
* @param controls
*/
public final void onLoad(@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 (isStateless(control)) {
list.add(control);
} else {
Log.w(TAG, "onLoad: control is not stateless.");
list.add(new Control.StatelessBuilder(control).build());
}
}
try {
mCallback.onLoad(mToken, list);
} catch (RemoteException ex) {
ex.rethrowAsRuntimeException();
}
}
/**
* Sends a list of the controls requested by {@link ControlsProviderService#subscribe} with
* their state.
* @param statefulControls
*/
public final void onRefreshState(@NonNull List<Control> statefulControls) {
Preconditions.checkNotNull(statefulControls);
try {
mCallback.onRefreshState(mToken, statefulControls);
} catch (RemoteException ex) {
ex.rethrowAsRuntimeException();
}
}
/**
* Sends the response of a command in the specified {@link Control}.
* @param controlId
* @param response
*/
public final void onControlActionResponse(
@NonNull String controlId, @ControlAction.ResponseResult int response) {
Preconditions.checkNotNull(controlId);
if (!ControlAction.isValidResponse(response)) {
Log.e(TAG, "Not valid response result: " + response);
response = ControlAction.RESPONSE_UNKNOWN;
}
try {
mCallback.onControlActionResponse(mToken, controlId, response);
} catch (RemoteException ex) {
ex.rethrowAsRuntimeException();
}
}
private boolean isStateless(Control control) {
return (control.getStatus() == Control.STATUS_UNKNOWN
&& control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NONE
&& TextUtils.isEmpty(control.getStatusText()));
}
@Override
public IBinder onBind(Intent intent) {
mHandler = new RequestHandler(Looper.getMainLooper());
Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
IBinder callbackBinder = bundle.getBinder(CALLBACK_BINDER);
mToken = bundle.getBinder(CALLBACK_TOKEN);
mCallback = IControlsProviderCallback.Stub.asInterface(callbackBinder);
return new IControlsProvider.Stub() {
public void load() {
mHandler.sendEmptyMessage(RequestHandler.MSG_LOAD);
}
public void subscribe(List<String> ids) {
mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, ids).sendToTarget();
}
public void unsubscribe() {
mHandler.sendEmptyMessage(RequestHandler.MSG_UNSUBSCRIBE);
}
public void onAction(String id, ControlAction action) {
ActionMessage msg = new ActionMessage(id, action);
mHandler.obtainMessage(RequestHandler.MSG_ON_ACTION, msg).sendToTarget();
}
};
}
@Override
public boolean onUnbind(Intent intent) {
mCallback = null;
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_UNSUBSCRIBE = 3;
private static final int MSG_ON_ACTION = 4;
RequestHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
switch(msg.what) {
case MSG_LOAD:
ControlsProviderService.this.load();
break;
case MSG_SUBSCRIBE:
List<String> ids = (List<String>) msg.obj;
ControlsProviderService.this.subscribe(ids);
break;
case MSG_UNSUBSCRIBE:
ControlsProviderService.this.unsubscribe();
break;
case MSG_ON_ACTION:
ActionMessage aMsg = (ActionMessage) msg.obj;
ControlsProviderService.this.onAction(aMsg.mId, aMsg.mAction);
break;
}
}
}
private class ActionMessage {
final String mId;
final ControlAction mAction;
ActionMessage(String id, ControlAction action) {
this.mId = id;
this.mAction = action;
}
}
}