Skip to content

Commit

Permalink
Implement direct calls with Stream Google users for staging app (#863)
Browse files Browse the repository at this point in the history
Add Direct Call screen and functionality
  • Loading branch information
liviu-timar authored Oct 25, 2023
1 parent ec7b55b commit f8506c3
Show file tree
Hide file tree
Showing 25 changed files with 903 additions and 255 deletions.
5 changes: 5 additions & 0 deletions dogfooding/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ dependencies {
implementation(libs.androidx.hilt.navigation)
implementation(libs.landscapist.coil)
implementation(libs.accompanist.permission)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.coil.compose)

// hilt
implementation(libs.hilt.android)
Expand All @@ -251,6 +253,9 @@ dependencies {
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.analytics)

// moshi
implementation(libs.moshi.kotlin)

// Play Install Referrer library - used to extract the meeting link from demo flow after install
implementation(libs.play.install.referrer)

Expand Down
2 changes: 1 addition & 1 deletion dogfooding/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
</activity>

<activity
android:name="io.getstream.video.android.RingCallActivity"
android:name="io.getstream.video.android.DirectCallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:exported="true"
android:hardwareAccelerated="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import java.util.UUID
import javax.inject.Inject

@AndroidEntryPoint
class RingCallActivity : ComponentActivity() {
class DirectCallActivity : ComponentActivity() {

@Inject
lateinit var dataStore: StreamUserDataStore
Expand Down Expand Up @@ -87,9 +87,9 @@ class RingCallActivity : ComponentActivity() {
if (result is Result.Failure) {
// Failed to recover the current state of the call
// TODO: Automaticly call this in the SDK?
Log.e("RingCallActivity", "Call.create failed ${result.value}")
Log.e("DirectCallActivity", "Call.create failed ${result.value}")
Toast.makeText(
this@RingCallActivity,
this@DirectCallActivity,
"Failed get call status (${result.value.message})",
Toast.LENGTH_SHORT,
).show()
Expand Down Expand Up @@ -163,7 +163,7 @@ class RingCallActivity : ComponentActivity() {
callId: String? = null,
members: List<String>,
): Intent {
return Intent(context, RingCallActivity::class.java).apply {
return Intent(context, DirectCallActivity::class.java).apply {
putExtra(EXTRA_CID, callId)
putExtra(EXTRA_MEMBERS_ARRAY, members.toTypedArray())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import io.getstream.result.Result
import io.getstream.video.android.compose.theme.VideoTheme
import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
Expand All @@ -46,6 +47,7 @@ import io.getstream.video.android.util.StreamVideoInitHelper
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class IncomingCallActivity : ComponentActivity() {

@Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.data.dto

import io.getstream.video.android.models.GoogleAccount
import io.getstream.video.android.util.UserIdHelper
import java.util.Locale

data class GetGoogleAccountsResponseDto(
val people: List<GoogleAccountDto>,
)

data class GoogleAccountDto(
val photos: List<PhotoDto>?,
val emailAddresses: List<EmailAddressDto>,
)

data class PhotoDto(
val url: String,
)

data class EmailAddressDto(
val value: String,
)

fun GoogleAccountDto.asDomainModel(): GoogleAccount {
val email = emailAddresses.firstOrNull()?.value

return GoogleAccount(
email = email,
id = email?.let { UserIdHelper.getUserIdFromEmail(it) },
name = email
?.split("@")
?.firstOrNull()
?.split(".")
?.firstOrNull()
?.capitalize(Locale.ROOT) ?: email,
photoUrl = photos?.firstOrNull()?.url,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.data.repositories

import android.content.Context
import android.util.Log
import com.google.android.gms.auth.GoogleAuthUtil
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.hilt.android.qualifiers.ApplicationContext
import io.getstream.video.android.data.dto.GetGoogleAccountsResponseDto
import io.getstream.video.android.data.dto.asDomainModel
import io.getstream.video.android.models.GoogleAccount
import io.getstream.video.android.util.GoogleSignInHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import javax.inject.Inject

class GoogleAccountRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val baseUrl = "https://people.googleapis.com/v1/people:listDirectoryPeople"

suspend fun getAllAccounts(): List<GoogleAccount>? {
val readMask = "readMask=emailAddresses,names,photos"
val sources = "sources=DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE"
val pageSize = "pageSize=1000"

return if (silentSignIn()) {
GoogleSignIn.getLastSignedInAccount(context)?.let { account ->
withContext(Dispatchers.IO) {
getAccessToken(account)?.let { accessToken ->
val urlString = "$baseUrl?access_token=$accessToken&$readMask&$sources&$pageSize"
val request = buildRequest(urlString)
val okHttpClient = buildOkHttpClient()
var responseBody: String?

okHttpClient.newCall(request).execute().let { response ->
if (response.isSuccessful) {
responseBody = response.body?.string()
responseBody?.let { parseUserListJson(it) }
} else {
null
}
}
}
}
}
} else {
null
}
}

private fun silentSignIn(): Boolean { // Used to refresh token
val gsc = GoogleSignInHelper.getGoogleSignInClient(context)
val task = gsc.silentSignIn()

// Below code needed for debugging silent sign-in failures
if (task.isSuccessful) {
Log.d("Google Silent Sign In", "Successful")
return true
} else {
task.addOnCompleteListener {
try {
val signInAccount = task.getResult(ApiException::class.java)
Log.d("Google Silent Sign In", signInAccount.email.toString())
} catch (apiException: ApiException) {
// You can get from apiException.getStatusCode() the detailed error code
// e.g. GoogleSignInStatusCodes.SIGN_IN_REQUIRED means user needs to take
// explicit action to finish sign-in;
// Please refer to GoogleSignInStatusCodes Javadoc for details
Log.d("Google Silent Sign In", apiException.statusCode.toString())
}
}
return false
}
}

private fun getAccessToken(account: GoogleSignInAccount) =
try {
GoogleAuthUtil.getToken(
context,
account.account,
"oauth2:profile email",
)
} catch (e: Exception) {
null
}

private fun buildRequest(urlString: String) = Request.Builder()
.url(urlString.toHttpUrl())
.build()

private fun buildOkHttpClient() = OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.build()

@OptIn(ExperimentalStdlibApi::class)
private fun parseUserListJson(jsonString: String): List<GoogleAccount>? {
val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val jsonAdapter: JsonAdapter<GetGoogleAccountsResponseDto> = moshi.adapter()

val response = jsonAdapter.fromJson(jsonString)
return response?.people?.map { it.asDomainModel() }
}

fun getCurrentUser(): GoogleAccount {
val currentUser = GoogleSignIn.getLastSignedInAccount(context)
return GoogleAccount(
email = currentUser?.email ?: "",
id = currentUser?.id ?: "",
name = currentUser?.givenName ?: "",
photoUrl = currentUser?.photoUrl?.toString(),
isFavorite = false,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package io.getstream.video.android.di

import android.content.Context
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.getstream.video.android.data.repositories.GoogleAccountRepository
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
import javax.inject.Singleton

Expand All @@ -31,4 +34,9 @@ object AppModule {
fun provideUserDataStore(): StreamUserDataStore {
return StreamUserDataStore.instance()
}

@Provides
fun provideGoogleAccountRepository(
@ApplicationContext context: Context,
) = GoogleAccountRepository(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.models

data class GoogleAccount(
val email: String?,
val id: String?,
val name: String?,
val photoUrl: String?,
val isFavorite: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.models

data class StreamUser(
val email: String,
val id: String,
val name: String,
val avatarUrl: String?,
val isFavorite: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import io.getstream.video.android.RingCallActivity
import io.getstream.video.android.DirectCallActivity
import io.getstream.video.android.ui.join.CallJoinScreen
import io.getstream.video.android.ui.lobby.CallLobbyScreen
import io.getstream.video.android.ui.login.LoginScreen
import io.getstream.video.android.ui.outgoing.DebugCallScreen
import io.getstream.video.android.ui.outgoing.DirectCallJoinScreen

@Composable
fun AppNavHost(
Expand Down Expand Up @@ -62,8 +62,8 @@ fun AppNavHost(
popUpTo(AppScreens.CallJoin.destination) { inclusive = true }
}
},
navigateToRingTest = {
navController.navigate(AppScreens.DebugCall.destination)
navigateToDirectCallJoin = {
navController.navigate(AppScreens.DirectCallJoin.destination)
},
)
}
Expand All @@ -79,15 +79,14 @@ fun AppNavHost(
},
)
}
composable(AppScreens.DebugCall.destination) {
composable(AppScreens.DirectCallJoin.destination) {
val context = LocalContext.current
DebugCallScreen(
navigateToRingCall = { callId, members ->
DirectCallJoinScreen(
navigateToDirectCall = { members ->
context.startActivity(
RingCallActivity.createIntent(
DirectCallActivity.createIntent(
context,
members = members.split(","),
callId = callId,
),
)
},
Expand All @@ -100,5 +99,5 @@ enum class AppScreens(val destination: String) {
Login("login"),
CallJoin("call_join"),
CallLobby("call_preview"),
DebugCall("debug_call"),
DirectCallJoin("direct_call_join"),
}
Loading

0 comments on commit f8506c3

Please sign in to comment.