/* * Copyright (C) 2018 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.hardware.power; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.car.Car; import android.car.CarManagerBase; import android.car.builtin.util.Slogf; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Pair; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.Executor; /** * API to receive power policy change notifications. */ public class CarPowerManager extends CarManagerBase { /** @hide */ public static final String TAG = CarPowerManager.class.getSimpleName(); private final Object mLock = new Object(); private final ICarPower mService; @GuardedBy("mLock") private final ArrayMap> mPolicyListenerMap = new ArrayMap<>(); // key: power component, value: number of listeners to have interest in the component @GuardedBy("mLock") private final SparseIntArray mInterestedComponentMap = new SparseIntArray(); private final ICarPowerPolicyListener mPolicyChangeBinderCallback = new ICarPowerPolicyListener.Stub() { @Override public void onPolicyChanged(CarPowerPolicy appliedPolicy, CarPowerPolicy accumulatedPolicy) { long identityToken = Binder.clearCallingIdentity(); notifyPowerPolicyListeners(appliedPolicy, accumulatedPolicy); Binder.restoreCallingIdentity(identityToken); } }; @GuardedBy("mLock") private CarPowerStateListener mListener; @GuardedBy("mLock") private CarPowerStateListenerWithCompletion mListenerWithCompletion; @GuardedBy("mLock") private CompletablePowerStateChangeFutureImpl mFuture; @GuardedBy("mLock") private ICarPowerStateListener mListenerToService; @GuardedBy("mLock") private Executor mExecutor; // The following power state definitions must match the ones located in the native // CarPowerManager: packages/services/Car/car-lib/native/include/CarPowerManager.h /** * Power state to represent the current one is unavailable, unknown, or invalid. * * @hide */ @SystemApi public static final int STATE_INVALID = 0; /** * Power state to represent Android is up, but waits for the vendor to give a signal to start * main functionality. * * @hide */ @SystemApi public static final int STATE_WAIT_FOR_VHAL = 1; /** * Power state to represent the system enters deep sleep (suspend to RAM). * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for suspend * enter is 5 seconds by default and can be configured by setting * {@code config_shutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_SUSPEND_ENTER = 2; /** * Power state to represent the system wakes up from suspend. * * @hide */ @SystemApi public static final int STATE_SUSPEND_EXIT = 3; /** * Power state to represent the system enters shutdown state. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for shutdown * enter is 5 seconds by default and can be configured by setting * {@code config_shutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_SHUTDOWN_ENTER = 5; /** * Power state to represent the system is at on state. * * @hide */ @SystemApi public static final int STATE_ON = 6; /** * Power state to represent the system is getting ready for shutdown or suspend. Application is * expected to cleanup and be ready to suspend. * *

The maximum duration of shutdown preprare is 15 minutes by default, and can be increased * by setting {@code maxGarageModeRunningDurationInSecs} in the car service resource. * * @hide */ @SystemApi public static final int STATE_SHUTDOWN_PREPARE = 7; /** * Power state to represent shutdown is cancelled, returning to normal state. * * @hide */ @SystemApi public static final int STATE_SHUTDOWN_CANCELLED = 8; /** * Power state to represent the system enters hibernation (suspend to disk) state. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for hibernation * enter is 5 seconds by default and can be configured by setting * {@code config_shutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_HIBERNATION_ENTER = 9; /** * Power state to represent the system wakes up from hibernation. * * @hide */ @SystemApi public static final int STATE_HIBERNATION_EXIT = 10; /** * Power state to represent system shutdown is initiated, but output components such as display * is still on. UI to show a device is about to shutdown can be presented at this state. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for pre shutdown * prepare is 5 seconds by default and can be configured by setting * {@code config_preShutdownPrepareTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_PRE_SHUTDOWN_PREPARE = 11; /** * Power state to represent car power management service and VHAL finish processing to enter * deep sleep and the device is about to sleep. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for post suspend * enter is 5 seconds by default and can be configured by setting * {@code config_postShutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_POST_SUSPEND_ENTER = 12; /** * Power state to represent car power management service and VHAL finish processing to shutdown * and the device is about to power off. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for post * shutdown enter is 5 seconds by default and can be configured by setting * {@code config_postShutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_POST_SHUTDOWN_ENTER = 13; /** * Power state to represent car power management service and VHAL finish processing to enter * hibernation and the device is about to hibernate. * *

In case of using {@link CarPowerStateListenerWithCompletion}, the timeout for post * hibernation enter is 5 seconds by default and can be configured by setting * {@code config_postShutdownEnterTimeout} in the car service resource. * * @hide */ @SystemApi public static final int STATE_POST_HIBERNATION_ENTER = 14; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "STATE_", value = { STATE_INVALID, STATE_WAIT_FOR_VHAL, STATE_SUSPEND_ENTER, STATE_SUSPEND_EXIT, STATE_SHUTDOWN_ENTER, STATE_ON, STATE_SHUTDOWN_PREPARE, STATE_SHUTDOWN_CANCELLED, STATE_HIBERNATION_ENTER, STATE_HIBERNATION_EXIT, STATE_PRE_SHUTDOWN_PREPARE, STATE_POST_SUSPEND_ENTER, STATE_POST_SHUTDOWN_ENTER, STATE_POST_HIBERNATION_ENTER, }) @Target({ElementType.TYPE_USE}) public @interface CarPowerState {} /** * An interface passed from {@link CarPowerStateListenerWithCompletion}. * *

The listener uses this interface to tell {@link CarPowerManager} that it completed the * task relevant to the power state change. * * @hide */ @SystemApi public interface CompletablePowerStateChangeFuture { /** * Tells {@link CarPowerManager} that the listener completed the task to handle the power * state change. */ void complete(); /** * Gets the timestamp when the timeout happens. * *

The timestamp is system elapsed time in milliseconds. */ long getExpirationTime(); } /** * Applications set a {@link CarPowerStateListener} for power state event updates. * * @hide */ @SystemApi public interface CarPowerStateListener { /** * Called when power state changes. * * @param state New power state of the system. */ void onStateChanged(@CarPowerState int state); } /** * Applications set a {@link CarPowerStateListenerWithCompletion} for power state * event updates where a {@link CompletablePowerStateChangeFuture} is used. * * @hide */ @SystemApi public interface CarPowerStateListenerWithCompletion { /** * Called when power state changes. * *

Some {@code state}s allow for completion and the listeners are supposed to tell the * completion of handling the power state change. Those states include: *

* If the listeners don't complete before the timeout expires, car power management service * moves to the next step, anyway. The timeout given to the listener can be queried by * {@link CompletablePowerStateChangeFuture#getExpirationTime()}. * * @param state New power state of the system. * @param future CompletablePowerStateChangeFuture used by listeners to notify * CarPowerManager that they are ready to move to the next step. Car power * management service waits until the listeners call * {@code CompletablePowerStateChangeFuture#complete()} or timeout happens. * In the case {@code state} doesn't allow for completion, {@code future} is * {@code null}. */ void onStateChanged(@CarPowerState int state, @Nullable CompletablePowerStateChangeFuture future); } /** * Listeners to receive power policy change. * *

Applications interested in power policy change register * {@code CarPowerPolicyListener} and will be notified when power policy changes. */ public interface CarPowerPolicyListener { /** * Called with {@link CarPowerPolicy} when power policy changes. * * @param policy The current power policy. */ void onPolicyChanged(@NonNull CarPowerPolicy policy); } /** * Gets an instance of the CarPowerManager. * *

Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead. * * @hide */ public CarPowerManager(Car car, IBinder service) { super(car); mService = ICarPower.Stub.asInterface(service); } /** * Requests power manager to shutdown in lieu of suspend at the next opportunity. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_POWER) public void requestShutdownOnNextSuspend() { try { mService.requestShutdownOnNextSuspend(); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Schedules next wake up time in CarPowerManagementService. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_POWER) public void scheduleNextWakeupTime(int seconds) { try { mService.scheduleNextWakeupTime(seconds); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Returns the current power state. * * @return One of the values defined in {@link CarPowerStateListener}. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CAR_POWER) public @CarPowerState int getPowerState() { try { return mService.getPowerState(); } catch (RemoteException e) { return handleRemoteExceptionFromCarService(e, STATE_INVALID); } } /** * Sets a listener to receive power state changes. Only one listener may be set at a * time for an instance of CarPowerManager. * *

The listener is assumed to completely handle the {@code onStateChanged} before returning. * * @param listener The listener which will receive the power state change. * @throws IllegalStateException When a listener is already set for the power state change. * @throws IllegalArgumentException When the given listener is null. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CAR_POWER) public void setListener(@NonNull @CallbackExecutor Executor executor, @NonNull CarPowerStateListener listener) { checkArgument(executor != null, "excutor cannot be null"); checkArgument(listener != null, "listener cannot be null"); synchronized (mLock) { if (mListener != null || mListenerWithCompletion != null) { throw new IllegalStateException("Listener must be cleared first"); } // Updates listener mListener = listener; mExecutor = executor; setServiceForListenerLocked(/* useCompletion= */ false); } } /** * Sets a listener to receive power state changes. Only one listener may be set at a time for an * instance of CarPowerManager. * *

For calls that require completion before continue, we attach a * {@link CompletablePowerStateChangeFuture} which is being used as a signal that caller is * finished and ready to proceed. * Once the future is completed, car power management service knows that the application has * handled the power state transition and moves to the next state. * * @param listener The listener which will receive the power state change. * @throws IllegalStateException When a listener is already set for the power state change. * @throws IllegalArgumentException When the given listener is null. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CONTROL_SHUTDOWN_PROCESS) public void setListenerWithCompletion(@NonNull @CallbackExecutor Executor executor, @NonNull CarPowerStateListenerWithCompletion listener) { checkArgument(executor != null, "executor cannot be null"); checkArgument(listener != null, "listener cannot be null"); synchronized (mLock) { if (mListener != null || mListenerWithCompletion != null) { throw new IllegalStateException("Listener must be cleared first"); } // Updates listener mListenerWithCompletion = listener; mExecutor = executor; setServiceForListenerLocked(/* useCompletion= */ true); } } /** * Removes the power state listener. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CAR_POWER) public void clearListener() { ICarPowerStateListener listenerToService; synchronized (mLock) { listenerToService = mListenerToService; mListenerToService = null; mListener = null; mListenerWithCompletion = null; mExecutor = null; cleanupFutureLocked(); } if (listenerToService == null) { Slogf.w(TAG, "clearListener: listener was not registered"); return; } try { mService.unregisterListener(listenerToService); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Gets the current power policy. * *

The returned power policy has ID of the power policy applied most recently. If no power * policy has been applied, the ID is an empty string. Note that enabled components and disabled * components might be different from those of the latest power policy applied. This is because * the returned power policy contains the current state of all power components determined by * applying power policies in an accumulative way. * * @return The power policy containing the latest state of all power components. */ @RequiresPermission(Car.PERMISSION_READ_CAR_POWER_POLICY) @Nullable public CarPowerPolicy getCurrentPowerPolicy() { try { return mService.getCurrentPowerPolicy(); } catch (RemoteException e) { return handleRemoteExceptionFromCarService(e, null); } } /** * Applies the given power policy. * *

Power components are turned on or off as specified in the given power policy. Power * policies are defined at {@code /vendor/etc/automotive/power_policy.xml}. * If the given power policy doesn't exist, this method throws * {@link java.lang.IllegalArgumentException}. * * @param policyId ID of power policy. * @throws IllegalArgumentException if {@code policyId} is null. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CONTROL_CAR_POWER_POLICY) public void applyPowerPolicy(@NonNull String policyId) { checkArgument(policyId != null, "Null policyId"); try { mService.applyPowerPolicy(policyId); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Sets the current power policy group. * *

Power policy group defines a rule to apply a certain power policy according to the power * state transition. For example, a power policy named "default_for_on" is supposed to be * applied when the power state becomes ON. This rule is specified in the power policy group. * Many power policy groups can be pre-defined, and one of them is set for the current one using * {@code setPowerPolicyGroup}. * * @param policyGroupId ID of power policy group. * @throws IllegalArgumentException if {@code policyGroupId} is null. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CONTROL_CAR_POWER_POLICY) public void setPowerPolicyGroup(@NonNull String policyGroupId) { checkArgument(policyGroupId != null, "Null policyGroupId"); try { mService.setPowerPolicyGroup(policyGroupId); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Subscribes to power policy change. * *

If the same listener is added with different filters, the listener is notified based on * the last added filter. * * @param executor Executor where the listener method is called. * @param listener Listener to be notified. * @param filter Filter specifying power components of interest. * @throws IllegalArgumentException if {@code executor}, {@code listener}, or {@code filter} is * null. */ @RequiresPermission(Car.PERMISSION_READ_CAR_POWER_POLICY) public void addPowerPolicyListener(@NonNull @CallbackExecutor Executor executor, @NonNull CarPowerPolicyFilter filter, @NonNull CarPowerPolicyListener listener) { assertPermission(Car.PERMISSION_READ_CAR_POWER_POLICY); checkArgument(executor != null, "Null executor"); checkArgument(filter != null, "Null filter"); checkArgument(listener != null, "Null listener"); boolean updateCallbackNeeded = false; CarPowerPolicyFilter newFilter = null; synchronized (mLock) { mPolicyListenerMap.remove(listener); int[] filterComponents = filter.getComponents().clone(); Pair pair = new Pair<>(executor, new CarPowerPolicyFilter(filterComponents)); mPolicyListenerMap.put(listener, pair); for (int i = 0; i < filterComponents.length; i++) { int key = filterComponents[i]; int currentCount = mInterestedComponentMap.get(key); if (currentCount == 0) { updateCallbackNeeded = true; mInterestedComponentMap.put(key, 1); } else { mInterestedComponentMap.put(key, currentCount + 1); } } if (updateCallbackNeeded) { newFilter = createFilterFromInterestedComponentsLocked(); } } if (updateCallbackNeeded) { updatePowerPolicyChangeCallback(newFilter); } } /** * Unsubscribes from power policy change. * * @param listener Listener that will not be notified any more. * @throws IllegalArgumentException if {@code listener} is null. */ @RequiresPermission(Car.PERMISSION_READ_CAR_POWER_POLICY) public void removePowerPolicyListener(@NonNull CarPowerPolicyListener listener) { assertPermission(Car.PERMISSION_READ_CAR_POWER_POLICY); checkArgument(listener != null, "Null listener"); boolean updateCallbackNeeded = false; CarPowerPolicyFilter filter = null; synchronized (mLock) { Pair pair = mPolicyListenerMap.remove(listener); if (pair == null) { return; } int[] filterComponents = pair.second.getComponents(); for (int i = 0; i < filterComponents.length; i++) { int key = filterComponents[i]; int currentCount = mInterestedComponentMap.get(key); if (currentCount == 0 || currentCount == 1) { mInterestedComponentMap.delete(key); updateCallbackNeeded = true; } else { mInterestedComponentMap.put(key, currentCount - 1); } } if (updateCallbackNeeded) { filter = createFilterFromInterestedComponentsLocked(); } } if (updateCallbackNeeded) { updatePowerPolicyChangeCallback(filter); } } /** * Turns on or off the individual display. * *

Changing the driver display is not allowed. * * @param displayId ID of the display * @param enable Display power state to set * @throws UnsupportedOperationException When trying to change the driver display power state. * * @hide */ @SystemApi @RequiresPermission(Car.PERMISSION_CAR_POWER) public void setDisplayPowerState(int displayId, boolean enable) { try { mService.setDisplayPowerState(displayId, enable); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Notifies that user activity has happened in the given display. * * @param displayId ID of the display * @hide */ @RequiresPermission(Car.PERMISSION_CAR_POWER) public void notifyUserActivity(int displayId) { try { mService.notifyUserActivity(displayId); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Returns whether listen completion is allowed for {@code state}. * * @hide */ @TestApi public static boolean isCompletionAllowed(@CarPowerState int state) { switch (state) { case CarPowerManager.STATE_PRE_SHUTDOWN_PREPARE: case CarPowerManager.STATE_SHUTDOWN_PREPARE: case CarPowerManager.STATE_SHUTDOWN_ENTER: case CarPowerManager.STATE_SUSPEND_ENTER: case CarPowerManager.STATE_HIBERNATION_ENTER: case CarPowerManager.STATE_POST_SHUTDOWN_ENTER: case CarPowerManager.STATE_POST_SUSPEND_ENTER: case CarPowerManager.STATE_POST_HIBERNATION_ENTER: return true; default: return false; } } @GuardedBy("mLock") private void setServiceForListenerLocked(boolean useCompletion) { if (mListenerToService == null) { ICarPowerStateListener listenerToService = new ICarPowerStateListener.Stub() { @Override public void onStateChanged(int state, long expirationTimeMs) throws RemoteException { if (useCompletion) { CarPowerStateListenerWithCompletion listenerWithCompletion; CompletablePowerStateChangeFuture future; Executor executor; synchronized (mLock) { // Updates CompletablePowerStateChangeFuture. This will recreate it or // just clean it up. updateFutureLocked(state, expirationTimeMs); listenerWithCompletion = mListenerWithCompletion; future = mFuture; executor = mExecutor; } // Notifies the user that the state has changed and supply a future. if (listenerWithCompletion != null && executor != null) { long identityToken = Binder.clearCallingIdentity(); try { executor.execute( () -> listenerWithCompletion.onStateChanged(state, future)); } finally { Binder.restoreCallingIdentity(identityToken); } } } else { CarPowerStateListener listener; Executor executor; synchronized (mLock) { listener = mListener; executor = mExecutor; } // Notifies the user without supplying a future. if (listener != null && executor != null) { long identityToken = Binder.clearCallingIdentity(); try { executor.execute(() -> listener.onStateChanged(state)); } finally { Binder.restoreCallingIdentity(identityToken); } } } } }; try { if (useCompletion) { mService.registerListenerWithCompletion(listenerToService); } else { mService.registerListener(listenerToService); } mListenerToService = listenerToService; } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } } @GuardedBy("mLock") private void updateFutureLocked(@CarPowerState int state, long expirationTimeMs) { cleanupFutureLocked(); if (isCompletionAllowed(state)) { // Creates a CompletablePowerStateChangeFuture and passes it to the listener. // When the listener completes, tells CarPowerManagementService that this action is // finished. mFuture = new CompletablePowerStateChangeFutureImpl(() -> { ICarPowerStateListener listenerToService; synchronized (mLock) { listenerToService = mListenerToService; } try { mService.finished(state, listenerToService); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } }, expirationTimeMs); } } @GuardedBy("mLock") private void cleanupFutureLocked() { if (mFuture != null) { mFuture.invalidate(); Slogf.w(TAG, "The current future becomes invalid"); mFuture = null; } } @GuardedBy("mLock") private CarPowerPolicyFilter createFilterFromInterestedComponentsLocked() { CarPowerPolicyFilter newFilter = null; int componentCount = mInterestedComponentMap.size(); if (componentCount != 0) { int[] components = new int[componentCount]; for (int i = 0; i < componentCount; i++) { components[i] = mInterestedComponentMap.keyAt(i); } newFilter = new CarPowerPolicyFilter(components); } return newFilter; } private void updatePowerPolicyChangeCallback(CarPowerPolicyFilter filter) { try { if (filter == null) { mService.removePowerPolicyListener(mPolicyChangeBinderCallback); } else { mService.addPowerPolicyListener(filter, mPolicyChangeBinderCallback); } } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } private void notifyPowerPolicyListeners(CarPowerPolicy appliedPolicy, CarPowerPolicy accumulatedPolicy) { ArrayList> listeners = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mPolicyListenerMap.size(); i++) { CarPowerPolicyListener listener = mPolicyListenerMap.keyAt(i); Pair pair = mPolicyListenerMap.valueAt(i); if (PowerComponentUtil.hasComponents(appliedPolicy, pair.second)) { listeners.add( new Pair(listener, pair.first)); } } } for (int i = 0; i < listeners.size(); i++) { Pair pair = listeners.get(i); pair.second.execute(() -> pair.first.onPolicyChanged(accumulatedPolicy)); } } private void assertPermission(String permission) { if (getContext().checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) { throw new SecurityException("requires " + permission); } } private void checkArgument(boolean test, String message) { if (!test) { throw new IllegalArgumentException(message); } } /** @hide */ @Override public void onCarDisconnected() { synchronized (mLock) { mListener = null; mListenerWithCompletion = null; } } private static final class CompletablePowerStateChangeFutureImpl implements CompletablePowerStateChangeFuture { private final Runnable mRunnableForCompletion; private final long mExpirationTimeMs; private final Object mCompletionLock = new Object(); @GuardedBy("mCompletionLock") private boolean mCanBeCompleted = true; private CompletablePowerStateChangeFutureImpl(Runnable runnable, long expirationTimeMs) { mRunnableForCompletion = Objects.requireNonNull(runnable); mExpirationTimeMs = expirationTimeMs; } @Override public void complete() { synchronized (mCompletionLock) { if (!mCanBeCompleted) { Slogf.w(TAG, "Cannot complete: already completed or invalid state"); return; } // Once completed, this instance cannot be completed again. mCanBeCompleted = false; } mRunnableForCompletion.run(); } @Override public long getExpirationTime() { return mExpirationTimeMs; } private void invalidate() { synchronized (mCompletionLock) { mCanBeCompleted = false; } } } }