| /* |
| * 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.hardware.devicestate; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.util.ArrayMap; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.annotations.VisibleForTesting.Visibility; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Provides communication with the device state system service on behalf of applications. |
| * |
| * @see DeviceStateManager |
| * |
| * @hide |
| */ |
| @VisibleForTesting(visibility = Visibility.PACKAGE) |
| public final class DeviceStateManagerGlobal { |
| private static DeviceStateManagerGlobal sInstance; |
| |
| /** |
| * Returns an instance of {@link DeviceStateManagerGlobal}. May return {@code null} if a |
| * connection with the device state service couldn't be established. |
| */ |
| @Nullable |
| static DeviceStateManagerGlobal getInstance() { |
| synchronized (DeviceStateManagerGlobal.class) { |
| if (sInstance == null) { |
| IBinder b = ServiceManager.getService(Context.DEVICE_STATE_SERVICE); |
| if (b != null) { |
| sInstance = new DeviceStateManagerGlobal(IDeviceStateManager |
| .Stub.asInterface(b)); |
| } |
| } |
| return sInstance; |
| } |
| } |
| |
| private final Object mLock = new Object(); |
| @NonNull |
| private final IDeviceStateManager mDeviceStateManager; |
| @Nullable |
| private DeviceStateManagerCallback mCallback; |
| |
| @GuardedBy("mLock") |
| private final ArrayList<DeviceStateCallbackWrapper> mCallbacks = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final ArrayMap<IBinder, DeviceStateRequestWrapper> mRequests = new ArrayMap<>(); |
| |
| @Nullable |
| @GuardedBy("mLock") |
| private DeviceStateInfo mLastReceivedInfo; |
| |
| @VisibleForTesting |
| public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) { |
| mDeviceStateManager = deviceStateManager; |
| } |
| |
| /** |
| * Returns the set of supported device states. |
| * |
| * @see DeviceStateManager#getSupportedStates() |
| */ |
| public int[] getSupportedStates() { |
| synchronized (mLock) { |
| final DeviceStateInfo currentInfo; |
| if (mLastReceivedInfo != null) { |
| // If we have mLastReceivedInfo a callback is registered for this instance and it |
| // is receiving the most recent info from the server. Use that info here. |
| currentInfo = mLastReceivedInfo; |
| } else { |
| // If mLastReceivedInfo is null there is no registered callback so we manually |
| // fetch the current info. |
| try { |
| currentInfo = mDeviceStateManager.getDeviceStateInfo(); |
| } catch (RemoteException ex) { |
| throw ex.rethrowFromSystemServer(); |
| } |
| } |
| |
| return Arrays.copyOf(currentInfo.supportedStates, currentInfo.supportedStates.length); |
| } |
| } |
| |
| /** |
| * Submits a {@link DeviceStateRequest request} to modify the device state. |
| * |
| * @see DeviceStateManager#requestState(DeviceStateRequest, Executor, |
| * DeviceStateRequest.Callback) |
| * @see DeviceStateRequest |
| */ |
| public void requestState(@NonNull DeviceStateRequest request, |
| @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { |
| if (callback == null && executor != null) { |
| throw new IllegalArgumentException("Callback must be supplied with executor."); |
| } else if (executor == null && callback != null) { |
| throw new IllegalArgumentException("Executor must be supplied with callback."); |
| } |
| |
| synchronized (mLock) { |
| registerCallbackIfNeededLocked(); |
| |
| if (findRequestTokenLocked(request) != null) { |
| // This request has already been submitted. |
| return; |
| } |
| |
| // Add the request wrapper to the mRequests array before requesting the state as the |
| // callback could be triggered immediately if the mDeviceStateManager IBinder is in the |
| // same process as this instance. |
| IBinder token = new Binder(); |
| mRequests.put(token, new DeviceStateRequestWrapper(request, callback, executor)); |
| |
| try { |
| mDeviceStateManager.requestState(token, request.getState(), request.getFlags()); |
| } catch (RemoteException ex) { |
| mRequests.remove(token); |
| throw ex.rethrowFromSystemServer(); |
| } |
| } |
| } |
| |
| /** |
| * Cancels a {@link DeviceStateRequest request} previously submitted with a call to |
| * {@link #requestState(DeviceStateRequest, DeviceStateRequest.Callback, Executor)}. |
| * |
| * @see DeviceStateManager#cancelRequest(DeviceStateRequest) |
| */ |
| public void cancelRequest(@NonNull DeviceStateRequest request) { |
| synchronized (mLock) { |
| registerCallbackIfNeededLocked(); |
| |
| final IBinder token = findRequestTokenLocked(request); |
| if (token == null) { |
| // This request has not been submitted. |
| return; |
| } |
| |
| try { |
| mDeviceStateManager.cancelRequest(token); |
| } catch (RemoteException ex) { |
| throw ex.rethrowFromSystemServer(); |
| } |
| } |
| } |
| |
| /** |
| * Registers a callback to receive notifications about changes in device state. |
| * |
| * @see DeviceStateManager#registerCallback(Executor, DeviceStateCallback) |
| */ |
| @VisibleForTesting(visibility = Visibility.PACKAGE) |
| public void registerDeviceStateCallback(@NonNull DeviceStateCallback callback, |
| @NonNull Executor executor) { |
| synchronized (mLock) { |
| int index = findCallbackLocked(callback); |
| if (index != -1) { |
| // This callback is already registered. |
| return; |
| } |
| |
| registerCallbackIfNeededLocked(); |
| |
| // Add the callback wrapper to the mCallbacks array after registering the callback as |
| // the callback could be triggered immediately if the mDeviceStateManager IBinder is in |
| // the same process as this instance. |
| DeviceStateCallbackWrapper wrapper = new DeviceStateCallbackWrapper(callback, executor); |
| mCallbacks.add(wrapper); |
| |
| if (mLastReceivedInfo != null) { |
| // Copy the array to prevent the callback from modifying the internal state. |
| final int[] supportedStates = Arrays.copyOf(mLastReceivedInfo.supportedStates, |
| mLastReceivedInfo.supportedStates.length); |
| wrapper.notifySupportedStatesChanged(supportedStates); |
| wrapper.notifyBaseStateChanged(mLastReceivedInfo.baseState); |
| wrapper.notifyStateChanged(mLastReceivedInfo.currentState); |
| } |
| } |
| } |
| |
| /** |
| * Unregisters a callback previously registered with |
| * {@link #registerDeviceStateCallback(DeviceStateCallback, Executor)}}. |
| * |
| * @see DeviceStateManager#unregisterCallback(DeviceStateCallback) |
| */ |
| @VisibleForTesting(visibility = Visibility.PACKAGE) |
| public void unregisterDeviceStateCallback(@NonNull DeviceStateCallback callback) { |
| synchronized (mLock) { |
| int indexToRemove = findCallbackLocked(callback); |
| if (indexToRemove != -1) { |
| mCallbacks.remove(indexToRemove); |
| } |
| } |
| } |
| |
| private void registerCallbackIfNeededLocked() { |
| if (mCallback == null) { |
| mCallback = new DeviceStateManagerCallback(); |
| try { |
| mDeviceStateManager.registerCallback(mCallback); |
| } catch (RemoteException ex) { |
| mCallback = null; |
| throw ex.rethrowFromSystemServer(); |
| } |
| } |
| } |
| |
| private int findCallbackLocked(DeviceStateCallback callback) { |
| for (int i = 0; i < mCallbacks.size(); i++) { |
| if (mCallbacks.get(i).mDeviceStateCallback.equals(callback)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @Nullable |
| private IBinder findRequestTokenLocked(@NonNull DeviceStateRequest request) { |
| for (int i = 0; i < mRequests.size(); i++) { |
| if (mRequests.valueAt(i).mRequest.equals(request)) { |
| return mRequests.keyAt(i); |
| } |
| } |
| return null; |
| } |
| |
| /** Handles a call from the server that the device state info has changed. */ |
| private void handleDeviceStateInfoChanged(@NonNull DeviceStateInfo info) { |
| ArrayList<DeviceStateCallbackWrapper> callbacks; |
| DeviceStateInfo oldInfo; |
| synchronized (mLock) { |
| oldInfo = mLastReceivedInfo; |
| mLastReceivedInfo = info; |
| callbacks = new ArrayList<>(mCallbacks); |
| } |
| |
| final int diff = oldInfo == null ? ~0 : info.diff(oldInfo); |
| if ((diff & DeviceStateInfo.CHANGED_SUPPORTED_STATES) > 0) { |
| for (int i = 0; i < callbacks.size(); i++) { |
| // Copy the array to prevent callbacks from modifying the internal state. |
| final int[] supportedStates = Arrays.copyOf(info.supportedStates, |
| info.supportedStates.length); |
| callbacks.get(i).notifySupportedStatesChanged(supportedStates); |
| } |
| } |
| if ((diff & DeviceStateInfo.CHANGED_BASE_STATE) > 0) { |
| for (int i = 0; i < callbacks.size(); i++) { |
| callbacks.get(i).notifyBaseStateChanged(info.baseState); |
| } |
| } |
| if ((diff & DeviceStateInfo.CHANGED_CURRENT_STATE) > 0) { |
| for (int i = 0; i < callbacks.size(); i++) { |
| callbacks.get(i).notifyStateChanged(info.currentState); |
| } |
| } |
| } |
| |
| /** |
| * Handles a call from the server that a request for the supplied {@code token} has become |
| * active. |
| */ |
| private void handleRequestActive(IBinder token) { |
| DeviceStateRequestWrapper request; |
| synchronized (mLock) { |
| request = mRequests.get(token); |
| } |
| if (request != null) { |
| request.notifyRequestActive(); |
| } |
| } |
| |
| /** |
| * Handles a call from the server that a request for the supplied {@code token} has become |
| * suspended. |
| */ |
| private void handleRequestSuspended(IBinder token) { |
| DeviceStateRequestWrapper request; |
| synchronized (mLock) { |
| request = mRequests.get(token); |
| } |
| if (request != null) { |
| request.notifyRequestSuspended(); |
| } |
| } |
| |
| /** |
| * Handles a call from the server that a request for the supplied {@code token} has become |
| * canceled. |
| */ |
| private void handleRequestCanceled(IBinder token) { |
| DeviceStateRequestWrapper request; |
| synchronized (mLock) { |
| request = mRequests.remove(token); |
| } |
| if (request != null) { |
| request.notifyRequestCanceled(); |
| } |
| } |
| |
| private final class DeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub { |
| @Override |
| public void onDeviceStateInfoChanged(DeviceStateInfo info) { |
| handleDeviceStateInfoChanged(info); |
| } |
| |
| @Override |
| public void onRequestActive(IBinder token) { |
| handleRequestActive(token); |
| } |
| |
| @Override |
| public void onRequestSuspended(IBinder token) { |
| handleRequestSuspended(token); |
| } |
| |
| @Override |
| public void onRequestCanceled(IBinder token) { |
| handleRequestCanceled(token); |
| } |
| } |
| |
| private static final class DeviceStateCallbackWrapper { |
| @NonNull |
| private final DeviceStateCallback mDeviceStateCallback; |
| @NonNull |
| private final Executor mExecutor; |
| |
| DeviceStateCallbackWrapper(@NonNull DeviceStateCallback callback, |
| @NonNull Executor executor) { |
| mDeviceStateCallback = callback; |
| mExecutor = executor; |
| } |
| |
| void notifySupportedStatesChanged(int[] newSupportedStates) { |
| mExecutor.execute(() -> |
| mDeviceStateCallback.onSupportedStatesChanged(newSupportedStates)); |
| } |
| |
| void notifyBaseStateChanged(int newBaseState) { |
| mExecutor.execute(() -> mDeviceStateCallback.onBaseStateChanged(newBaseState)); |
| } |
| |
| void notifyStateChanged(int newDeviceState) { |
| mExecutor.execute(() -> mDeviceStateCallback.onStateChanged(newDeviceState)); |
| } |
| } |
| |
| private static final class DeviceStateRequestWrapper { |
| private final DeviceStateRequest mRequest; |
| @Nullable |
| private final DeviceStateRequest.Callback mCallback; |
| @Nullable |
| private final Executor mExecutor; |
| |
| DeviceStateRequestWrapper(@NonNull DeviceStateRequest request, |
| @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { |
| mRequest = request; |
| mCallback = callback; |
| mExecutor = executor; |
| } |
| |
| void notifyRequestActive() { |
| if (mCallback == null) { |
| return; |
| } |
| |
| mExecutor.execute(() -> mCallback.onRequestActivated(mRequest)); |
| } |
| |
| void notifyRequestSuspended() { |
| if (mCallback == null) { |
| return; |
| } |
| |
| mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest)); |
| } |
| |
| void notifyRequestCanceled() { |
| if (mCallback == null) { |
| return; |
| } |
| |
| mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest)); |
| } |
| } |
| } |