diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b878eca04..21fbcb748 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: languages: ${{ matrix.language }} - name: Set up JDK 17 @@ -46,4 +46,4 @@ jobs: echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" ./gradlew assembleDebug - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index e2ed9c15a..469b8068a 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -19,4 +19,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: gradle/wrapper-validation-action@27152f6fa06a6b8062ef7195c795692e51fc2c81 # v2.0.0 + - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # v2.1.1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index dd9432112..8f25e2c28 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,6 +37,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0 + uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v3.24.3 with: sarif_file: results.sarif diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6606e0f2f..719ff2630 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,7 +25,7 @@ jobs: distribution: "temurin" java-version: 17 - name: Run unit tests with coverage - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # v3.0.0 + uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 with: arguments: testGplayDebugUnit - name: Upload test artifacts diff --git a/app/build.gradle b/app/build.gradle index a18f2622d..930cf273c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,8 +48,8 @@ android { // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) // xx .xxx .xx .xx - versionCode 180010010 - versionName "18.1.0 Alpha 10" + versionCode 190000001 + versionName "19.0.0 Alpha 01" flavorDimensions "default" renderscriptTargetApi 19 @@ -247,7 +247,7 @@ dependencies { implementation 'com.github.wooplr:Spotlight:1.3' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.github.nextcloud-deps:ChatKit:0.4.2' - implementation 'joda-time:joda-time:2.12.6' + implementation 'joda-time:joda-time:2.12.7' implementation "io.coil-kt:coil:${coilKtVersion}" implementation "io.coil-kt:coil-gif:${coilKtVersion}" implementation "io.coil-kt:coil-svg:${coilKtVersion}" @@ -308,11 +308,11 @@ dependencies { spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4' gplayImplementation 'com.google.android.gms:play-services-base:18.3.0' - gplayImplementation "com.google.firebase:firebase-messaging:23.4.0" + gplayImplementation "com.google.firebase:firebase-messaging:23.4.1" implementation 'androidx.activity:activity-ktx:1.8.2' - implementation 'com.github.nextcloud.android-common:ui:0.14.0' + implementation 'com.github.nextcloud.android-common:ui:0.15.0' implementation 'com.github.nextcloud-deps:android-talk-webrtc:110.5481.0' } diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml index 4032681e4..7015fc328 100644 --- a/app/src/gplay/AndroidManifest.xml +++ b/app/src/gplay/AndroidManifest.xml @@ -23,8 +23,6 @@ - - MICROPHONE_VALUE_THRESHOLD - if (isCurrentlySpeaking && !isSpeakingLongTerm) { + if (microphoneOn && isCurrentlySpeaking && !isSpeakingLongTerm) { isSpeakingLongTerm = true sendIsSpeakingMessage(true) } else if (!isCurrentlySpeaking && isSpeakingLongTerm) { diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index e05e849df..d3cb4165d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -31,10 +31,12 @@ package com.nextcloud.talk.chat import android.Manifest import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor import android.content.res.Resources @@ -42,7 +44,9 @@ import android.database.Cursor import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable +import android.media.AudioFocusRequest import android.media.AudioFormat +import android.media.AudioManager import android.media.AudioRecord import android.media.MediaPlayer import android.media.MediaRecorder @@ -389,6 +393,20 @@ class ChatActivity : private var lastRecordMediaPosition: Int = 0 private var lastRecordedSeeked: Boolean = false + private val audioFocusChangeListener = getAudioFocusChangeListener() + + private val noisyAudioStreamReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + chatViewModel.isPausedDueToBecomingNoisy = true + if (isVoicePreviewPlaying) { + pausePreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + pausePlayback(currentlyPlayedVoiceMessage!!) + } + } + } + private lateinit var participantPermissions: ParticipantPermissions private var videoURI: Uri? = null @@ -1217,7 +1235,9 @@ class ChatActivity : binding.messageInputView.micInputCloud.setOnClickListener { if (mediaRecorderState == MediaRecorderState.RECORDING) { - recorder?.stop() + audioFocusRequest(false) { + recorder?.stop() + } mediaRecorderState = MediaRecorderState.INITIAL stopMicInputRecordingAnimation() showPreviewVoiceRecording(true) @@ -1446,8 +1466,11 @@ class ChatActivity : duration = it.duration.toLong() interpolator = LinearInterpolator() } - voicePreviewMediaPlayer!!.start() - voicePreviewObjectAnimator!!.start() + audioFocusRequest(true) { + voicePreviewMediaPlayer!!.start() + voicePreviewObjectAnimator!!.start() + handleBecomingNoisyBroadcast(register = true) + } } setOnCompletionListener { @@ -1461,15 +1484,21 @@ class ChatActivity : if (voicePreviewMediaPlayer == null) { initPreviewVoiceRecording() } else { - voicePreviewMediaPlayer!!.start() - voicePreviewObjectAnimator!!.resume() + audioFocusRequest(true) { + voicePreviewMediaPlayer!!.start() + voicePreviewObjectAnimator!!.resume() + handleBecomingNoisyBroadcast(register = true) + } } } private fun pausePreviewVoicePlaying() { Log.d(TAG, "paused preview voice recording") - voicePreviewMediaPlayer!!.pause() - voicePreviewObjectAnimator!!.pause() + audioFocusRequest(false) { + voicePreviewMediaPlayer!!.pause() + voicePreviewObjectAnimator!!.pause() + handleBecomingNoisyBroadcast(register = false) + } } private fun stopPreviewVoicePlaying() { @@ -1479,9 +1508,12 @@ class ChatActivity : voicePreviewObjectAnimator!!.end() voicePreviewObjectAnimator = null binding.messageInputView.seekBar.clearAnimation() - voicePreviewMediaPlayer!!.stop() - voicePreviewMediaPlayer!!.release() - voicePreviewMediaPlayer = null + audioFocusRequest(false) { + voicePreviewMediaPlayer!!.stop() + voicePreviewMediaPlayer!!.release() + voicePreviewMediaPlayer = null + handleBecomingNoisyBroadcast(register = false) + } } } @@ -1935,6 +1967,73 @@ class ChatActivity : } } + private fun getAudioFocusChangeListener(): AudioManager.OnAudioFocusChangeListener { + return AudioManager.OnAudioFocusChangeListener { flag -> + when (flag) { + AudioManager.AUDIOFOCUS_LOSS -> { + chatViewModel.isPausedDueToBecomingNoisy = false + if (isVoicePreviewPlaying) { + stopPreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + stopMediaPlayer(currentlyPlayedVoiceMessage!!) + } + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + chatViewModel.isPausedDueToBecomingNoisy = false + if (isVoicePreviewPlaying) { + pausePreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + pausePlayback(currentlyPlayedVoiceMessage!!) + } + } + } + } + } + + private fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) { + if (chatViewModel.isPausedDueToBecomingNoisy) { + onGranted() + return + } + val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + + val isGranted: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val focusRequest = AudioFocusRequest.Builder(duration) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + if (shouldRequestFocus) { + audioManager.requestAudioFocus(focusRequest) + } else { + audioManager.abandonAudioFocusRequest(focusRequest) + } + } else { + @Deprecated("This method was deprecated in API level 26.") + if (shouldRequestFocus) { + audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, duration) + } else { + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + } + if (isGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + onGranted() + } + } + + private fun handleBecomingNoisyBroadcast(register: Boolean) { + if (register && !chatViewModel.receiverRegistered) { + registerReceiver(noisyAudioStreamReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) + chatViewModel.receiverRegistered = true + } else if (!chatViewModel.receiverUnregistered) { + unregisterReceiver(noisyAudioStreamReceiver) + chatViewModel.receiverUnregistered = true + chatViewModel.receiverRegistered = false + } + } + private fun startPlayback(message: ChatMessage) { if (!active) { // don't begin to play voice message if screen is not visible anymore. @@ -1948,8 +2047,10 @@ class ChatActivity : mediaPlayer?.let { if (!it.isPlaying) { - it.start() - Log.d(TAG, "MediaPlayer has Started") + audioFocusRequest(true) { + it.start() + handleBecomingNoisyBroadcast(register = true) + } } mediaPlayerHandler = Handler() @@ -1984,8 +2085,10 @@ class ChatActivity : private fun pausePlayback(message: ChatMessage) { if (mediaPlayer!!.isPlaying) { - mediaPlayer!!.pause() - Log.d(TAG, "MediaPlayer is paused") + audioFocusRequest(false) { + mediaPlayer!!.pause() + handleBecomingNoisyBroadcast(register = false) + } } message.isPlayingVoiceMessage = false @@ -2043,7 +2146,10 @@ class ChatActivity : mediaPlayer?.let { if (it.isPlaying) { Log.d(TAG, "media player is stopped") - it.stop() + audioFocusRequest(false) { + it.stop() + handleBecomingNoisyBroadcast(register = false) + } } } } catch (e: IllegalStateException) { @@ -2259,8 +2365,10 @@ class ChatActivity : private fun stopMicInputRecordingAnimation() { if (micInputAudioRecordThread != null) { Log.d(TAG, "Mic Animation Ended") - micInputAudioRecorder.stop() - micInputAudioRecorder.release() + audioFocusRequest(false) { + micInputAudioRecorder.stop() + micInputAudioRecorder.release() + } isMicInputAudioThreadRunning = false micInputAudioRecordThread = null } @@ -2311,7 +2419,9 @@ class ChatActivity : } try { - start() + audioFocusRequest(true) { + start() + } mediaRecorderState = MediaRecorderState.RECORDING Log.d(TAG, "recording started") } catch (e: IllegalStateException) { @@ -2352,8 +2462,10 @@ class ChatActivity : recorder?.apply { try { if (mediaRecorderState == MediaRecorderState.RECORDING) { - stop() - reset() + audioFocusRequest(false) { + stop() + reset() + } mediaRecorderState = MediaRecorderState.INITIAL Log.d(TAG, "stopped recorder") } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index d285f112a..3bad3aad9 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -48,6 +48,11 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) open class GetReminderExistState(val reminder: Reminder) : ViewState private val _getReminderExistState: MutableLiveData = MutableLiveData(GetReminderStartState) + + var isPausedDueToBecomingNoisy = false + var receiverRegistered = false + var receiverUnregistered = false + val getReminderExistState: LiveData get() = _getReminderExistState diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt index 1d6796b52..0dd0b9ba5 100644 --- a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt @@ -44,6 +44,7 @@ import androidx.core.view.marginBottom import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment +import androidx.media3.common.AudioAttributes import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer @@ -165,7 +166,10 @@ class FullScreenMediaActivity : AppCompatActivity() { } private fun initializePlayer() { - player = ExoPlayer.Builder(applicationContext).build() + player = ExoPlayer.Builder(applicationContext) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .build() binding.playerView.player = player } @@ -202,6 +206,7 @@ class FullScreenMediaActivity : AppCompatActivity() { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) supportActionBar?.show() } + private fun applyWindowInsets() { val playerView = binding.playerView val exoControls = playerView.findViewById(R.id.exo_bottom_bar) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index 404352225..f171b8f62 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -2,7 +2,9 @@ * Nextcloud Talk application * * @author Marcel Hibbe + * @author Parneet Singh * Copyright (C) 2021-2022 Marcel Hibbe + * Copyright (C) 2024-2025 Parneet Singh * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +24,6 @@ package com.nextcloud.talk.jobs import android.Manifest import android.app.Activity -import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -86,7 +87,6 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa private var mNotifyManager: NotificationManager? = null private var mBuilder: NotificationCompat.Builder? = null - private lateinit var notification: Notification private var notificationId: Int = 0 lateinit var roomToken: String @@ -168,7 +168,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa } if (uploadSuccess) { - mNotifyManager?.cancel(notificationId) + cancelNotification() return Result.success() } else if (isStopped) { // since work is cancelled the result would be ignored anyways @@ -196,12 +196,12 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa } override fun onTransferProgress(percentage: Int) { - notification = mBuilder!! + val progressUpdateNotification = mBuilder!! .setProgress(HUNDRED_PERCENT, percentage, false) .setContentText(getNotificationContentText(percentage)) .build() - mNotifyManager!!.notify(notificationId, notification) + mNotifyManager!!.notify(notificationId, progressUpdateNotification) } override fun onStopped() { @@ -223,22 +223,60 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa } private fun initNotificationWithPercentage() { - notification = mBuilder!! - .setContentTitle(getResourceString(context, R.string.nc_upload_in_progess)) + val initNotification = mBuilder!! + .setContentTitle(context.resources.getString(R.string.nc_upload_in_progess)) .setContentText(getNotificationContentText(ZERO_PERCENT)) .setSmallIcon(R.drawable.upload_white) .setOngoing(true) .setProgress(HUNDRED_PERCENT, ZERO_PERCENT, false) .setPriority(NotificationCompat.PRIORITY_LOW) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) .setContentIntent(getIntentToOpenConversation()) .addAction( - R.drawable.ic_cancel_white_24dp, getResourceString(context, R.string.nc_cancel), + R.drawable.ic_cancel_white_24dp, + getResourceString(context, R.string.nc_cancel), getCancelUploadIntent() ) .build() notificationId = SystemClock.uptimeMillis().toInt() - mNotifyManager!!.notify(notificationId, notification) + mNotifyManager!!.notify(notificationId, initNotification) + // only need one summary notification but multiple upload worker can call it more than once but it is safe + // because of the same notification object config and id. + makeSummaryNotification() + } + + private fun makeSummaryNotification() { + // summary notification encapsulating the group of notifications + val summaryNotification = NotificationCompat.Builder( + context, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_UPLOADS.name + ).setSmallIcon(R.drawable.upload_white) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) + .setGroupSummary(true) + .build() + + mNotifyManager?.notify(NotificationUtils.GROUP_SUMMARY_NOTIFICATION_ID, summaryNotification) + } + + private fun getActiveUploadNotifications(): Int? { + // filter out active notifications that are upload notifications using group + return mNotifyManager?.activeNotifications?.filter { + it.notification.group == NotificationUtils + .KEY_UPLOAD_GROUP + }?.size + } + + private fun cancelNotification() { + mNotifyManager?.cancel(notificationId) + // summary notification would not get dismissed automatically + // if child notifications are cancelled programmatically + // so check if only 1 notification left if yes + // then cancel it (which is summary notification) + if (getActiveUploadNotifications() == 1) { + mNotifyManager?.cancel(NotificationUtils.GROUP_SUMMARY_NOTIFICATION_ID) + } } private fun getNotificationContentText(percentage: Int): String { @@ -289,17 +327,21 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa getResourceString(context, R.string.nc_upload_failed_notification_text), fileName ) - notification = mBuilder!! + val failureNotification = NotificationCompat.Builder( + context, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_UPLOADS.name + ) .setContentTitle(failureTitle) .setContentText(failureText) .setSmallIcon(R.drawable.baseline_error_24) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) .setOngoing(false) .build() - // Cancel original notification mNotifyManager?.cancel(notificationId) - // Then show information about failure - mNotifyManager!!.notify(SystemClock.uptimeMillis().toInt(), notification) + // update current notification with failure info + mNotifyManager!!.notify(SystemClock.uptimeMillis().toInt(), failureNotification) } private fun getResourceString(context: Context, resourceId: Int): String { diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index ef9d0400a..82c987544 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -68,6 +68,10 @@ object NotificationUtils { // RemoteInput key - used for replies sent directly from notification const val KEY_DIRECT_REPLY = "key_direct_reply" + // notification group keys + const val KEY_UPLOAD_GROUP = "com.nextcloud.talk.utils.KEY_UPLOAD_GROUP" + const val GROUP_SUMMARY_NOTIFICATION_ID = -1 + @TargetApi(Build.VERSION_CODES.O) private fun createNotificationChannel( context: Context, diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 4ee0cb109..b451e9d67 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -109,6 +109,7 @@ 会話に参加するか、新しいのを開始 友達や同僚に挨拶しましょう! コピー + 投票を作成 今日 昨日 削除 @@ -144,6 +145,7 @@ 1日 1時間 1週間 + チャットメッセージは、一定時間経過後に期限切れにすることができます。注意: チャットで共有されたファイルは、ファイル所有者は削除されませんが、会話では共有されなくなります。 シグナリング設定を取得できませんでした ターゲットのサーバーは、モバイル経由での公開の会話への参加をサポートしていません。Webブラウザー経由で会話への参加を試みてください。 申し訳ありませんが、何かがおかしいです! @@ -330,6 +332,7 @@ テーマ ライト テーマ + 自分のタイピング状況を共有し、他の人のタイピング状況を表示する プロキシは資格情報を必要とします 警告 現在のアカウントのみ再認可できます @@ -377,8 +380,11 @@ アプリでファイルを開く 音声メッセージを再生/一時停止 オプションを追加 + 投票を終了する + 複数の答え オプション プライベート投票 + 質問 結果 設定 投票 @@ -386,9 +392,13 @@ すべて ストレージからのファイル共有は権限がなければ不可能です 録画開始をキャンセル + レコーディングが失敗しました。あなたの管理者に連絡してください。 録画を開始 録画を停止 + 全ての通話には録音の同意が必要です。 + この会話に参加する前に、録音の同意が必要です。 レコーディングの同意 + 通話は録音されるかもしれません。 記録中 保存 信頼できるサーバーのみと同期 @@ -417,6 +427,7 @@ ファイル メディア その他 + 投票 音声番号 お気に入り 通話を開始することが許可されていません @@ -436,6 +447,7 @@ 今日 明日 翻訳 + 翻訳されたテキストをコピー 言語を検出する デバイスの設定 言語を検出できませんでした diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 0e71b8d14..9b3efbef9 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -264,7 +264,7 @@ Impostatziones Su contu chi tenias giai est istadu agiornadu, imbetzes de nd\'agiùnghere unu nou Avantzadu - Visibilidade + Aspetu Mutidas Imparat a sa tastiera a disativare s\'imparu personale (chene garantzias) Tastiera in incògnita diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 72937015b..4754b748a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -143,7 +143,10 @@ - + + + + @@ -1879,6 +1882,14 @@ + + + + + + + + @@ -1887,6 +1898,14 @@ + + + + + + + + @@ -1895,6 +1914,14 @@ + + + + + + + + @@ -2225,6 +2252,14 @@ + + + + + + + +