Screen record status bar icon

Adds a status bar icon that will show for system ui screen recordings.
The icon displays a countdown and then a static recording indicator.

Bug: 137153302
Test: manual
Change-Id: Ie30b47c5b08bab042f92e9b29cc42a4a58b10ced
This commit is contained in:
Beth Thibodeau
2020-02-13 13:55:37 -05:00
parent 307f10a4fc
commit 8bfbf1ffef
12 changed files with 377 additions and 37 deletions

View File

@@ -59,6 +59,7 @@
<item><xliff:g id="id">@string/status_bar_airplane</xliff:g></item>
<item><xliff:g id="id">@string/status_bar_battery</xliff:g></item>
<item><xliff:g id="id">@string/status_bar_sensors_off</xliff:g></item>
<item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
</string-array>
<string translatable="false" name="status_bar_rotate">rotate</string>
@@ -94,6 +95,7 @@
<string translatable="false" name="status_bar_camera">camera</string>
<string translatable="false" name="status_bar_airplane">airplane</string>
<string translatable="false" name="status_bar_sensors_off">sensors_off</string>
<string translatable="false" name="status_bar_screen_record">screen_record</string>
<!-- Flag indicating whether the surface flinger has limited
alpha compositing functionality in hardware. If set, the window

View File

@@ -2908,6 +2908,7 @@
<java-symbol type="string" name="status_bar_microphone" />
<java-symbol type="string" name="status_bar_camera" />
<java-symbol type="string" name="status_bar_sensors_off" />
<java-symbol type="string" name="status_bar_screen_record" />
<!-- Locale picker -->
<java-symbol type="id" name="locale_search_menu" />

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorError"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M10,0L14,0A10,10 0,0 1,24 10L24,10A10,10 0,0 1,14 20L10,20A10,10 0,0 1,0 10L0,10A10,10 0,0 1,10 0z"
android:fillColor="@android:color/white"/>
</vector>

View File

@@ -0,0 +1,16 @@
<!--
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.
-->
<com.android.systemui.statusbar.ScreenRecordDrawable />

View File

@@ -0,0 +1,18 @@
<!--
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.
-->
<com.android.systemui.statusbar.ScreenRecordDrawable
level="1"
/>

View File

@@ -0,0 +1,18 @@
<!--
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.
-->
<com.android.systemui.statusbar.ScreenRecordDrawable
level="2"
/>

View File

@@ -0,0 +1,18 @@
<!--
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.
-->
<com.android.systemui.statusbar.ScreenRecordDrawable
level="3"
/>

View File

@@ -1228,6 +1228,8 @@
<!-- Screen Record -->
<dimen name="screenrecord_dialog_padding">18dp</dimen>
<dimen name="screenrecord_logo_size">24dp</dimen>
<dimen name="screenrecord_status_text_size">14sp</dimen>
<dimen name="screenrecord_status_icon_radius">5dp</dimen>
<dimen name="kg_user_switcher_text_size">16sp</dimen>
</resources>

View File

