浏览代码

Merge remote-tracking branch 'origin/master' into feature/edit_messages

Marcel Hibbe 1 年之前
父节点
当前提交
9a4bf14e09

+ 2 - 2
.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

+ 1 - 1
.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

+ 1 - 1
.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

+ 1 - 1
.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

+ 5 - 5
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'
 }

+ 0 - 2
app/src/gplay/AndroidManifest.xml

@@ -23,8 +23,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools">
 
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
-
     <application
         android:name=".application.NextcloudTalkApplication"
         android:allowBackup="true"

+ 1 - 1
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

@@ -1117,7 +1117,7 @@ class CallActivity : CallBaseActivity() {
                         micInputAudioRecorder.read(byteArr, 0, byteArr.size)
                         val isCurrentlySpeaking = abs(byteArr[0].toDouble()) > MICROPHONE_VALUE_THRESHOLD
 
-                        if (isCurrentlySpeaking && !isSpeakingLongTerm) {
+                        if (microphoneOn && isCurrentlySpeaking && !isSpeakingLongTerm) {
                             isSpeakingLongTerm = true
                             sendIsSpeakingMessage(true)
                         } else if (!isCurrentlySpeaking && isSpeakingLongTerm) {

+ 132 - 20
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")
                 }

+ 5 - 0
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<ViewState> = MutableLiveData(GetReminderStartState)
+
+    var isPausedDueToBecomingNoisy = false
+    var receiverRegistered = false
+    var receiverUnregistered = false
+
     val getReminderExistState: LiveData<ViewState>
         get() = _getReminderExistState
 

+ 6 - 1
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<FrameLayout>(R.id.exo_bottom_bar)

+ 55 - 13
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 <dev@mhibbe.de>
+ * Copyright (C) 2024-2025 Parneet Singh <gurayaparneet@gmail.com>
  *
  * 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 {

+ 4 - 0
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,

+ 12 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -109,6 +109,7 @@
     <string name="nc_conversations_empty">会話に参加するか、新しいのを開始</string>
     <string name="nc_conversations_empty_details">友達や同僚に挨拶しましょう!</string>
     <string name="nc_copy_message">コピー</string>
+    <string name="nc_create_poll">投票を作成</string>
     <string name="nc_date_header_today">今日</string>
     <string name="nc_date_header_yesterday">昨日</string>
     <string name="nc_delete">削除</string>
@@ -144,6 +145,7 @@
     <string name="nc_expire_message_one_day">1日</string>
     <string name="nc_expire_message_one_hour">1時間</string>
     <string name="nc_expire_message_one_week">1週間</string>
+    <string name="nc_expire_messages_explanation">チャットメッセージは、一定時間経過後に期限切れにすることができます。注意: チャットで共有されたファイルは、ファイル所有者は削除されませんが、会話では共有されなくなります。</string>
     <string name="nc_external_server_failed">シグナリング設定を取得できませんでした</string>
     <string name="nc_failed_signaling_settings">ターゲットのサーバーは、モバイル経由での公開の会話への参加をサポートしていません。Webブラウザー経由で会話への参加を試みてください。</string>
     <string name="nc_failed_to_perform_operation">申し訳ありませんが、何かがおかしいです!</string>
@@ -330,6 +332,7 @@
     <string name="nc_settings_theme_key">テーマ</string>
     <string name="nc_settings_theme_light">ライト</string>
     <string name="nc_settings_theme_title">テーマ</string>
+    <string name="nc_settings_typing_status_desc">自分のタイピング状況を共有し、他の人のタイピング状況を表示する</string>
     <string name="nc_settings_use_credentials_title">プロキシは資格情報を必要とします</string>
     <string name="nc_settings_warning">警告</string>
     <string name="nc_settings_wrong_account">現在のアカウントのみ再認可できます</string>
@@ -377,8 +380,11 @@
     <string name="open_in_files_app">アプリでファイルを開く</string>
     <string name="play_pause_voice_message">音声メッセージを再生/一時停止</string>
     <string name="polls_add_option">オプションを追加</string>
+    <string name="polls_end_poll">投票を終了する</string>
+    <string name="polls_multiple_answers">複数の答え</string>
     <string name="polls_options">オプション</string>
     <string name="polls_private_poll">プライベート投票</string>
+    <string name="polls_question">質問</string>
     <string name="polls_results_subtitle">結果</string>
     <string name="polls_settings">設定</string>
     <string name="polls_submit_vote">投票</string>
@@ -386,9 +392,13 @@
     <string name="reactions_tab_all">すべて</string>
     <string name="read_storage_no_permission">ストレージからのファイル共有は権限がなければ不可能です</string>
     <string name="record_cancel_start">録画開始をキャンセル</string>
+    <string name="record_failed_info">レコーディングが失敗しました。あなたの管理者に連絡してください。</string>
     <string name="record_start_description">録画を開始</string>
     <string name="record_stop_description">録画を停止</string>
+    <string name="recording_consent_all">全ての通話には録音の同意が必要です。</string>
+    <string name="recording_consent_for_conversation_description">この会話に参加する前に、録音の同意が必要です。</string>
     <string name="recording_consent_for_conversation_title">レコーディングの同意</string>
+    <string name="recording_consent_title">通話は録音されるかもしれません。</string>
     <string name="recording_settings_title">記録中</string>
     <string name="save">保存</string>
     <string name="scope_federated_description">信頼できるサーバーのみと同期</string>
@@ -417,6 +427,7 @@
     <string name="shared_items_file">ファイル</string>
     <string name="shared_items_media">メディア</string>
     <string name="shared_items_other">その他</string>
+    <string name="shared_items_poll">投票</string>
     <string name="shared_items_voice">音声番号</string>
     <string name="starred">お気に入り</string>
     <string name="startCallForbidden">通話を開始することが許可されていません</string>
@@ -436,6 +447,7 @@
     <string name="today">今日</string>
     <string name="tomorrow">明日</string>
     <string name="translate">翻訳</string>
+    <string name="translation_copy_translated_text">翻訳されたテキストをコピー</string>
     <string name="translation_detect_language">言語を検出する</string>
     <string name="translation_device_settings">デバイスの設定</string>
     <string name="translation_error_message">言語を検出できませんでした</string>

+ 1 - 1
app/src/main/res/values-sc/strings.xml

@@ -264,7 +264,7 @@
     <string name="nc_settings">Impostatziones</string>
     <string name="nc_settings_account_updated">Su contu chi tenias giai est istadu agiornadu, imbetzes de nd\'agiùnghere unu nou</string>
     <string name="nc_settings_advanced_title">Avantzadu</string>
-    <string name="nc_settings_appearance">Visibilidade</string>
+    <string name="nc_settings_appearance">Aspetu</string>
     <string name="nc_settings_call_ringtone">Mutidas</string>
     <string name="nc_settings_incognito_keyboard_desc">Imparat a sa tastiera a disativare s\'imparu personale (chene garantzias)</string>
     <string name="nc_settings_incognito_keyboard_title">Tastiera in incògnita</string>

+ 36 - 1
gradle/verification-metadata.xml

@@ -143,7 +143,10 @@
          <trusted-key id="AFCC4C7594D09E2182C60E0F7A01B0F236E5430F" group="com.google.code.gson"/>
          <trusted-key id="B02335AA54CCF21E52BBF9ABD9C565AA72BA2FDD" group="io.grpc"/>
          <trusted-key id="B087A0EB8416563AFE64CEBA4604091C01C3086A" group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.4"/>
-         <trusted-key id="B41089A2DA79B0FA5810252872385FF0AF338D52" group="joda-time" name="joda-time" version="2.12.6"/>
+         <trusted-key id="B41089A2DA79B0FA5810252872385FF0AF338D52">
+            <trusting group="joda-time" name="joda-time" version="2.12.6"/>
+            <trusting group="joda-time" name="joda-time" version="2.12.7"/>
+         </trusted-key>
          <trusted-key id="B6E73D84EA4FCC47166087253FAAD2CD5ECBB314" group="org.apache.commons" name="commons-parent" version="52"/>
          <trusted-key id="B801E2F8EF035068EC1139CC29579F18FA8FD93B" group="com.google.j2objc" name="j2objc-annotations" version="1.3"/>
          <trusted-key id="BC87A3FD0A54480F0BADBEBD21939FF0CA2A6567" group="commons-codec" name="commons-codec" version="1.15"/>
@@ -1879,6 +1882,14 @@
             <sha256 value="89c21ebe5a3aedd8c910bbe0f7c0c6ea6f30dc9dba58d68b39bee3759a7dc52f" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud.android-common" name="core" version="0.15.0">
+         <artifact name="core-0.15.0.aar">
+            <sha256 value="d370010eeae5928f525f31c09e33f9c78ed5e610af4b9f84b1ba68ce0727267b" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="core-0.15.0.module">
+            <sha256 value="609090237e7d6c9745dcb883819e53bb4c9337b89740aa88fd1c56d73ecfa56b" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud.android-common" name="material-color-utilities" version="0.14.0">
          <artifact name="material-color-utilities-0.14.0.jar">
             <sha256 value="bfcd5205b056b948bc6ff9a556898188f4a4e92179c723906e10f1164b776eed" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1887,6 +1898,14 @@
             <sha256 value="b12eadfbfe39b7fb6e62a13c9aca93b468e66a0cf0a2d841b849fe86ebc2d605" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud.android-common" name="material-color-utilities" version="0.15.0">
+         <artifact name="material-color-utilities-0.15.0.jar">
+            <sha256 value="ced5cd660ebfa6aa7461a2157f67a8e76ae12830f759adec0b51631de4ac5434" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="material-color-utilities-0.15.0.module">
+            <sha256 value="edc221870d47808e96595f11d58dc6f45e50a9d8979658f024ae9a816778ecfb" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud.android-common" name="ui" version="0.14.0">
          <artifact name="ui-0.14.0.aar">
             <sha256 value="2ff1b8ca1ce4d4151ce967c4704a4def19a128d3f710491872e007217f3ac84f" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1895,6 +1914,14 @@
             <sha256 value="a600ef42a3ffe9a5900893f7f68052272ed8749ae2c5c44471f792f9d1d9d01f" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud.android-common" name="ui" version="0.15.0">
+         <artifact name="ui-0.15.0.aar">
+            <sha256 value="7e520034d730a11f5089548a8f15fbdd5d5054f8f0465f711664235b3939b78d" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="ui-0.15.0.module">
+            <sha256 value="ac04b3a93aae2e9e8585d01447210fe0c5c85dec13e241b5e7c8956a3ed8a962" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.wooplr" name="Spotlight" version="1.3">
          <artifact name="Spotlight-1.3.aar">
             <sha256 value="2216a78710c8626623d3fd8f6519ec49e26d86930757b51288b7889653b7b44e" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -2225,6 +2252,14 @@
             <sha256 value="f376e72c66dac4215b25d8dcda887ca355c661f630d2d155160e0c6dca7d4ace" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.google.firebase" name="firebase-messaging" version="23.4.1">
+         <artifact name="firebase-messaging-23.4.1.aar">
+            <sha256 value="15c2637c28fb6895b6a4c998a191b663f3b78ac815f6ac59ab7d673a8d9af202" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="firebase-messaging-23.4.1.pom">
+            <sha256 value="188ee169e2d73488e9b14c7610bf3fd9a834658c0759af51b16b0b255865f7c6" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google.guava" name="failureaccess" version="1.0.1">
          <artifact name="failureaccess-1.0.1.pom">
             <sha256 value="e96042ce78fecba0da2be964522947c87b40a291b5fd3cd672a434924103c4b9" origin="Generated by Gradle"/>