diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9282925ecb5e8..10ac675d712c9 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -59,6 +59,7 @@ @string/status_bar_airplane @string/status_bar_battery @string/status_bar_sensors_off + @string/status_bar_screen_record rotate @@ -94,6 +95,7 @@ camera airplane sensors_off + screen_record diff --git a/packages/SystemUI/res/drawable/ic_screen_record_background.xml b/packages/SystemUI/res/drawable/ic_screen_record_background.xml new file mode 100644 index 0000000000000..9195305e6681d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_screen_record_background.xml @@ -0,0 +1,25 @@ + + + + diff --git a/packages/SystemUI/res/drawable/stat_sys_screen_record.xml b/packages/SystemUI/res/drawable/stat_sys_screen_record.xml new file mode 100644 index 0000000000000..486af9eaf554b --- /dev/null +++ b/packages/SystemUI/res/drawable/stat_sys_screen_record.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/stat_sys_screen_record_1.xml b/packages/SystemUI/res/drawable/stat_sys_screen_record_1.xml new file mode 100644 index 0000000000000..ab2314ebc5436 --- /dev/null +++ b/packages/SystemUI/res/drawable/stat_sys_screen_record_1.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/stat_sys_screen_record_2.xml b/packages/SystemUI/res/drawable/stat_sys_screen_record_2.xml new file mode 100644 index 0000000000000..8764ff9f5c4a8 --- /dev/null +++ b/packages/SystemUI/res/drawable/stat_sys_screen_record_2.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/stat_sys_screen_record_3.xml b/packages/SystemUI/res/drawable/stat_sys_screen_record_3.xml new file mode 100644 index 0000000000000..0ff4d9a720ebd --- /dev/null +++ b/packages/SystemUI/res/drawable/stat_sys_screen_record_3.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ec56c1fe2c053..15575a49bb5e7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1228,6 +1228,8 @@ 18dp 24dp + 14sp + 5dp 16sp 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 88a30a166c075..84891ec7fcd44 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -31,20 +31,26 @@ import javax.inject.Inject; /** * Quick settings tile for screen recording */ -public class ScreenRecordTile extends QSTileImpl { +public class ScreenRecordTile extends QSTileImpl + 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 { 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 { 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(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index 188501e410444..6ad9c40ccaf41 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -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 { 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 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() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScreenRecordDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScreenRecordDrawable.java new file mode 100644 index 0000000000000..44ef6b448002b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScreenRecordDrawable.java @@ -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(); + } +} 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 41d896856daad..260f94cdcfcf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -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)); + } }