@@ -31,20 +31,26 @@ import javax.inject.Inject;
/**
* Quick settings tile for screen recording
*/
public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> {
public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
implements RecordingController.RecordingStateChangeCallback {
private static final String TAG = "ScreenRecordTile";
private RecordingController mController;
private long mMillisUntilFinished = 0;
private Callback mCallback = new Callback();
@Inject
public ScreenRecordTile(QSHost host, RecordingController controller) {
super(host);
mController = controller;
mController.observe(this, mCallback);
}
@Override
public BooleanState newTileState() {
return new BooleanState();
BooleanState state = new BooleanState();
state.label = mContext.getString(R.string.quick_settings_screen_record_label);
state.handlesLongClick = false;
return state;
}
@Override
@@ -59,24 +65,13 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> {
refreshState();
}
/**
* Refresh tile state
* @param millisUntilFinished Time until countdown completes, or 0 if not counting down
*/
public void refreshState(long millisUntilFinished) {
mMillisUntilFinished = millisUntilFinished;
refreshState();
}
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
boolean isStarting = mController.isStarting();
boolean isRecording = mController.isRecording();
state.label = mContext.getString(R.string.quick_settings_screen_record_label);
state.value = isRecording || isStarting;
state.state = (isRecording || isStarting) ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
state.handlesLongClick = false;
if (isRecording) {
state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
@@ -125,4 +120,22 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> {
Log.d(TAG, "Stopping recording from tile");
mController.stopRecording();
}
private final class Callback implements RecordingController.RecordingStateChangeCallback {
@Override
public void onCountdown(long millisUntilFinished) {
mMillisUntilFinished = millisUntilFinished;
refreshState();
}
@Override
public void onRecordingStart() {
refreshState();
}
@Override
public void onRecordingEnd() {
refreshState();
}
}
}

View File

@@ -24,6 +24,9 @@ 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;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -32,7 +35,8 @@ import javax.inject.Singleton;
* Helper class to initiate a screen recording
*/
@Singleton
public class RecordingController {
public class RecordingController
implements CallbackController<RecordingController.RecordingStateChangeCallback> {
private static final String TAG = "RecordingController";
private static final String SYSUI_PACKAGE = "com.android.systemui";
private static final String SYSUI_SCREENRECORD_LAUNCHER =
@@ -41,10 +45,11 @@ public class RecordingController {
private final Context mContext;
private boolean mIsStarting;
private boolean mIsRecording;
private ScreenRecordTile mTileToUpdate;
private PendingIntent mStopIntent;
private CountDownTimer mCountDownTimer = null;
private ArrayList<RecordingStateChangeCallback> mListeners = new ArrayList<>();
/**
* Create a new RecordingController
* @param context Context for the controller
@@ -63,10 +68,7 @@ public class RecordingController {
final Intent intent = new Intent();
intent.setComponent(launcherComponent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("com.android.systemui.screenrecord.EXTRA_SETTINGS_ONLY", true);
mContext.startActivity(intent);
mTileToUpdate = tileToUpdate;
}
/**
@@ -82,16 +84,21 @@ public class RecordingController {
mCountDownTimer = new CountDownTimer(ms, 1000) {
@Override
public void onTick(long millisUntilFinished) {
refreshTile(millisUntilFinished);
for (RecordingStateChangeCallback cb : mListeners) {
cb.onCountdown(millisUntilFinished);
}
}
@Override
public void onFinish() {
mIsStarting = false;
mIsRecording = true;
refreshTile();
for (RecordingStateChangeCallback cb : mListeners) {
cb.onRecordingEnd();
}
try {
startIntent.send();
Log.d(TAG, "sent start intent");
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
}
@@ -101,18 +108,6 @@ public class RecordingController {
mCountDownTimer.start();
}
private void refreshTile() {
refreshTile(0);
}
private void refreshTile(long millisUntilFinished) {
if (mTileToUpdate != null) {
mTileToUpdate.refreshState(millisUntilFinished);
} else {
Log.e(TAG, "No tile to refresh");
}
}
/**
* Cancel a countdown in progress. This will not stop the recording if it already started.
*/
@@ -123,7 +118,10 @@ public class RecordingController {
Log.e(TAG, "Timer was null");
}
mIsStarting = false;
refreshTile();
for (RecordingStateChangeCallback cb : mListeners) {
cb.onRecordingEnd();
}
}
/**
@@ -152,7 +150,10 @@ public class RecordingController {
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Error stopping: " + e.getMessage());
}
refreshTile();
for (RecordingStateChangeCallback cb : mListeners) {
cb.onRecordingEnd();
}
}
/**
@@ -161,6 +162,44 @@ public class RecordingController {
*/
public void updateState(boolean isRecording) {
mIsRecording = isRecording;
refreshTile();
for (RecordingStateChangeCallback cb : mListeners) {
if (isRecording) {
cb.onRecordingStart();
} else {
cb.onRecordingEnd();
}
}
}
@Override
public void addCallback(RecordingStateChangeCallback listener) {
mListeners.add(listener);
}
@Override
public void removeCallback(RecordingStateChangeCallback listener) {
mListeners.remove(listener);
}
/**
* A callback for changes in the screen recording state
*/
public interface RecordingStateChangeCallback {
/**
* Called when a countdown to recording has updated
*
* @param millisUntilFinished Time in ms remaining in the countdown
*/
default void onCountdown(long millisUntilFinished) {}
/**
* Called when a screen recording has started
*/
default void onRecordingStart() {}
/**
* Called when a screen recording has ended
*/
default void onRecordingEnd() {}
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.statusbar;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableWrapper;
import android.util.AttributeSet;
import com.android.systemui.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* The screen record drawable draws a colored background and either a countdown or circle to
* indicate that the screen is being recorded.
*/
public class ScreenRecordDrawable extends DrawableWrapper {
private Drawable mFillDrawable;
private int mHorizontalPadding;
private int mLevel;
private float mTextSize;
private float mIconRadius;
private Paint mPaint;
/** No-arg constructor used by drawable inflation. */
public ScreenRecordDrawable() {
super(null);
}
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs, theme);
setDrawable(r.getDrawable(R.drawable.ic_screen_record_background, theme).mutate());
mFillDrawable = r.getDrawable(R.drawable.ic_screen_record_background, theme).mutate();
mHorizontalPadding = r.getDimensionPixelSize(R.dimen.status_bar_horizontal_padding);
mTextSize = r.getDimensionPixelSize(R.dimen.screenrecord_status_text_size);
mIconRadius = r.getDimensionPixelSize(R.dimen.screenrecord_status_icon_radius);
mLevel = attrs.getAttributeIntValue(null, "level", 0);
mPaint = new Paint();
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(mTextSize);
mPaint.setFakeBoldText(true);
}
@Override
public boolean canApplyTheme() {
return mFillDrawable.canApplyTheme() || super.canApplyTheme();
}
@Override
public void applyTheme(Resources.Theme t) {
super.applyTheme(t);
mFillDrawable.applyTheme(t);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mFillDrawable.setBounds(bounds);
}
@Override
public boolean onLayoutDirectionChanged(int layoutDirection) {
mFillDrawable.setLayoutDirection(layoutDirection);
return super.onLayoutDirectionChanged(layoutDirection);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mFillDrawable.draw(canvas);
Rect b = mFillDrawable.getBounds();
if (mLevel > 0) {
String val = String.valueOf(mLevel);
Rect textBounds = new Rect();
mPaint.getTextBounds(val, 0, val.length(), textBounds);
float yOffset = textBounds.height() / 4; // half, and half again since it's centered
canvas.drawText(val, b.centerX(), b.centerY() + yOffset, mPaint);
} else {
canvas.drawCircle(b.centerX(), b.centerY() - mIconRadius / 2, mIconRadius, mPaint);
}
}
@Override
public boolean getPadding(Rect padding) {
padding.left += mHorizontalPadding;
padding.right += mHorizontalPadding;
padding.top = 0;
padding.bottom = 0;
android.util.Log.d("ScreenRecordDrawable", "set zero top/bottom pad");
return true;
}
@Override
public void setAlpha(int alpha) {
super.setAlpha(alpha);
mFillDrawable.setAlpha(alpha);
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
mFillDrawable.setVisible(visible, restart);
return super.setVisible(visible, restart);
}
@Override
public Drawable mutate() {
mFillDrawable.mutate();
return super.mutate();
}
}

View File

@@ -43,6 +43,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.BluetoothController;
import com.android.systemui.statusbar.policy.CastController;
@@ -76,7 +77,8 @@ public class PhoneStatusBarPolicy
ZenModeController.Callback,
DeviceProvisionedListener,
KeyguardStateController.Callback,
LocationController.LocationChangeCallback {
LocationController.LocationChangeCallback,
RecordingController.RecordingStateChangeCallback {
private static final String TAG = "PhoneStatusBarPolicy";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -98,6 +100,7 @@ public class PhoneStatusBarPolicy
private final String mSlotMicrophone;
private final String mSlotCamera;
private final String mSlotSensorsOff;
private final String mSlotScreenRecord;
private final Context mContext;
private final Handler mHandler = new Handler();
@@ -116,6 +119,7 @@ public class PhoneStatusBarPolicy
private final LocationController mLocationController;
private final Executor mUiBgExecutor;
private final SensorPrivacyController mSensorPrivacyController;
private final RecordingController mRecordingController;
// Assume it's all good unless we hear otherwise. We don't always seem
// to get broadcasts that it *is* there.
@@ -149,6 +153,7 @@ public class PhoneStatusBarPolicy
mKeyguardStateController = Dependency.get(KeyguardStateController.class);
mLocationController = Dependency.get(LocationController.class);
mSensorPrivacyController = Dependency.get(SensorPrivacyController.class);
mRecordingController = Dependency.get(RecordingController.class);
mUiBgExecutor = uiBgExecutor;
mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
@@ -167,6 +172,8 @@ public class PhoneStatusBarPolicy
mSlotMicrophone = context.getString(com.android.internal.R.string.status_bar_microphone);
mSlotCamera = context.getString(com.android.internal.R.string.status_bar_camera);
mSlotSensorsOff = context.getString(com.android.internal.R.string.status_bar_sensors_off);
mSlotScreenRecord = context.getString(
com.android.internal.R.string.status_bar_screen_record);
// listen for broadcasts
IntentFilter filter = new IntentFilter();
@@ -235,6 +242,10 @@ public class PhoneStatusBarPolicy
mIconController.setIconVisibility(mSlotSensorsOff,
mSensorPrivacyController.isSensorPrivacyEnabled());
// screen record
mIconController.setIcon(mSlotScreenRecord, R.drawable.stat_sys_screen_record, null);
mIconController.setIconVisibility(mSlotScreenRecord, false);
mRotationLockController.addCallback(this);
mBluetooth.addCallback(this);
mProvisionedController.addCallback(this);
@@ -246,6 +257,7 @@ public class PhoneStatusBarPolicy
mKeyguardStateController.addCallback(this);
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
mLocationController.addCallback(this);
mRecordingController.addCallback(this);
commandQueue.addCallback(this);
}
@@ -438,7 +450,7 @@ public class PhoneStatusBarPolicy
}
if (DEBUG) Log.v(TAG, "updateCast: isCasting: " + isCasting);
mHandler.removeCallbacks(mRemoveCastIconRunnable);
if (isCasting) {
if (isCasting && !mRecordingController.isRecording()) { // screen record has its own icon
mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast,
mContext.getString(R.string.accessibility_casting));
mIconController.setIconVisibility(mSlotCast, true);
@@ -643,4 +655,40 @@ public class PhoneStatusBarPolicy
mIconController.setIconVisibility(mSlotCast, false);
}
};
// Screen Recording
@Override
public void onCountdown(long millisUntilFinished) {
if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished);
int countdown = (int) Math.floorDiv(millisUntilFinished + 500, 1000);
int resourceId = R.drawable.stat_sys_screen_record;
switch (countdown) {
case 1:
resourceId = R.drawable.stat_sys_screen_record_1;
break;
case 2:
resourceId = R.drawable.stat_sys_screen_record_2;
break;
case 3:
resourceId = R.drawable.stat_sys_screen_record_3;
break;
}
mIconController.setIcon(mSlotScreenRecord, resourceId, null);
mIconController.setIconVisibility(mSlotScreenRecord, true);
}
@Override
public void onRecordingStart() {
if (DEBUG) Log.d(TAG, "screenrecord: showing icon");
mIconController.setIcon(mSlotScreenRecord,
R.drawable.stat_sys_screen_record, null);
mIconController.setIconVisibility(mSlotScreenRecord, true);
}
@Override
public void onRecordingEnd() {
// Ensure this is on the main thread, since it could be called during countdown
if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
}
}