CarResultStub.java
/*
* Copyright 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 androidx.car.app.hardware.common;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.util.Objects.requireNonNull;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.IOnDoneCallback;
import androidx.car.app.hardware.ICarHardwareResult;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.RemoteUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Convenience class wrapping all the calls to car hardware host
*
* @param <T> represents the result data type which this stub is returning
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class CarResultStub<T> extends ICarHardwareResult.Stub {
private final CarHardwareHostDispatcher mHostDispatcher;
private final int mResultType;
@Nullable private final Bundleable mBundle;
private final boolean mIsSingleShot;
private final Map<OnCarDataAvailableListener<T>, Executor> mListeners = new HashMap<>();
private final T mUnsupportedValue;
/**
* Creates an instance of the result stub.
*
* @param resultType the result type to fetch
* @param bundle optional parameters
* @param isSingleShot whether the result stub will return a single value or multiple values
* @param unsupportedValue value to be returned if the host does not support the result type
* @param hostDispatcher dispatcher to be used for host calls
*
* @throws NullPointerException if {@code unsupportedValue} is {@code null} or if
* {@code hostDispatcher} is {@code null}
*/
public CarResultStub(int resultType, @Nullable Bundleable bundle, boolean isSingleShot,
@NonNull T unsupportedValue,
@NonNull CarHardwareHostDispatcher hostDispatcher) {
mHostDispatcher = requireNonNull(hostDispatcher);
mResultType = resultType;
mBundle = bundle;
mIsSingleShot = isSingleShot;
mUnsupportedValue = requireNonNull(unsupportedValue);
}
/**
* Adds a listener for the given result type and replaces any previous parameter.
*
* <p>This call also kicks off the initial call to the host if needed. If the
* {@code listener} was added previously then the executor is updated.
*
* @param executor the executor which will be used for invoking the listener
* @param listener listener for the results
*
* @throws NullPointerException if {@code executor} is {@code null} or if {@code listener} is
* {@code null}
*/
public void addListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<T> listener) {
boolean alreadySubscribedToHost = !mListeners.isEmpty();
mListeners.put(requireNonNull(listener), executor);
if (alreadySubscribedToHost) {
return;
}
if (mIsSingleShot) {
mHostDispatcher.dispatchGetCarHardwareResult(mResultType, mBundle, this);
} else {
mHostDispatcher.dispatchSubscribeCarHardwareResult(mResultType, mBundle, this);
}
}
/**
* Removes a previously registered listener and returns {@code true} if there are no more
* listeners attached to this stub.
*
* @throws NullPointerException if {@code listener} is {@code null}
*/
public boolean removeListener(@NonNull OnCarDataAvailableListener<T> listener) {
mListeners.remove(requireNonNull(listener));
if (!mListeners.isEmpty()) {
return false;
}
if (!mIsSingleShot) {
mHostDispatcher.dispatchUnsubscribeCarHardwareResult(mResultType, mBundle);
}
return true;
}
@Override
public void onCarHardwareResult(int resultType, boolean isSupported, @NonNull Bundleable result,
@NonNull IBinder callback) throws RemoteException {
IOnDoneCallback doneCallback = IOnDoneCallback.Stub.asInterface(callback);
RemoteUtils.dispatchCallFromHost(doneCallback, "onCarHardwareResult",
() -> {
notifyResults(isSupported, result);
return null;
});
}
private void notifyResults(boolean isSupported, @NonNull Bundleable result)
throws BundlerException {
T data = isSupported ? convertAndRecast(result) : mUnsupportedValue;
for (Map.Entry<OnCarDataAvailableListener<T>, Executor> entry: mListeners.entrySet()) {
entry.getValue().execute(() -> entry.getKey().onCarDataAvailable(data));
}
if (mIsSingleShot) {
mListeners.clear();
}
}
@SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof T
private T convertAndRecast(@NonNull Bundleable bundleable) throws BundlerException {
Object object = bundleable.get();
T data;
try {
data = (T) object;
} catch (ClassCastException e) {
throw new BundlerException("Incorrect type unbundled", e);
}
return data;
}
};