/* * Copyright (C) 2015 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 com.android.camera.device; import com.android.camera.async.Lifetime; import com.android.camera.async.SafeCloseable; import com.android.camera.debug.Log.Tag; import com.android.camera.debug.Logger; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** * Internal state machine for dealing with the interactions of opening * a physical device. Since there are 4 device states and 2 target * states the transition table looks like this: * * Device | Target * Opening Opened -> Nothing. * Opened Opened -> Execute onDeviceOpened. * Closing Opened -> Nothing. * Closed Opened -> Execute onDeviceOpening. * Device moves to Opening. * Opening Closed -> Nothing. * Opened Closed -> Execute onDeviceClosing. * Device moves to Closing. * Closing Closed -> Nothing. * Closed Closed -> Execute onDeviceClosed. * */ @ThreadSafe @ParametersAreNonnullByDefault public class SingleDeviceStateMachine implements SingleDeviceCloseListener, SingleDeviceOpenListener { private static final Tag TAG = new Tag("DeviceStateM"); /** Physical state of the device. */ private enum DeviceState { OPENING, OPENED, CLOSING, CLOSED } /** Physical state the state machine should reach. */ private enum TargetState { OPENED, CLOSED } private final ReentrantLock mLock; private final Lifetime mDeviceLifetime; private final SingleDeviceActions mDeviceActions; private final SingleDeviceShutdownListener mShutdownListener; private final TKey mDeviceKey; private final Logger mLogger; @GuardedBy("mLock") private boolean mIsShutdown; @GuardedBy("mLock") private TargetState mTargetState; @GuardedBy("mLock") private DeviceState mDeviceState; @Nullable @GuardedBy("mLock") private SingleDeviceRequest mDeviceRequest; @Nullable @GuardedBy("mLock") private TDevice mOpenDevice; /** * This creates a new state machine with a listener to represent * the physical states of a device. Both the target and current * state of the device are initially set to "Closed" */ public SingleDeviceStateMachine(SingleDeviceActions deviceActions, TKey deviceKey, SingleDeviceShutdownListener deviceShutdownListener, Logger.Factory logFactory) { mDeviceActions = deviceActions; mShutdownListener = deviceShutdownListener; mDeviceKey = deviceKey; mLock = new ReentrantLock(); mDeviceLifetime = new Lifetime(); mLogger = logFactory.create(TAG); mIsShutdown = false; mTargetState = TargetState.CLOSED; mDeviceState = DeviceState.CLOSED; } /** * Request that the state machine move towards an open state. */ public void requestOpen() { mLock.lock(); try { if (mIsShutdown) { return; } mTargetState = TargetState.OPENED; update(); } finally { mLock.unlock(); } } /** * Request that the state machine move towards a closed state. */ public void requestClose() { mLock.lock(); try { if (mIsShutdown) { return; } mTargetState = TargetState.CLOSED; update(); } finally { mLock.unlock(); } } /** * When a new request is set, the previous request should be canceled * if it has not been completed. */ public void setRequest(final SingleDeviceRequest deviceRequest) { mLock.lock(); try { if (mIsShutdown) { deviceRequest.close(); return; } SingleDeviceRequest previous = mDeviceRequest; mDeviceRequest = deviceRequest; mDeviceLifetime.add(deviceRequest); deviceRequest.getLifetime().add(new SafeCloseable() { @Override public void close() { requestCloseIfCurrentRequest(deviceRequest); } }); if (mOpenDevice != null) { mDeviceRequest.set(mOpenDevice); } if (previous != null) { previous.close(); } } finally { mLock.unlock(); } } @Override public void onDeviceOpened(TDevice device) { mLock.lock(); try { if (mIsShutdown) { return; } mOpenDevice = device; mDeviceState = DeviceState.OPENED; update(); } finally { mLock.unlock(); } } @Override public void onDeviceOpenException(Throwable throwable) { mLock.lock(); try { if (mIsShutdown) { return; } closeRequestWithException(throwable); shutdown(); } finally { mLock.unlock(); } } @Override public void onDeviceOpenException(TDevice tDevice) { mLock.lock(); try { if (mIsShutdown) { return; } closeRequestWithException(new CameraOpenException(-1)); mDeviceState = DeviceState.CLOSING; mTargetState = TargetState.CLOSED; executeClose(tDevice); } finally { mLock.unlock(); } } @Override public void onDeviceClosed() { mLock.lock(); try { if (mIsShutdown) { return; } mOpenDevice = null; mDeviceState = DeviceState.CLOSED; update(); } finally { mLock.unlock(); } } @Override public void onDeviceClosingException(Throwable throwable) { mLock.lock(); try { if (mIsShutdown) { return; } closeRequestWithException(throwable); shutdown(); } finally { mLock.unlock(); } } @GuardedBy("mLock") private void update() { if (mIsShutdown) { return; } if (mDeviceState == DeviceState.CLOSED && mTargetState == TargetState.OPENED) { executeOpen(); } else if (mDeviceState == DeviceState.OPENED && mTargetState == TargetState.OPENED) { executeOpened(); } else if (mDeviceState == DeviceState.OPENED && mTargetState == TargetState.CLOSED) { executeClose(); } else if (mDeviceState == DeviceState.CLOSED && mTargetState == TargetState.CLOSED) { shutdown(); } } @GuardedBy("mLock") private void executeOpen() { mDeviceState = DeviceState.OPENING; try { mDeviceActions.executeOpen(this, mDeviceLifetime); } catch (Exception e) { onDeviceOpenException(e); } // TODO: Consider adding a timeout to the open call so that requests // are not left un-resolved. } @GuardedBy("mLock") private void executeOpened() { if(mDeviceRequest != null) { mDeviceRequest.set(mOpenDevice); } // TODO: Consider performing a shutdown if there is no open // device request. } @GuardedBy("mLock") private void executeClose() { // TODO: Consider adding a timeout to the close call so that requests // are not left un-resolved. final TDevice device = mOpenDevice; mOpenDevice = null; executeClose(device); } @GuardedBy("mLock") private void executeClose(@Nullable TDevice device) { if (device != null) { mDeviceState = DeviceState.CLOSING; mTargetState = TargetState.CLOSED; closeRequest(); try { mDeviceActions.executeClose(this, device); } catch (Exception e) { onDeviceClosingException(e); } } else { shutdown(); } } @GuardedBy("mLock") private void requestCloseIfCurrentRequest(SingleDeviceRequest request) { if (mDeviceRequest == null || mDeviceRequest == request) { requestClose(); } } @GuardedBy("mLock") private void closeRequestWithException(Throwable exception) { mOpenDevice = null; if (mDeviceRequest != null) { mLogger.w("There was a problem closing device: " + mDeviceKey, exception); mDeviceRequest.closeWithException(exception); mDeviceRequest = null; } } @GuardedBy("mLock") private void closeRequest() { if (mDeviceRequest != null) { mDeviceRequest.close(); } mDeviceRequest = null; } /** * Cancel requests, and set internal device state back to * a clean set of values. */ private void shutdown() { mLock.lock(); try { if (!mIsShutdown) { mIsShutdown = true; mLogger.i("Shutting down the device lifecycle for: " + mDeviceKey); mOpenDevice = null; mDeviceState = DeviceState.CLOSED; mTargetState = TargetState.CLOSED; closeRequest(); mDeviceLifetime.close(); mShutdownListener.onShutdown(mDeviceKey); } else { mLogger.w("Shutdown was called multiple times!"); } } finally { mLock.unlock(); } } }