diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 84891ec7fcd44..666323766c12c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -108,7 +108,7 @@ public class ScreenRecordTile extends QSTileImpl Log.d(TAG, "Starting countdown"); // Close QS, otherwise the permission dialog appears beneath it getHost().collapsePanels(); - mController.launchRecordPrompt(this); + mController.launchRecordPrompt(); } private void cancelCountdown() { @@ -128,6 +128,11 @@ public class ScreenRecordTile extends QSTileImpl refreshState(); } + @Override + public void onCountdownEnd() { + refreshState(); + } + @Override public void onRecordingStart() { refreshState(); diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index 6ad9c40ccaf41..8dad08e49d80a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.os.CountDownTimer; import android.util.Log; -import com.android.systemui.qs.tiles.ScreenRecordTile; import com.android.systemui.statusbar.policy.CallbackController; import java.util.ArrayList; @@ -62,7 +61,7 @@ public class RecordingController /** * Show dialog of screen recording options to user. */ - public void launchRecordPrompt(ScreenRecordTile tileToUpdate) { + public void launchRecordPrompt() { final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE, SYSUI_SCREENRECORD_LAUNCHER); final Intent intent = new Intent(); @@ -73,15 +72,17 @@ public class RecordingController /** * Start counting down in preparation to start a recording - * @param ms Time in ms to count down + * @param ms Total time in ms to wait before starting + * @param interval Time in ms per countdown step * @param startIntent Intent to start a recording * @param stopIntent Intent to stop a recording */ - public void startCountdown(long ms, PendingIntent startIntent, PendingIntent stopIntent) { + public void startCountdown(long ms, long interval, PendingIntent startIntent, + PendingIntent stopIntent) { mIsStarting = true; mStopIntent = stopIntent; - mCountDownTimer = new CountDownTimer(ms, 1000) { + mCountDownTimer = new CountDownTimer(ms, interval) { @Override public void onTick(long millisUntilFinished) { for (RecordingStateChangeCallback cb : mListeners) { @@ -94,7 +95,7 @@ public class RecordingController mIsStarting = false; mIsRecording = true; for (RecordingStateChangeCallback cb : mListeners) { - cb.onRecordingEnd(); + cb.onCountdownEnd(); } try { startIntent.send(); @@ -120,7 +121,7 @@ public class RecordingController mIsStarting = false; for (RecordingStateChangeCallback cb : mListeners) { - cb.onRecordingEnd(); + cb.onCountdownEnd(); } } @@ -144,16 +145,12 @@ public class RecordingController * Stop the recording */ public void stopRecording() { - updateState(false); try { mStopIntent.send(); + updateState(false); } catch (PendingIntent.CanceledException e) { Log.e(TAG, "Error stopping: " + e.getMessage()); } - - for (RecordingStateChangeCallback cb : mListeners) { - cb.onRecordingEnd(); - } } /** @@ -192,6 +189,12 @@ public class RecordingController */ default void onCountdown(long millisUntilFinished) {} + /** + * Called when a countdown to recording has ended. This is a separate method so that if + * needed, listeners can handle cases where recording fails to start + */ + default void onCountdownEnd() {} + /** * Called when a screen recording has started */ diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 566f12b427957..26973d092209c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -34,6 +34,7 @@ import javax.inject.Inject; */ public class ScreenRecordDialog extends Activity { private static final long DELAY_MS = 3000; + private static final long INTERVAL_MS = 1000; private final RecordingController mController; private Switch mAudioSwitch; @@ -83,6 +84,6 @@ public class ScreenRecordDialog extends Activity { RecordingService.REQUEST_CODE, RecordingService.getStopIntent(this), PendingIntent.FLAG_UPDATE_CURRENT); - mController.startCountdown(DELAY_MS, startIntent, stopIntent); + mController.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 260f94cdcfcf9..1ab36c5e37601 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -677,6 +677,12 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotScreenRecord, true); } + @Override + public void onCountdownEnd() { + if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown"); + mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); + } + @Override public void onRecordingStart() { if (DEBUG) Log.d(TAG, "screenrecord: showing icon"); @@ -687,7 +693,7 @@ public class PhoneStatusBarPolicy @Override public void onRecordingEnd() { - // Ensure this is on the main thread, since it could be called during countdown + // Ensure this is on the main thread if (DEBUG) Log.d(TAG, "screenrecord: hiding icon"); mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java new file mode 100644 index 0000000000000..e8e98b49c47fc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -0,0 +1,136 @@ +/* + * 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 com.android.systemui.qs.tiles; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.service.quicksettings.Tile; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.qs.QSTileHost; +import com.android.systemui.screenrecord.RecordingController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class ScreenRecordTileTest extends SysuiTestCase { + + @Mock + private RecordingController mController; + @Mock + private QSTileHost mHost; + + private TestableLooper mTestableLooper; + private ScreenRecordTile mTile; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mTestableLooper = TestableLooper.get(this); + mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); + mController = mDependency.injectMockDependency(RecordingController.class); + + when(mHost.getContext()).thenReturn(mContext); + + mTile = new ScreenRecordTile(mHost, mController); + } + + // Test that the tile is inactive and labeled correctly when the controller is neither starting + // or recording, and that clicking on the tile in this state brings up the record prompt + @Test + public void testNotActive() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + + mTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertEquals(Tile.STATE_INACTIVE, mTile.getState().state); + assertTrue(mTile.getState().secondaryLabel.toString().equals( + mContext.getString(R.string.quick_settings_screen_record_start))); + + mTile.handleClick(); + verify(mController, times(1)).launchRecordPrompt(); + } + + // Test that the tile is active and labeled correctly when the controller is starting + @Test + public void testIsStarting() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + + mTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertEquals(Tile.STATE_ACTIVE, mTile.getState().state); + assertTrue(mTile.getState().secondaryLabel.toString().endsWith("...")); + } + + // Test that the tile cancels countdown if it is clicked when the controller is starting + @Test + public void testCancelRecording() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + + mTile.handleClick(); + + verify(mController, times(1)).cancelCountdown(); + } + + // Test that the tile is active and labeled correctly when the controller is recording + @Test + public void testIsRecording() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + + mTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertEquals(Tile.STATE_ACTIVE, mTile.getState().state); + assertTrue(mTile.getState().secondaryLabel.toString().equals( + mContext.getString(R.string.quick_settings_screen_record_stop))); + } + + // Test that the tile stops the recording if it is clicked when the controller is recording + @Test + public void testStopRecording() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + + mTile.handleClick(); + + verify(mController, times(1)).stopRecording(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java new file mode 100644 index 0000000000000..b877c7fa6859f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -0,0 +1,124 @@ +/* + * 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 com.android.systemui.screenrecord; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.os.Looper; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +/** + * Tests for exception handling and bitmap configuration in adding smart actions to Screenshot + * Notification. + */ +public class RecordingControllerTest extends SysuiTestCase { + + @Mock + RecordingController.RecordingStateChangeCallback mCallback; + + RecordingController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new RecordingController(mContext); + mController.addCallback(mCallback); + } + + // Test that when a countdown in progress is cancelled, the controller goes from starting to not + // starting, and notifies listeners. + @Test + public void testCancelCountdown() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + mController.startCountdown(100, 10, null, null); + + assertTrue(mController.isStarting()); + assertFalse(mController.isRecording()); + + mController.cancelCountdown(); + + assertFalse(mController.isStarting()); + assertFalse(mController.isRecording()); + + verify(mCallback).onCountdownEnd(); + } + + // Test that when recording is started, the start intent is sent and listeners are notified. + @Test + public void testStartRecording() throws PendingIntent.CanceledException { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + PendingIntent startIntent = Mockito.mock(PendingIntent.class); + mController.startCountdown(0, 0, startIntent, null); + + verify(mCallback).onCountdownEnd(); + verify(startIntent).send(); + } + + // Test that when recording is stopped, the stop intent is sent and listeners are notified. + @Test + public void testStopRecording() throws PendingIntent.CanceledException { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + PendingIntent startIntent = Mockito.mock(PendingIntent.class); + PendingIntent stopIntent = Mockito.mock(PendingIntent.class); + + mController.startCountdown(0, 0, startIntent, stopIntent); + mController.stopRecording(); + + assertFalse(mController.isStarting()); + assertFalse(mController.isRecording()); + verify(stopIntent).send(); + verify(mCallback).onRecordingEnd(); + } + + // Test that updating the controller state works and notifies listeners. + @Test + public void testUpdateState() { + mController.updateState(true); + assertTrue(mController.isRecording()); + verify(mCallback).onRecordingStart(); + + mController.updateState(false); + assertFalse(mController.isRecording()); + verify(mCallback).onRecordingEnd(); + } +}