remove effortlessPermissions lib

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2023-07-20 20:48:03 +02:00
parent 2ac9ec7ac8
commit 80af04be9a
5 changed files with 187 additions and 132 deletions

View File

@ -239,7 +239,6 @@ dependencies {
implementation 'net.orange-box.storebox:storebox-lib:1.4.0' implementation 'net.orange-box.storebox:storebox-lib:1.4.0'
implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'me.zhanghai.android.effortlesspermissions:library:1.1.0'
implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.github.wooplr:Spotlight:1.3' implementation 'com.github.wooplr:Spotlight:1.3'
implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.google.code.findbugs:jsr305:3.0.2'

View File

@ -34,7 +34,6 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
@ -151,12 +150,9 @@ import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import autodagger.AutoInjector; import autodagger.AutoInjector;
@ -165,11 +161,7 @@ import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import me.zhanghai.android.effortlesspermissions.AfterPermissionDenied;
import me.zhanghai.android.effortlesspermissions.EffortlessPermissions;
import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment;
import okhttp3.Cache; import okhttp3.Cache;
import pub.devrel.easypermissions.AfterPermissionGranted;
import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY; import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY;
@ -220,11 +212,6 @@ public class CallActivity extends CallBaseActivity {
public CallRecordingViewModel callRecordingViewModel; public CallRecordingViewModel callRecordingViewModel;
public RaiseHandViewModel raiseHandViewModel; public RaiseHandViewModel raiseHandViewModel;
private static final String[] PERMISSIONS_CALL = {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
};
private static final String[] PERMISSIONS_CAMERA = { private static final String[] PERMISSIONS_CAMERA = {
Manifest.permission.CAMERA Manifest.permission.CAMERA
}; };
@ -362,13 +349,62 @@ public class CallActivity extends CallBaseActivity {
private AudioOutputDialog audioOutputDialog; private AudioOutputDialog audioOutputDialog;
private MoreCallActionsDialog moreCallActionsDialog; private MoreCallActionsDialog moreCallActionsDialog;
private final ActivityResultLauncher<String> requestBluetoothPermissionLauncher = ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionMap -> {
if (isGranted) {
enableBluetoothManager(); List<String> rationaleList = new ArrayList<>();
Boolean audioPermission = permissionMap.get(Manifest.permission.RECORD_AUDIO);
if (audioPermission != null) {
if (Boolean.TRUE.equals(audioPermission)) {
if (!microphoneOn) {
onMicrophoneClick();
}
} else {
rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint)));
}
}
Boolean cameraPermission = permissionMap.get(Manifest.permission.CAMERA);
if (cameraPermission != null) {
if (Boolean.TRUE.equals(cameraPermission)) {
if (!videoOn) {
onCameraClick();
}
if (cameraEnumerator.getDeviceNames().length == 0) {
binding.cameraButton.setVisibility(View.GONE);
}
if (cameraEnumerator.getDeviceNames().length > 1) {
binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
}
} else {
rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint)));
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Boolean bluetoothPermission = permissionMap.get(Manifest.permission.BLUETOOTH_CONNECT);
if (bluetoothPermission != null) {
if (Boolean.TRUE.equals(bluetoothPermission)) {
enableBluetoothManager();
} else {
// Only ask for bluetooth when already asking to grant microphone or camera access. Asking
// for bluetooth solely is not important enough here and would most likely annoy the user.
if (!rationaleList.isEmpty()) {
rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint)));
}
}
}
}
if (!rationaleList.isEmpty()) {
showRationaleDialogForSettings(rationaleList);
} }
}); });
private boolean canPublishAudioStream; private boolean canPublishAudioStream;
private boolean canPublishVideoStream; private boolean canPublishVideoStream;
@ -502,16 +538,15 @@ public class CallActivity extends CallBaseActivity {
.setRepeatCount(PulseAnimation.INFINITE) .setRepeatCount(PulseAnimation.INFINITE)
.setRepeatMode(PulseAnimation.REVERSE); .setRepeatMode(PulseAnimation.REVERSE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestBluetoothPermission();
}
basicInitialization(); basicInitialization();
callParticipants = new HashMap<>(); callParticipants = new HashMap<>();
participantDisplayItems = new HashMap<>(); participantDisplayItems = new HashMap<>();
initViews(); initViews();
if (!isConnectionEstablished()) { if (!isConnectionEstablished()) {
initiateCall(); initiateCall();
} }
updateSelfVideoViewPosition(); updateSelfVideoViewPosition();
reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils); reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils);
@ -546,15 +581,6 @@ public class CallActivity extends CallBaseActivity {
active = false; active = false;
} }
@RequiresApi(api = Build.VERSION_CODES.S)
private void requestBluetoothPermission() {
if (ContextCompat.checkSelfPermission(
getContext(), Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_DENIED) {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT);
}
}
private void enableBluetoothManager() { private void enableBluetoothManager() {
if (audioManager != null) { if (audioManager != null) {
audioManager.startBluetoothManager(); audioManager.startBluetoothManager();
@ -936,36 +962,28 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private void checkDevicePermissions() { private void checkDevicePermissions() {
if (isVoiceOnlyCall) { List<String> permissionsToRequest = new ArrayList<>();
onMicrophoneClick(); List<String> rationaleList = new ArrayList<>();
} else {
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CALL)) {
onPermissionsGranted();
} else {
requestPermissions(PERMISSIONS_CALL, 100);
}
}
}
private boolean isConnectionEstablished() {
return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
}
@AfterPermissionGranted(100)
private void onPermissionsGranted() {
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CALL)) {
if (!videoOn && !isVoiceOnlyCall) {
onCameraClick();
}
if (permissionUtil.isMicrophonePermissionGranted()) {
if (!microphoneOn) { if (!microphoneOn) {
onMicrophoneClick(); onMicrophoneClick();
} }
if (!isVoiceOnlyCall) { } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
permissionsToRequest.add(Manifest.permission.RECORD_AUDIO);
rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint)));
} else {
permissionsToRequest.add(Manifest.permission.RECORD_AUDIO);
}
if (!isVoiceOnlyCall) {
if (permissionUtil.isCameraPermissionGranted()) {
if (!videoOn) {
onCameraClick();
}
if (cameraEnumerator.getDeviceNames().length == 0) { if (cameraEnumerator.getDeviceNames().length == 0) {
binding.cameraButton.setVisibility(View.GONE); binding.cameraButton.setVisibility(View.GONE);
} }
@ -973,44 +991,32 @@ public class CallActivity extends CallBaseActivity {
if (cameraEnumerator.getDeviceNames().length > 1) { if (cameraEnumerator.getDeviceNames().length > 1) {
binding.switchSelfVideoButton.setVisibility(View.VISIBLE); binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
} }
} } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
permissionsToRequest.add(Manifest.permission.CAMERA);
if (!isConnectionEstablished()) { rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint)));
fetchSignalingSettings();
}
} else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_CALL)) {
checkIfSomeAreApproved();
}
}
private void checkIfSomeAreApproved() {
if (!isVoiceOnlyCall) {
if (cameraEnumerator.getDeviceNames().length == 0) {
binding.cameraButton.setVisibility(View.GONE);
}
if (cameraEnumerator.getDeviceNames().length > 1) {
binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
}
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) && canPublishVideoStream) {
if (!videoOn) {
onCameraClick();
}
} else { } else {
binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px); permissionsToRequest.add(Manifest.permission.CAMERA);
binding.cameraButton.setAlpha(0.7f);
binding.switchSelfVideoButton.setVisibility(View.GONE);
} }
} }
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE) && canPublishAudioStream) {
if (!microphoneOn) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
onMicrophoneClick(); if (permissionUtil.isBluetoothPermissionGranted()) {
enableBluetoothManager();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT);
rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint)));
} else {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT);
}
}
if (!permissionsToRequest.isEmpty()) {
if (!rationaleList.isEmpty()) {
showRationaleDialog(permissionsToRequest, rationaleList);
} else {
requestPermissionLauncher.launch(permissionsToRequest.toArray(new String[permissionsToRequest.size()]));
} }
} else {
binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
} }
if (!isConnectionEstablished()) { if (!isConnectionEstablished()) {
@ -1018,22 +1024,63 @@ public class CallActivity extends CallBaseActivity {
} }
} }
@AfterPermissionDenied(100) private void showRationaleDialog(String permissionToRequest, String rationale) {
private void onPermissionsDenied() { List<String> rationaleList = new ArrayList<String>();
if (!isVoiceOnlyCall) { List<String> permissionsToRequest = new ArrayList<String>();
if (cameraEnumerator.getDeviceNames().length == 0) {
binding.cameraButton.setVisibility(View.GONE); rationaleList.add(rationale);
} else if (cameraEnumerator.getDeviceNames().length == 1) { permissionsToRequest.add(permissionToRequest);
binding.switchSelfVideoButton.setVisibility(View.GONE);
} showRationaleDialog(permissionsToRequest, rationaleList);
}
private void showRationaleDialog(List<String> permissionsToRequest, List<String> rationaleList) {
StringBuilder rationalesWithLineBreaks = new StringBuilder();
for (String rationale : rationaleList) {
rationalesWithLineBreaks.append(rationale).append("\n\n");
} }
if ((EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) || MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this)
EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE))) { .setTitle(R.string.nc_permissions_rationale_dialog_title)
checkIfSomeAreApproved(); .setMessage(rationalesWithLineBreaks)
} else if (!isConnectionEstablished()) { .setPositiveButton(R.string.nc_permissions_ask, (dialog, which) ->
fetchSignalingSettings(); requestPermissionLauncher.launch(
permissionsToRequest.toArray(new String[permissionsToRequest.size()])
)
)
.setNegativeButton(R.string.nc_common_dismiss, null);
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder);
dialogBuilder.show();
}
private void showRationaleDialogForSettings(List<String> rationaleList) {
StringBuilder rationalesWithLineBreaks = new StringBuilder();
rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_denied));
rationalesWithLineBreaks.append('\n');
rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_settings_hint));
rationalesWithLineBreaks.append("\n\n");
for (String rationale : rationaleList) {
rationalesWithLineBreaks.append(rationale).append("\n\n");
} }
MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.nc_permissions_rationale_dialog_title)
.setMessage(rationalesWithLineBreaks)
.setPositiveButton(R.string.nc_permissions_settings, (dialog, which) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
})
.setNegativeButton(R.string.nc_common_dismiss, null);
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder);
dialogBuilder.show();
}
private boolean isConnectionEstablished() {
return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
} }
private void onAudioManagerDevicesChanged( private void onAudioManagerDevicesChanged(
@ -1118,7 +1165,6 @@ public class CallActivity extends CallBaseActivity {
} }
public void onMicrophoneClick() { public void onMicrophoneClick() {
if (!canPublishAudioStream) { if (!canPublishAudioStream) {
microphoneOn = false; microphoneOn = false;
binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px); binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
@ -1134,7 +1180,7 @@ public class CallActivity extends CallBaseActivity {
return; return;
} }
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE)) { if (permissionUtil.isMicrophonePermissionGranted()) {
if (!appPreferences.getPushToTalkIntroShown()) { if (!appPreferences.getPushToTalkIntroShown()) {
int primary = viewThemeUtils.getScheme(binding.audioOutputButton.getContext()).getPrimary(); int primary = viewThemeUtils.getScheme(binding.audioOutputButton.getContext()).getPrimary();
@ -1182,19 +1228,17 @@ public class CallActivity extends CallBaseActivity {
pulseAnimation.start(); pulseAnimation.start();
toggleMedia(true, false); toggleMedia(true, false);
} }
} else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_MICROPHONE)) { } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
// Microphone permission is permanently denied so we cannot request it normally. showRationaleDialog(
Manifest.permission.RECORD_AUDIO,
OpenAppDetailsDialogFragment.show( getResources().getString(R.string.nc_microphone_permission_hint)
R.string.nc_microphone_permission_permanently_denied, );
R.string.nc_permissions_settings, (AppCompatActivity) this);
} else { } else {
requestPermissions(PERMISSIONS_MICROPHONE, 100); requestPermissionLauncher.launch(PERMISSIONS_MICROPHONE);
} }
} }
public void onCameraClick() { public void onCameraClick() {
if (!canPublishVideoStream) { if (!canPublishVideoStream) {
videoOn = false; videoOn = false;
binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px); binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
@ -1202,7 +1246,7 @@ public class CallActivity extends CallBaseActivity {
return; return;
} }
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA)) { if (permissionUtil.isCameraPermissionGranted()) {
videoOn = !videoOn; videoOn = !videoOn;
if (videoOn) { if (videoOn) {
@ -1216,15 +1260,14 @@ public class CallActivity extends CallBaseActivity {
} }
toggleMedia(videoOn, true); toggleMedia(videoOn, true);
} else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_CAMERA)) { } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
// Camera permission is permanently denied so we cannot request it normally. showRationaleDialog(
OpenAppDetailsDialogFragment.show( Manifest.permission.CAMERA,
R.string.nc_camera_permission_permanently_denied, getResources().getString(R.string.nc_camera_permission_hint)
R.string.nc_permissions_settings, (AppCompatActivity) this); );
} else { } else {
requestPermissions(PERMISSIONS_CAMERA, 100); requestPermissionLauncher.launch(PERMISSIONS_CAMERA);
} }
} }
public void switchCamera() { public void switchCamera() {
@ -2411,7 +2454,7 @@ public class CallActivity extends CallBaseActivity {
if (!isVoiceOnlyCall) { if (!isVoiceOnlyCall) {
boolean enableVideo = proximitySensorEvent.getProximitySensorEventType() == boolean enableVideo = proximitySensorEvent.getProximitySensorEventType() ==
ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && videoOn; ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && videoOn;
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) && if (permissionUtil.isCameraPermissionGranted() &&
(currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn (currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn
&& enableVideo != localVideoTrack.enabled()) { && enableVideo != localVideoTrack.enabled()) {
toggleMedia(enableVideo, true); toggleMedia(enableVideo, true);
@ -2459,15 +2502,6 @@ public class CallActivity extends CallBaseActivity {
} }
} }
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EffortlessPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults,
this);
}
private void addParticipantDisplayItem(CallParticipantModel callParticipantModel, String videoStreamType) { private void addParticipantDisplayItem(CallParticipantModel callParticipantModel, String videoStreamType) {
if (callParticipantModel.isInternal() != null && callParticipantModel.isInternal()) { if (callParticipantModel.isInternal() != null && callParticipantModel.isInternal()) {
return; return;

View File

@ -24,6 +24,7 @@ package com.nextcloud.talk.utils.permissions
interface PlatformPermissionUtil { interface PlatformPermissionUtil {
val privateBroadcastPermission: String val privateBroadcastPermission: String
fun isCameraPermissionGranted(): Boolean fun isCameraPermissionGranted(): Boolean
fun isMicrophonePermissionGranted(): Boolean
fun isBluetoothPermissionGranted(): Boolean
fun isFilesPermissionGranted(): Boolean fun isFilesPermissionGranted(): Boolean
} }

View File

@ -25,6 +25,7 @@ import android.Manifest
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.PermissionChecker import androidx.core.content.PermissionChecker
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
@ -39,6 +40,21 @@ class PlatformPermissionUtilImpl(private val context: Context) : PlatformPermiss
) == PermissionChecker.PERMISSION_GRANTED ) == PermissionChecker.PERMISSION_GRANTED
} }
@RequiresApi(Build.VERSION_CODES.S)
override fun isBluetoothPermissionGranted(): Boolean {
return PermissionChecker.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_CONNECT
) == PermissionChecker.PERMISSION_GRANTED
}
override fun isMicrophonePermissionGranted(): Boolean {
return PermissionChecker.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PermissionChecker.PERMISSION_GRANTED
}
override fun isFilesPermissionGranted(): Boolean { override fun isFilesPermissionGranted(): Boolean {
return when { return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {

View File

@ -28,7 +28,7 @@
How to work with translations as a developer: How to work with translations as a developer:
- Only add new translations to values/stings.xml, don't do this for other languages (it will be done via transifex). - Only add new translations to values/stings.xml, don't do this for other languages (it will be done via transifex).
- Never change the key of a translation. If it really has to be changed, delete it and create a new entry instead. - Never change the key of a translation. If it really has to be changed, delete it and create a new entry instead.
- If you change a value in values/stings.xml, also backport this to all stable branches. - If you change a value in values/strings.xml, also backport this to all stable branches.
- Translations are synced every night (or when manually triggered): - Translations are synced every night (or when manually triggered):
- New entries are added to transifex.com - New entries are added to transifex.com
- Updated translations from transifex are added via Nextcloud bot to this repo. - Updated translations from transifex are added via Nextcloud bot to this repo.
@ -216,9 +216,14 @@ How to translate with transifex:
<string name="nc_contacts_done">Done</string> <string name="nc_contacts_done">Done</string>
<!-- Permissions --> <!-- Permissions -->
<string name="nc_camera_permission_permanently_denied">To enable video communication please grant \"Camera\" permission in the system settings.</string> <string name="nc_permissions_rationale_dialog_title">Please allow permissions</string>
<string name="nc_microphone_permission_permanently_denied">To enable voice communication please grant \"Microphone\" permission in the system settings.</string> <string name="nc_permissions_denied">Some permissions were denied.</string>
<string name="nc_permissions_settings_hint">Please grant permissions at Settings > Permissions</string>
<string name="nc_permissions_settings">Open settings</string> <string name="nc_permissions_settings">Open settings</string>
<string name="nc_permissions_ask">Set permissions</string>
<string name="nc_camera_permission_hint">To enable video communication please grant \"Camera\" permission.</string>
<string name="nc_microphone_permission_hint">To enable voice communication please grant \"Microphone\" permission.</string>
<string name="nc_bluetooth_permission_hint">To enable bluetooth speakers please grant \"Nearby devices\" permission.</string>
<!-- Call --> <!-- Call -->
<string name="nc_call_voice">%s voice call</string> <string name="nc_call_voice">%s voice call</string>