[Feature][Calling] CSS call history (#710)

This commit is contained in:
pavelprystinka 2023-02-28 12:22:49 -08:00 коммит произвёл GitHub
Родитель e7975fcab9
Коммит ccc4d89ff9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
35 изменённых файлов: 643 добавлений и 577 удалений

Просмотреть файл

@ -5,7 +5,7 @@ buildscript {
}
ext {
call_library_version_name = '1.2.0-beta.2'
call_library_version_name = '1.2.0'
chat_library_version_name = '1.0.0-beta.1'
ui_library_version_code = getVersionCode()

Просмотреть файл

@ -99,12 +99,16 @@ dependencies {
implementation "com.microsoft.fluentui:fluentui_persona:$microsoft_fluent_ui_version"
implementation "com.microsoft.fluentui:fluentui_transients:$microsoft_fluent_ui_version"
api 'com.jakewharton.threetenabp:threetenabp:1.4.4'
testImplementation "androidx.arch.core:core-testing:$androidx_core_testing_version"
testImplementation "junit:junit:$junit_version"
testImplementation "org.mockito:mockito-inline:$mockito_inline_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$jetbrains_kotlinx_coroutines_test_version"
testImplementation('org.threeten:threetenbp:1.6.5') {
exclude group: 'com.jakewharton.threetenabp', module: 'threetenabp'
}
androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidx_espresso_core_version"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$androidx_espresso_contrib_version"

Просмотреть файл

@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.data.CallHistoryRepositoryImpl
import com.azure.android.communication.ui.calling.logger.DefaultLogger
import com.jakewharton.threetenabp.AndroidThreeTen
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
import org.threeten.bp.OffsetDateTime
import java.util.UUID
// To test CallHistoryRepository we need to have context, so putting this test the androidTest
internal class CallHistoryRepositoryTest {
@Test
@ExperimentalCoroutinesApi
fun callHistoryService_onCallStateUpdate_callsRepositoryInsert() = runTest {
val context: Context = ApplicationProvider.getApplicationContext()
AndroidThreeTen.init(context.applicationContext)
val repository: CallHistoryRepository = CallHistoryRepositoryImpl(context, DefaultLogger())
val originalList = repository.getAll()
val olderThanMonth = originalList.firstOrNull {
// should not have records older then now() - 31 days.
// Subtract a min to account possible delay while executing.
it.callStartedOn.isBefore(OffsetDateTime.now().minusDays(31).minusMinutes(1))
}
Assert.assertNull(olderThanMonth)
var callId = UUID.randomUUID().toString()
var callStartDate = OffsetDateTime.now()
// inserting a record older then 31 days
repository.insert(callId, callStartDate.minusDays(32))
// should not return new record
var freshList = repository.getAll()
Assert.assertEquals(originalList.count(), freshList.count())
Assert.assertNull(freshList.find { it.callId == callId })
// inset new record
repository.insert(callId, callStartDate)
freshList = repository.getAll()
Assert.assertEquals(originalList.count() + 1, freshList.count())
val retrievedNewRecord = freshList.find { it.callId == callId }
Assert.assertNotNull(retrievedNewRecord)
Assert.assertEquals(callStartDate, retrievedNewRecord!!.callStartedOn)
}
}

Просмотреть файл

@ -23,8 +23,9 @@ import com.azure.android.communication.ui.calling.models.CallCompositeSetPartici
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator;
import com.azure.android.communication.ui.calling.presentation.CallCompositeActivity;
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager;
import com.jakewharton.threetenabp.AndroidThreeTen;
import static com.azure.android.communication.ui.calling.models.CallCompositeDebugInfoExtensionsKt.buildCallCompositeDebugInfo;
import static com.azure.android.communication.ui.calling.CallCompositeExtentionsKt.createDebugInfoManager;
import static com.azure.android.communication.ui.calling.service.sdk.TypeConversionsKt.into;
import java.lang.ref.WeakReference;
@ -213,25 +214,32 @@ public final class CallComposite {
*
* @return {@link CallCompositeDebugInfo}
*/
public CallCompositeDebugInfo getDebugInfo() {
final DebugInfoManager debugInfoManager = getDebugInfoManager();
return debugInfoManager != null
? debugInfoManager.getDebugInfo()
: buildCallCompositeDebugInfo();
public CallCompositeDebugInfo getDebugInfo(final Context context) {
AndroidThreeTen.init(context.getApplicationContext());
final DebugInfoManager debugInfoManager = getDebugInfoManager(context.getApplicationContext());
return debugInfoManager.getDebugInfo();
}
void setDependencyInjectionContainer(final DependencyInjectionContainer diContainer) {
this.diContainer = new WeakReference<DependencyInjectionContainer>(diContainer);
this.diContainer = new WeakReference<>(diContainer);
}
private DebugInfoManager getDebugInfoManager() {
return diContainer != null ? diContainer.get().getDebugInfoManager() : null;
private DebugInfoManager getDebugInfoManager(final Context context) {
if (diContainer != null) {
final DependencyInjectionContainer container = diContainer.get();
if (container != null) {
return container.getDebugInfoManager();
}
}
return createDebugInfoManager(context.getApplicationContext());
}
private void launchComposite(final Context context,
final CallCompositeRemoteOptions remoteOptions,
final CallCompositeLocalOptions localOptions,
final boolean isTest) {
AndroidThreeTen.init(context.getApplicationContext());
UUID groupId = null;
String meetingLink = null;

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling
import android.content.Context
import com.azure.android.communication.ui.calling.data.CallHistoryRepositoryImpl
import com.azure.android.communication.ui.calling.logger.DefaultLogger
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManagerImpl
internal fun createDebugInfoManager(context: Context): DebugInfoManager {
return DebugInfoManagerImpl(CallHistoryRepositoryImpl(context, DefaultLogger()))
}

Просмотреть файл

@ -7,7 +7,7 @@ internal class DiagnosticConfig {
val tags: Array<String> by lazy { arrayOf(getApplicationId()) }
private fun getApplicationId(): String {
val callingCompositeVersionName = "1.2.0-beta.2"
val callingCompositeVersionName = "1.2.0"
val baseTag = "ac"
// Tag template is: acXYYY/<version>
// Where:

Просмотреть файл

@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling.data
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import com.azure.android.communication.ui.calling.data.model.CallHistoryRecordData
import com.azure.android.communication.ui.calling.logger.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.threeten.bp.Instant
import org.threeten.bp.OffsetDateTime
import org.threeten.bp.ZoneId
internal interface CallHistoryRepository {
suspend fun insert(callId: String, callDateTime: OffsetDateTime)
suspend fun getAll(): List<CallHistoryRecordData>
}
internal class CallHistoryRepositoryImpl(
private val context: Context,
private val logger: Logger
) : CallHistoryRepository {
override suspend fun insert(callId: String, callDateTime: OffsetDateTime) {
return withContext(Dispatchers.IO) {
// SQLite does not allow concurrent writes. Need to queue them via lock.
synchronized(dbAccessLock) {
// Using a new db instance instead of caching one as we do not have a
// reliable event when to dispose it.
DbHelper(context).writableDatabase
.use { db ->
val values = ContentValues().apply {
put(CallHistoryContract.COLUMN_NAME_CALL_ID, callId)
put(CallHistoryContract.COLUMN_NAME_CALL_DATE, callDateTime.toInstant().toEpochMilli())
}
val result = db.insert(CallHistoryContract.TABLE_NAME, null, values)
if (result == -1L) {
logger.warning("Failed to save call history record.")
}
// Execute cleanup separately (not in one transaction) in case of it fails,
// so it does not affect insert.
cleanupOldRecords(db)
}
}
}
}
override suspend fun getAll(): List<CallHistoryRecordData> {
return withContext(Dispatchers.IO) {
// Using a new db instance instead of caching one as we do not have a
// reliable event when to dispose it.
DbHelper(context).writableDatabase.use { db ->
synchronized(dbAccessLock) {
cleanupOldRecords(db)
}
val items = mutableListOf<CallHistoryRecordData>()
db.rawQuery(
"SELECT ${CallHistoryContract.COLUMN_NAME_ID}, " +
"${CallHistoryContract.COLUMN_NAME_CALL_DATE}, " +
"${CallHistoryContract.COLUMN_NAME_CALL_ID} " +
"FROM ${CallHistoryContract.TABLE_NAME}",
null
).use {
if (it.moveToFirst()) {
val idColumnIndex = it.getColumnIndexOrThrow(CallHistoryContract.COLUMN_NAME_ID)
val nameColumnIndex = it.getColumnIndexOrThrow(CallHistoryContract.COLUMN_NAME_CALL_ID)
val dateColumnIndex = it.getColumnIndexOrThrow(CallHistoryContract.COLUMN_NAME_CALL_DATE)
do {
items.add(
CallHistoryRecordData(
id = it.getInt(idColumnIndex),
callId = it.getString(nameColumnIndex),
callStartedOn = OffsetDateTime.ofInstant(
Instant.ofEpochMilli(it.getLong(dateColumnIndex)), ZoneId.systemDefault()
),
)
)
} while (it.moveToNext())
}
}
return@withContext items
}
}
}
private fun cleanupOldRecords(db: SQLiteDatabase) {
val threshold = OffsetDateTime.now().minusDays(31).toInstant().toEpochMilli()
db.delete(
CallHistoryContract.TABLE_NAME,
"${CallHistoryContract.COLUMN_NAME_CALL_DATE} < ?", arrayOf(threshold.toString())
)
}
private companion object {
val dbAccessLock = Any()
}
}

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling.data
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
internal class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CallHistoryContract.SQL_CREATE_CALL_HISTORY)
db.execSQL(CallHistoryContract.SQL_CREATE_CALL_HISTORY_INDEX)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
companion object {
// If you change the database schema, you must increment the database version.
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "com.azure.android.communication.ui.calling.CallHistoryReader.db"
}
}
internal object CallHistoryContract {
const val COLUMN_NAME_ID = BaseColumns._ID
const val TABLE_NAME = "call_history"
const val COLUMN_NAME_CALL_ID = "call_id"
const val COLUMN_NAME_CALL_DATE = "call_date"
const val SQL_CREATE_CALL_HISTORY =
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$COLUMN_NAME_ID INTEGER PRIMARY KEY AUTOINCREMENT," +
"$COLUMN_NAME_CALL_ID TEXT NOT NULL," +
"$COLUMN_NAME_CALL_DATE INTEGER NOT NULL)"
const val SQL_CREATE_CALL_HISTORY_INDEX =
"CREATE INDEX IF NOT EXISTS call_dateindex ON $TABLE_NAME($COLUMN_NAME_CALL_DATE);"
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling.data.model
import org.threeten.bp.OffsetDateTime
internal data class CallHistoryRecordData(
val id: Int,
val callId: String,
val callStartedOn: OffsetDateTime,
)

Просмотреть файл

@ -5,6 +5,7 @@ package com.azure.android.communication.ui.calling.di
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.configuration.CallCompositeConfiguration
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.error.ErrorHandler
import com.azure.android.communication.ui.calling.handlers.RemoteParticipantHandler
import com.azure.android.communication.ui.calling.logger.Logger
@ -21,6 +22,7 @@ import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.middleware.handler.CallingMiddlewareActionHandler
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.service.CallHistoryService
import com.azure.android.communication.ui.calling.service.NotificationService
// Dependency Container for the Call Composite Activity
@ -51,7 +53,11 @@ internal interface DependencyInjectionContainer {
val audioFocusManager: AudioFocusManager
val networkManager: NetworkManager
val debugInfoManager: DebugInfoManager
val callHistoryService: CallHistoryService
// UI
val videoViewManager: VideoViewManager
// Data
val callHistoryRepository: CallHistoryRepository
}

Просмотреть файл

@ -5,10 +5,12 @@ package com.azure.android.communication.ui.calling.di
import android.content.Context
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.data.CallHistoryRepositoryImpl
import com.azure.android.communication.ui.calling.error.ErrorHandler
import com.azure.android.communication.ui.calling.getConfig
import com.azure.android.communication.ui.calling.handlers.RemoteParticipantHandler
import com.azure.android.communication.ui.calling.logger.DefaultLogger
import com.azure.android.communication.ui.calling.logger.Logger
import com.azure.android.communication.ui.calling.presentation.VideoStreamRendererFactory
import com.azure.android.communication.ui.calling.presentation.VideoStreamRendererFactoryImpl
import com.azure.android.communication.ui.calling.presentation.VideoViewManager
@ -45,6 +47,8 @@ import com.azure.android.communication.ui.calling.redux.state.ReduxState
import com.azure.android.communication.ui.calling.service.CallingService
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManagerImpl
import com.azure.android.communication.ui.calling.service.CallHistoryService
import com.azure.android.communication.ui.calling.service.CallHistoryServiceImpl
import com.azure.android.communication.ui.calling.service.NotificationService
import com.azure.android.communication.ui.calling.service.sdk.CallingSDK
import com.azure.android.communication.ui.calling.service.sdk.CallingSDKEventHandler
@ -111,7 +115,14 @@ internal class DependencyInjectionContainerImpl(
}
override val debugInfoManager: DebugInfoManager by lazy {
DebugInfoManagerImpl(
callHistoryRepository,
)
}
override val callHistoryService: CallHistoryService by lazy {
CallHistoryServiceImpl(
appStore,
callHistoryRepository
)
}
@ -158,6 +169,10 @@ internal class DependencyInjectionContainerImpl(
RemoteParticipantHandler(configuration, appStore, callingSDKWrapper)
}
override val callHistoryRepository by lazy {
CallHistoryRepositoryImpl(applicationContext, logger)
}
//region Redux
// Initial State
private val initialState by lazy { AppReduxState(configuration.callConfig?.displayName) }
@ -199,7 +214,7 @@ internal class DependencyInjectionContainerImpl(
//region System
private val applicationContext get() = parentContext.applicationContext
override val logger by lazy { DefaultLogger() }
override val logger: Logger by lazy { DefaultLogger() }
private val callingSDKWrapper: CallingSDK by lazy {
customCallingSDK

Просмотреть файл

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling.models;
import java.util.List;
import org.threeten.bp.OffsetDateTime;
/**
* Call history.
*/
public class CallCompositeCallHistoryRecord {
private final OffsetDateTime callStartedOn;
private final List<String> callIds;
CallCompositeCallHistoryRecord(final OffsetDateTime callStartedOn, final List<String> callIds) {
this.callStartedOn = callStartedOn;
this.callIds = callIds;
}
/**
* Get offset date call started on.
* @return
*/
public OffsetDateTime getCallStartedOn() {
return callStartedOn;
}
/**
* Call Id list associated with particular call.
* @return
*/
public List<String> getCallIds() {
return callIds;
}
}

Просмотреть файл

@ -3,30 +3,24 @@
package com.azure.android.communication.ui.calling.models;
import java.util.List;
/**
* A Call Composite Debug information.
*/
public final class CallCompositeDebugInfo {
private String lastCallId;
private final List<CallCompositeCallHistoryRecord> callHistoryRecord;
CallCompositeDebugInfo() { }
/**
* Set last call id.
* @param lastCallId last call id.
* @return {@link CallCompositeDebugInfo}
*/
CallCompositeDebugInfo setLastCallId(final String lastCallId) {
this.lastCallId = lastCallId;
return this;
CallCompositeDebugInfo(final List<CallCompositeCallHistoryRecord> callHistoryRecord) {
this.callHistoryRecord = callHistoryRecord;
}
/**
* Get last call id.
* @return {@link String}
* The history of calls up to 30 days. Ordered ascending by call started date.
* @return
*/
public String getLastCallId() {
return lastCallId;
public List<CallCompositeCallHistoryRecord> getCallHistoryRecords() {
return callHistoryRecord;
}
}

Просмотреть файл

@ -3,8 +3,11 @@
package com.azure.android.communication.ui.calling.models
internal fun CallCompositeDebugInfo.setCallId(lastKnownCallId: String?) {
this.lastCallId = lastKnownCallId
}
import org.threeten.bp.OffsetDateTime
internal fun buildCallCompositeDebugInfo(): CallCompositeDebugInfo = CallCompositeDebugInfo()
internal fun buildCallCompositeDebugInfo(callHistoryRecordList: List<CallCompositeCallHistoryRecord>) =
CallCompositeDebugInfo(callHistoryRecordList)
internal fun buildCallHistoryRecord(callStartedOn: OffsetDateTime, callIds: List<String>): CallCompositeCallHistoryRecord {
return CallCompositeCallHistoryRecord(callStartedOn, callIds)
}

Просмотреть файл

@ -61,7 +61,7 @@ internal class CallCompositeActivity : AppCompatActivity() {
private val callingMiddlewareActionHandler get() = container.callingMiddlewareActionHandler
private val videoViewManager get() = container.videoViewManager
private val instanceId get() = intent.getIntExtra(KEY_INSTANCE_ID, -1)
private val diagnosticsService get() = container.debugInfoManager
private val callHistoryService get() = container.callHistoryService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -117,7 +117,7 @@ internal class CallCompositeActivity : AppCompatActivity() {
}
notificationService.start(lifecycleScope)
diagnosticsService.start(lifecycleScope)
callHistoryService.start(lifecycleScope)
}
override fun onStart() {

Просмотреть файл

@ -12,7 +12,7 @@ internal class MoreCallOptionsListViewModel(
private val unknown = "UNKNOWN"
val callId: String
get() {
val lastKnownCallId = debugInfoManager.debugInfo.lastCallId
val lastKnownCallId = debugInfoManager.getDebugInfo().callHistoryRecords.lastOrNull()?.callIds?.lastOrNull()
return "Call ID: \"${if (lastKnownCallId.isNullOrEmpty()) unknown else lastKnownCallId}\""
}

Просмотреть файл

@ -3,35 +3,40 @@
package com.azure.android.communication.ui.calling.presentation.manager
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.models.CallCompositeCallHistoryRecord
import com.azure.android.communication.ui.calling.models.CallCompositeDebugInfo
import com.azure.android.communication.ui.calling.models.buildCallCompositeDebugInfo
import com.azure.android.communication.ui.calling.models.setCallId
import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import com.azure.android.communication.ui.calling.models.buildCallHistoryRecord
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
internal interface DebugInfoManager {
fun start(coroutineScope: CoroutineScope)
val debugInfo: CallCompositeDebugInfo
fun getDebugInfo(): CallCompositeDebugInfo
}
internal class DebugInfoManagerImpl(
private val store: Store<ReduxState>,
private val callHistoryRepository: CallHistoryRepository,
) : DebugInfoManager {
override var debugInfo = buildCallCompositeDebugInfo()
override fun start(coroutineScope: CoroutineScope) {
coroutineScope.launch {
store.getStateFlow().collect {
if (!it.callState.callId.isNullOrEmpty()) {
val newDebugInfo = buildCallCompositeDebugInfo()
newDebugInfo.setCallId(it.callState.callId)
debugInfo = newDebugInfo
}
}
override fun getDebugInfo(): CallCompositeDebugInfo {
val callHistory = runBlocking {
withContext(Dispatchers.IO) { getCallHistory() }
}
return buildCallCompositeDebugInfo(callHistory)
}
private suspend fun getCallHistory(): List<CallCompositeCallHistoryRecord> {
return callHistoryRepository.getAll()
.groupBy {
it.callStartedOn
}
.map { mapped ->
buildCallHistoryRecord(mapped.key, mapped.value.map { it.callId })
}
.sortedBy {
it.callStartedOn
}
}
}

Просмотреть файл

@ -6,6 +6,7 @@ package com.azure.android.communication.ui.calling.redux.reducer
import com.azure.android.communication.ui.calling.redux.action.Action
import com.azure.android.communication.ui.calling.redux.action.CallingAction
import com.azure.android.communication.ui.calling.redux.state.CallingState
import org.threeten.bp.OffsetDateTime
internal interface CallStateReducer : Reducer<CallingState>
@ -22,7 +23,7 @@ internal class CallStateReducerImpl : CallStateReducer {
callingState.copy(isTranscribing = action.isTranscribing)
}
is CallingAction.CallStartRequested -> {
callingState.copy(joinCallIsRequested = true)
callingState.copy(joinCallIsRequested = true, callStartDateTime = OffsetDateTime.now())
}
is CallingAction.CallIdUpdated -> {
callingState.copy(callId = action.callId)

Просмотреть файл

@ -3,6 +3,8 @@
package com.azure.android.communication.ui.calling.redux.state
import org.threeten.bp.OffsetDateTime
internal enum class CallingStatus {
NONE,
EARLY_MEDIA,
@ -24,6 +26,8 @@ internal data class CallingState(
val joinCallIsRequested: Boolean = false,
val isRecording: Boolean = false,
val isTranscribing: Boolean = false,
// set once for the duration of the call in the CallStateReducer when call start requested.
val callStartDateTime: OffsetDateTime? = null,
)
internal fun CallingState.isDisconnected() =

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.calling.service
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.redux.Store
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
internal interface CallHistoryService {
fun start(coroutineScope: CoroutineScope)
}
internal class CallHistoryServiceImpl(
private val store: Store<ReduxState>,
private val callHistoryRepository: CallHistoryRepository,
) : CallHistoryService {
private val callIdStateFlow = MutableStateFlow<String?>(null)
override fun start(coroutineScope: CoroutineScope) {
coroutineScope.launch {
store.getStateFlow().collect {
callIdStateFlow.value = it.callState.callId
}
}
coroutineScope.launch {
callIdStateFlow.collect { callId ->
val callStartDateTime = store.getCurrentState().callState.callStartDateTime
if (callId != null && callStartDateTime != null) {
callHistoryRepository.insert(callId, callStartDateTime)
}
}
}
}
}

Просмотреть файл

@ -4,22 +4,18 @@
package com.azure.android.communication.ui.presentation.manager
import com.azure.android.communication.ui.ACSBaseTestCoroutine
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.data.model.CallHistoryRecordData
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManagerImpl
import com.azure.android.communication.ui.calling.redux.AppStore
import com.azure.android.communication.ui.calling.redux.state.AppReduxState
import com.azure.android.communication.ui.calling.redux.state.CallingState
import com.azure.android.communication.ui.calling.redux.state.CallingStatus
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import org.threeten.bp.OffsetDateTime
@RunWith(MockitoJUnitRunner::class)
internal class DebugInfoManagerTest : ACSBaseTestCoroutine() {
@ -30,45 +26,23 @@ internal class DebugInfoManagerTest : ACSBaseTestCoroutine() {
runScopedTest {
// arrange
val appState1 = AppReduxState("")
val historyList = mutableListOf(
CallHistoryRecordData(1, "callId1", OffsetDateTime.now().minusDays(6)),
CallHistoryRecordData(2, "callId2", OffsetDateTime.now().minusDays(4)),
CallHistoryRecordData(3, "callId3", OffsetDateTime.now().minusDays(3)),
CallHistoryRecordData(4, "callId4", OffsetDateTime.now().minusDays(1)),
)
val stateFlow = MutableStateFlow<ReduxState>(appState1)
val mockAppStore = mock<AppStore<ReduxState>> {
on { getStateFlow() } doAnswer { stateFlow }
val callHistoryRepository = mock<CallHistoryRepository> {
onBlocking { getAll() } doAnswer { historyList }
}
val debugInfoManager: DebugInfoManager = DebugInfoManagerImpl(mockAppStore)
val flowJob = launch {
debugInfoManager.start(coroutineScope = this)
}
val debugInfoManager: DebugInfoManager = DebugInfoManagerImpl(callHistoryRepository)
val diagnostics1 = debugInfoManager.debugInfo
Assert.assertNotNull(diagnostics1)
Assert.assertNull(diagnostics1.lastCallId)
// update state
val appState2 = AppReduxState("")
val callID = "callID"
appState2.callState = CallingState(CallingStatus.CONNECTING, callID)
stateFlow.value = appState2
val diagnostics2 = debugInfoManager.debugInfo
Assert.assertNotSame(diagnostics1, diagnostics2)
Assert.assertNotNull(diagnostics2)
Assert.assertEquals(callID, diagnostics2.lastCallId)
// redux state loosing CallID
// update state
val appState3 = AppReduxState("")
appState3.callState = CallingState(CallingStatus.CONNECTING, null)
stateFlow.value = appState3
val diagnostics3 = debugInfoManager.debugInfo
Assert.assertSame(diagnostics2, diagnostics3)
Assert.assertNotNull(diagnostics3)
Assert.assertEquals(callID, diagnostics3.lastCallId)
flowJob.cancel()
val debugIndo = debugInfoManager.getDebugInfo()
Assert.assertNotNull(debugIndo)
Assert.assertEquals(historyList.count(), debugIndo.callHistoryRecords.count())
Assert.assertEquals(historyList.last().id, 4)
}
}
}

Просмотреть файл

@ -40,5 +40,6 @@ internal class CallingReducerUnitTest {
// assert
Assert.assertEquals(CallingStatus.NONE, newState.callingStatus)
Assert.assertNotNull(newState.callStartDateTime)
}
}

Просмотреть файл

@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.service
import com.azure.android.communication.ui.ACSBaseTestCoroutine
import com.azure.android.communication.ui.calling.data.CallHistoryRepository
import com.azure.android.communication.ui.calling.redux.AppStore
import com.azure.android.communication.ui.calling.redux.state.AppReduxState
import com.azure.android.communication.ui.calling.redux.state.CallingState
import com.azure.android.communication.ui.calling.redux.state.CallingStatus
import com.azure.android.communication.ui.calling.redux.state.ReduxState
import com.azure.android.communication.ui.calling.service.CallHistoryService
import com.azure.android.communication.ui.calling.service.CallHistoryServiceImpl
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.times
import org.mockito.kotlin.eq
import org.threeten.bp.OffsetDateTime
@RunWith(MockitoJUnitRunner::class)
internal class CallHistoryServiceTest : ACSBaseTestCoroutine() {
@Test
@ExperimentalCoroutinesApi
fun callHistoryService_onCallStateUpdate_callsRepositoryInsert() {
runScopedTest {
// arrange
val appState1 = AppReduxState("")
appState1.callState = CallingState(CallingStatus.NONE)
val stateFlow = MutableStateFlow<ReduxState>(appState1)
val mockAppStore = mock<AppStore<ReduxState>> {
on { getStateFlow() } doAnswer { stateFlow }
on { getCurrentState() } doAnswer { stateFlow.value }
}
val callHistoryRepository = mock<CallHistoryRepository> {
onBlocking { insert(any(), any()) } doAnswer { }
}
val callHistoryService: CallHistoryService = CallHistoryServiceImpl(mockAppStore, callHistoryRepository)
val flowJob = launch {
callHistoryService.start(coroutineScope = this)
}
// update state
val appState2 = AppReduxState("")
val callID = "callID"
appState2.callState = CallingState(CallingStatus.CONNECTING, callID, callStartDateTime = OffsetDateTime.now())
stateFlow.value = appState2
verify(callHistoryRepository, times(1)).insert(eq(callID), any())
flowJob.cancel()
}
}
}

Просмотреть файл

@ -6,7 +6,7 @@ import org.junit.Test
internal class DiagnosticConfigUnitTests {
private val expectedPrefix = "aca110/"
private val expectedVersion = "${expectedPrefix}1.2.0-beta.2"
private val expectedVersion = "${expectedPrefix}1.2.0"
@Test
fun test_Expected_Tag() {

Просмотреть файл

@ -13,8 +13,8 @@ import com.azure.android.communication.ui.chat.redux.state.ChatStatus
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter
internal class MessageViewTest : BaseUiTest() {
@get:Rule

Просмотреть файл

@ -70,7 +70,6 @@ class CallWithChatLauncherActivity : AppCompatActivity(), AlertHandler {
}
binding.run {
tokenFunctionUrlText.setText(BuildConfig.TOKEN_FUNCTION_URL)
if (!deeplinkAcsToken.isNullOrEmpty()) {
acsTokenText.setText(deeplinkAcsToken)

Просмотреть файл

@ -79,14 +79,4 @@ class CallingCompositeACSTokenTest : BaseUiTest() {
homeScreen.clickAlertDialogOkButton()
}
@Test
fun testEmptyAcsToken() {
val homeScreen = HomeScreenRobot()
.setGroupIdOrTeamsMeetingUrl(CallIdentifiersHelper.getGroupId())
.setEmptyAcsToken()
val setupScreen = homeScreen.clickLaunchButton()
homeScreen.clickAlertDialogOkButton()
}
}

Просмотреть файл

@ -15,18 +15,16 @@ import com.azure.android.communication.ui.callingcompositedemoapp.databinding.Ac
import com.azure.android.communication.ui.callingcompositedemoapp.features.AdditionalFeatures
import com.azure.android.communication.ui.callingcompositedemoapp.features.FeatureFlags
import com.azure.android.communication.ui.callingcompositedemoapp.features.conditionallyRegisterDiagnostics
import com.azure.android.communication.ui.callingcompositedemoapp.launcher.CallingCompositeLauncher
import com.microsoft.appcenter.AppCenter
import com.microsoft.appcenter.analytics.Analytics
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.distribute.Distribute
import org.threeten.bp.format.DateTimeFormatter
import java.util.UUID
class CallLauncherActivity : AppCompatActivity() {
private lateinit var binding: ActivityCallLauncherBinding
private val callLauncherViewModel: CallLauncherViewModel by viewModels()
private val isTokenFunctionOptionSelected: String = "isTokenFunctionOptionSelected"
private val isKotlinLauncherOptionSelected: String = "isKotlinLauncherOptionSelected"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -56,23 +54,7 @@ class CallLauncherActivity : AppCompatActivity() {
val deeplinkGroupId = data?.getQueryParameter("groupid")
val deeplinkTeamsUrl = data?.getQueryParameter("teamsurl")
if (savedInstanceState != null) {
if (savedInstanceState.getBoolean(isTokenFunctionOptionSelected)) {
callLauncherViewModel.useTokenFunction()
} else {
callLauncherViewModel.useAcsToken()
}
if (savedInstanceState.getBoolean(isKotlinLauncherOptionSelected)) {
callLauncherViewModel.setKotlinLauncher()
} else {
callLauncherViewModel.setJavaLauncher()
}
}
binding.run {
tokenFunctionUrlText.setText(BuildConfig.TOKEN_FUNCTION_URL)
if (!deeplinkAcsToken.isNullOrEmpty()) {
acsTokenText.setText(deeplinkAcsToken)
} else {
@ -98,31 +80,9 @@ class CallLauncherActivity : AppCompatActivity() {
}
launchButton.setOnClickListener {
launchButton.isEnabled = false
callLauncherViewModel.doLaunch(
tokenFunctionUrlText.text.toString(),
acsTokenText.text.toString()
)
launch()
}
tokenFunctionRadioButton.setOnClickListener {
if (tokenFunctionRadioButton.isChecked) {
tokenFunctionUrlText.requestFocus()
tokenFunctionUrlText.isEnabled = true
acsTokenText.isEnabled = false
acsTokenRadioButton.isChecked = false
callLauncherViewModel.useTokenFunction()
}
}
acsTokenRadioButton.setOnClickListener {
if (acsTokenRadioButton.isChecked) {
acsTokenText.requestFocus()
acsTokenText.isEnabled = true
tokenFunctionUrlText.isEnabled = false
tokenFunctionRadioButton.isChecked = false
callLauncherViewModel.useAcsToken()
}
}
groupCallRadioButton.setOnClickListener {
if (groupCallRadioButton.isChecked) {
groupIdOrTeamsMeetingLinkText.setText(BuildConfig.GROUP_CALL_ID)
@ -136,12 +96,8 @@ class CallLauncherActivity : AppCompatActivity() {
}
}
javaButton.setOnClickListener {
callLauncherViewModel.setJavaLauncher()
}
kotlinButton.setOnClickListener {
callLauncherViewModel.setKotlinLauncher()
showCallHistoryButton.setOnClickListener {
showCallHistory()
}
if (BuildConfig.DEBUG) {
@ -150,31 +106,17 @@ class CallLauncherActivity : AppCompatActivity() {
versionText.text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
}
}
callLauncherViewModel.fetchResult.observe(this) {
processResult(it)
}
}
override fun onDestroy() {
callLauncherViewModel.destroy()
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
saveState(outState)
super.onSaveInstanceState(outState)
}
// check whether new Activity instance was brought to top of stack,
// so that finishing this will get us to the last viewed screen
private fun shouldFinish() = BuildConfig.CHECK_TASK_ROOT && !isTaskRoot
fun showAlert(message: String) {
private fun showAlert(message: String, title: String = "Alert") {
runOnUiThread {
val builder = AlertDialog.Builder(this).apply {
setMessage(message)
setTitle("Alert")
setTitle(title)
setPositiveButton("OK") { _, _ ->
}
}
@ -182,31 +124,12 @@ class CallLauncherActivity : AppCompatActivity() {
}
}
private fun processResult(result: Result<CallingCompositeLauncher?>) {
if (result.isFailure) {
result.exceptionOrNull()?.let {
if (it.message != null) {
val causeMessage = it.cause?.message ?: ""
showAlert(it.toString() + causeMessage)
binding.launchButton.isEnabled = true
} else {
showAlert("Unknown error")
}
}
}
if (result.isSuccess) {
result.getOrNull()?.let { launcherObject ->
launch(launcherObject)
binding.launchButton.isEnabled = true
}
}
}
private fun launch(launcher: CallingCompositeLauncher) {
private fun launch() {
val userName = binding.userNameText.text.toString()
val acsToken = binding.acsTokenText.text.toString()
var groupId: UUID? = null
if (binding.groupCallRadioButton.isChecked) {
val groupId: UUID
try {
groupId =
UUID.fromString(binding.groupIdOrTeamsMeetingLinkText.text.toString().trim())
@ -215,32 +138,41 @@ class CallLauncherActivity : AppCompatActivity() {
showAlert(message)
return
}
launcher.launch(
this@CallLauncherActivity,
userName,
groupId,
null,
::showAlert
)
}
var meetingLink: String? = null
if (binding.teamsMeetingRadioButton.isChecked) {
val meetingLink = binding.groupIdOrTeamsMeetingLinkText.text.toString()
meetingLink = binding.groupIdOrTeamsMeetingLinkText.text.toString()
if (meetingLink.isBlank()) {
val message = "Teams meeting link is invalid or empty."
showAlert(message)
return
}
launcher.launch(
this@CallLauncherActivity,
userName,
null,
meetingLink,
::showAlert,
)
}
callLauncherViewModel.launch(
this@CallLauncherActivity,
acsToken,
userName,
groupId,
meetingLink,
)
}
private fun showCallHistory() {
val history = callLauncherViewModel
.getCallHistory(this@CallLauncherActivity)
.sortedBy { it.callStartedOn }
val title = "Total calls: ${history.count()}"
var message = "Last Call: none"
history.lastOrNull()?.let {
message = "Last Call: ${it.callStartedOn.format(DateTimeFormatter.ofPattern("MMM dd 'at' hh:mm"))}"
it.callIds.forEach { callId ->
message += "\nCallId: $callId"
}
}
showAlert(message, title)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -256,12 +188,4 @@ class CallLauncherActivity : AppCompatActivity() {
}
else -> super.onOptionsItemSelected(item)
}
private fun saveState(outState: Bundle?) {
outState?.putBoolean(
isTokenFunctionOptionSelected,
callLauncherViewModel.isTokenFunctionOptionSelected
)
outState?.putBoolean(isKotlinLauncherOptionSelected, callLauncherViewModel.isKotlinLauncher)
}
}

Просмотреть файл

@ -3,30 +3,46 @@
package com.azure.android.communication.ui.callingcompositedemoapp
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.CallCompositeEventHandler
import com.azure.android.communication.ui.calling.models.CallCompositeErrorEvent
import com.microsoft.appcenter.utils.HandlerUtils.runOnUiThread
import java.lang.ref.WeakReference
// Handles forwarding of error messages to the CallLauncherActivity
//
// CallLauncherActivity is loosely coupled and will detach the weak reference after disposed.
class CallLauncherActivityErrorHandler(
context: Context,
private val callComposite: CallComposite,
callLauncherActivity: CallLauncherActivity,
) :
CallCompositeEventHandler<CallCompositeErrorEvent> {
private val activityWr: WeakReference<CallLauncherActivity> =
WeakReference(callLauncherActivity)
private val activityWr: WeakReference<Context> = WeakReference(context)
override fun handle(it: CallCompositeErrorEvent) {
println("================= application is logging exception =================")
println("call id: " + (callComposite.debugInfo.lastCallId ?: ""))
println(it.cause)
println(it.errorCode)
activityWr.get()
?.showAlert("${it.errorCode} ${it.cause?.message}. Call id: ${callComposite.debugInfo.lastCallId ?: ""}")
println("====================================================================")
activityWr.get()?.let { context ->
val lastCallId = callComposite.getDebugInfo(context).callHistoryRecords
.lastOrNull()?.callIds?.lastOrNull()?.toString() ?: ""
println("================= application is logging exception =================")
println("call id: $lastCallId")
println(it.cause)
println(it.errorCode)
runOnUiThread {
val builder = AlertDialog.Builder(context).apply {
setMessage("${it.errorCode} ${it.cause?.message}. Call id: $lastCallId")
setTitle("Alert")
setPositiveButton("OK") { _, _ ->
}
}
builder.show()
}
println("====================================================================")
}
}
}

Просмотреть файл

@ -3,75 +3,90 @@
package com.azure.android.communication.ui.callingcompositedemoapp
import android.webkit.URLUtil
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import android.content.Context
import androidx.lifecycle.ViewModel
import com.azure.android.communication.ui.callingcompositedemoapp.launcher.CallingCompositeJavaLauncher
import com.azure.android.communication.ui.callingcompositedemoapp.launcher.CallingCompositeKotlinLauncher
import com.azure.android.communication.ui.callingcompositedemoapp.launcher.CallingCompositeLauncher
import com.azure.android.communication.ui.demoapp.UrlTokenFetcher
import java.util.concurrent.Callable
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.CallCompositeBuilder
import com.azure.android.communication.ui.calling.models.CallCompositeCallHistoryRecord
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeJoinLocator
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions
import com.azure.android.communication.ui.calling.models.CallCompositeLocalizationOptions
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeSetupScreenViewData
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator
import com.azure.android.communication.ui.callingcompositedemoapp.features.AdditionalFeatures
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures
import java.util.UUID
class CallLauncherViewModel : ViewModel() {
private var token: String? = null
private val fetchResultInternal = MutableLiveData<Result<CallingCompositeLauncher?>>()
val fetchResult: LiveData<Result<CallingCompositeLauncher?>> = fetchResultInternal
var isKotlinLauncher = true; private set
var isTokenFunctionOptionSelected = false; private set
fun launch(
context: Context,
private fun launcher(tokenRefresher: Callable<String>) = if (isKotlinLauncher) {
CallingCompositeKotlinLauncher(tokenRefresher)
} else {
CallingCompositeJavaLauncher(tokenRefresher)
}
acsToken: String,
displayName: String,
groupId: UUID?,
meetingLink: String?,
) {
val callComposite = createCallComposite(context)
callComposite.addOnErrorEventHandler(CallLauncherActivityErrorHandler(context, callComposite))
fun destroy() {
fetchResultInternal.value = Result.success(null)
}
fun setJavaLauncher() {
isKotlinLauncher = false
}
fun setKotlinLauncher() {
isKotlinLauncher = true
}
fun useTokenFunction() {
isTokenFunctionOptionSelected = true
}
fun useAcsToken() {
isTokenFunctionOptionSelected = false
}
fun doLaunch(tokenFunctionURL: String, acsToken: String) {
when {
isTokenFunctionOptionSelected && urlIsValid(tokenFunctionURL) -> {
token = null
fetchResultInternal.postValue(
Result.success(
launcher(
UrlTokenFetcher(tokenFunctionURL)
)
)
)
}
acsToken.isNotBlank() -> {
token = acsToken
fetchResultInternal.postValue(
Result.success(launcher(CachedTokenFetcher(acsToken)))
)
}
else -> {
fetchResultInternal.postValue(
Result.failure(IllegalStateException("Invalid Token function URL or acs Token"))
)
}
if (SettingsFeatures.getRemoteParticipantPersonaInjectionSelection()) {
callComposite.addOnRemoteParticipantJoinedEventHandler(
RemoteParticipantJoinedHandler(callComposite, context)
)
}
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions({ acsToken }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val locator: CallCompositeJoinLocator =
if (groupId != null) CallCompositeGroupCallLocator(groupId)
else CallCompositeTeamsMeetingLinkLocator(meetingLink)
val remoteOptions =
CallCompositeRemoteOptions(locator, communicationTokenCredential, displayName)
val localOptions = CallCompositeLocalOptions()
.setParticipantViewData(SettingsFeatures.getParticipantViewData(context.applicationContext))
.setSetupScreenViewData(
CallCompositeSetupScreenViewData()
.setTitle(SettingsFeatures.getTitle())
.setSubtitle(SettingsFeatures.getSubtitle())
)
callComposite.launch(context, remoteOptions, localOptions)
}
private fun urlIsValid(url: String) = url.isNotBlank() && URLUtil.isValidUrl(url.trim())
fun getCallHistory(context: Context): List<CallCompositeCallHistoryRecord> {
return (callComposite ?: createCallComposite(context)).getDebugInfo(context).callHistoryRecords
}
private fun createCallComposite(context: Context): CallComposite {
SettingsFeatures.initialize(context.applicationContext)
val selectedLanguage = SettingsFeatures.language()
val locale = selectedLanguage?.let { SettingsFeatures.locale(it) }
val callCompositeBuilder = CallCompositeBuilder()
.localization(CallCompositeLocalizationOptions(locale!!, SettingsFeatures.getLayoutDirection()))
if (AdditionalFeatures.secondaryThemeFeature.active)
callCompositeBuilder.theme(R.style.MyCompany_Theme_Calling)
val callComposite = callCompositeBuilder.build()
// For test purposes we will keep a static ref to CallComposite
CallLauncherViewModel.callComposite = callComposite
return callComposite
}
companion object {
var callComposite: CallComposite? = null
}
}

Просмотреть файл

@ -3,6 +3,7 @@
package com.azure.android.communication.ui.callingcompositedemoapp
import android.content.Context
import android.graphics.BitmapFactory
import com.azure.android.communication.common.CommunicationIdentifier
import com.azure.android.communication.common.CommunicationUserIdentifier
@ -18,13 +19,13 @@ import java.net.URL
class RemoteParticipantJoinedHandler(
private val callComposite: CallComposite,
private val callLauncherActivity: CallLauncherActivity,
private val context: Context,
) :
CallCompositeEventHandler<CallCompositeRemoteParticipantJoinedEvent> {
override fun handle(event: CallCompositeRemoteParticipantJoinedEvent) {
event.identifiers.forEach { communicationIdentifier ->
if (callLauncherActivity.resources.getBoolean(R.bool.remote_url_persona_injection)) {
if (context.resources.getBoolean(R.bool.remote_url_persona_injection)) {
getImageFromServer(communicationIdentifier)
} else {
selectRandomAvatar(communicationIdentifier)
@ -90,12 +91,12 @@ class RemoteParticipantJoinedHandler(
)
images[number].let {
val bitMap =
BitmapFactory.decodeResource(callLauncherActivity.resources, it)
BitmapFactory.decodeResource(context.resources, it)
val result = callComposite.setRemoteParticipantViewData(
communicationIdentifier,
CallCompositeParticipantViewData()
.setDisplayName(
callLauncherActivity.resources.getResourceEntryName(
context.resources.getResourceEntryName(
it
)
)

Просмотреть файл

@ -1,92 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.callingcompositedemoapp.launcher;
import com.azure.android.communication.common.CommunicationTokenCredential;
import com.azure.android.communication.common.CommunicationTokenRefreshOptions;
import com.azure.android.communication.ui.calling.CallComposite;
import com.azure.android.communication.ui.calling.CallCompositeBuilder;
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator;
import com.azure.android.communication.ui.calling.models.CallCompositeJoinLocator;
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions;
import com.azure.android.communication.ui.calling.models.CallCompositeLocalizationOptions;
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions;
import com.azure.android.communication.ui.calling.models.CallCompositeSetupScreenViewData;
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator;
import com.azure.android.communication.ui.callingcompositedemoapp.CallLauncherActivity;
import com.azure.android.communication.ui.callingcompositedemoapp.CallLauncherActivityErrorHandler;
import com.azure.android.communication.ui.callingcompositedemoapp.R;
import com.azure.android.communication.ui.callingcompositedemoapp.RemoteParticipantJoinedHandler;
import com.azure.android.communication.ui.callingcompositedemoapp.features.AdditionalFeatures;
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.Callable;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public class CallingCompositeJavaLauncher implements CallingCompositeLauncher {
private static CallComposite callComposite;
private final Callable<String> tokenRefresher;
public CallingCompositeJavaLauncher(final Callable<String> tokenRefresher) {
this.tokenRefresher = tokenRefresher;
}
@Override
public void launch(final CallLauncherActivity callLauncherActivity,
final String displayName,
final UUID groupId,
final String meetingLink,
final Function1<? super String, Unit> showAlert) {
final CallCompositeBuilder builder = new CallCompositeBuilder();
SettingsFeatures.initialize(callLauncherActivity.getApplicationContext());
final String selectedLanguage = SettingsFeatures.language();
final Locale locale = SettingsFeatures.locale(selectedLanguage);
builder.localization(new CallCompositeLocalizationOptions(locale,
SettingsFeatures.getLayoutDirection()));
if (AdditionalFeatures.Companion.getSecondaryThemeFeature().getActive()) {
builder.theme(R.style.MyCompany_Theme_Calling);
}
final CallComposite callComposite = builder.build();
callComposite.addOnErrorEventHandler(new CallLauncherActivityErrorHandler(callComposite, callLauncherActivity));
if (SettingsFeatures.getRemoteParticipantPersonaInjectionSelection()) {
callComposite.addOnRemoteParticipantJoinedEventHandler(
new RemoteParticipantJoinedHandler(callComposite, callLauncherActivity));
}
final CommunicationTokenRefreshOptions communicationTokenRefreshOptions =
new CommunicationTokenRefreshOptions(tokenRefresher, true);
final CommunicationTokenCredential communicationTokenCredential =
new CommunicationTokenCredential(communicationTokenRefreshOptions);
final CallCompositeJoinLocator locator = groupId != null
? new CallCompositeGroupCallLocator(groupId)
: new CallCompositeTeamsMeetingLinkLocator(meetingLink);
final CallCompositeRemoteOptions remoteOptions =
new CallCompositeRemoteOptions(locator, communicationTokenCredential, displayName);
final CallCompositeLocalOptions localOptions = new CallCompositeLocalOptions()
.setParticipantViewData(SettingsFeatures
.getParticipantViewData(callLauncherActivity.getApplicationContext()))
.setSetupScreenViewData(
new CallCompositeSetupScreenViewData()
.setTitle(SettingsFeatures.getTitle())
.setSubtitle(SettingsFeatures.getSubtitle()));
callComposite.launch(callLauncherActivity, remoteOptions, localOptions);
// For test purposes we will keep a static ref to CallComposite
this.callComposite = callComposite;
}
}

Просмотреть файл

@ -1,99 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.callingcompositedemoapp.launcher
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.ui.calling.CallComposite
import com.azure.android.communication.ui.calling.CallCompositeBuilder
import com.azure.android.communication.ui.calling.models.CallCompositeGroupCallLocator
import com.azure.android.communication.ui.calling.models.CallCompositeJoinLocator
import com.azure.android.communication.ui.calling.models.CallCompositeLocalOptions
import com.azure.android.communication.ui.calling.models.CallCompositeLocalizationOptions
import com.azure.android.communication.ui.calling.models.CallCompositeRemoteOptions
import com.azure.android.communication.ui.calling.models.CallCompositeSetupScreenViewData
import com.azure.android.communication.ui.calling.models.CallCompositeTeamsMeetingLinkLocator
import com.azure.android.communication.ui.callingcompositedemoapp.CallLauncherActivity
import com.azure.android.communication.ui.callingcompositedemoapp.CallLauncherActivityErrorHandler
import com.azure.android.communication.ui.callingcompositedemoapp.R
import com.azure.android.communication.ui.callingcompositedemoapp.RemoteParticipantJoinedHandler
import com.azure.android.communication.ui.callingcompositedemoapp.features.AdditionalFeatures
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.getLayoutDirection
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.getParticipantViewData
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.getRemoteParticipantPersonaInjectionSelection
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.getSubtitle
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.getTitle
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.initialize
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.language
import com.azure.android.communication.ui.callingcompositedemoapp.features.SettingsFeatures.Companion.locale
import java.util.UUID
import java.util.concurrent.Callable
class CallingCompositeKotlinLauncher(private val tokenRefresher: Callable<String>) :
CallingCompositeLauncher {
override fun launch(
callLauncherActivity: CallLauncherActivity,
displayName: String,
groupId: UUID?,
meetingLink: String?,
showAlert: ((String) -> Unit)?,
) {
initialize(callLauncherActivity.applicationContext)
val selectedLanguage = language()
val locale = selectedLanguage?.let { locale(it) }
val callComposite =
if (AdditionalFeatures.secondaryThemeFeature.active)
CallCompositeBuilder().theme(R.style.MyCompany_Theme_Calling)
.localization(CallCompositeLocalizationOptions(locale!!, getLayoutDirection()))
.build()
else
CallCompositeBuilder()
.localization(CallCompositeLocalizationOptions(locale!!, getLayoutDirection()))
.build()
callComposite.addOnErrorEventHandler(
CallLauncherActivityErrorHandler(
callComposite,
callLauncherActivity
)
)
if (getRemoteParticipantPersonaInjectionSelection()) {
callComposite.addOnRemoteParticipantJoinedEventHandler(
RemoteParticipantJoinedHandler(callComposite, callLauncherActivity)
)
}
val communicationTokenRefreshOptions =
CommunicationTokenRefreshOptions(tokenRefresher, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val locator: CallCompositeJoinLocator =
if (groupId != null) CallCompositeGroupCallLocator(groupId)
else CallCompositeTeamsMeetingLinkLocator(meetingLink)
val remoteOptions =
CallCompositeRemoteOptions(locator, communicationTokenCredential, displayName)
val localOptions = CallCompositeLocalOptions()
.setParticipantViewData(getParticipantViewData(callLauncherActivity.applicationContext))
.setSetupScreenViewData(
CallCompositeSetupScreenViewData()
.setTitle(getTitle())
.setSubtitle(getSubtitle())
)
callComposite.launch(callLauncherActivity, remoteOptions, localOptions)
// For test purposes we will keep a static ref to CallComposite
CallingCompositeKotlinLauncher.callComposite = callComposite
}
companion object {
var callComposite: CallComposite? = null
}
}

Просмотреть файл

@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.callingcompositedemoapp.launcher;
import com.azure.android.communication.ui.callingcompositedemoapp.CallLauncherActivity;
import java.util.UUID;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public interface CallingCompositeLauncher {
void launch(CallLauncherActivity callLauncherActivity,
String userName,
UUID groupId,
String meetingLink,
Function1<? super String, Unit> showAlert);
}

Просмотреть файл

@ -17,37 +17,6 @@
tools:context=".CallLauncherActivity"
>
<RadioButton
android:id="@+id/tokenFunctionRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/token_function_radio_button_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<EditText
android:id="@+id/tokenFunctionUrlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/token_url_hint"
android:inputType="textUri"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tokenFunctionRadioButton"
/>
<RadioButton
android:id="@+id/acsTokenRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/acs_token_radio_button_text"
app:layout_constraintStart_toStartOf="@id/tokenFunctionUrlText"
app:layout_constraintTop_toBottomOf="@id/tokenFunctionUrlText"
/>
<EditText
android:id="@+id/acsTokenText"
android:layout_width="match_parent"
@ -56,7 +25,7 @@
android:hint="@string/acs_token_hint"
android:inputType="textUri"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/acsTokenRadioButton"
app:layout_constraintTop_toTopOf="parent"
/>
<EditText
@ -104,43 +73,15 @@
app:layout_constraintTop_toBottomOf="@id/teamsMeetingRadioButton"
/>
<RadioGroup
android:id="@+id/javaOrKotlinContainer"
<Button
android:id="@+id/showCallHistoryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="@id/groupIdOrTeamsMeetingLinkText"
app:layout_constraintTop_toBottomOf="@id/groupIdOrTeamsMeetingLinkText"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/launch_type_text"
android:textSize="17sp"
/>
<RadioButton
android:id="@+id/kotlinButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:checked="true"
android:padding="5dp"
android:text="@string/kotlin"
android:textSize="17sp"
/>
<RadioButton
android:id="@+id/javaButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/java"
android:textSize="17sp"
/>
</RadioGroup>
android:text="call history"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/groupIdOrTeamsMeetingLinkText"
/>
<Button
android:id="@+id/launchButton"
@ -149,7 +90,7 @@
android:text="@string/launch_button_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/javaOrKotlinContainer"
app:layout_constraintTop_toBottomOf="@+id/showCallHistoryButton"
/>
<LinearLayout