Merge "Add screen recording QS tile"

This commit is contained in:
TreeHugger Robot
2020-01-15 21:29:10 +00:00
committed by Android (Google) Code Review
13 changed files with 395 additions and 79 deletions

View File

@@ -308,7 +308,8 @@
</receiver>
<activity android:name=".screenrecord.ScreenRecordDialog"
android:theme="@style/ScreenRecord" />
android:theme="@style/ScreenRecord"
android:excludeFromRecents="true" />
<service android:name=".screenrecord.RecordingService" />
<receiver android:name=".SysuiRestartReceiver"

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,10.48L18,6c0,-1.1 -0.9,-2 -2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11l-4,3.98zM16,9.69L16,18L4,18L4,6h12v3.69z"/>
</vector>

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="top"
android:orientation="vertical"
android:padding="@dimen/global_actions_padding"
android:background="@drawable/rounded_bg_full">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<CheckBox
android:id="@+id/checkbox_mic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/screenrecord_mic_label"/>
<CheckBox
android:id="@+id/checkbox_taps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/screenrecord_taps_label"/>
<Button
android:id="@+id/record_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/screenrecord_start_label"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -109,7 +109,7 @@
<!-- The default tiles to display in QuickSettings -->
<string name="quick_settings_tiles_default" translatable="false">
wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast
wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast,screenrecord
</string>
<!-- The minimum number of tiles to display in QuickSettings -->
@@ -117,7 +117,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,controls
wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,controls,screenrecord
</string>
<!-- The tiles to display in QuickSettings -->

View File

@@ -928,6 +928,13 @@
<!-- QuickSettings: NFC (on) [CHAR LIMIT=NONE] -->
<string name="quick_settings_nfc_on">NFC is enabled</string>
<!-- QuickSettings: Screen record tile [CHAR LIMIT=NONE] -->
<string name="quick_settings_screen_record_label">Screen Record</string>
<!-- QuickSettings: Text to prompt the user to begin a new recording [CHAR LIMIT=20] -->
<string name="quick_settings_screen_record_start">Start</string>
<!-- QuickSettings: Text to prompt the user to stop an ongoing recording [CHAR LIMIT=20] -->
<string name="quick_settings_screen_record_stop">Stop</string>
<!-- Recents: Text that shows above the navigation bar after launching a few apps. [CHAR LIMIT=NONE] -->
<string name="recents_swipe_up_onboarding">Swipe up to switch apps</string>
<!-- Recents: Text that shows above the navigation bar after launching several apps. [CHAR LIMIT=NONE] -->

View File

