Loading android/app/src/com/android/bluetooth/btservice/AdapterService.java +7 −4 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ import static com.android.bluetooth.Utils.callerIsSystemOrActiveOrManagedUser; import static com.android.bluetooth.Utils.getBytesFromAddress; import static com.android.bluetooth.Utils.isDualModeAudioEnabled; import static com.android.bluetooth.Utils.isPackageNameAccurate; import static com.android.modules.utils.build.SdkLevel.isAtLeastV; import static java.util.Objects.requireNonNull; Loading Loading @@ -90,7 +91,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; import android.hardware.devicestate.DeviceStateManager; import android.os.AsyncTask; import android.os.BatteryStatsManager; import android.os.Binder; Loading Loading @@ -731,10 +732,10 @@ public class AdapterService extends Service { mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); if (Flags.adapterSuspendMgmt()) { if (Flags.adapterSuspendMgmt() && isAtLeastV()) { mAdapterSuspend = new AdapterSuspend( mNativeInterface, mLooper, getSystemService(DisplayManager.class)); mNativeInterface, mLooper, getSystemService(DeviceStateManager.class)); } invalidateBluetoothCaches(); Loading Loading @@ -1480,7 +1481,9 @@ public class AdapterService extends Service { } if (mAdapterSuspend != null) { if (Flags.adapterSuspendMgmt() && isAtLeastV()) { mAdapterSuspend.cleanup(); } mAdapterSuspend = null; } Loading android/app/src/com/android/bluetooth/btservice/AdapterSuspend.java +69 −31 Original line number Diff line number Diff line Loading @@ -21,16 +21,19 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE; import static java.util.Objects.requireNonNull; import android.hardware.display.DisplayManager; import android.annotation.NonNull; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import androidx.annotation.RequiresApi; import java.util.Arrays; import com.android.internal.annotations.VisibleForTesting; @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public class AdapterSuspend { private static final String TAG = "BtAdapterSuspend"; Loading @@ -39,42 +42,78 @@ public class AdapterSuspend { private static final long MASK_DISCONNECT_CMPLT = 1 << 4; private static final long MASK_MODE_CHANGE = 1 << 19; private boolean mSuspended = false; private static final String DEVICE_STATE_LAPTOP = "LAPTOP"; private static final String DEVICE_STATE_TABLET = "TABLET"; private static final String DEVICE_STATE_DOCKED = "DOCKED"; private static final String DEVICE_STATE_CLOSED = "CLOSED"; private static final String DEVICE_STATE_DISPLAY_OFF = "DISPLAY_OFF"; private final AdapterNativeInterface mAdapterNativeInterface; private final Looper mLooper; private final DisplayManager mDisplayManager; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) {} private final DeviceStateManager mDeviceStateManager; public final DeviceStateManager.DeviceStateCallback mDeviceStateCallback = new DeviceStateManager.DeviceStateCallback() { @Override public void onDisplayRemoved(int displayId) {} public void onDeviceStateChanged(@NonNull DeviceState state) { String nextState = state.getName(); Log.d(TAG, "Handle state transition: " + mCurrentState + " => " + nextState); if (mCurrentState.equals("None")) { mCurrentState = nextState; Log.i(TAG, "Initialize device state to " + nextState); return; } @Override public void onDisplayChanged(int displayId) { if (isScreenOn()) { switch (nextState) { case DEVICE_STATE_CLOSED -> { switch (mCurrentState) { case DEVICE_STATE_DISPLAY_OFF -> Log.d(TAG, "No action for state " + nextState); default -> handleSuspend(false); } } case DEVICE_STATE_DISPLAY_OFF -> { switch (mCurrentState) { case DEVICE_STATE_TABLET -> handleSuspend(false); case DEVICE_STATE_DOCKED, DEVICE_STATE_LAPTOP -> handleSuspend(true); default -> Log.d(TAG, "No action for state " + nextState); } } case DEVICE_STATE_LAPTOP, DEVICE_STATE_DOCKED, DEVICE_STATE_TABLET -> { switch (mCurrentState) { case DEVICE_STATE_CLOSED, DEVICE_STATE_DISPLAY_OFF -> handleResume(); } else { handleSuspend(); default -> Log.d(TAG, "No action for state " + nextState); } } default -> { Log.wtf(TAG, "Unknown state transition to " + nextState); return; } } mCurrentState = nextState; } }; private boolean mSuspended = false; // Value should be initialized when registering the mDeviceStateCallback. private String mCurrentState = "None"; private final AdapterNativeInterface mAdapterNativeInterface; public AdapterSuspend( AdapterNativeInterface adapterNativeInterface, Looper looper, DisplayManager displayManager) { DeviceStateManager deviceStateManager) { mAdapterNativeInterface = requireNonNull(adapterNativeInterface); mLooper = requireNonNull(looper); mDisplayManager = requireNonNull(displayManager); Handler handler = new Handler(requireNonNull(looper)); mDisplayManager.registerDisplayListener(mDisplayListener, new Handler(mLooper)); mDeviceStateManager = requireNonNull(deviceStateManager); mDeviceStateManager.registerCallback(handler::post, mDeviceStateCallback); } void cleanup() { mDisplayManager.unregisterDisplayListener(mDisplayListener); mDeviceStateManager.unregisterCallback(mDeviceStateCallback); } @VisibleForTesting Loading @@ -82,13 +121,8 @@ public class AdapterSuspend { return mSuspended; } private boolean isScreenOn() { return Arrays.stream(mDisplayManager.getDisplays()) .anyMatch(display -> display.getState() == Display.STATE_ON); } @VisibleForTesting void handleSuspend() { void handleSuspend(boolean allowWakeByHid) { if (mSuspended) { return; } Loading @@ -106,7 +140,11 @@ public class AdapterSuspend { mAdapterNativeInterface.clearEventFilter(); mAdapterNativeInterface.clearFilterAcceptList(); mAdapterNativeInterface.disconnectAllAcls(); if (allowWakeByHid) { mAdapterNativeInterface.allowWakeByHid(); Log.i(TAG, "configure wake by hid"); } Log.i(TAG, "ready to suspend"); } Loading android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterSuspendTest.java +5 −5 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.verify; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.devicestate.DeviceStateManager; import android.os.test.TestLooper; import androidx.test.InstrumentationRegistry; Loading @@ -44,7 +44,7 @@ import org.mockito.junit.MockitoRule; @RunWith(AndroidJUnit4.class) public class AdapterSuspendTest { private TestLooper mTestLooper; private DisplayManager mDisplayManager; private DeviceStateManager mDeviceStateManager; private AdapterSuspend mAdapterSuspend; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); Loading @@ -54,15 +54,15 @@ public class AdapterSuspendTest { public void setUp() throws Exception { Context context = InstrumentationRegistry.getTargetContext(); mTestLooper = new TestLooper(); mDisplayManager = context.getSystemService(DisplayManager.class); mDeviceStateManager = context.getSystemService(DeviceStateManager.class); mAdapterSuspend = new AdapterSuspend( mAdapterNativeInterface, mTestLooper.getLooper(), mDisplayManager); mAdapterNativeInterface, mTestLooper.getLooper(), mDeviceStateManager); } private void triggerSuspend() throws Exception { mAdapterSuspend.handleSuspend(); mAdapterSuspend.handleSuspend(true); } private void triggerResume() throws Exception { Loading Loading
android/app/src/com/android/bluetooth/btservice/AdapterService.java +7 −4 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ import static com.android.bluetooth.Utils.callerIsSystemOrActiveOrManagedUser; import static com.android.bluetooth.Utils.getBytesFromAddress; import static com.android.bluetooth.Utils.isDualModeAudioEnabled; import static com.android.bluetooth.Utils.isPackageNameAccurate; import static com.android.modules.utils.build.SdkLevel.isAtLeastV; import static java.util.Objects.requireNonNull; Loading Loading @@ -90,7 +91,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; import android.hardware.devicestate.DeviceStateManager; import android.os.AsyncTask; import android.os.BatteryStatsManager; import android.os.Binder; Loading Loading @@ -731,10 +732,10 @@ public class AdapterService extends Service { mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); if (Flags.adapterSuspendMgmt()) { if (Flags.adapterSuspendMgmt() && isAtLeastV()) { mAdapterSuspend = new AdapterSuspend( mNativeInterface, mLooper, getSystemService(DisplayManager.class)); mNativeInterface, mLooper, getSystemService(DeviceStateManager.class)); } invalidateBluetoothCaches(); Loading Loading @@ -1480,7 +1481,9 @@ public class AdapterService extends Service { } if (mAdapterSuspend != null) { if (Flags.adapterSuspendMgmt() && isAtLeastV()) { mAdapterSuspend.cleanup(); } mAdapterSuspend = null; } Loading
android/app/src/com/android/bluetooth/btservice/AdapterSuspend.java +69 −31 Original line number Diff line number Diff line Loading @@ -21,16 +21,19 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE; import static java.util.Objects.requireNonNull; import android.hardware.display.DisplayManager; import android.annotation.NonNull; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import androidx.annotation.RequiresApi; import java.util.Arrays; import com.android.internal.annotations.VisibleForTesting; @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) public class AdapterSuspend { private static final String TAG = "BtAdapterSuspend"; Loading @@ -39,42 +42,78 @@ public class AdapterSuspend { private static final long MASK_DISCONNECT_CMPLT = 1 << 4; private static final long MASK_MODE_CHANGE = 1 << 19; private boolean mSuspended = false; private static final String DEVICE_STATE_LAPTOP = "LAPTOP"; private static final String DEVICE_STATE_TABLET = "TABLET"; private static final String DEVICE_STATE_DOCKED = "DOCKED"; private static final String DEVICE_STATE_CLOSED = "CLOSED"; private static final String DEVICE_STATE_DISPLAY_OFF = "DISPLAY_OFF"; private final AdapterNativeInterface mAdapterNativeInterface; private final Looper mLooper; private final DisplayManager mDisplayManager; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) {} private final DeviceStateManager mDeviceStateManager; public final DeviceStateManager.DeviceStateCallback mDeviceStateCallback = new DeviceStateManager.DeviceStateCallback() { @Override public void onDisplayRemoved(int displayId) {} public void onDeviceStateChanged(@NonNull DeviceState state) { String nextState = state.getName(); Log.d(TAG, "Handle state transition: " + mCurrentState + " => " + nextState); if (mCurrentState.equals("None")) { mCurrentState = nextState; Log.i(TAG, "Initialize device state to " + nextState); return; } @Override public void onDisplayChanged(int displayId) { if (isScreenOn()) { switch (nextState) { case DEVICE_STATE_CLOSED -> { switch (mCurrentState) { case DEVICE_STATE_DISPLAY_OFF -> Log.d(TAG, "No action for state " + nextState); default -> handleSuspend(false); } } case DEVICE_STATE_DISPLAY_OFF -> { switch (mCurrentState) { case DEVICE_STATE_TABLET -> handleSuspend(false); case DEVICE_STATE_DOCKED, DEVICE_STATE_LAPTOP -> handleSuspend(true); default -> Log.d(TAG, "No action for state " + nextState); } } case DEVICE_STATE_LAPTOP, DEVICE_STATE_DOCKED, DEVICE_STATE_TABLET -> { switch (mCurrentState) { case DEVICE_STATE_CLOSED, DEVICE_STATE_DISPLAY_OFF -> handleResume(); } else { handleSuspend(); default -> Log.d(TAG, "No action for state " + nextState); } } default -> { Log.wtf(TAG, "Unknown state transition to " + nextState); return; } } mCurrentState = nextState; } }; private boolean mSuspended = false; // Value should be initialized when registering the mDeviceStateCallback. private String mCurrentState = "None"; private final AdapterNativeInterface mAdapterNativeInterface; public AdapterSuspend( AdapterNativeInterface adapterNativeInterface, Looper looper, DisplayManager displayManager) { DeviceStateManager deviceStateManager) { mAdapterNativeInterface = requireNonNull(adapterNativeInterface); mLooper = requireNonNull(looper); mDisplayManager = requireNonNull(displayManager); Handler handler = new Handler(requireNonNull(looper)); mDisplayManager.registerDisplayListener(mDisplayListener, new Handler(mLooper)); mDeviceStateManager = requireNonNull(deviceStateManager); mDeviceStateManager.registerCallback(handler::post, mDeviceStateCallback); } void cleanup() { mDisplayManager.unregisterDisplayListener(mDisplayListener); mDeviceStateManager.unregisterCallback(mDeviceStateCallback); } @VisibleForTesting Loading @@ -82,13 +121,8 @@ public class AdapterSuspend { return mSuspended; } private boolean isScreenOn() { return Arrays.stream(mDisplayManager.getDisplays()) .anyMatch(display -> display.getState() == Display.STATE_ON); } @VisibleForTesting void handleSuspend() { void handleSuspend(boolean allowWakeByHid) { if (mSuspended) { return; } Loading @@ -106,7 +140,11 @@ public class AdapterSuspend { mAdapterNativeInterface.clearEventFilter(); mAdapterNativeInterface.clearFilterAcceptList(); mAdapterNativeInterface.disconnectAllAcls(); if (allowWakeByHid) { mAdapterNativeInterface.allowWakeByHid(); Log.i(TAG, "configure wake by hid"); } Log.i(TAG, "ready to suspend"); } Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterSuspendTest.java +5 −5 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.verify; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.devicestate.DeviceStateManager; import android.os.test.TestLooper; import androidx.test.InstrumentationRegistry; Loading @@ -44,7 +44,7 @@ import org.mockito.junit.MockitoRule; @RunWith(AndroidJUnit4.class) public class AdapterSuspendTest { private TestLooper mTestLooper; private DisplayManager mDisplayManager; private DeviceStateManager mDeviceStateManager; private AdapterSuspend mAdapterSuspend; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); Loading @@ -54,15 +54,15 @@ public class AdapterSuspendTest { public void setUp() throws Exception { Context context = InstrumentationRegistry.getTargetContext(); mTestLooper = new TestLooper(); mDisplayManager = context.getSystemService(DisplayManager.class); mDeviceStateManager = context.getSystemService(DeviceStateManager.class); mAdapterSuspend = new AdapterSuspend( mAdapterNativeInterface, mTestLooper.getLooper(), mDisplayManager); mAdapterNativeInterface, mTestLooper.getLooper(), mDeviceStateManager); } private void triggerSuspend() throws Exception { mAdapterSuspend.handleSuspend(); mAdapterSuspend.handleSuspend(true); } private void triggerResume() throws Exception { Loading