blob: 9b0cce4535decf64b2b034a9d97ddf0ca6854761 [file] [log] [blame]
/*
* Copyright (C) 2021 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.car.cluster;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.car.Car;
import android.car.CarManagerBase;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
/** @hide */
public class ClusterHomeManager extends CarManagerBase {
private static final String TAG = ClusterHomeManager.class.getSimpleName();
/**
* When the client reports ClusterHome state and if there is no UI in the sub area, it can
* reports UI_TYPE_CLUSTER_NONE instead.
*/
public static final int UI_TYPE_CLUSTER_NONE = -1;
public static final int UI_TYPE_CLUSTER_HOME = 0;
/** @hide */
@IntDef(flag = true, prefix = { "CONFIG_" }, value = {
CONFIG_DISPLAY_ON_OFF,
CONFIG_DISPLAY_BOUNDS,
CONFIG_DISPLAY_INSETS,
CONFIG_UI_TYPE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Config {}
/** Bit fields indicates which fields of {@link ClusterState} are changed */
public static final int CONFIG_DISPLAY_ON_OFF = 0x01;
public static final int CONFIG_DISPLAY_BOUNDS = 0x02;
public static final int CONFIG_DISPLAY_INSETS = 0x04;
public static final int CONFIG_UI_TYPE = 0x08;
public static final int CONFIG_DISPLAY_ID = 0x10;
/**
* Callback for ClusterHome to get notifications
*/
public interface ClusterHomeCallback {
/**
* Called when ClusterOS changes the cluster display state, the geometry of cluster display,
* or the uiType.
* @param state newly updated {@link ClusterState}
* @param changes the flag indicates which fields are updated
*/
void onClusterStateChanged(ClusterState state, @Config int changes);
/** Called when the App who owns the navigation focus casts the new navigation state. */
void onNavigationState(byte[] navigationState);
}
private static class CallbackRecord {
final Executor mExecutor;
final ClusterHomeCallback mCallback;
CallbackRecord(Executor executor, ClusterHomeCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CallbackRecord)) {
return false;
}
return mCallback == ((CallbackRecord) obj).mCallback;
}
@Override
public int hashCode() {
return mCallback.hashCode();
}
}
private final IClusterHomeService mService;
private final IClusterHomeCallbackImpl mBinderCallback;
private final CopyOnWriteArrayList<CallbackRecord> mCallbacks = new CopyOnWriteArrayList<>();
/** @hide */
@VisibleForTesting
public ClusterHomeManager(Car car, IBinder service) {
super(car);
mService = IClusterHomeService.Stub.asInterface(service);
mBinderCallback = new IClusterHomeCallbackImpl(this);
}
/**
* Registers the callback for ClusterHome.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public void registerClusterHomeCallback(
@NonNull Executor executor, @NonNull ClusterHomeCallback callback) {
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
CallbackRecord callbackRecord = new CallbackRecord(executor, callback);
if (!mCallbacks.addIfAbsent(callbackRecord)) {
return;
}
if (mCallbacks.size() == 1) {
try {
mService.registerCallback(mBinderCallback);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Unregisters the callback.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public void unregisterClusterHomeCallback(@NonNull ClusterHomeCallback callback) {
Objects.requireNonNull(callback, "callback cannot be null");
if (!mCallbacks.remove(new CallbackRecord(/* executor= */ null, callback))) {
return;
}
if (mCallbacks.isEmpty()) {
try {
mService.unregisterCallback(mBinderCallback);
} catch (RemoteException ignored) {
// ignore for unregistering
}
}
}
private static class IClusterHomeCallbackImpl extends IClusterHomeCallback.Stub {
private final WeakReference<ClusterHomeManager> mManager;
private IClusterHomeCallbackImpl(ClusterHomeManager manager) {
mManager = new WeakReference<>(manager);
}
@Override
public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) {
ClusterHomeManager manager = mManager.get();
if (manager != null) {
for (CallbackRecord cb : manager.mCallbacks) {
cb.mExecutor.execute(
() -> cb.mCallback.onClusterStateChanged(state, changes));
}
}
}
@Override
public void onNavigationStateChanged(@NonNull byte[] navigationState) {
ClusterHomeManager manager = mManager.get();
if (manager != null) {
for (CallbackRecord cb : manager.mCallbacks) {
cb.mExecutor.execute(() -> cb.mCallback.onNavigationState(navigationState));
}
}
}
}
/**
* Reports the current ClusterUI state.
* @param uiTypeMain uiType that ClusterHome tries to show in main area
* @param uiTypeSub uiType that ClusterHome tries to show in sub area
* @param uiAvailability the byte array to represent the availability of ClusterUI.
* 0 indicates non-available and 1 indicates available.
* Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) {
try {
mService.reportState(uiTypeMain, uiTypeSub, uiAvailability);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Requests to turn the cluster display on to show some ClusterUI.
* @param uiType uiType that ClusterHome tries to show in main area
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public void requestDisplay(int uiType) {
try {
mService.requestDisplay(uiType);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Returns the current {@code ClusterState}.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
@Nullable
public ClusterState getClusterState() {
ClusterState state = null;
try {
state = mService.getClusterState();
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
return state;
}
/**
* Start an activity as specified user. The activity is considered as in fixed mode for
* the cluster display and will be re-launched if the activity crashes, the package
* is updated or goes to background for whatever reason.
* Only one activity can exist in fixed mode for the display and calling this multiple
* times with different {@code Intent} will lead into making all previous activities into
* non-fixed normal state (= will not be re-launched.)
* @param intent the Intent to start
* @param options additional options for how the Activity should be started
* @param userId the user the new activity should run as
* @return true if it launches the given Intent as FixedActivity successfully
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public boolean startFixedActivityModeAsUser(
Intent intent, @Nullable Bundle options, int userId) {
try {
return mService.startFixedActivityModeAsUser(intent, options, userId);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
return false;
}
/**
* The activity launched on the cluster display is no longer in fixed mode. Re-launching or
* finishing should not trigger re-launching any more. Note that Activity for non-current user
* will be auto-stopped and there is no need to call this for user switching. Note that this
* does not stop the activity but it will not be re-launched any more.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
public void stopFixedActivityMode() {
try {
mService.stopFixedActivityMode();
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
@Override
protected void onCarDisconnected() {
mCallbacks.clear();
}
}