@@ -56,6 +56,7 @@ import com.android.systemui.power.EnhancedEstimates;
import com.android.systemui.power.PowerUI;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
@@ -323,6 +324,7 @@ public class Dependency {
@Inject Lazy<DisplayWindowController> mDisplayWindowController;
@Inject Lazy<SystemWindows> mSystemWindows;
@Inject Lazy<DisplayImeController> mDisplayImeController;
@Inject Lazy<RecordingController> mRecordingController;
@Inject
public Dependency() {
@@ -519,6 +521,8 @@ public class Dependency {
// Dependency problem.
mProviders.put(AutoHideController.class, mAutoHideController::get);
mProviders.put(RecordingController.class, mRecordingController::get);
sDependency = this;
}

View File

@@ -20,6 +20,7 @@ import android.app.Activity;
import com.android.systemui.ForegroundServicesDialog;
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.screenrecord.ScreenRecordDialog;
import com.android.systemui.settings.BrightnessDialog;
import com.android.systemui.tuner.TunerActivity;
@@ -29,7 +30,7 @@ import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
/**
* Services and Activities that are injectable should go here.
* Activities that are injectable should go here.
*/
@Module
public abstract class DefaultActivityBinder {
@@ -56,4 +57,10 @@ public abstract class DefaultActivityBinder {
@IntoMap
@ClassKey(BrightnessDialog.class)
public abstract Activity bindBrightnessDialog(BrightnessDialog activity);
/** Inject into ScreenRecordDialog */
@Binds
@IntoMap
@ClassKey(ScreenRecordDialog.class)
public abstract Activity bindScreenRecordDialog(ScreenRecordDialog activity);
}

View File

@@ -22,6 +22,7 @@ import com.android.systemui.ImageWallpaper;
import com.android.systemui.SystemUIService;
import com.android.systemui.doze.DozeService;
import com.android.systemui.keyguard.KeyguardService;
import com.android.systemui.screenrecord.RecordingService;
import com.android.systemui.screenshot.TakeScreenshotService;
import dagger.Binds;
@@ -63,4 +64,10 @@ public abstract class DefaultServiceBinder {
@IntoMap
@ClassKey(TakeScreenshotService.class)
public abstract Service bindTakeScreenshotService(TakeScreenshotService service);
/** Inject into RecordingService */
@Binds
@IntoMap
@ClassKey(RecordingService.class)
public abstract Service bindRecordingService(RecordingService service);
}

View File

@@ -42,6 +42,7 @@ import com.android.systemui.qs.tiles.LocationTile;
import com.android.systemui.qs.tiles.NfcTile;
import com.android.systemui.qs.tiles.NightDisplayTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.qs.tiles.ScreenRecordTile;
import com.android.systemui.qs.tiles.UiModeNightTile;
import com.android.systemui.qs.tiles.UserTile;
import com.android.systemui.qs.tiles.WifiTile;
@@ -77,6 +78,7 @@ public class QSFactoryImpl implements QSFactory {
private final Provider<NfcTile> mNfcTileProvider;
private final Provider<GarbageMonitor.MemoryTile> mMemoryTileProvider;
private final Provider<UiModeNightTile> mUiModeNightTileProvider;
private final Provider<ScreenRecordTile> mScreenRecordTileProvider;
private QSTileHost mHost;
@@ -100,7 +102,8 @@ public class QSFactoryImpl implements QSFactory {
Provider<NightDisplayTile> nightDisplayTileProvider,
Provider<NfcTile> nfcTileProvider,
Provider<GarbageMonitor.MemoryTile> memoryTileProvider,
Provider<UiModeNightTile> uiModeNightTileProvider) {
Provider<UiModeNightTile> uiModeNightTileProvider,
Provider<ScreenRecordTile> screenRecordTileProvider) {
mWifiTileProvider = wifiTileProvider;
mBluetoothTileProvider = bluetoothTileProvider;
mControlsTileProvider = controlsTileProvider;
@@ -121,6 +124,7 @@ public class QSFactoryImpl implements QSFactory {
mNfcTileProvider = nfcTileProvider;
mMemoryTileProvider = memoryTileProvider;
mUiModeNightTileProvider = uiModeNightTileProvider;
mScreenRecordTileProvider = screenRecordTileProvider;
}
public void setHost(QSTileHost host) {
@@ -179,6 +183,8 @@ public class QSFactoryImpl implements QSFactory {
return mNfcTileProvider.get();
case "dark":
return mUiModeNightTileProvider.get();
case "screenrecord":
return mScreenRecordTileProvider.get();
}
// Custom tiles

View File

@@ -0,0 +1,132 @@
/*
* 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 android.content.Intent;
import android.service.quicksettings.Tile;
import android.util.Log;
import com.android.systemui.R;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.screenrecord.RecordingController;
import javax.inject.Inject;
/**
* Quick settings tile for screen recording
*/
public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> {
private static final String TAG = "ScreenRecordTile";
private RecordingController mController;
private long mMillisUntilFinished = 0;
@Inject
public ScreenRecordTile(QSHost host, RecordingController controller) {
super(host);
mController = controller;
}
@Override
public BooleanState newTileState() {
return new BooleanState();
}
@Override
protected void handleClick() {
if (mController.isStarting()) {
cancelCountdown();
} else if (mController.isRecording()) {
stopRecording();
} else {
startCountdown();
}
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);
state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_stop);
} else if (isStarting) {
// round, since the timer isn't exact
int countdown = (int) Math.floorDiv(mMillisUntilFinished + 500, 1000);
// TODO update icon
state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
state.secondaryLabel = String.format("%d...", countdown);
} else {
// TODO update icon
state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord);
state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
}
}
@Override
public int getMetricsCategory() {
return 0;
}
@Override
public Intent getLongClickIntent() {
return null;
}
@Override
protected void handleSetListening(boolean listening) {
}
@Override
public CharSequence getTileLabel() {
return mContext.getString(R.string.quick_settings_screen_record_label);
}
private void startCountdown() {
Log.d(TAG, "Starting countdown");
// Close QS, otherwise the permission dialog appears beneath it
getHost().collapsePanels();
mController.launchRecordPrompt(this);
}
private void cancelCountdown() {
Log.d(TAG, "Cancelling countdown");
mController.cancelCountdown();
}
private void stopRecording() {
Log.d(TAG, "Stopping recording from tile");
mController.stopRecording();
}
}

View File

@@ -0,0 +1,166 @@
/*
* 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 android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.CountDownTimer;
import android.util.Log;
import com.android.systemui.qs.tiles.ScreenRecordTile;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Helper class to initiate a screen recording
*/
@Singleton
public class RecordingController {
private static final String TAG = "RecordingController";
private static final String SYSUI_PACKAGE = "com.android.systemui";
private static final String SYSUI_SCREENRECORD_LAUNCHER =
"com.android.systemui.screenrecord.ScreenRecordDialog";
private final Context mContext;
private boolean mIsStarting;
private boolean mIsRecording;
private ScreenRecordTile mTileToUpdate;
private PendingIntent mStopIntent;
private CountDownTimer mCountDownTimer = null;
/**
* Create a new RecordingController
* @param context Context for the controller
*/
@Inject
public RecordingController(Context context) {
mContext = context;
}
/**
* Show dialog of screen recording options to user.
*/
public void launchRecordPrompt(ScreenRecordTile tileToUpdate) {
final ComponentName launcherComponent = new ComponentName(SYSUI_PACKAGE,
SYSUI_SCREENRECORD_LAUNCHER);
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;
}
/**
* Start counting down in preparation to start a recording
* @param ms Time in ms to count down
* @param startIntent Intent to start a recording
* @param stopIntent Intent to stop a recording
*/
public void startCountdown(long ms, PendingIntent startIntent, PendingIntent stopIntent) {
mIsStarting = true;
mStopIntent = stopIntent;
mCountDownTimer = new CountDownTimer(ms, 1000) {
@Override
public void onTick(long millisUntilFinished) {
refreshTile(millisUntilFinished);
}
@Override
public void onFinish() {
mIsStarting = false;
mIsRecording = true;
refreshTile();
try {
startIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
}
}
};
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.
*/
public void cancelCountdown() {
if (mCountDownTimer != null) {
mCountDownTimer.cancel();
} else {
Log.e(TAG, "Timer was null");
}
mIsStarting = false;
refreshTile();
}
/**
* Check if the recording is currently counting down to begin
* @return
*/
public boolean isStarting() {
return mIsStarting;
}
/**
* Check if the recording is ongoing
* @return
*/
public boolean isRecording() {
return mIsRecording;
}
/**
* Stop the recording
*/
public void stopRecording() {
updateState(false);
try {
mStopIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Error stopping: " + e.getMessage());
}
refreshTile();
}
/**
* Update the current status
* @param isRecording
*/
public void updateState(boolean isRecording) {
mIsRecording = isRecording;
refreshTile();
}
}

View File

@@ -53,10 +53,14 @@ import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.inject.Inject;
/**
* A service which records the device screen and optionally microphone input.
*/
public class RecordingService extends Service {
public static final int REQUEST_CODE = 2;
private static final int NOTIFICATION_ID = 1;
private static final String TAG = "RecordingService";
private static final String CHANNEL_ID = "screen_record";
@@ -65,7 +69,6 @@ public class RecordingService extends Service {
private static final String EXTRA_PATH = "extra_path";
private static final String EXTRA_USE_AUDIO = "extra_useAudio";
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
private static final int REQUEST_CODE = 2;
private static final String ACTION_START = "com.android.systemui.screenrecord.START";
private static final String ACTION_STOP = "com.android.systemui.screenrecord.STOP";
@@ -81,6 +84,7 @@ public class RecordingService extends Service {
private static final int AUDIO_BIT_RATE = 16;
private static final int AUDIO_SAMPLE_RATE = 44100;
private final RecordingController mController;
private MediaProjectionManager mMediaProjectionManager;
private MediaProjection mMediaProjection;
private Surface mInputSurface;
@@ -92,6 +96,11 @@ public class RecordingService extends Service {
private boolean mShowTaps;
private File mTempFile;
@Inject
public RecordingService(RecordingController controller) {
mController = controller;
}
/**
* Get an intent to start the recording service.
*
@@ -272,6 +281,7 @@ public class RecordingService extends Service {
null);
mMediaRecorder.start();
mController.updateState(true);
} catch (IOException e) {
Log.e(TAG, "Error starting screen recording: " + e.getMessage());
e.printStackTrace();
@@ -285,7 +295,7 @@ public class RecordingService extends Service {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
getString(R.string.screenrecord_name),
NotificationManager.IMPORTANCE_HIGH);
NotificationManager.IMPORTANCE_LOW);
channel.setDescription(getString(R.string.screenrecord_channel_description));
channel.enableVibration(true);
NotificationManager notificationManager =
@@ -399,6 +409,7 @@ public class RecordingService extends Service {
mInputSurface.release();
mVirtualDisplay.release();
stopSelf();
mController.updateState(false);
}
private void saveRecording(NotificationManager notificationManager) {
@@ -439,7 +450,12 @@ public class RecordingService extends Service {
Settings.System.SHOW_TOUCHES, value);
}
private static Intent getStopIntent(Context context) {
/**
* Get an intent to stop the recording service.
* @param context Context from the requesting activity
* @return
*/
public static Intent getStopIntent(Context context) {
return new Intent(context, RecordingService.class).setAction(ACTION_STOP);
}

View File

@@ -18,55 +18,41 @@ package com.android.systemui.screenrecord;
import android.Manifest;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
import com.android.systemui.R;
import javax.inject.Inject;
/**
* Activity to select screen recording options
*/
public class ScreenRecordDialog extends Activity {
private static final String TAG = "ScreenRecord";
private static final int REQUEST_CODE_VIDEO_ONLY = 200;
private static final int REQUEST_CODE_VIDEO_TAPS = 201;
private static final int REQUEST_CODE_PERMISSIONS = 299;
private static final int REQUEST_CODE_VIDEO_AUDIO = 300;
private static final int REQUEST_CODE_VIDEO_AUDIO_TAPS = 301;
private static final int REQUEST_CODE_PERMISSIONS_AUDIO = 399;
private boolean mUseAudio;
private boolean mShowTaps;
private static final long DELAY_MS = 3000;
private final RecordingController mController;
@Inject
public ScreenRecordDialog(RecordingController controller) {
mController = controller;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.screen_record_dialog);
final CheckBox micCheckBox = findViewById(R.id.checkbox_mic);
final CheckBox tapsCheckBox = findViewById(R.id.checkbox_taps);
final Button recordButton = findViewById(R.id.record_button);
recordButton.setOnClickListener(v -> {
mUseAudio = micCheckBox.isChecked();
mShowTaps = tapsCheckBox.isChecked();
Log.d(TAG, "Record button clicked: audio " + mUseAudio + ", taps " + mShowTaps);
if (mUseAudio && checkSelfPermission(Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Requesting permission for audio");
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},
REQUEST_CODE_PERMISSIONS_AUDIO);
} else {
requestScreenCapture();
}
});
requestScreenCapture();
}
private void requestScreenCapture() {
@@ -74,18 +60,23 @@ public class ScreenRecordDialog extends Activity {
Context.MEDIA_PROJECTION_SERVICE);
Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
if (mUseAudio) {
// TODO get saved settings
boolean useAudio = false;
boolean showTaps = false;
if (useAudio) {
startActivityForResult(permissionIntent,
mShowTaps ? REQUEST_CODE_VIDEO_AUDIO_TAPS : REQUEST_CODE_VIDEO_AUDIO);
showTaps ? REQUEST_CODE_VIDEO_AUDIO_TAPS : REQUEST_CODE_VIDEO_AUDIO);
} else {
startActivityForResult(permissionIntent,
mShowTaps ? REQUEST_CODE_VIDEO_TAPS : REQUEST_CODE_VIDEO_ONLY);
showTaps ? REQUEST_CODE_VIDEO_TAPS : REQUEST_CODE_VIDEO_ONLY);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mShowTaps = (requestCode == REQUEST_CODE_VIDEO_TAPS
boolean showTaps = (requestCode == REQUEST_CODE_VIDEO_TAPS
|| requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
boolean useAudio = (requestCode == REQUEST_CODE_VIDEO_AUDIO
|| requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
switch (requestCode) {
case REQUEST_CODE_VIDEO_TAPS:
@@ -93,11 +84,17 @@ public class ScreenRecordDialog extends Activity {
case REQUEST_CODE_VIDEO_ONLY:
case REQUEST_CODE_VIDEO_AUDIO:
if (resultCode == RESULT_OK) {
mUseAudio = (requestCode == REQUEST_CODE_VIDEO_AUDIO
|| requestCode == REQUEST_CODE_VIDEO_AUDIO_TAPS);
startForegroundService(
RecordingService.getStartIntent(this, resultCode, data, mUseAudio,
mShowTaps));
PendingIntent startIntent = PendingIntent.getForegroundService(
this, RecordingService.REQUEST_CODE, RecordingService.getStartIntent(
ScreenRecordDialog.this, resultCode, data, useAudio,
showTaps),
PendingIntent.FLAG_UPDATE_CURRENT
);
PendingIntent stopIntent = PendingIntent.getService(
this, RecordingService.REQUEST_CODE,
RecordingService.getStopIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
mController.startCountdown(DELAY_MS, startIntent, stopIntent);
} else {
Toast.makeText(this,
getResources().getString(R.string.screenrecord_permission_error),